From 82bf53bf2624efb1bf985a52501b58144fce5592 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Thu, 23 Nov 2023 22:27:00 +0000 Subject: [PATCH] feat: gready render mode --- .../dockview/components/panel/content.spec.ts | 10 +- packages/dockview-core/src/dnd/dnd.ts | 50 +++-- packages/dockview-core/src/dnd/droptarget.ts | 175 +++++++++--------- .../src/dockview/components/panel/content.ts | 84 ++++++++- .../dockview/components/panel/renderCache.ts | 109 +++++++++++ .../src/dockview/dockviewComponent.ts | 10 + .../src/dockview/dockviewGroupPanelModel.ts | 100 +++++----- .../dockview-core/src/dockview/options.ts | 1 + packages/dockview/src/dockview/dockview.tsx | 1 + 9 files changed, 383 insertions(+), 157 deletions(-) create mode 100644 packages/dockview-core/src/dockview/components/panel/renderCache.ts diff --git a/packages/dockview-core/src/__tests__/dockview/components/panel/content.spec.ts b/packages/dockview-core/src/__tests__/dockview/components/panel/content.spec.ts index 34ba707a3..6d0f7bd97 100644 --- a/packages/dockview-core/src/__tests__/dockview/components/panel/content.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/components/panel/content.spec.ts @@ -9,6 +9,7 @@ import { CompositeDisposable } from '../../../../lifecycle'; import { PanelUpdateEvent } from '../../../../panel/types'; import { IDockviewPanel } from '../../../../dockview/dockviewPanel'; import { IDockviewPanelModel } from '../../../../dockview/dockviewPanelModel'; +import { DockviewComponent } from '../../../../dockview/dockviewComponent'; class TestContentRenderer extends CompositeDisposable @@ -56,7 +57,14 @@ describe('contentContainer', () => { let blur = 0; const disposable = new CompositeDisposable(); - const cut = new ContentContainer(); + + const dockviewComponent = jest.fn(() => { + return { + renderMode: 'destructive', + } as DockviewComponent; + }); + + const cut = new ContentContainer(dockviewComponent(), jest.fn() as any); disposable.addDisposables( cut.onDidFocus(() => { diff --git a/packages/dockview-core/src/dnd/dnd.ts b/packages/dockview-core/src/dnd/dnd.ts index 533be7e11..2120f2994 100644 --- a/packages/dockview-core/src/dnd/dnd.ts +++ b/packages/dockview-core/src/dnd/dnd.ts @@ -21,14 +21,43 @@ export class DragAndDropObserver extends CompositeDisposable { this.registerListeners(); } + onDragEnter(e: DragEvent): void { + this.target = e.target; + this.callbacks.onDragEnter(e); + } + + onDragOver(e: DragEvent): void { + e.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome) + + if (this.callbacks.onDragOver) { + this.callbacks.onDragOver(e); + } + } + + onDragLeave(e: DragEvent): void { + if (this.target === e.target) { + this.target = null; + + this.callbacks.onDragLeave(e); + } + } + + onDragEnd(e: DragEvent): void { + this.target = null; + this.callbacks.onDragEnd(e); + } + + onDrop(e: DragEvent): void { + this.callbacks.onDrop(e); + } + private registerListeners(): void { this.addDisposables( addDisposableListener( this.element, 'dragenter', (e: DragEvent) => { - this.target = e.target; - this.callbacks.onDragEnter(e); + this.onDragEnter(e); }, true ) @@ -39,11 +68,7 @@ export class DragAndDropObserver extends CompositeDisposable { this.element, 'dragover', (e: DragEvent) => { - e.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome) - - if (this.callbacks.onDragOver) { - this.callbacks.onDragOver(e); - } + this.onDragOver(e); }, true ) @@ -51,24 +76,19 @@ export class DragAndDropObserver extends CompositeDisposable { this.addDisposables( addDisposableListener(this.element, 'dragleave', (e: DragEvent) => { - if (this.target === e.target) { - this.target = null; - - this.callbacks.onDragLeave(e); - } + this.onDragLeave(e); }) ); this.addDisposables( addDisposableListener(this.element, 'dragend', (e: DragEvent) => { - this.target = null; - this.callbacks.onDragEnd(e); + this.onDragEnd(e); }) ); this.addDisposables( addDisposableListener(this.element, 'drop', (e: DragEvent) => { - this.callbacks.onDrop(e); + this.onDrop(e); }) ); } diff --git a/packages/dockview-core/src/dnd/droptarget.ts b/packages/dockview-core/src/dnd/droptarget.ts index 003f8b045..357d4c32f 100644 --- a/packages/dockview-core/src/dnd/droptarget.ts +++ b/packages/dockview-core/src/dnd/droptarget.ts @@ -63,6 +63,8 @@ export class Droptarget extends CompositeDisposable { private readonly _onDrop = new Emitter(); readonly onDrop: Event = this._onDrop.event; + readonly dnd: DragAndDropObserver; + private static USED_EVENT_ID = '__dockview_droptarget_event_is_used__'; get state(): Position | undefined { @@ -90,98 +92,97 @@ export class Droptarget extends CompositeDisposable { this.options.acceptedTargetZones ); - this.addDisposables( - this._onDrop, - new DragAndDropObserver(this.element, { - onDragEnter: () => undefined, - onDragOver: (e) => { - if (this._acceptedTargetZonesSet.size === 0) { - this.removeDropTarget(); - return; - } - - const width = this.element.clientWidth; - const height = this.element.clientHeight; - - if (width === 0 || height === 0) { - return; // avoid div!0 - } - - const rect = ( - e.currentTarget as HTMLElement - ).getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - - const quadrant = this.calculateQuadrant( - this._acceptedTargetZonesSet, - x, - y, - width, - height - ); - - /** - * If the event has already been used by another DropTarget instance - * then don't show a second drop target, only one target should be - * active at any one time - */ - if (this.isAlreadyUsed(e) || quadrant === null) { - // no drop target should be displayed - this.removeDropTarget(); - return; - } + this.dnd = new DragAndDropObserver(this.element, { + onDragEnter: () => undefined, + onDragOver: (e) => { + if (this._acceptedTargetZonesSet.size === 0) { + this.removeDropTarget(); + return; + } + + const width = this.element.clientWidth; + const height = this.element.clientHeight; + + if (width === 0 || height === 0) { + return; // avoid div!0 + } + + const rect = ( + e.currentTarget as HTMLElement + ).getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + const quadrant = this.calculateQuadrant( + this._acceptedTargetZonesSet, + x, + y, + width, + height + ); + + /** + * If the event has already been used by another DropTarget instance + * then don't show a second drop target, only one target should be + * active at any one time + */ + if (this.isAlreadyUsed(e) || quadrant === null) { + // no drop target should be displayed + this.removeDropTarget(); + return; + } - if (typeof this.options.canDisplayOverlay === 'boolean') { - if (!this.options.canDisplayOverlay) { - this.removeDropTarget(); - return; - } - } else if (!this.options.canDisplayOverlay(e, quadrant)) { + if (typeof this.options.canDisplayOverlay === 'boolean') { + if (!this.options.canDisplayOverlay) { this.removeDropTarget(); return; } - - this.markAsUsed(e); - - if (!this.targetElement) { - this.targetElement = document.createElement('div'); - this.targetElement.className = 'drop-target-dropzone'; - this.overlayElement = document.createElement('div'); - this.overlayElement.className = 'drop-target-selection'; - this._state = 'center'; - this.targetElement.appendChild(this.overlayElement); - - this.element.classList.add('drop-target'); - this.element.append(this.targetElement); - } - - this.toggleClasses(quadrant, width, height); - - this.setState(quadrant); - }, - onDragLeave: () => { - this.removeDropTarget(); - }, - onDragEnd: () => { + } else if (!this.options.canDisplayOverlay(e, quadrant)) { this.removeDropTarget(); - }, - onDrop: (e) => { - e.preventDefault(); - - const state = this._state; - - this.removeDropTarget(); - - if (state) { - // only stop the propagation of the event if we are dealing with it - // which is only when the target has state - e.stopPropagation(); - this._onDrop.fire({ position: state, nativeEvent: e }); - } - }, - }) - ); + return; + } + + this.markAsUsed(e); + + if (!this.targetElement) { + this.targetElement = document.createElement('div'); + this.targetElement.className = 'drop-target-dropzone'; + this.overlayElement = document.createElement('div'); + this.overlayElement.className = 'drop-target-selection'; + this._state = 'center'; + this.targetElement.appendChild(this.overlayElement); + + this.element.classList.add('drop-target'); + this.element.append(this.targetElement); + } + + this.toggleClasses(quadrant, width, height); + + this.setState(quadrant); + }, + onDragLeave: () => { + this.removeDropTarget(); + }, + onDragEnd: () => { + this.removeDropTarget(); + }, + onDrop: (e) => { + e.preventDefault(); + + const state = this._state; + + this.removeDropTarget(); + + if (state) { + // only stop the propagation of the event if we are dealing with it + // which is only when the target has state + e.stopPropagation(); + this._onDrop.fire({ position: state, nativeEvent: e }); + } + }, + }); + + this.addDisposables(this._onDrop, this.dnd); } setTargetZones(acceptedTargetZones: Position[]): void { diff --git a/packages/dockview-core/src/dockview/components/panel/content.ts b/packages/dockview-core/src/dockview/components/panel/content.ts index 9934c5ec4..7ffb4c72a 100644 --- a/packages/dockview-core/src/dockview/components/panel/content.ts +++ b/packages/dockview-core/src/dockview/components/panel/content.ts @@ -1,13 +1,21 @@ import { CompositeDisposable, + Disposable, IDisposable, MutableDisposable, } from '../../../lifecycle'; import { Emitter, Event } from '../../../events'; import { trackFocus } from '../../../dom'; import { IDockviewPanel } from '../../dockviewPanel'; +import { DockviewComponent } from '../../dockviewComponent'; +import { DragAndDropObserver } from '../../../dnd/dnd'; +import { Droptarget } from '../../../dnd/droptarget'; +import { DockviewGroupPanelModel } from '../../dockviewGroupPanelModel'; +import { getPanelData } from '../../../dnd/dataTransfer'; +import { DockviewDropTargets } from '../../types'; export interface IContentContainer extends IDisposable { + readonly dropTarget: Droptarget; onDidFocus: Event; onDidBlur: Event; element: HTMLElement; @@ -36,7 +44,12 @@ export class ContentContainer return this._element; } - constructor() { + readonly dropTarget: Droptarget; + + constructor( + private readonly accessor: DockviewComponent, + private readonly group: DockviewGroupPanelModel + ) { super(); this._element = document.createElement('div'); this._element.className = 'content-container'; @@ -49,6 +62,51 @@ export class ContentContainer // 2) register window dragStart events to disable pointer events // 3) register dragEnd events // 4) register mouseMove events (if no buttons are present we take this as a dragEnd event) + + this.dropTarget = new Droptarget(this.element, { + acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'], + canDisplayOverlay: (event, position) => { + if ( + this.group.locked === 'no-drop-target' || + (this.group.locked && position === 'center') + ) { + return false; + } + + const data = getPanelData(); + + if (!data && event.shiftKey && !this.group.isFloating) { + return false; + } + + if (data && data.viewId === this.accessor.id) { + if (data.groupId === this.group.id) { + if (position === 'center') { + // don't allow to drop on self for center position + return false; + } + if (data.panelId === null) { + // don't allow group move to drop anywhere on self + return false; + } + } + + const groupHasOnePanelAndIsActiveDragElement = + this.group.panels.length === 1 && + data.groupId === this.group.id; + + return !groupHasOnePanelAndIsActiveDragElement; + } + + return this.group.canDisplayOverlay( + event, + position, + DockviewDropTargets.Panel + ); + }, + }); + + this.addDisposables(this.dropTarget); } show(): void { @@ -63,9 +121,15 @@ export class ContentContainer if (this.panel === panel) { return; } + if (this.panel) { if (this.panel.view?.content) { - this._element.removeChild(this.panel.view.content.element); + if ( + this.panel.view.content.element.parentElement === + this._element + ) { + this._element.removeChild(this.panel.view.content.element); + } } this.panel = undefined; } @@ -96,7 +160,17 @@ export class ContentContainer ); } - this._element.appendChild(this.panel.view.content.element); + switch (this.accessor.renderMode) { + case 'gready': + this.accessor.renderCache.setReferenceContentContainer( + panel, + this + ); + break; + case 'destructive': + this._element.appendChild(this.panel.view.content.element); + break; + } } this.disposable.value = disposable; @@ -108,7 +182,9 @@ export class ContentContainer public closePanel(): void { if (this.panel?.view?.content?.element) { - this._element.removeChild(this.panel.view.content.element); + if (this.accessor.options.renderMode === 'destructive') { + this._element.removeChild(this.panel.view.content.element); + } this.panel = undefined; } } diff --git a/packages/dockview-core/src/dockview/components/panel/renderCache.ts b/packages/dockview-core/src/dockview/components/panel/renderCache.ts new file mode 100644 index 000000000..028c5574e --- /dev/null +++ b/packages/dockview-core/src/dockview/components/panel/renderCache.ts @@ -0,0 +1,109 @@ +import { DragAndDropObserver } from '../../../dnd/dnd'; +import { + CompositeDisposable, + Disposable, + IDisposable, +} from '../../../lifecycle'; +import { DockviewComponent } from '../../dockviewComponent'; +import { IDockviewPanel } from '../../dockviewPanel'; +import { ContentContainer } from './content'; + +function getDomNodePagePosition(domNode: Element): { + left: number; + top: number; + width: number; + height: number; +} { + const { left, top, width, height } = domNode.getBoundingClientRect(); + return { + left: left + window.scrollX, + top: top + window.scrollY, + width: width, + height: height, + }; +} + +export class RenderCache { + constructor(private readonly accessor: DockviewComponent) { + // + } + + private readonly map: Record< + string, + { + disposable: IDisposable; + } + > = {}; + + remove(panel: IDockviewPanel): boolean { + if (this.map[panel.api.id]) { + this.map[panel.api.id].disposable.dispose(); + delete this.map[panel.api.id]; + return true; + } + return false; + } + + setReferenceContentContainer( + panel: IDockviewPanel, + reference: ContentContainer + ) { + if (!this.map[panel.api.id]) { + this.map[panel.api.id] = { disposable: Disposable.NONE }; + } + + this.map[panel.api.id]?.disposable.dispose(); + + const el = this.accessor.element.querySelector('.dv-dockview')!; + if (panel.view.content.element.parentElement !== el) { + el.appendChild(panel.view.content.element); + } + + function resize() { + const box = getDomNodePagePosition(reference.element); + const box2 = getDomNodePagePosition(el); + panel.view.content.element.style.left = `${box.left - box2.left}px`; + panel.view.content.element.style.top = `${box.top - box2.top}px`; + panel.view.content.element.style.width = `${box.width}px`; + panel.view.content.element.style.height = `${box.height}px`; + panel.view.content.element.style.position = 'absolute'; + panel.view.content.element.style.outline = '1px solid red'; + panel.view.content.element.style.outlineOffset = '-1px'; + panel.view.content.element.style.zIndex = '1'; + } + + const disposable = new CompositeDisposable( + new DragAndDropObserver(panel.view.content.element, { + onDragEnd: (e) => { + reference.dropTarget.dnd.onDragEnd(e); + }, + onDragEnter: (e) => { + reference.dropTarget.dnd.onDragEnter(e); + }, + onDragLeave: (e) => { + reference.dropTarget.dnd.onDragLeave(e); + }, + onDrop: (e) => { + reference.dropTarget.dnd.onDrop(e); + }, + onDragOver: (e) => { + reference.dropTarget.dnd.onDragOver(e); + }, + }), + panel.api.onDidVisibilityChange((event) => { + panel.view.content.element.style.display = event.isVisible + ? '' + : 'none'; + }), + panel.api.onDidDimensionsChange((event) => { + resize(); + }) + ); + + queueMicrotask(() => { + resize(); + }); + + this.map[panel.api.id].disposable = disposable; + } +} diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index e7ab959c2..05a50b778 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -55,6 +55,7 @@ import { GroupDragEvent, TabDragEvent, } from './components/titlebar/tabsContainer'; +import { RenderCache } from './components/panel/renderCache'; const DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE = 100; @@ -245,6 +246,8 @@ export class DockviewComponent private _options: Exclude; private watermark: IWatermarkRenderer | null = null; + readonly renderCache: RenderCache; + private readonly _onWillDragPanel = new Emitter(); readonly onWillDragPanel: Event = this._onWillDragPanel.event; @@ -299,6 +302,10 @@ export class DockviewComponent return activeGroup.activePanel; } + get renderMode(): 'destructive' | 'gready' { + return this.options.renderMode ?? 'destructive'; + } + constructor(options: DockviewComponentOptions) { super({ proportionalLayout: true, @@ -308,6 +315,8 @@ export class DockviewComponent disableAutoResizing: options.disableAutoResizing, }); + this.renderCache = new RenderCache(this); + toggleClass(this.gridview.element, 'dv-dockview', true); this.addDisposables( @@ -1041,6 +1050,7 @@ export class DockviewComponent group.model.removePanel(panel); if (!options.skipDispose) { + this.renderCache.remove(panel); panel.dispose(); } diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index 43ee03b01..aa6b07f29 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -136,7 +136,7 @@ export class DockviewGroupPanelModel { private readonly tabsContainer: ITabsContainer; private readonly contentContainer: IContentContainer; - private readonly dropTarget: Droptarget; + // private readonly dropTarget: Droptarget; private _activePanel: IDockviewPanel | undefined; private watermark?: IWatermarkRenderer; private _isGroupActive = false; @@ -248,7 +248,7 @@ export class DockviewGroupPanelModel set isFloating(value: boolean) { this._isFloating = value; - this.dropTarget.setTargetZones( + this.contentContainer.dropTarget.setTargetZones( value ? ['center'] : ['top', 'bottom', 'left', 'right', 'center'] ); @@ -272,49 +272,49 @@ export class DockviewGroupPanelModel this.tabsContainer = new TabsContainer(this.accessor, this.groupPanel); - this.contentContainer = new ContentContainer(); - - this.dropTarget = new Droptarget(this.contentContainer.element, { - acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'], - canDisplayOverlay: (event, position) => { - if ( - this.locked === 'no-drop-target' || - (this.locked && position === 'center') - ) { - return false; - } - - const data = getPanelData(); - - if (!data && event.shiftKey && !this.isFloating) { - return false; - } - - if (data && data.viewId === this.accessor.id) { - if (data.groupId === this.id) { - if (position === 'center') { - // don't allow to drop on self for center position - return false; - } - if (data.panelId === null) { - // don't allow group move to drop anywhere on self - return false; - } - } - - const groupHasOnePanelAndIsActiveDragElement = - this._panels.length === 1 && data.groupId === this.id; - - return !groupHasOnePanelAndIsActiveDragElement; - } - - return this.canDisplayOverlay( - event, - position, - DockviewDropTargets.Panel - ); - }, - }); + this.contentContainer = new ContentContainer(this.accessor, this); + + // this.dropTarget = new Droptarget(this.contentContainer.element, { + // acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'], + // canDisplayOverlay: (event, position) => { + // if ( + // this.locked === 'no-drop-target' || + // (this.locked && position === 'center') + // ) { + // return false; + // } + + // const data = getPanelData(); + + // if (!data && event.shiftKey && !this.isFloating) { + // return false; + // } + + // if (data && data.viewId === this.accessor.id) { + // if (data.groupId === this.id) { + // if (position === 'center') { + // // don't allow to drop on self for center position + // return false; + // } + // if (data.panelId === null) { + // // don't allow group move to drop anywhere on self + // return false; + // } + // } + + // const groupHasOnePanelAndIsActiveDragElement = + // this._panels.length === 1 && data.groupId === this.id; + + // return !groupHasOnePanelAndIsActiveDragElement; + // } + + // return this.canDisplayOverlay( + // event, + // position, + // DockviewDropTargets.Panel + // ); + // }, + // }); container.append( this.tabsContainer.element, @@ -342,7 +342,7 @@ export class DockviewGroupPanelModel this.contentContainer.onDidBlur(() => { // noop }), - this.dropTarget.onDrop((event) => { + this.contentContainer.dropTarget.onDrop((event) => { this.handleDropEvent(event.nativeEvent, event.position); }), this._onMove, @@ -687,15 +687,15 @@ export class DockviewGroupPanelModel const existingPanel = this._panels.indexOf(panel); const hasExistingPanel = existingPanel > -1; + this.tabsContainer.show(); + this.contentContainer.show(); + this.tabsContainer.openPanel(panel, index); if (!skipSetActive) { this.contentContainer.openPanel(panel); } - this.tabsContainer.show(); - this.contentContainer.show(); - if (hasExistingPanel) { // TODO - need to ensure ordering hasn't changed and if it has need to re-order this.panels return; @@ -849,7 +849,7 @@ export class DockviewGroupPanelModel panel.dispose(); } - this.dropTarget.dispose(); + // this.dropTarget.dispose(); this.tabsContainer.dispose(); this.contentContainer.dispose(); } diff --git a/packages/dockview-core/src/dockview/options.ts b/packages/dockview-core/src/dockview/options.ts index 8bd23025a..da3590a19 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -96,6 +96,7 @@ export interface DockviewComponentOptions extends DockviewRenderFunctions { minimumHeightWithinViewport?: number; minimumWidthWithinViewport?: number; }; + renderMode?: 'gready' | 'destructive'; } export interface PanelOptions

{ diff --git a/packages/dockview/src/dockview/dockview.tsx b/packages/dockview/src/dockview/dockview.tsx index c9fd48b09..e4c5127fc 100644 --- a/packages/dockview/src/dockview/dockview.tsx +++ b/packages/dockview/src/dockview/dockview.tsx @@ -175,6 +175,7 @@ export const DockviewReact = React.forwardRef( singleTabMode: props.singleTabMode, disableFloatingGroups: props.disableFloatingGroups, floatingGroupBounds: props.floatingGroupBounds, + renderMode: 'gready', }); const { clientWidth, clientHeight } = domRef.current;