diff --git a/packages/dockview-core/src/__tests__/dnd/abstractDragHandler.spec.ts b/packages/dockview-core/src/__tests__/dnd/abstractDragHandler.spec.ts index 312916ae5..df57caa66 100644 --- a/packages/dockview-core/src/__tests__/dnd/abstractDragHandler.spec.ts +++ b/packages/dockview-core/src/__tests__/dnd/abstractDragHandler.spec.ts @@ -118,4 +118,62 @@ describe('abstractDragHandler', () => { expect(webview.style.pointerEvents).toBe('auto'); expect(span.style.pointerEvents).toBeFalsy(); }); + + test('that .preventDefault() is called for cancelled events', () => { + const element = document.createElement('div'); + + const handler = new (class TestClass extends DragHandler { + constructor(el: HTMLElement) { + super(el); + } + + protected isCancelled(_event: DragEvent): boolean { + return true; + } + + getData(): IDisposable { + return { + dispose: () => { + // / + }, + }; + } + })(element); + + const event = new Event('dragstart'); + const spy = jest.spyOn(event, 'preventDefault'); + fireEvent(element, event); + expect(spy).toBeCalledTimes(1); + + handler.dispose(); + }); + + test('that .preventDefault() is not called for non-cancelled events', () => { + const element = document.createElement('div'); + + const handler = new (class TestClass extends DragHandler { + constructor(el: HTMLElement) { + super(el); + } + + protected isCancelled(_event: DragEvent): boolean { + return false; + } + + getData(): IDisposable { + return { + dispose: () => { + // / + }, + }; + } + })(element); + + const event = new Event('dragstart'); + const spy = jest.spyOn(event, 'preventDefault'); + fireEvent(element, event); + expect(spy).toBeCalledTimes(0); + + handler.dispose(); + }); }); diff --git a/packages/dockview-core/src/__tests__/dnd/droptarget.spec.ts b/packages/dockview-core/src/__tests__/dnd/droptarget.spec.ts index 5875637ec..32de9729e 100644 --- a/packages/dockview-core/src/__tests__/dnd/droptarget.spec.ts +++ b/packages/dockview-core/src/__tests__/dnd/droptarget.spec.ts @@ -34,6 +34,48 @@ describe('droptarget', () => { jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 200); }); + test('that dragover events are marked', () => { + droptarget = new Droptarget(element, { + canDisplayOverlay: () => true, + acceptedTargetZones: ['center'], + }); + + fireEvent.dragEnter(element); + const event = new Event('dragover'); + fireEvent(element, event); + + expect( + (event as any)['__dockview_droptarget_event_is_used__'] + ).toBeTruthy(); + }); + + test('that the drop target is removed when receiving a marked dragover event', () => { + let position: Position | undefined = undefined; + + droptarget = new Droptarget(element, { + canDisplayOverlay: () => true, + acceptedTargetZones: ['center'], + }); + + droptarget.onDrop((event) => { + position = event.position; + }); + + fireEvent.dragEnter(element); + fireEvent.dragOver(element); + + const target = element.querySelector( + '.drop-target-dropzone' + ) as HTMLElement; + fireEvent.drop(target); + expect(position).toBe('center'); + + const event = new Event('dragover'); + (event as any)['__dockview_droptarget_event_is_used__'] = true; + fireEvent(element, event); + expect(element.querySelector('.drop-target-dropzone')).toBeNull(); + }); + test('directionToPosition', () => { expect(directionToPosition('above')).toBe('top'); expect(directionToPosition('below')).toBe('bottom'); diff --git a/packages/dockview-core/src/__tests__/dnd/groupDragHandler.spec.ts b/packages/dockview-core/src/__tests__/dnd/groupDragHandler.spec.ts new file mode 100644 index 000000000..36497e5de --- /dev/null +++ b/packages/dockview-core/src/__tests__/dnd/groupDragHandler.spec.ts @@ -0,0 +1,101 @@ +import { fireEvent } from '@testing-library/dom'; +import { GroupDragHandler } from '../../dnd/groupDragHandler'; +import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel'; +import { LocalSelectionTransfer, PanelTransfer } from '../../dnd/dataTransfer'; + +describe('groupDragHandler', () => { + test('that the dnd transfer object is setup and torndown', () => { + const element = document.createElement('div'); + + const groupMock = jest.fn(() => { + const partial: Partial = { + id: 'test_group_id', + isFloating: false, + }; + return partial as DockviewGroupPanel; + }); + const group = new groupMock(); + + const cut = new GroupDragHandler(element, 'test_accessor_id', group); + + fireEvent.dragStart(element, new Event('dragstart')); + + expect( + LocalSelectionTransfer.getInstance().hasData( + PanelTransfer.prototype + ) + ).toBeTruthy(); + const transferObject = + LocalSelectionTransfer.getInstance().getData( + PanelTransfer.prototype + )![0]; + expect(transferObject).toBeTruthy(); + expect(transferObject.viewId).toBe('test_accessor_id'); + expect(transferObject.groupId).toBe('test_group_id'); + expect(transferObject.panelId).toBeNull(); + + fireEvent.dragStart(element, new Event('dragend')); + expect( + LocalSelectionTransfer.getInstance().hasData( + PanelTransfer.prototype + ) + ).toBeFalsy(); + + cut.dispose(); + }); + test('that the event is cancelled when isFloating and shiftKey=true', () => { + const element = document.createElement('div'); + + const groupMock = jest.fn(() => { + const partial: Partial = { + isFloating: true, + }; + return partial as DockviewGroupPanel; + }); + const group = new groupMock(); + + const cut = new GroupDragHandler(element, 'accessor_id', group); + + const event = new KeyboardEvent('dragstart', { shiftKey: false }); + + const spy = jest.spyOn(event, 'preventDefault'); + fireEvent(element, event); + expect(spy).toBeCalledTimes(1); + + const event2 = new KeyboardEvent('dragstart', { shiftKey: true }); + + const spy2 = jest.spyOn(event2, 'preventDefault'); + fireEvent(element, event); + expect(spy2).toBeCalledTimes(0); + + cut.dispose(); + }); + + test('that the event is never cancelled when the group is not floating', () => { + const element = document.createElement('div'); + + const groupMock = jest.fn(() => { + const partial: Partial = { + isFloating: false, + }; + return partial as DockviewGroupPanel; + }); + const group = new groupMock(); + + const cut = new GroupDragHandler(element, 'accessor_id', group); + + const event = new KeyboardEvent('dragstart', { shiftKey: false }); + + const spy = jest.spyOn(event, 'preventDefault'); + fireEvent(element, event); + expect(spy).toBeCalledTimes(0); + + const event2 = new KeyboardEvent('dragstart', { shiftKey: true }); + + const spy2 = jest.spyOn(event2, 'preventDefault'); + fireEvent(element, event); + expect(spy2).toBeCalledTimes(0); + + cut.dispose(); + }); +}); diff --git a/packages/dockview-core/src/__tests__/dnd/overlay.spec.ts b/packages/dockview-core/src/__tests__/dnd/overlay.spec.ts new file mode 100644 index 000000000..c2cd8df2f --- /dev/null +++ b/packages/dockview-core/src/__tests__/dnd/overlay.spec.ts @@ -0,0 +1,78 @@ +import { Overlay } from '../../dnd/overlay'; + +describe('overlay', () => { + test('toJSON', () => { + const container = document.createElement('div'); + const content = document.createElement('div'); + + document.body.appendChild(container); + container.appendChild(content); + + const cut = new Overlay({ + height: 200, + width: 100, + left: 10, + top: 20, + minX: 0, + minY: 0, + container, + content, + }); + + jest.spyOn( + container.childNodes.item(0) as HTMLElement, + 'getBoundingClientRect' + ).mockImplementation(() => { + return { left: 80, top: 100, width: 40, height: 50 } as any; + }); + jest.spyOn(container, 'getBoundingClientRect').mockImplementation( + () => { + return { left: 20, top: 30, width: 100, height: 100 } as any; + } + ); + + expect(cut.toJSON()).toEqual({ + top: 70, + left: 60, + width: 40, + height: 50, + }); + }); + + test('that the resize handles are added', () => { + const container = document.createElement('div'); + const content = document.createElement('div'); + + const cut = new Overlay({ + height: 500, + width: 500, + left: 100, + top: 200, + minX: 0, + minY: 0, + container, + content, + }); + + expect(container.querySelector('.dv-resize-handle-top')).toBeTruthy(); + expect( + container.querySelector('.dv-resize-handle-bottom') + ).toBeTruthy(); + expect(container.querySelector('.dv-resize-handle-left')).toBeTruthy(); + expect(container.querySelector('.dv-resize-handle-right')).toBeTruthy(); + expect( + container.querySelector('.dv-resize-handle-topleft') + ).toBeTruthy(); + expect( + container.querySelector('.dv-resize-handle-topright') + ).toBeTruthy(); + expect( + container.querySelector('.dv-resize-handle-bottomleft') + ).toBeTruthy(); + expect( + container.querySelector('.dv-resize-handle-bottomright') + ).toBeTruthy(); + + cut.dispose(); + }); +}); diff --git a/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts b/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts index 77efaca4c..7384dc100 100644 --- a/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts @@ -8,6 +8,7 @@ import { DockviewGroupPanel } from '../../../../dockview/dockviewGroupPanel'; import { DockviewGroupPanelModel } from '../../../../dockview/dockviewGroupPanelModel'; import { fireEvent } from '@testing-library/dom'; import { TestPanel } from '../../dockviewGroupPanelModel.spec'; +import { IDockviewPanel } from '../../../../dockview/dockviewPanel'; describe('tabsContainer', () => { test('that an external event does not render a drop target and calls through to the group mode', () => { @@ -463,4 +464,165 @@ describe('tabsContainer', () => { expect(query.length).toBe(1); expect(query[0].children.length).toBe(0); }); + + test('that a tab will become floating when clicked if not floating and shift is selected', () => { + const accessorMock = jest.fn(() => { + return (>{ + options: {}, + onDidAddPanel: jest.fn(), + onDidRemovePanel: jest.fn(), + element: document.createElement('div'), + addFloatingGroup: jest.fn(), + }) as DockviewComponent; + }); + + const groupPanelMock = jest.fn(() => { + return (>{ + isFloating: false, + }) as DockviewGroupPanel; + }); + + const accessor = new accessorMock(); + const groupPanel = new groupPanelMock(); + + const cut = new TabsContainer(accessor, groupPanel); + + const container = cut.element.querySelector('.void-container')!; + expect(container).toBeTruthy(); + + jest.spyOn(cut.element, 'getBoundingClientRect').mockImplementation( + () => { + return { top: 50, left: 100, width: 0, height: 0 } as any; + } + ); + jest.spyOn( + accessor.element, + 'getBoundingClientRect' + ).mockImplementation(() => { + return { top: 10, left: 20, width: 0, height: 0 } as any; + }); + + const event = new KeyboardEvent('mousedown', { shiftKey: true }); + const eventPreventDefaultSpy = jest.spyOn(event, 'preventDefault'); + fireEvent(container, event); + + expect(accessor.addFloatingGroup).toBeCalledWith(groupPanel, { + x: 100, + y: 60, + }); + expect(accessor.addFloatingGroup).toBeCalledTimes(1); + expect(eventPreventDefaultSpy).toBeCalledTimes(1); + + const event2 = new KeyboardEvent('mousedown', { shiftKey: false }); + const eventPreventDefaultSpy2 = jest.spyOn(event2, 'preventDefault'); + fireEvent(container, event2); + + expect(accessor.addFloatingGroup).toBeCalledTimes(1); + expect(eventPreventDefaultSpy2).toBeCalledTimes(0); + }); + + test('that a tab that is already floating cannot be floated again', () => { + const accessorMock = jest.fn(() => { + return (>{ + options: {}, + onDidAddPanel: jest.fn(), + onDidRemovePanel: jest.fn(), + element: document.createElement('div'), + addFloatingGroup: jest.fn(), + }) as DockviewComponent; + }); + + const groupPanelMock = jest.fn(() => { + return (>{ + isFloating: true, + }) as DockviewGroupPanel; + }); + + const accessor = new accessorMock(); + const groupPanel = new groupPanelMock(); + + const cut = new TabsContainer(accessor, groupPanel); + + const container = cut.element.querySelector('.void-container')!; + expect(container).toBeTruthy(); + + jest.spyOn(cut.element, 'getBoundingClientRect').mockImplementation( + () => { + return { top: 50, left: 100, width: 0, height: 0 } as any; + } + ); + jest.spyOn( + accessor.element, + 'getBoundingClientRect' + ).mockImplementation(() => { + return { top: 10, left: 20, width: 0, height: 0 } as any; + }); + + const event = new KeyboardEvent('mousedown', { shiftKey: true }); + const eventPreventDefaultSpy = jest.spyOn(event, 'preventDefault'); + fireEvent(container, event); + + expect(accessor.addFloatingGroup).toBeCalledTimes(0); + expect(eventPreventDefaultSpy).toBeCalledTimes(0); + + const event2 = new KeyboardEvent('mousedown', { shiftKey: false }); + const eventPreventDefaultSpy2 = jest.spyOn(event2, 'preventDefault'); + fireEvent(container, event2); + + expect(accessor.addFloatingGroup).toBeCalledTimes(0); + expect(eventPreventDefaultSpy2).toBeCalledTimes(0); + }); + + test('that selecting a tab which shift down will move that tab into a new floating group', () => { + const accessorMock = jest.fn(() => { + return (>{ + options: {}, + onDidAddPanel: jest.fn(), + onDidRemovePanel: jest.fn(), + element: document.createElement('div'), + addFloatingGroup: jest.fn(), + getGroupPanel: jest.fn(), + }) as DockviewComponent; + }); + + const groupPanelMock = jest.fn(() => { + return (>{ + isFloating: true, + }) as DockviewGroupPanel; + }); + + const accessor = new accessorMock(); + const groupPanel = new groupPanelMock(); + + const cut = new TabsContainer(accessor, groupPanel); + + const panelMock = jest.fn(() => { + const partial: Partial = { + id: 'test_id', + + view: { + tab: { + element: document.createElement('div'), + } as any, + content: { + element: document.createElement('div'), + } as any, + } as any, + }; + return partial as IDockviewPanel; + }); + const panel = new panelMock(); + + cut.openPanel(panel); + + const el = cut.element.querySelector('.tab')!; + expect(el).toBeTruthy(); + + const event = new KeyboardEvent('mousedown', { shiftKey: true }); + const preventDefaultSpy = jest.spyOn(event, 'preventDefault'); + fireEvent(el, event); + + expect(preventDefaultSpy).toBeCalledTimes(1); + expect(accessor.addFloatingGroup).toBeCalledTimes(1); + }); }); diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts index 15461e713..2dbb20bf1 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -2647,4 +2647,992 @@ describe('dockviewComponent', () => { dockview.removePanel(panel); expect(dockview.groups.length).toBe(0); }); + + test('floating: move a floating group of one tab to a new fixed group', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + + dockview.layout(1000, 500); + + const panel1 = dockview.addPanel({ + id: 'panel_1', + component: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel_2', + component: 'default', + floating: true, + }); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(2); + expect(dockview.panels.length).toBe(2); + + dockview.moveGroupOrPanel( + panel1.group, + panel2.group.id, + undefined, + 'right' + ); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeFalsy(); + expect(dockview.groups.length).toBe(2); + expect(dockview.panels.length).toBe(2); + }); + + test('floating: move a floating group of one tab to an existing fixed group', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + + dockview.layout(1000, 500); + + const panel1 = dockview.addPanel({ + id: 'panel_1', + component: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel_2', + component: 'default', + floating: true, + }); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(2); + expect(dockview.panels.length).toBe(2); + + dockview.moveGroupOrPanel( + panel1.group, + panel2.group.id, + undefined, + 'center' + ); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeFalsy(); + expect(dockview.groups.length).toBe(1); + expect(dockview.panels.length).toBe(2); + }); + + test('floating: move a floating group of one tab to an existing floating group', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + + dockview.layout(1000, 500); + + const panel1 = dockview.addPanel({ + id: 'panel_1', + component: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel_2', + component: 'default', + floating: true, + }); + + const panel3 = dockview.addPanel({ + id: 'panel_3', + component: 'default', + floating: true, + }); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeTruthy(); + expect(panel3.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(3); + expect(dockview.panels.length).toBe(3); + + dockview.moveGroupOrPanel( + panel2.group, + panel3.group.id, + undefined, + 'center' + ); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeTruthy(); + expect(panel3.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(2); + expect(dockview.panels.length).toBe(3); + }); + + test('floating: move a floating group of many tabs to a new fixed group', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + + dockview.layout(1000, 500); + + const panel1 = dockview.addPanel({ + id: 'panel_1', + component: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel_2', + component: 'default', + floating: true, + }); + + const panel3 = dockview.addPanel({ + id: 'panel_3', + component: 'default', + position: { referencePanel: panel2 }, + }); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeTruthy(); + expect(panel3.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(2); + expect(dockview.panels.length).toBe(3); + + dockview.moveGroupOrPanel( + panel1.group, + panel2.group.id, + undefined, + 'right' + ); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeFalsy(); + expect(panel3.group.model.isFloating).toBeFalsy(); + expect(dockview.groups.length).toBe(2); + expect(dockview.panels.length).toBe(3); + }); + + test('floating: move a floating group of many tabs to an existing fixed group', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + + dockview.layout(1000, 500); + + const panel1 = dockview.addPanel({ + id: 'panel_1', + component: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel_2', + component: 'default', + floating: true, + }); + + const panel3 = dockview.addPanel({ + id: 'panel_3', + component: 'default', + position: { referencePanel: panel2 }, + }); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeTruthy(); + expect(panel3.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(2); + expect(dockview.panels.length).toBe(3); + + dockview.moveGroupOrPanel( + panel1.group, + panel2.group.id, + undefined, + 'center' + ); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeFalsy(); + expect(panel3.group.model.isFloating).toBeFalsy(); + expect(dockview.groups.length).toBe(1); + expect(dockview.panels.length).toBe(3); + }); + + test('floating: move a floating group of many tabs to an existing floating group', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + + dockview.layout(1000, 500); + + const panel1 = dockview.addPanel({ + id: 'panel_1', + component: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel_2', + component: 'default', + floating: true, + }); + + const panel3 = dockview.addPanel({ + id: 'panel_3', + component: 'default', + position: { referencePanel: panel2 }, + }); + + const panel4 = dockview.addPanel({ + id: 'panel_4', + component: 'default', + floating: true, + }); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeTruthy(); + expect(panel3.group.model.isFloating).toBeTruthy(); + expect(panel4.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(3); + expect(dockview.panels.length).toBe(4); + + dockview.moveGroupOrPanel( + panel4.group, + panel2.group.id, + undefined, + 'center' + ); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeTruthy(); + expect(panel3.group.model.isFloating).toBeTruthy(); + expect(panel4.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(2); + expect(dockview.panels.length).toBe(4); + }); + + test('floating: move a floating tab of one tab to a new fixed group', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + + dockview.layout(1000, 500); + + const panel1 = dockview.addPanel({ + id: 'panel_1', + component: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel_2', + component: 'default', + floating: true, + }); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(2); + expect(dockview.panels.length).toBe(2); + + dockview.moveGroupOrPanel( + panel1.group, + panel2.group.id, + panel2.id, + 'right' + ); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeFalsy(); + expect(dockview.groups.length).toBe(2); + expect(dockview.panels.length).toBe(2); + }); + + test('floating: move a floating tab of one tab to an existing fixed group', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + + dockview.layout(1000, 500); + + const panel1 = dockview.addPanel({ + id: 'panel_1', + component: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel_2', + component: 'default', + floating: true, + }); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(2); + expect(dockview.panels.length).toBe(2); + + dockview.moveGroupOrPanel( + panel1.group, + panel2.group.id, + panel2.id, + 'center' + ); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeFalsy(); + expect(dockview.groups.length).toBe(1); + expect(dockview.panels.length).toBe(2); + }); + + test('floating: move a floating tab of one tab to an existing floating group', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + + dockview.layout(1000, 500); + + const panel1 = dockview.addPanel({ + id: 'panel_1', + component: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel_2', + component: 'default', + floating: true, + }); + + const panel3 = dockview.addPanel({ + id: 'panel_3', + component: 'default', + floating: true, + }); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeTruthy(); + expect(panel3.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(3); + expect(dockview.panels.length).toBe(3); + + dockview.moveGroupOrPanel( + panel2.group, + panel3.group.id, + panel3.id, + 'center' + ); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeTruthy(); + expect(panel3.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(2); + expect(dockview.panels.length).toBe(3); + }); + + test('floating: move a floating tab of many tabs to a new fixed group', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + + dockview.layout(1000, 500); + + const panel1 = dockview.addPanel({ + id: 'panel_1', + component: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel_2', + component: 'default', + floating: true, + }); + + const panel3 = dockview.addPanel({ + id: 'panel_3', + component: 'default', + position: { referencePanel: panel2 }, + }); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeTruthy(); + expect(panel3.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(2); + expect(dockview.panels.length).toBe(3); + + dockview.moveGroupOrPanel( + panel1.group, + panel2.group.id, + panel2.id, + 'right' + ); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeFalsy(); + expect(panel3.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(3); + expect(dockview.panels.length).toBe(3); + }); + + test('floating: move a floating tab of many tabs to an existing fixed group', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + + dockview.layout(1000, 500); + + const panel1 = dockview.addPanel({ + id: 'panel_1', + component: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel_2', + component: 'default', + floating: true, + }); + + const panel3 = dockview.addPanel({ + id: 'panel_3', + component: 'default', + position: { referencePanel: panel2 }, + }); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeTruthy(); + expect(panel3.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(2); + expect(dockview.panels.length).toBe(3); + + dockview.moveGroupOrPanel( + panel1.group, + panel2.group.id, + panel2.id, + 'center' + ); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeFalsy(); + expect(panel3.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(2); + expect(dockview.panels.length).toBe(3); + }); + + test('floating: move a floating tab of many tabs to an existing floating group', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + + dockview.layout(1000, 500); + + const panel1 = dockview.addPanel({ + id: 'panel_1', + component: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel_2', + component: 'default', + floating: true, + }); + + const panel3 = dockview.addPanel({ + id: 'panel_3', + component: 'default', + position: { referencePanel: panel2 }, + }); + + const panel4 = dockview.addPanel({ + id: 'panel_4', + component: 'default', + floating: true, + }); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeTruthy(); + expect(panel3.group.model.isFloating).toBeTruthy(); + expect(panel4.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(3); + expect(dockview.panels.length).toBe(4); + + dockview.moveGroupOrPanel( + panel4.group, + panel2.group.id, + panel2.id, + 'center' + ); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeTruthy(); + expect(panel3.group.model.isFloating).toBeTruthy(); + expect(panel4.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(3); + expect(dockview.panels.length).toBe(4); + }); + + test('floating: move a fixed tab of one tab to an existing floating group', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + + dockview.layout(1000, 500); + + const panel1 = dockview.addPanel({ + id: 'panel_1', + component: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel_2', + component: 'default', + position: { direction: 'right' }, + }); + + const panel3 = dockview.addPanel({ + id: 'panel_3', + component: 'default', + floating: true, + }); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeFalsy(); + expect(panel3.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(3); + expect(dockview.panels.length).toBe(3); + + dockview.moveGroupOrPanel( + panel3.group, + panel1.group.id, + panel1.id, + 'center' + ); + + expect(panel1.group.model.isFloating).toBeTruthy(); + expect(panel2.group.model.isFloating).toBeFalsy(); + expect(panel3.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(2); + expect(dockview.panels.length).toBe(3); + }); + + test('floating: move a fixed tab of many tabs to an existing floating group', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + + dockview.layout(1000, 500); + + const panel1 = dockview.addPanel({ + id: 'panel_1', + component: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel_2', + component: 'default', + }); + + const panel3 = dockview.addPanel({ + id: 'panel_3', + component: 'default', + floating: true, + }); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeFalsy(); + expect(panel3.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(2); + expect(dockview.panels.length).toBe(3); + + dockview.moveGroupOrPanel( + panel3.group, + panel1.group.id, + panel1.id, + 'center' + ); + + expect(panel1.group.model.isFloating).toBeTruthy(); + expect(panel2.group.model.isFloating).toBeFalsy(); + expect(panel3.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(2); + expect(dockview.panels.length).toBe(3); + }); + + test('floating: move a fixed group of one tab to an existing floating group', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + + dockview.layout(1000, 500); + + const panel1 = dockview.addPanel({ + id: 'panel_1', + component: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel_2', + component: 'default', + position: { direction: 'right' }, + }); + + const panel3 = dockview.addPanel({ + id: 'panel_3', + component: 'default', + floating: true, + }); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeFalsy(); + expect(panel3.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(3); + expect(dockview.panels.length).toBe(3); + + dockview.moveGroupOrPanel( + panel3.group, + panel1.group.id, + undefined, + 'center' + ); + + expect(panel1.group.model.isFloating).toBeTruthy(); + expect(panel2.group.model.isFloating).toBeFalsy(); + expect(panel3.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(2); + expect(dockview.panels.length).toBe(3); + }); + + test('floating: move a fixed group of many tabs to an existing floating group', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + + dockview.layout(1000, 500); + + const panel1 = dockview.addPanel({ + id: 'panel_1', + component: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel_2', + component: 'default', + }); + + const panel3 = dockview.addPanel({ + id: 'panel_3', + component: 'default', + floating: true, + }); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeFalsy(); + expect(panel3.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(2); + expect(dockview.panels.length).toBe(3); + + dockview.moveGroupOrPanel( + panel3.group, + panel1.group.id, + undefined, + 'center' + ); + + expect(panel1.group.model.isFloating).toBeTruthy(); + expect(panel2.group.model.isFloating).toBeTruthy(); + expect(panel3.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(1); + expect(dockview.panels.length).toBe(3); + }); + + test('floating: move a fixed tab of one tab to a new floating group', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + + dockview.layout(1000, 500); + + const panel1 = dockview.addPanel({ + id: 'panel_1', + component: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel_2', + component: 'default', + position: { direction: 'right' }, + }); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeFalsy(); + expect(dockview.groups.length).toBe(2); + expect(dockview.panels.length).toBe(2); + + dockview.addFloatingGroup(panel2); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(2); + expect(dockview.panels.length).toBe(2); + }); + + test('floating: move a fixed tab of many tabs to a new floating group', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + + dockview.layout(1000, 500); + + const panel1 = dockview.addPanel({ + id: 'panel_1', + component: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel_2', + component: 'default', + }); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeFalsy(); + expect(dockview.groups.length).toBe(1); + expect(dockview.panels.length).toBe(2); + + dockview.addFloatingGroup(panel2); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(2); + expect(dockview.panels.length).toBe(2); + }); + + test('floating: move a fixed group of one tab to a new floating group', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + + dockview.layout(1000, 500); + + const panel1 = dockview.addPanel({ + id: 'panel_1', + component: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel_2', + component: 'default', + position: { direction: 'right' }, + }); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeFalsy(); + expect(dockview.groups.length).toBe(2); + expect(dockview.panels.length).toBe(2); + + dockview.addFloatingGroup(panel2.group); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(2); + expect(dockview.panels.length).toBe(2); + }); + + test('floating: move a fixed group of many tabs to a new floating group', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + + dockview.layout(1000, 500); + + const panel1 = dockview.addPanel({ + id: 'panel_1', + component: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel_2', + component: 'default', + }); + + expect(panel1.group.model.isFloating).toBeFalsy(); + expect(panel2.group.model.isFloating).toBeFalsy(); + expect(dockview.groups.length).toBe(1); + expect(dockview.panels.length).toBe(2); + + dockview.addFloatingGroup(panel2.group); + + expect(panel1.group.model.isFloating).toBeTruthy(); + expect(panel2.group.model.isFloating).toBeTruthy(); + expect(dockview.groups.length).toBe(1); + expect(dockview.panels.length).toBe(2); + }); }); diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts index 249167341..5d5657b80 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts @@ -247,8 +247,8 @@ describe('groupview', () => { id: 'dockview-1', removePanel: removePanelMock, removeGroup: removeGroupMock, - onDidAddPanel: jest.fn(), - onDidRemovePanel: jest.fn(), + onDidAddPanel: () => ({ dispose: jest.fn() }), + onDidRemovePanel: () => ({ dispose: jest.fn() }), }) as DockviewComponent; groupview = new DockviewGroupPanel(dockview, 'groupview-1', options); @@ -858,6 +858,47 @@ describe('groupview', () => { ).toBe(0); }); + test('that the watermark is removed when dispose is called', () => { + const groupviewMock = jest.fn, []>( + () => { + return { + canDisplayOverlay: jest.fn(), + }; + } + ); + + const groupView = new groupviewMock() as DockviewGroupPanelModel; + + const groupPanelMock = jest.fn, []>(() => { + return { + id: 'testgroupid', + model: groupView, + }; + }); + + const container = document.createElement('div'); + + const cut = new DockviewGroupPanelModel( + container, + dockview, + 'groupviewid', + {}, + new groupPanelMock() as DockviewGroupPanel + ); + + cut.initialize(); + + expect( + container.getElementsByClassName('watermark-test-container').length + ).toBe(1); + + cut.dispose(); + + expect( + container.getElementsByClassName('watermark-test-container').length + ).toBe(0); + }); + test('that watermark is added', () => { const groupviewMock = jest.fn, []>( () => { diff --git a/packages/dockview-core/src/dnd/droptarget.scss b/packages/dockview-core/src/dnd/droptarget.scss index 5a17b75f2..2767c2c24 100644 --- a/packages/dockview-core/src/dnd/droptarget.scss +++ b/packages/dockview-core/src/dnd/droptarget.scss @@ -7,7 +7,7 @@ top: 0px; height: 100%; width: 100%; - z-index: 10000; + z-index: 1000; pointer-events: none; > .drop-target-selection { diff --git a/packages/dockview-core/src/dnd/droptarget.ts b/packages/dockview-core/src/dnd/droptarget.ts index 7bccf6481..1aafbf425 100644 --- a/packages/dockview-core/src/dnd/droptarget.ts +++ b/packages/dockview-core/src/dnd/droptarget.ts @@ -54,17 +54,6 @@ export type CanDisplayOverlay = | boolean | ((dragEvent: DragEvent, state: Position) => boolean); -const eventMarkTag = 'dv_droptarget_marked'; - -function markEvent(event: DragEvent): void { - (event as any)[eventMarkTag] = true; -} - -function isEventMarked(event: DragEvent) { - const value = (event as any)[eventMarkTag]; - return typeof value === 'boolean' && value; -} - export class Droptarget extends CompositeDisposable { private targetElement: HTMLElement | undefined; private overlayElement: HTMLElement | undefined; @@ -74,6 +63,8 @@ export class Droptarget extends CompositeDisposable { private readonly _onDrop = new Emitter(); readonly onDrop: Event = this._onDrop.event; + private static USED_EVENT_ID = '__dockview_droptarget_event_is_used__'; + get state(): Position | undefined { return this._state; } @@ -125,7 +116,12 @@ export class Droptarget extends CompositeDisposable { height ); - if (isEventMarked(e) || quadrant === null) { + /** + * 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; @@ -139,7 +135,7 @@ export class Droptarget extends CompositeDisposable { return; } - markEvent(e); + this.markAsUsed(e); if (!this.targetElement) { this.targetElement = document.createElement('div'); @@ -193,11 +189,26 @@ export class Droptarget extends CompositeDisposable { this._acceptedTargetZonesSet = new Set(acceptedTargetZones); } - public dispose(): void { + dispose(): void { this.removeDropTarget(); super.dispose(); } + /** + * Add a property to the event object for other potential listeners to check + */ + private markAsUsed(event: DragEvent): void { + (event as any)[Droptarget.USED_EVENT_ID] = true; + } + + /** + * Check is the event has already been used by another instance od DropTarget + */ + private isAlreadyUsed(event: DragEvent): boolean { + const value = (event as any)[Droptarget.USED_EVENT_ID]; + return typeof value === 'boolean' && value; + } + private toggleClasses( quadrant: Position, width: number, diff --git a/packages/dockview-core/src/dnd/groupDragHandler.ts b/packages/dockview-core/src/dnd/groupDragHandler.ts index 08be61781..12b2667fd 100644 --- a/packages/dockview-core/src/dnd/groupDragHandler.ts +++ b/packages/dockview-core/src/dnd/groupDragHandler.ts @@ -17,7 +17,7 @@ export class GroupDragHandler extends DragHandler { } override isCancelled(_event: DragEvent): boolean { - if (this.group.model.isFloating && !_event.shiftKey) { + if (this.group.isFloating && !_event.shiftKey) { return true; } return false; diff --git a/packages/dockview-core/src/dnd/overlay.scss b/packages/dockview-core/src/dnd/overlay.scss index 92ceb5044..5f95b379a 100644 --- a/packages/dockview-core/src/dnd/overlay.scss +++ b/packages/dockview-core/src/dnd/overlay.scss @@ -27,10 +27,10 @@ .dv-resize-container { position: absolute; - z-index: 9997; + z-index: 997; - &.dv-resize-container-priority { - z-index: 9998; + &.dv-bring-to-front { + z-index: 998; } border: 1px solid var(--dv-tab-divider-color); @@ -45,7 +45,7 @@ width: calc(100% - 8px); left: 4px; top: -2px; - z-index: 9999; + z-index: 999; position: absolute; cursor: ns-resize; } @@ -55,7 +55,7 @@ width: calc(100% - 8px); left: 4px; bottom: -2px; - z-index: 9999; + z-index: 999; position: absolute; cursor: ns-resize; } @@ -65,7 +65,7 @@ width: 4px; left: -2px; top: 4px; - z-index: 9999; + z-index: 999; position: absolute; cursor: ew-resize; } @@ -75,7 +75,7 @@ width: 4px; right: -2px; top: 4px; - z-index: 9999; + z-index: 999; position: absolute; cursor: ew-resize; } @@ -85,7 +85,7 @@ width: 4px; top: -2px; left: -2px; - z-index: 9999; + z-index: 999; position: absolute; cursor: nw-resize; } @@ -95,7 +95,7 @@ width: 4px; right: -2px; top: -2px; - z-index: 9999; + z-index: 999; position: absolute; cursor: ne-resize; } @@ -105,7 +105,7 @@ width: 4px; left: -2px; bottom: -2px; - z-index: 9999; + z-index: 999; position: absolute; cursor: sw-resize; } @@ -115,7 +115,7 @@ width: 4px; right: -2px; bottom: -2px; - z-index: 9999; + z-index: 999; position: absolute; cursor: se-resize; } diff --git a/packages/dockview-core/src/dnd/overlay.ts b/packages/dockview-core/src/dnd/overlay.ts index c4d58b454..819178ea2 100644 --- a/packages/dockview-core/src/dnd/overlay.ts +++ b/packages/dockview-core/src/dnd/overlay.ts @@ -13,10 +13,10 @@ const bringElementToFront = (() => { function pushToTop(element: HTMLElement) { if (previous !== element && previous !== null) { - toggleClass(previous, 'dv-resize-container-priority', false); + toggleClass(previous, 'dv-bring-to-front', false); } - toggleClass(element, 'dv-resize-container-priority', true); + toggleClass(element, 'dv-bring-to-front', true); previous = element; } @@ -46,7 +46,6 @@ export class Overlay extends CompositeDisposable { this.addDisposables(this._onDidChange); this.setupOverlay(); - // this.setupDrag(true,this._element); this.setupResize('top'); this.setupResize('bottom'); this.setupResize('left'); @@ -58,8 +57,6 @@ export class Overlay extends CompositeDisposable { this._element.appendChild(this.options.content); this.options.container.appendChild(this._element); - - // this.renderWithinBoundaryConditions(); } toJSON(): { top: number; left: number; height: number; width: number } { @@ -250,11 +247,14 @@ export class Overlay extends CompositeDisposable { this._element.style.width = `${this.options.width}px`; this._element.style.left = `${this.options.left}px`; this._element.style.top = `${this.options.top}px`; - // + this._element.className = 'dv-resize-container'; } - setupDrag(connect: boolean, dragTarget: HTMLElement): void { + setupDrag( + dragTarget: HTMLElement, + options: { inDragMode: boolean } = { inDragMode: false } + ): void { const move = new MutableDisposable(); const track = () => { @@ -327,10 +327,7 @@ export class Overlay extends CompositeDisposable { this.addDisposables( move, addDisposableListener(dragTarget, 'mousedown', (event) => { - if ( - // event.shiftKey || - event.defaultPrevented - ) { + if (event.defaultPrevented) { event.preventDefault(); return; } @@ -362,7 +359,7 @@ export class Overlay extends CompositeDisposable { bringElementToFront(this._element); - if (connect) { + if (options.inDragMode) { track(); } } diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts index 4baffe1c4..050addb6d 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts @@ -191,7 +191,7 @@ export class TabsContainer this.voidContainer.element, 'mousedown', (event) => { - if (event.shiftKey && !this.group.model.isFloating) { + if (event.shiftKey && !this.group.isFloating) { event.preventDefault(); const { top, left } = @@ -203,7 +203,6 @@ export class TabsContainer x: left - rootLeft + 20, y: top - rootTop + 20, }); - event.preventDefault(); } } ), diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index c40a423e7..4ec602de4 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -125,7 +125,7 @@ export interface IDockviewComponent extends IBaseGrid { readonly onDidLayoutFromJSON: Event; readonly onDidActivePanelChange: Event; addFloatingGroup( - item: DockviewPanel | DockviewGroupPanel, + item: IDockviewPanel | DockviewGroupPanel, coord?: { x: number; y: number } ): void; } @@ -299,7 +299,7 @@ export class DockviewComponent addFloatingGroup( item: DockviewPanel | DockviewGroupPanel, coord?: { x?: number; y?: number; height?: number; width?: number }, - options?: { skipRemoveGroup: boolean; connect: boolean } + options?: { skipRemoveGroup: boolean; inDragMode: boolean } ): void { let group: DockviewGroupPanel; @@ -326,8 +326,6 @@ export class DockviewComponent group.model.isFloating = true; - const { left, top } = this.element.getBoundingClientRect(); - const overlayLeft = typeof coord?.x === 'number' ? Math.max(coord.x, 0) : 100; const overlayTop = @@ -347,10 +345,12 @@ export class DockviewComponent const el = group.element.querySelector('#dv-group-float-drag-handle'); if (el) { - overlay.setupDrag( - typeof options?.connect === 'boolean' ? options.connect : true, - el as HTMLElement - ); + overlay.setupDrag(el as HTMLElement, { + inDragMode: + typeof options?.inDragMode === 'boolean' + ? options.inDragMode + : true, + }); } const instance = { @@ -582,14 +582,12 @@ export class DockviewComponent this.layout(width, height); - const serializedFloatingGroups = data.floatingGroups || []; + const serializedFloatingGroups = data.floatingGroups ?? []; for (const serializedFloatingGroup of serializedFloatingGroups) { const { data, position } = serializedFloatingGroup; const group = createGroupFromSerializedState(data); - const { left, top } = this.element.getBoundingClientRect(); - this.addFloatingGroup( group, { @@ -598,7 +596,7 @@ export class DockviewComponent height: position.height, width: position.width, }, - { skipRemoveGroup: true, connect: false } + { skipRemoveGroup: true, inDragMode: false } ); } @@ -646,7 +644,7 @@ export class DockviewComponent } } - addPanel(options: AddPanelOptions): IDockviewPanel { + addPanel(options: AddPanelOptions): DockviewPanel { if (this.panels.find((_) => _.id === options.id)) { throw new Error(`panel with id ${options.id} already exists`); } @@ -697,7 +695,7 @@ export class DockviewComponent referenceGroup = this.activeGroup; } - let panel: IDockviewPanel; + let panel: DockviewPanel; if (referenceGroup) { const target = toTarget( @@ -716,7 +714,7 @@ export class DockviewComponent : {}; this.addFloatingGroup(group, o, { - connect: false, + inDragMode: false, skipRemoveGroup: true, }); } else if (referenceGroup.model.isFloating || target === 'center') { @@ -745,7 +743,7 @@ export class DockviewComponent : {}; this.addFloatingGroup(group, o, { - connect: false, + inDragMode: false, skipRemoveGroup: true, }); } else { @@ -886,8 +884,8 @@ export class DockviewComponent group: DockviewGroupPanel, options?: | { - skipActive?: boolean | undefined; - skipDispose?: boolean | undefined; + skipActive?: boolean; + skipDispose?: boolean; } | undefined ): void { @@ -907,8 +905,8 @@ export class DockviewComponent group: DockviewGroupPanel, options?: | { - skipActive?: boolean | undefined; - skipDispose?: boolean | undefined; + skipActive?: boolean; + skipDispose?: boolean; } | undefined ): DockviewGroupPanel { @@ -1161,7 +1159,7 @@ export class DockviewComponent private createPanel( options: AddPanelOptions, group: DockviewGroupPanel - ): IDockviewPanel { + ): DockviewPanel { const contentComponent = options.component; const tabComponent = options.tabComponent || this.options.defaultTabComponent; diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanel.ts index a99ab64c3..11bce86fa 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanel.ts @@ -4,7 +4,6 @@ import { GridviewPanelApi } from '../api/gridviewPanelApi'; import { DockviewGroupPanelModel, GroupOptions, - GroupPanelViewState, IDockviewGroupPanelModel, IHeader, } from './dockviewGroupPanelModel'; @@ -53,6 +52,10 @@ export class DockviewGroupPanel this._model.locked = value; } + get isFloating(): boolean { + return this._model.isFloating; + } + get header(): IHeader { return this._model.header; } diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index cc8030a7d..69b5f782d 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -792,6 +792,7 @@ export class DockviewGroupPanelModel public dispose(): void { super.dispose(); + this.watermark?.element.remove(); this.watermark?.dispose?.(); for (const panel of this.panels) { diff --git a/packages/dockview-core/src/dockview/options.ts b/packages/dockview-core/src/dockview/options.ts index e1d9d7861..04314fcc3 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -18,7 +18,6 @@ import { IDisposable } from '../lifecycle'; import { Position } from '../dnd/droptarget'; import { IDockviewPanel } from './dockviewPanel'; import { FrameworkFactory } from '../panel/componentFactory'; -import { Optional } from '../types'; export interface IHeaderActionsRenderer extends IDisposable { readonly element: HTMLElement; @@ -88,6 +87,7 @@ export interface DockviewComponentOptions extends DockviewRenderFunctions { ) => IHeaderActionsRenderer; singleTabMode?: 'fullwidth' | 'default'; parentElement?: HTMLElement; + disableFloatingGroups?: boolean; } export interface PanelOptions { diff --git a/packages/dockview-core/src/events.ts b/packages/dockview-core/src/events.ts index cb8d95930..9474ad317 100644 --- a/packages/dockview-core/src/events.ts +++ b/packages/dockview-core/src/events.ts @@ -74,7 +74,7 @@ export class Emitter implements IDisposable { static ENABLE_TRACKING = false; static readonly MEMORY_LEAK_WATCHER = new LeakageMonitor(); - static setLeakageMonitorEnabled(isEnabled: boolean) { + static setLeakageMonitorEnabled(isEnabled: boolean): void { if (isEnabled !== Emitter.ENABLE_TRACKING) { Emitter.MEMORY_LEAK_WATCHER.clear(); } diff --git a/packages/dockview-core/src/math.ts b/packages/dockview-core/src/math.ts index 4ee363902..1e4f7e1e9 100644 --- a/packages/dockview-core/src/math.ts +++ b/packages/dockview-core/src/math.ts @@ -5,7 +5,7 @@ export const clamp = (value: number, min: number, max: number): number => { return Math.min(max, Math.max(value, min)); }; -export const sequentialNumberGenerator = () => { +export const sequentialNumberGenerator = (): { next: () => string } => { let value = 1; return { next: () => (value++).toString() }; }; diff --git a/packages/docs/docs/components/dockview.mdx b/packages/docs/docs/components/dockview.mdx index 8293bf027..cc8d652bb 100644 --- a/packages/docs/docs/components/dockview.mdx +++ b/packages/docs/docs/components/dockview.mdx @@ -367,6 +367,24 @@ any drag and drop logic for other controls. Dockview has built-in support for floating groups. Each floating container can contain a single group with many panels and you can have as many floating containers as needed. You cannot dock multiple groups together in the same floating container. +Floating groups can be interacted with whilst holding the `shift` key activating the `event.shiftKey` boolean property on `KeyboardEvent` events. + +> Float an existing tab by holding `shift` whilst interacting with the tab + + + +> Move a floating tab by holding `shift` whilst moving the cursor or dragging the empty +> header space + + + +> Move an entire floating group by holding `shift` whilst dragging the empty header space + + + +Floating groups can be programatically added through the dockview `api` method `api.addFloatingGroup(...)` and you can check whether +a group is floating via the `group.isFloating` property. See examples for full code. + diff --git a/packages/docs/static/img/float_add.svg b/packages/docs/static/img/float_add.svg new file mode 100644 index 000000000..7d12de4d6 --- /dev/null +++ b/packages/docs/static/img/float_add.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/packages/docs/static/img/float_group.svg b/packages/docs/static/img/float_group.svg new file mode 100644 index 000000000..fcb06299a --- /dev/null +++ b/packages/docs/static/img/float_group.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/docs/static/img/float_move.svg b/packages/docs/static/img/float_move.svg new file mode 100644 index 000000000..b8f5ccfcb --- /dev/null +++ b/packages/docs/static/img/float_move.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +