diff --git a/packages/dockview/src/__tests__/groupview/groupview.spec.ts b/packages/dockview/src/__tests__/groupview/groupview.spec.ts index 838d7affa..15a6bea24 100644 --- a/packages/dockview/src/__tests__/groupview/groupview.spec.ts +++ b/packages/dockview/src/__tests__/groupview/groupview.spec.ts @@ -16,16 +16,14 @@ import { GroupOptions, Groupview, } from '../../groupview/groupview'; -import { - DockviewPanelApi, - DockviewPanelApiImpl, -} from '../../api/groupPanelApi'; +import { DockviewPanelApi } from '../../api/groupPanelApi'; import { DefaultGroupPanelView, IGroupPanelView, } from '../../dockview/defaultGroupPanelView'; import { GroupPanel } from '../../groupview/groupviewPanel'; -import { DockviewApi } from '../../api/component.api'; +import { fireEvent } from '@testing-library/dom'; +import { LocalSelectionTransfer, PanelTransfer } from '../../dnd/dataTransfer'; class Watermark implements IWatermarkRenderer { public readonly element = document.createElement('div'); @@ -134,7 +132,7 @@ class TestHeaderPart implements ITabRenderer { } } -class TestPanel implements IDockviewPanel { +export class TestPanel implements IDockviewPanel { private _view: IGroupPanelView | undefined; private _group: GroupPanel | undefined; private _params: IGroupPanelInitParameters; @@ -148,7 +146,7 @@ class TestPanel implements IDockviewPanel { } get group() { - return this._group; + return this._group!; } get view() { @@ -544,7 +542,7 @@ describe('groupview', () => { ); const contentContainer = groupviewContainer .getElementsByClassName('content-container') - .item(0).childNodes; + .item(0)!.childNodes; const panel1 = new TestPanel('id_1', null); @@ -568,4 +566,246 @@ describe('groupview', () => { expect(contentContainer.length).toBe(1); expect(contentContainer.item(0)).toBe(panel3.view.content.element); }); + + test('that should not show drop target is external event', () => { + const accessorMock = jest.fn, []>(() => { + return { + id: 'testcomponentid', + options: { + showDndOverlay: jest.fn(), + }, + getPanel: jest.fn(), + }; + }); + const accessor = new accessorMock() as DockviewComponent; + const groupviewMock = jest.fn, []>(() => { + return { + canDisplayOverlay: jest.fn(), + }; + }); + + const groupView = new groupviewMock() as Groupview; + + const groupPanelMock = jest.fn, []>(() => { + return { + id: 'testgroupid', + model: groupView, + }; + }); + + const container = document.createElement('div'); + const cut = new Groupview( + container, + accessor, + 'groupviewid', + {}, + new groupPanelMock() as GroupPanel + ); + + const element = container + .getElementsByClassName('content-container') + .item(0)!; + + jest.spyOn(element, 'clientHeight', 'get').mockImplementation( + () => 100 + ); + jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100); + + fireEvent.dragEnter(element); + fireEvent.dragOver(element); + + expect(accessor.options.showDndOverlay).toBeCalledTimes(1); + + expect( + element.getElementsByClassName('drop-target-dropzone').length + ).toBe(0); + }); + + test('that should not show drop target if dropping on self', () => { + const accessorMock = jest.fn, []>(() => { + return { + id: 'testcomponentid', + options: { + showDndOverlay: jest.fn(), + }, + getPanel: jest.fn(), + doSetGroupActive: jest.fn(), + }; + }); + const accessor = new accessorMock() as DockviewComponent; + const groupviewMock = jest.fn, []>(() => { + return { + canDisplayOverlay: jest.fn(), + }; + }); + + const groupView = new groupviewMock() as Groupview; + + const groupPanelMock = jest.fn, []>(() => { + return { + id: 'testgroupid', + model: groupView, + }; + }); + + const container = document.createElement('div'); + const cut = new Groupview( + container, + accessor, + 'groupviewid', + {}, + new groupPanelMock() as GroupPanel + ); + + cut.openPanel(new TestPanel('panel1', jest.fn() as any)); + + const element = container + .getElementsByClassName('content-container') + .item(0)!; + + jest.spyOn(element, 'clientHeight', 'get').mockImplementation( + () => 100 + ); + jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100); + + LocalSelectionTransfer.getInstance().setData( + [new PanelTransfer('testcomponentid', 'groupviewid', 'panel1')], + PanelTransfer.prototype + ); + + fireEvent.dragEnter(element); + fireEvent.dragOver(element); + + expect(accessor.options.showDndOverlay).toBeCalledTimes(0); + + expect( + element.getElementsByClassName('drop-target-dropzone').length + ).toBe(0); + }); + + test('that should allow drop when not dropping on self for same component id', () => { + const accessorMock = jest.fn, []>(() => { + return { + id: 'testcomponentid', + options: { + showDndOverlay: jest.fn(), + }, + getPanel: jest.fn(), + doSetGroupActive: jest.fn(), + }; + }); + const accessor = new accessorMock() as DockviewComponent; + const groupviewMock = jest.fn, []>(() => { + return { + canDisplayOverlay: jest.fn(), + }; + }); + + const groupView = new groupviewMock() as Groupview; + + const groupPanelMock = jest.fn, []>(() => { + return { + id: 'testgroupid', + model: groupView, + }; + }); + + const container = document.createElement('div'); + const cut = new Groupview( + container, + accessor, + 'groupviewid', + {}, + new groupPanelMock() as GroupPanel + ); + + cut.openPanel(new TestPanel('panel1', jest.fn() as any)); + cut.openPanel(new TestPanel('panel2', jest.fn() as any)); + + const element = container + .getElementsByClassName('content-container') + .item(0)!; + + jest.spyOn(element, 'clientHeight', 'get').mockImplementation( + () => 100 + ); + jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100); + + LocalSelectionTransfer.getInstance().setData( + [new PanelTransfer('testcomponentid', 'groupviewid', 'panel1')], + PanelTransfer.prototype + ); + + fireEvent.dragEnter(element); + fireEvent.dragOver(element); + + expect(accessor.options.showDndOverlay).toBeCalledTimes(0); + + expect( + element.getElementsByClassName('drop-target-dropzone').length + ).toBe(1); + }); + + test('that should not allow drop when not dropping for different component id', () => { + const accessorMock = jest.fn, []>(() => { + return { + id: 'testcomponentid', + options: { + showDndOverlay: jest.fn(), + }, + getPanel: jest.fn(), + doSetGroupActive: jest.fn(), + }; + }); + const accessor = new accessorMock() as DockviewComponent; + const groupviewMock = jest.fn, []>(() => { + return { + canDisplayOverlay: jest.fn(), + }; + }); + + const groupView = new groupviewMock() as Groupview; + + const groupPanelMock = jest.fn, []>(() => { + return { + id: 'testgroupid', + model: groupView, + }; + }); + + const container = document.createElement('div'); + const cut = new Groupview( + container, + accessor, + 'groupviewid', + {}, + new groupPanelMock() as GroupPanel + ); + + cut.openPanel(new TestPanel('panel1', jest.fn() as any)); + cut.openPanel(new TestPanel('panel2', jest.fn() as any)); + + const element = container + .getElementsByClassName('content-container') + .item(0)!; + + jest.spyOn(element, 'clientHeight', 'get').mockImplementation( + () => 100 + ); + jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100); + + LocalSelectionTransfer.getInstance().setData( + [new PanelTransfer('anothercomponentid', 'groupviewid', 'panel1')], + PanelTransfer.prototype + ); + + fireEvent.dragEnter(element); + fireEvent.dragOver(element); + + expect(accessor.options.showDndOverlay).toBeCalledTimes(1); + + expect( + element.getElementsByClassName('drop-target-dropzone').length + ).toBe(0); + }); }); diff --git a/packages/dockview/src/__tests__/groupview/tab.spec.ts b/packages/dockview/src/__tests__/groupview/tab.spec.ts index a1e3c762e..abe02fcf9 100644 --- a/packages/dockview/src/__tests__/groupview/tab.spec.ts +++ b/packages/dockview/src/__tests__/groupview/tab.spec.ts @@ -1,3 +1,8 @@ +import { fireEvent } from '@testing-library/dom'; +import { LocalSelectionTransfer, PanelTransfer } from '../../dnd/dataTransfer'; +import { DockviewComponent } from '../../dockview/dockviewComponent'; +import { Groupview } from '../../groupview/groupview'; +import { GroupPanel } from '../../groupview/groupviewPanel'; import { Tab } from '../../groupview/tab'; describe('tab', () => { @@ -22,4 +27,251 @@ describe('tab', () => { cut.setActive(false); expect(cut.element.className).toBe('tab inactive-tab'); }); + + test('that an external event does not render a drop target and calls through to the group model', () => { + const accessorMock = jest.fn, []>(() => { + return { + id: 'testcomponentid', + }; + }); + const groupviewMock = jest.fn, []>(() => { + return { + canDisplayOverlay: jest.fn(), + }; + }); + + const groupView = new groupviewMock() as Groupview; + + const groupPanelMock = jest.fn, []>(() => { + return { + id: 'testgroupid', + model: groupView, + }; + }); + + const accessor = new accessorMock() as DockviewComponent; + const groupPanel = new groupPanelMock() as GroupPanel; + + const cut = new Tab('panelId', accessor, groupPanel); + + jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation( + () => 100 + ); + jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation( + () => 100 + ); + + fireEvent.dragEnter(cut.element); + fireEvent.dragOver(cut.element); + + expect(groupView.canDisplayOverlay).toBeCalled(); + + expect( + cut.element.getElementsByClassName('drop-target-dropzone').length + ).toBe(0); + }); + + test('that if you drag over yourself no drop target is shown', () => { + const accessorMock = jest.fn, []>(() => { + return { + id: 'testcomponentid', + }; + }); + const groupviewMock = jest.fn, []>(() => { + return { + canDisplayOverlay: jest.fn(), + }; + }); + + const groupView = new groupviewMock() as Groupview; + + const groupPanelMock = jest.fn, []>(() => { + return { + id: 'testgroupid', + model: groupView, + }; + }); + + const accessor = new accessorMock() as DockviewComponent; + const groupPanel = new groupPanelMock() as GroupPanel; + + const cut = new Tab('panel1', accessor, groupPanel); + + jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation( + () => 100 + ); + jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation( + () => 100 + ); + + LocalSelectionTransfer.getInstance().setData( + [new PanelTransfer('testcomponentid', 'anothergroupid', 'panel1')], + PanelTransfer.prototype + ); + + fireEvent.dragEnter(cut.element); + fireEvent.dragOver(cut.element); + + expect(groupView.canDisplayOverlay).toBeCalledTimes(0); + + expect( + cut.element.getElementsByClassName('drop-target-dropzone').length + ).toBe(0); + }); + + test('that if you drag over another tab a drop target is shown', () => { + const accessorMock = jest.fn, []>(() => { + return { + id: 'testcomponentid', + }; + }); + const groupviewMock = jest.fn, []>(() => { + return { + canDisplayOverlay: jest.fn(), + }; + }); + + const groupView = new groupviewMock() as Groupview; + + const groupPanelMock = jest.fn, []>(() => { + return { + id: 'testgroupid', + model: groupView, + }; + }); + + const accessor = new accessorMock() as DockviewComponent; + const groupPanel = new groupPanelMock() as GroupPanel; + + const cut = new Tab('panel1', accessor, groupPanel); + + jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation( + () => 100 + ); + jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation( + () => 100 + ); + + LocalSelectionTransfer.getInstance().setData( + [new PanelTransfer('testcomponentid', 'anothergroupid', 'panel2')], + PanelTransfer.prototype + ); + + fireEvent.dragEnter(cut.element); + fireEvent.dragOver(cut.element); + + expect(groupView.canDisplayOverlay).toBeCalledTimes(0); + + expect( + cut.element.getElementsByClassName('drop-target-dropzone').length + ).toBe(1); + }); + + test('that dropping on a tab with the same id but from a different component should not render a drop over and call through to the group model', () => { + const accessorMock = jest.fn, []>(() => { + return { + id: 'testcomponentid', + }; + }); + const groupviewMock = jest.fn, []>(() => { + return { + canDisplayOverlay: jest.fn(), + }; + }); + + const groupView = new groupviewMock() as Groupview; + + const groupPanelMock = jest.fn, []>(() => { + return { + id: 'testgroupid', + model: groupView, + }; + }); + + const accessor = new accessorMock() as DockviewComponent; + const groupPanel = new groupPanelMock() as GroupPanel; + + const cut = new Tab('panel1', accessor, groupPanel); + + jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation( + () => 100 + ); + jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation( + () => 100 + ); + + LocalSelectionTransfer.getInstance().setData( + [ + new PanelTransfer( + 'anothercomponentid', + 'anothergroupid', + 'panel1' + ), + ], + PanelTransfer.prototype + ); + + fireEvent.dragEnter(cut.element); + fireEvent.dragOver(cut.element); + + expect(groupView.canDisplayOverlay).toBeCalledTimes(1); + + expect( + cut.element.getElementsByClassName('drop-target-dropzone').length + ).toBe(0); + }); + + test('that dropping on a tab from a different component should not render a drop over and call through to the group model', () => { + const accessorMock = jest.fn, []>(() => { + return { + id: 'testcomponentid', + }; + }); + const groupviewMock = jest.fn, []>(() => { + return { + canDisplayOverlay: jest.fn(), + }; + }); + + const groupView = new groupviewMock() as Groupview; + + const groupPanelMock = jest.fn, []>(() => { + return { + id: 'testgroupid', + model: groupView, + }; + }); + + const accessor = new accessorMock() as DockviewComponent; + const groupPanel = new groupPanelMock() as GroupPanel; + + const cut = new Tab('panel1', accessor, groupPanel); + + jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation( + () => 100 + ); + jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation( + () => 100 + ); + + LocalSelectionTransfer.getInstance().setData( + [ + new PanelTransfer( + 'anothercomponentid', + 'anothergroupid', + 'panel2' + ), + ], + PanelTransfer.prototype + ); + + fireEvent.dragEnter(cut.element); + fireEvent.dragOver(cut.element); + + expect(groupView.canDisplayOverlay).toBeCalledTimes(1); + + expect( + cut.element.getElementsByClassName('drop-target-dropzone').length + ).toBe(0); + }); }); diff --git a/packages/dockview/src/__tests__/groupview/titlebar/tabsContainer.spec.ts b/packages/dockview/src/__tests__/groupview/titlebar/tabsContainer.spec.ts new file mode 100644 index 000000000..ab1dc25f2 --- /dev/null +++ b/packages/dockview/src/__tests__/groupview/titlebar/tabsContainer.spec.ts @@ -0,0 +1,305 @@ +import { DockviewComponent } from '../../../dockview/dockviewComponent'; +import { GroupPanel } from '../../../groupview/groupviewPanel'; +import { TabsContainer } from '../../../groupview/titlebar/tabsContainer'; +import { fireEvent } from '@testing-library/dom'; +import { Groupview } from '../../../groupview/groupview'; +import { + LocalSelectionTransfer, + PanelTransfer, +} from '../../../dnd/dataTransfer'; +import { TestPanel } from '../groupview.spec'; + +describe('tabsContainer', () => { + test('that an external event does not render a drop target and calls through to the group mode', () => { + const accessorMock = jest.fn, []>(() => { + return {}; + }); + const groupviewMock = jest.fn, []>(() => { + return { + canDisplayOverlay: jest.fn(), + }; + }); + + const groupView = new groupviewMock() as Groupview; + + const groupPanelMock = jest.fn, []>(() => { + return { + model: groupView, + }; + }); + + const accessor = new accessorMock() as DockviewComponent; + const groupPanel = new groupPanelMock() as GroupPanel; + + const cut = new TabsContainer(accessor, groupPanel, {}); + + const emptySpace = cut.element + .getElementsByClassName('void-container') + .item(0); + + if (!emptySpace) { + fail('element not found'); + } + + jest.spyOn(emptySpace, 'clientHeight', 'get').mockImplementation( + () => 100 + ); + jest.spyOn(emptySpace, 'clientWidth', 'get').mockImplementation( + () => 100 + ); + + fireEvent.dragEnter(emptySpace); + fireEvent.dragOver(emptySpace); + + expect(groupView.canDisplayOverlay).toBeCalled(); + + expect( + cut.element.getElementsByClassName('drop-target-dropzone').length + ).toBe(0); + }); + + test('that a drag over event from another tab should render a drop target', () => { + const accessorMock = jest.fn, []>(() => { + return { + id: 'testcomponentid', + }; + }); + const groupviewMock = jest.fn, []>(() => { + return { + canDisplayOverlay: jest.fn(), + }; + }); + + const groupView = new groupviewMock() as Groupview; + + const groupPanelMock = jest.fn, []>(() => { + return { + id: 'testgroupid', + model: groupView, + }; + }); + + const accessor = new accessorMock() as DockviewComponent; + const groupPanel = new groupPanelMock() as GroupPanel; + + const cut = new TabsContainer(accessor, groupPanel, {}); + + const emptySpace = cut.element + .getElementsByClassName('void-container') + .item(0); + + if (!emptySpace) { + fail('element not found'); + } + + jest.spyOn(emptySpace, 'clientHeight', 'get').mockImplementation( + () => 100 + ); + jest.spyOn(emptySpace, 'clientWidth', 'get').mockImplementation( + () => 100 + ); + + LocalSelectionTransfer.getInstance().setData( + [ + new PanelTransfer( + 'testcomponentid', + 'anothergroupid', + 'anotherpanelid' + ), + ], + PanelTransfer.prototype + ); + + fireEvent.dragEnter(emptySpace); + fireEvent.dragOver(emptySpace); + + expect(groupView.canDisplayOverlay).toBeCalledTimes(0); + + expect( + cut.element.getElementsByClassName('drop-target-dropzone').length + ).toBe(1); + }); + + test('that dropping the last tab should render no drop target', () => { + const accessorMock = jest.fn, []>(() => { + return { + id: 'testcomponentid', + }; + }); + const groupviewMock = jest.fn, []>(() => { + return { + canDisplayOverlay: jest.fn(), + }; + }); + + const groupView = new groupviewMock() as Groupview; + + const groupPanelMock = jest.fn, []>(() => { + return { + id: 'testgroupid', + model: groupView, + }; + }); + + const accessor = new accessorMock() as DockviewComponent; + const groupPanel = new groupPanelMock() as GroupPanel; + + const cut = new TabsContainer(accessor, groupPanel, {}); + + cut.openPanel(new TestPanel('panel1', jest.fn() as any)); + cut.openPanel(new TestPanel('panel2', jest.fn() as any)); + + const emptySpace = cut.element + .getElementsByClassName('void-container') + .item(0); + + if (!emptySpace) { + fail('element not found'); + } + + jest.spyOn(emptySpace, 'clientHeight', 'get').mockImplementation( + () => 100 + ); + jest.spyOn(emptySpace, 'clientWidth', 'get').mockImplementation( + () => 100 + ); + + LocalSelectionTransfer.getInstance().setData( + [new PanelTransfer('testcomponentid', 'anothergroupid', 'panel2')], + PanelTransfer.prototype + ); + + fireEvent.dragEnter(emptySpace); + fireEvent.dragOver(emptySpace); + + expect(groupView.canDisplayOverlay).toBeCalledTimes(0); + + expect( + cut.element.getElementsByClassName('drop-target-dropzone').length + ).toBe(0); + }); + + test('that dropping the first tab should render a drop target', () => { + const accessorMock = jest.fn, []>(() => { + return { + id: 'testcomponentid', + }; + }); + const groupviewMock = jest.fn, []>(() => { + return { + canDisplayOverlay: jest.fn(), + }; + }); + + const groupView = new groupviewMock() as Groupview; + + const groupPanelMock = jest.fn, []>(() => { + return { + id: 'testgroupid', + model: groupView, + }; + }); + + const accessor = new accessorMock() as DockviewComponent; + const groupPanel = new groupPanelMock() as GroupPanel; + + const cut = new TabsContainer(accessor, groupPanel, {}); + + cut.openPanel(new TestPanel('panel1', jest.fn() as any)); + cut.openPanel(new TestPanel('panel2', jest.fn() as any)); + + const emptySpace = cut.element + .getElementsByClassName('void-container') + .item(0); + + if (!emptySpace) { + fail('element not found'); + } + + jest.spyOn(emptySpace, 'clientHeight', 'get').mockImplementation( + () => 100 + ); + jest.spyOn(emptySpace, 'clientWidth', 'get').mockImplementation( + () => 100 + ); + + LocalSelectionTransfer.getInstance().setData( + [new PanelTransfer('testcomponentid', 'anothergroupid', 'panel1')], + PanelTransfer.prototype + ); + + fireEvent.dragEnter(emptySpace); + fireEvent.dragOver(emptySpace); + + expect(groupView.canDisplayOverlay).toBeCalledTimes(0); + + expect( + cut.element.getElementsByClassName('drop-target-dropzone').length + ).toBe(1); + }); + + test('that dropping a tab from another component should not render a drop target', () => { + const accessorMock = jest.fn, []>(() => { + return { + id: 'testcomponentid', + }; + }); + const groupviewMock = jest.fn, []>(() => { + return { + canDisplayOverlay: jest.fn(), + }; + }); + + const groupView = new groupviewMock() as Groupview; + + const groupPanelMock = jest.fn, []>(() => { + return { + id: 'testgroupid', + model: groupView, + }; + }); + + const accessor = new accessorMock() as DockviewComponent; + const groupPanel = new groupPanelMock() as GroupPanel; + + const cut = new TabsContainer(accessor, groupPanel, {}); + + cut.openPanel(new TestPanel('panel1', jest.fn() as any)); + cut.openPanel(new TestPanel('panel2', jest.fn() as any)); + + const emptySpace = cut.element + .getElementsByClassName('void-container') + .item(0); + + if (!emptySpace) { + fail('element not found'); + } + + jest.spyOn(emptySpace, 'clientHeight', 'get').mockImplementation( + () => 100 + ); + jest.spyOn(emptySpace, 'clientWidth', 'get').mockImplementation( + () => 100 + ); + + LocalSelectionTransfer.getInstance().setData( + [ + new PanelTransfer( + 'anothercomponentid', + 'anothergroupid', + 'panel1' + ), + ], + PanelTransfer.prototype + ); + + fireEvent.dragEnter(emptySpace); + fireEvent.dragOver(emptySpace); + + expect(groupView.canDisplayOverlay).toBeCalledTimes(1); + + expect( + cut.element.getElementsByClassName('drop-target-dropzone').length + ).toBe(0); + }); +}); diff --git a/packages/dockview/src/dockview/options.ts b/packages/dockview/src/dockview/options.ts index a46f6bd23..c36e77b24 100644 --- a/packages/dockview/src/dockview/options.ts +++ b/packages/dockview/src/dockview/options.ts @@ -12,6 +12,7 @@ import { GroupPanel } from '../groupview/groupviewPanel'; import { ISplitviewStyles, Orientation } from '../splitview/core/splitview'; import { FrameworkFactory } from '../types'; import { DockviewDropTargets } from '../groupview/dnd'; +import { PanelTransfer } from '../dnd/dataTransfer'; export interface GroupPanelFrameworkComponentFactory { content: FrameworkFactory; @@ -53,6 +54,7 @@ export interface DockviewDndOverlayEvent { nativeEvent: DragEvent; target: DockviewDropTargets; group: GroupPanel; + getData: () => PanelTransfer | undefined; } export interface DockviewComponentOptions extends DockviewRenderFunctions { diff --git a/packages/dockview/src/groupview/groupview.ts b/packages/dockview/src/groupview/groupview.ts index 4a7e09028..ce8c474f0 100644 --- a/packages/dockview/src/groupview/groupview.ts +++ b/packages/dockview/src/groupview/groupview.ts @@ -247,7 +247,7 @@ export class Groupview extends CompositeDisposable implements IGroupview { const data = getPanelData(); - if (data) { + if (data && data.viewId === this.accessor.id) { const groupHasOnePanelAndIsActiveDragElement = this._panels.length === 1 && data.groupId === this.id; @@ -670,6 +670,7 @@ export class Groupview extends CompositeDisposable implements IGroupview { nativeEvent: event, target, group: this.accessor.getPanel(this.id)!, + getData: getPanelData, }); } return false; diff --git a/packages/dockview/src/groupview/tab.ts b/packages/dockview/src/groupview/tab.ts index 62bfa3085..1dc002eba 100644 --- a/packages/dockview/src/groupview/tab.ts +++ b/packages/dockview/src/groupview/tab.ts @@ -52,7 +52,7 @@ export class Tab extends CompositeDisposable implements ITab { constructor( public readonly panelId: string, - accessor: IDockviewComponent, + private readonly accessor: IDockviewComponent, private readonly group: GroupPanel ) { super(); @@ -119,7 +119,7 @@ export class Tab extends CompositeDisposable implements ITab { validOverlays: 'none', canDisplayOverlay: (event) => { const data = getPanelData(); - if (data) { + if (data && this.accessor.id === data.viewId) { return this.panelId !== data.panelId; } diff --git a/packages/dockview/src/groupview/titlebar/tabsContainer.ts b/packages/dockview/src/groupview/titlebar/tabsContainer.ts index 19428d36d..24c6eac70 100644 --- a/packages/dockview/src/groupview/titlebar/tabsContainer.ts +++ b/packages/dockview/src/groupview/titlebar/tabsContainer.ts @@ -169,7 +169,7 @@ export class TabsContainer canDisplayOverlay: (event) => { const data = getPanelData(); - if (data) { + if (data && this.accessor.id === data.viewId) { // don't show the overlay if the tab being dragged is the last panel of this group return last(this.tabs)?.value.panelId !== data.panelId; } diff --git a/packages/dockview/src/index.ts b/packages/dockview/src/index.ts index ce1b14f44..1e57c384f 100644 --- a/packages/dockview/src/index.ts +++ b/packages/dockview/src/index.ts @@ -16,6 +16,7 @@ export * from './dockview/dockviewComponent'; export * from './gridview/gridviewComponent'; export * from './splitview/splitviewComponent'; export * from './paneview/paneviewComponent'; +export { PaneviewComponentOptions } from './paneview/options'; export * from './gridview/gridviewPanel'; export * from './splitview/splitviewPanel'; diff --git a/packages/dockview/src/paneview/draggablePaneviewPanel.ts b/packages/dockview/src/paneview/draggablePaneviewPanel.ts index ff3309eec..af58dcd8e 100644 --- a/packages/dockview/src/paneview/draggablePaneviewPanel.ts +++ b/packages/dockview/src/paneview/draggablePaneviewPanel.ts @@ -8,6 +8,7 @@ import { Droptarget, DroptargetEvent, Position } from '../dnd/droptarget'; import { Emitter } from '../events'; import { IDisposable } from '../lifecycle'; import { Orientation } from '../splitview/core/splitview'; +import { IPaneviewComponent } from './paneviewComponent'; import { IPaneviewPanel, PanePanelInitParameter, @@ -27,6 +28,7 @@ export abstract class DraggablePaneviewPanel extends PaneviewPanel { readonly onDidDrop = this._onDidDrop.event; constructor( + private readonly accessor: IPaneviewComponent, id: string, component: string, headerComponent: string | undefined, @@ -47,12 +49,13 @@ export abstract class DraggablePaneviewPanel extends PaneviewPanel { } const id = this.id; + const accessorId = this.accessor.id; this.header.draggable = true; this.handler = new (class PaneDragHandler extends DragHandler { getData(): IDisposable { LocalSelectionTransfer.getInstance().setData( - [new PaneTransfer('paneview', id)], + [new PaneTransfer(accessorId, id)], PaneTransfer.prototype ); @@ -68,14 +71,27 @@ export abstract class DraggablePaneviewPanel extends PaneviewPanel { this.target = new Droptarget(this.element, { validOverlays: 'vertical', - canDisplayOverlay: () => { + canDisplayOverlay: (event) => { const data = getPaneData(); - if (!data) { - return true; + if (data) { + if ( + data.paneId !== this.id && + data.viewId === this.accessor.id + ) { + return true; + } } - return data.paneId !== this.id; + if (this.accessor.options.showDndOverlay) { + return this.accessor.options.showDndOverlay({ + nativeEvent: event, + getData: getPaneData, + panel: this, + }); + } + + return false; }, }); @@ -92,11 +108,13 @@ export abstract class DraggablePaneviewPanel extends PaneviewPanel { private onDrop(event: DroptargetEvent) { const data = getPaneData(); - if (!data) { + if (!data || data.viewId !== this.accessor.id) { + // if there is no local drag event for this panel + // or if the drag event was creating by another Paneview instance this._onDidDrop.fire({ ...event, panel: this, - getData: () => getPaneData(), + getData: getPaneData, }); return; } @@ -107,10 +125,11 @@ export abstract class DraggablePaneviewPanel extends PaneviewPanel { const existingPanel = containerApi.getPanel(panelId); if (!existingPanel) { + // if the panel doesn't exist this._onDidDrop.fire({ ...event, panel: this, - getData: () => getPaneData(), + getData: getPaneData, }); return; } diff --git a/packages/dockview/src/paneview/options.ts b/packages/dockview/src/paneview/options.ts index 285cc58fa..72d4e0be7 100644 --- a/packages/dockview/src/paneview/options.ts +++ b/packages/dockview/src/paneview/options.ts @@ -1,4 +1,5 @@ import { FrameworkFactory } from '../types'; +import { PaneviewDndOverlayEvent } from './paneviewComponent'; import { IPaneBodyPart, IPaneHeaderPart, PaneviewPanel } from './paneviewPanel'; export interface PaneviewComponentOptions { @@ -23,4 +24,5 @@ export interface PaneviewComponentOptions { body: FrameworkFactory; }; disableDnd?: boolean; + showDndOverlay?: (event: PaneviewDndOverlayEvent) => boolean; } diff --git a/packages/dockview/src/paneview/paneviewComponent.ts b/packages/dockview/src/paneview/paneviewComponent.ts index 9e9eafcd4..d0995b3a6 100644 --- a/packages/dockview/src/paneview/paneviewComponent.ts +++ b/packages/dockview/src/paneview/paneviewComponent.ts @@ -24,6 +24,16 @@ import { PaneviewDropEvent2, } from './draggablePaneviewPanel'; import { DefaultHeader } from './defaultPaneviewHeader'; +import { sequentialNumberGenerator } from '../math'; +import { PaneTransfer } from '../dnd/dataTransfer'; + +const nextLayoutId = sequentialNumberGenerator(); + +export interface PaneviewDndOverlayEvent { + nativeEvent: DragEvent; + panel: IPaneviewPanel; + getData: () => PaneTransfer | undefined; +} export interface SerializedPaneviewPanel { snap?: boolean; @@ -57,9 +67,11 @@ export class PaneFramework extends DraggablePaneviewPanel { orientation: Orientation; isExpanded: boolean; disableDnd: boolean; + accessor: IPaneviewComponent; } ) { super( + options.accessor, options.id, options.component, options.headerComponent, @@ -94,11 +106,13 @@ export interface AddPaneviewComponentOptions { } export interface IPaneviewComponent extends IDisposable { + readonly id: string; readonly width: number; readonly height: number; readonly minimumSize: number; readonly maximumSize: number; readonly panels: IPaneviewPanel[]; + readonly options: PaneviewComponentOptions; readonly onDidAddView: Event; readonly onDidRemoveView: Event; readonly onDidDrop: Event; @@ -120,6 +134,8 @@ export class PaneviewComponent extends CompositeDisposable implements IPaneviewComponent { + private readonly _id = nextLayoutId.next(); + private _options: PaneviewComponentOptions; private _disposable = new MutableDisposable(); private _viewDisposables = new Map(); private _paneview!: Paneview; @@ -139,6 +155,10 @@ export class PaneviewComponent private readonly _onDidRemoveView = new Emitter(); readonly onDidRemoveView = this._onDidRemoveView.event; + get id(): string { + return this._id; + } + get panels(): PaneviewPanel[] { return this.paneview.getPanes(); } @@ -179,9 +199,7 @@ export class PaneviewComponent : this.paneview.orthogonalSize; } - private _options: PaneviewComponentOptions; - - get options() { + get options(): PaneviewComponentOptions { return this._options; } @@ -267,6 +285,7 @@ export class PaneviewComponent orientation: Orientation.VERTICAL, isExpanded: !!options.isExpanded, disableDnd: !!this.options.disableDnd, + accessor: this, }); this.doAddPanel(view); @@ -400,6 +419,7 @@ export class PaneviewComponent orientation: Orientation.VERTICAL, isExpanded: !!view.expanded, disableDnd: !!this.options.disableDnd, + accessor: this, }); this.doAddPanel(panel); diff --git a/packages/dockview/src/react/dockview/dockview.tsx b/packages/dockview/src/react/dockview/dockview.tsx index 85ca922e5..a0d738674 100644 --- a/packages/dockview/src/react/dockview/dockview.tsx +++ b/packages/dockview/src/react/dockview/dockview.tsx @@ -137,6 +137,7 @@ export const DockviewReact = React.forwardRef( styles: props.hideBorders ? { separatorBorder: 'transparent' } : undefined, + showDndOverlay: props.showDndOverlay, }); domRef.current?.appendChild(dockview.element); diff --git a/packages/dockview/src/react/paneview/paneview.tsx b/packages/dockview/src/react/paneview/paneview.tsx index f57809a77..8dbba197f 100644 --- a/packages/dockview/src/react/paneview/paneview.tsx +++ b/packages/dockview/src/react/paneview/paneview.tsx @@ -3,6 +3,7 @@ import { PaneviewPanelApi } from '../../api/paneviewPanelApi'; import { PaneviewComponent, IPaneviewComponent, + PaneviewDndOverlayEvent, } from '../../paneview/paneviewComponent'; import { usePortalsLifecycle } from '../react'; import { PaneviewApi } from '../../api/component.api'; @@ -33,6 +34,7 @@ export interface IPaneviewReactProps { className?: string; disableAutoResizing?: boolean; disableDnd?: boolean; + showDndOverlay?: (event: PaneviewDndOverlayEvent) => boolean; onDidDrop?(event: PaneviewDropEvent): void; } @@ -85,6 +87,7 @@ export const PaneviewReact = React.forwardRef( createComponent, }, }, + showDndOverlay: props.showDndOverlay, }); const api = new PaneviewApi(paneview); @@ -144,6 +147,15 @@ export const PaneviewReact = React.forwardRef( }; }, [props.onDidDrop]); + React.useEffect(() => { + if (!paneviewRef.current) { + return; + } + paneviewRef.current.updateOptions({ + showDndOverlay: props.showDndOverlay, + }); + }, [props.showDndOverlay]); + return (