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/api/dockviewPanelApi.ts b/packages/dockview-core/src/api/dockviewPanelApi.ts index e724c251d..16e3d2c23 100644 --- a/packages/dockview-core/src/api/dockviewPanelApi.ts +++ b/packages/dockview-core/src/api/dockviewPanelApi.ts @@ -2,14 +2,19 @@ import { Emitter, Event } from '../events'; import { GridviewPanelApiImpl, GridviewPanelApi } from './gridviewPanelApi'; import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel'; import { MutableDisposable } from '../lifecycle'; -import { IDockviewPanel } from '../dockview/dockviewPanel'; +import { DockviewPanel, IDockviewPanel } from '../dockview/dockviewPanel'; import { DockviewComponent } from '../dockview/dockviewComponent'; import { Position } from '../dnd/droptarget'; +import { RenderMode } from '../dockview/components/greadyRenderContainer'; export interface TitleEvent { readonly title: string; } +export interface RenderModeEvent { + renderMode: RenderMode; +} + /* * omit visibility modifiers since the visibility of a single group doesn't make sense * because it belongs to a groupview @@ -21,6 +26,7 @@ export interface DockviewPanelApi > { readonly group: DockviewGroupPanel; readonly isGroupActive: boolean; + readonly renderMode: RenderMode; readonly title: string | undefined; readonly onDidActiveGroupChange: Event; readonly onDidGroupChange: Event; @@ -48,6 +54,9 @@ export class DockviewPanelApiImpl private readonly _onDidGroupChange = new Emitter(); readonly onDidGroupChange = this._onDidGroupChange.event; + readonly _onDidRenderModeChange = new Emitter(); + readonly onDidRenderModeChange = this._onDidRenderModeChange.event; + private readonly disposable = new MutableDisposable(); get title(): string | undefined { @@ -58,6 +67,10 @@ export class DockviewPanelApiImpl return !!this.group?.isActive; } + get renderMode(): RenderMode { + return this.panel.renderMode; + } + set group(value: DockviewGroupPanel) { const isOldGroupActive = this.isGroupActive; @@ -81,7 +94,7 @@ export class DockviewPanelApiImpl } constructor( - private panel: IDockviewPanel, + private panel: DockviewPanel, group: DockviewGroupPanel, private readonly accessor: DockviewComponent ) { @@ -117,6 +130,10 @@ export class DockviewPanelApiImpl this.panel.setTitle(title); } + setRenderMode(renderMode: RenderMode): void { + this.panel.setRenderMode(renderMode); + } + close(): void { this.group.model.closePanel(this.panel); } 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/greadyReadyContainer.scss b/packages/dockview-core/src/dockview/components/greadyReadyContainer.scss new file mode 100644 index 000000000..cf67afd19 --- /dev/null +++ b/packages/dockview-core/src/dockview/components/greadyReadyContainer.scss @@ -0,0 +1,15 @@ +.dv-render-overlay { + position: absolute; + z-index: 1; + + &.dv-render-overlay-float { + z-index: 999; + } +} + +.dv-debug { + .dv-render-overlay { + outline: 1px solid red; + outline-offset: -1; + } +} diff --git a/packages/dockview-core/src/dockview/components/greadyRenderContainer.ts b/packages/dockview-core/src/dockview/components/greadyRenderContainer.ts new file mode 100644 index 000000000..8bc0340ef --- /dev/null +++ b/packages/dockview-core/src/dockview/components/greadyRenderContainer.ts @@ -0,0 +1,133 @@ +import { DragAndDropObserver } from '../../dnd/dnd'; +import { Droptarget } from '../../dnd/droptarget'; +import { toggleClass } from '../../dom'; +import { CompositeDisposable, Disposable, IDisposable } from '../../lifecycle'; +import { IDockviewPanel } from '../dockviewPanel'; + +export interface IRenderable { + readonly element: HTMLElement; + readonly dropTarget: Droptarget; +} + +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 type RenderMode = 'destructive' | 'gready'; + +export class GreadyRenderContainer { + private readonly map: Record< + string, + { + disposable: IDisposable; + } + > = {}; + + get allIds(): string[] { + return Object.keys(this.map); + } + + constructor(private readonly element: HTMLElement) { + // + } + + 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, + referenceContainer: IRenderable + ) { + if (!this.map[panel.api.id]) { + this.map[panel.api.id] = { disposable: Disposable.NONE }; + } + + this.map[panel.api.id]?.disposable.dispose(); + + if (panel.view.content.element.parentElement !== this.element) { + this.element.appendChild(panel.view.content.element); + } + + const resize = () => { + const box = getDomNodePagePosition(referenceContainer.element); + const box2 = getDomNodePagePosition(this.element); + 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`; + + toggleClass( + panel.view.content.element, + 'dv-render-overlay-float', + panel.group.api.isFloating + ); + }; + + const disposable = new CompositeDisposable( + /** + * dnd will not work as expected unless we explictly forward those events from the + * view to the underlying container + */ + new DragAndDropObserver(panel.view.content.element, { + onDragEnd: (e) => { + referenceContainer.dropTarget.dnd.onDragEnd(e); + }, + onDragEnter: (e) => { + referenceContainer.dropTarget.dnd.onDragEnter(e); + }, + onDragLeave: (e) => { + referenceContainer.dropTarget.dnd.onDragLeave(e); + }, + onDrop: (e) => { + referenceContainer.dropTarget.dnd.onDrop(e); + }, + onDragOver: (e) => { + referenceContainer.dropTarget.dnd.onDragOver(e); + }, + }), + panel.api.onDidVisibilityChange((event) => { + panel.view.content.element.style.display = event.isVisible + ? '' + : 'none'; + }), + panel.api.onDidDimensionsChange((event) => { + resize(); + }), + { + dispose: () => { + panel.view.content.element.style.display = ''; + toggleClass( + panel.view.content.element, + 'dv-render-overplay', + false + ); + }, + } + ); + + toggleClass(panel.view.content.element, 'dv-render-overlay', true); + + queueMicrotask(() => { + resize(); + }); + + this.map[panel.api.id].disposable = disposable; + } +} diff --git a/packages/dockview-core/src/dockview/components/panel/content.ts b/packages/dockview-core/src/dockview/components/panel/content.ts index 9934c5ec4..c284fc5c4 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; @@ -16,6 +24,7 @@ export interface IContentContainer extends IDisposable { closePanel: () => void; show(): void; hide(): void; + renderPanel(panel: IDockviewPanel): void; } export class ContentContainer @@ -36,7 +45,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 +63,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 { @@ -59,44 +118,92 @@ export class ContentContainer this.element.style.display = 'none'; } + renderPanel(panel: IDockviewPanel): void { + const isActive = panel === this.group.activePanel; + + switch (panel.api.renderMode) { + case 'destructive': + this.accessor.greadyRenderContainer.remove(panel); + if (isActive) { + if ( + this.panel && + this.panel.view.content.element.parentElement === + this._element + ) { + this._element.removeChild( + this.panel.view.content.element + ); + this._element.appendChild( + this.panel.view.content.element + ); + } + } + break; + case 'gready': + if ( + panel.view.content.element.parentElement === this._element + ) { + this._element.removeChild(panel.view.content.element); + } + this.accessor.greadyRenderContainer.setReferenceContentContainer( + panel, + this + ); + break; + } + } + public openPanel(panel: IDockviewPanel): void { if (this.panel === panel) { return; } - if (this.panel) { - if (this.panel.view?.content) { - this._element.removeChild(this.panel.view.content.element); - } - this.panel = undefined; + + const panelRenderMode = panel.api.renderMode; + + if ( + this.panel && + this.panel.view.content.element.parentElement === this._element + ) { + /** + * If the currently attached panel is mounted directly to the content then remove it + */ + this._element.removeChild(this.panel.view.content.element); } + this.panel = panel; const disposable = new CompositeDisposable(); - if (this.panel.view) { - const _onDidFocus = this.panel.view.content.onDidFocus; - const _onDidBlur = this.panel.view.content.onDidBlur; + const _onDidFocus = this.panel.view.content.onDidFocus; + const _onDidBlur = this.panel.view.content.onDidBlur; - const focusTracker = trackFocus(this._element); + const focusTracker = trackFocus(this._element); + disposable.addDisposables( + focusTracker, + focusTracker.onDidFocus(() => this._onDidFocus.fire()), + focusTracker.onDidBlur(() => this._onDidBlur.fire()) + ); + + if (_onDidFocus) { disposable.addDisposables( - focusTracker, - focusTracker.onDidFocus(() => this._onDidFocus.fire()), - focusTracker.onDidBlur(() => this._onDidBlur.fire()) + _onDidFocus(() => this._onDidFocus.fire()) ); + } + if (_onDidBlur) { + disposable.addDisposables(_onDidBlur(() => this._onDidBlur.fire())); + } - if (_onDidFocus) { - disposable.addDisposables( - _onDidFocus(() => this._onDidFocus.fire()) - ); - } - if (_onDidBlur) { - disposable.addDisposables( - _onDidBlur(() => this._onDidBlur.fire()) + switch (panelRenderMode) { + case 'gready': + this.accessor.greadyRenderContainer.setReferenceContentContainer( + panel, + this ); - } - - this._element.appendChild(this.panel.view.content.element); + break; + case 'destructive': + this._element.appendChild(this.panel.view.content.element); + break; } this.disposable.value = disposable; @@ -107,8 +214,10 @@ export class ContentContainer } public closePanel(): void { - if (this.panel?.view?.content?.element) { - this._element.removeChild(this.panel.view.content.element); + if (this.panel) { + 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/deserializer.ts b/packages/dockview-core/src/dockview/deserializer.ts index b8f308038..a92f7d600 100644 --- a/packages/dockview-core/src/dockview/deserializer.ts +++ b/packages/dockview-core/src/dockview/deserializer.ts @@ -52,7 +52,8 @@ export class DefaultDockviewDeserialzier implements IPanelDeserializer { this.layout, new DockviewApi(this.layout), group, - view + view, + { renderMode: this.layout.renderMode } ); panel.init({ diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index e7ab959c2..26b86b488 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -55,6 +55,10 @@ import { GroupDragEvent, TabDragEvent, } from './components/titlebar/tabsContainer'; +import { + GreadyRenderContainer, + RenderMode, +} from './components/greadyRenderContainer'; const DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE = 100; @@ -245,6 +249,8 @@ export class DockviewComponent private _options: Exclude; private watermark: IWatermarkRenderer | null = null; + readonly greadyRenderContainer: GreadyRenderContainer; + private readonly _onWillDragPanel = new Emitter(); readonly onWillDragPanel: Event = this._onWillDragPanel.event; @@ -299,6 +305,10 @@ export class DockviewComponent return activeGroup.activePanel; } + get renderMode(): RenderMode { + return this.options.renderMode ?? 'destructive'; + } + constructor(options: DockviewComponentOptions) { super({ proportionalLayout: true, @@ -308,7 +318,14 @@ export class DockviewComponent disableAutoResizing: options.disableAutoResizing, }); + const gready = document.createElement('div'); + gready.style.position = 'relative'; + this.gridview.element.appendChild(gready); + + this.greadyRenderContainer = new GreadyRenderContainer(gready); + toggleClass(this.gridview.element, 'dv-dockview', true); + toggleClass(this.element, 'dv-debug', !!options.debug); this.addDisposables( this._onWillDragPanel, @@ -427,6 +444,25 @@ export class DockviewComponent this.updateWatermark(); } + private resetRenderMode(): void { + /** + * First clear down the gready render container and then forcefully re-render everything + */ + + for (const id of this.greadyRenderContainer.allIds) { + const group = this.getPanel(id); + if (group) { + for (const panel of group.panels) { + this.greadyRenderContainer.remove(panel); + } + } + } + + for (const group of this.groups) { + // group.re; + } + } + addFloatingGroup( item: DockviewPanel | DockviewGroupPanel, coord?: { x?: number; y?: number; height?: number; width?: number }, @@ -1041,6 +1077,7 @@ export class DockviewComponent group.model.removePanel(panel); if (!options.skipDispose) { + this.greadyRenderContainer.remove(panel); panel.dispose(); } @@ -1463,8 +1500,10 @@ export class DockviewComponent this, this._api, group, - view + view, + { renderMode: this.renderMode } ); + panel.init({ title: options.title ?? options.id, params: options?.params ?? {}, diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index 43ee03b01..d009ee070 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, @@ -416,6 +416,10 @@ export class DockviewGroupPanelModel } } + rerender(panel: IDockviewPanel): void { + this.contentContainer.renderPanel(panel); + } + public indexOf(panel: IDockviewPanel): number { return this.tabsContainer.indexOf(panel.id); } @@ -687,15 +691,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 +853,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/dockviewPanel.ts b/packages/dockview-core/src/dockview/dockviewPanel.ts index 080abf1cf..1f7c3e54d 100644 --- a/packages/dockview-core/src/dockview/dockviewPanel.ts +++ b/packages/dockview-core/src/dockview/dockviewPanel.ts @@ -9,6 +9,7 @@ import { CompositeDisposable, IDisposable } from '../lifecycle'; import { IPanel, PanelUpdateEvent, Parameters } from '../panel/types'; import { IDockviewPanelModel } from './dockviewPanelModel'; import { DockviewComponent } from './dockviewComponent'; +import { RenderMode } from './components/greadyRenderContainer'; export interface IDockviewPanel extends IDisposable, IPanel { readonly view: IDockviewPanelModel; @@ -33,6 +34,8 @@ export class DockviewPanel private _title: string | undefined; + private _renderMode: RenderMode; + get params(): Parameters | undefined { return this._params; } @@ -45,14 +48,20 @@ export class DockviewPanel return this._group; } + get renderMode(): RenderMode { + return this._renderMode; + } + constructor( public readonly id: string, accessor: DockviewComponent, private readonly containerApi: DockviewApi, group: DockviewGroupPanel, - readonly view: IDockviewPanelModel + readonly view: IDockviewPanelModel, + options: { renderMode: RenderMode } ) { super(); + this._renderMode = options.renderMode; this._group = group; this.api = new DockviewPanelApiImpl(this, this._group, accessor); @@ -65,6 +74,9 @@ export class DockviewPanel // forward the resize event to the group since if you want to resize a panel // you are actually just resizing the panels parent which is the group this.group.api.setSize(event); + }), + this.api.onDidRenderModeChange((event) => { + this.group.model.rerender(this); }) ); } @@ -114,6 +126,14 @@ export class DockviewPanel } } + setRenderMode(renderMode: RenderMode): void { + const didChange = renderMode !== this.renderMode; + + if (didChange) { + this.api._onDidRenderModeChange.fire({ renderMode }); + } + } + public update(event: PanelUpdateEvent): void { // merge the new parameters with the existing parameters this._params = { diff --git a/packages/dockview-core/src/dockview/dockviewPanelModel.ts b/packages/dockview-core/src/dockview/dockviewPanelModel.ts index b264ae663..84824cae2 100644 --- a/packages/dockview-core/src/dockview/dockviewPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewPanelModel.ts @@ -40,7 +40,7 @@ export class DockviewPanelModel implements IDockviewPanelModel { private readonly accessor: IDockviewComponent, private readonly id: string, readonly contentComponent: string, - readonly tabComponent?: string + readonly tabComponent: string | undefined ) { this._content = this.createContentComponent(this.id, contentComponent); this._tab = this.createTabComponent(this.id, tabComponent); diff --git a/packages/dockview-core/src/dockview/options.ts b/packages/dockview-core/src/dockview/options.ts index 8bd23025a..314af8fb9 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -96,6 +96,8 @@ export interface DockviewComponentOptions extends DockviewRenderFunctions { minimumHeightWithinViewport?: number; minimumWidthWithinViewport?: number; }; + renderMode?: 'gready' | 'destructive'; + debug?: boolean; } export interface PanelOptions

{ diff --git a/packages/dockview-core/src/index.ts b/packages/dockview-core/src/index.ts index e85e335c0..c0b5a79a1 100644 --- a/packages/dockview-core/src/index.ts +++ b/packages/dockview-core/src/index.ts @@ -49,6 +49,8 @@ export * from './splitview/splitviewPanel'; export * from './paneview/paneviewPanel'; export * from './dockview/types'; +export { RenderMode } from './dockview/components/greadyRenderContainer'; + export { Position, positionToDirection, 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;