From 12b4a0d27b970dc969766cdca4ed30cc0cb3f36a Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Thu, 6 Jul 2023 21:13:18 +0100 Subject: [PATCH] feat: floating groups --- .../src/__tests__/dnd/overlay.spec.ts | 46 +++++++- .../dockview/dockviewComponent.spec.ts | 2 +- .../dockview-core/src/dnd/groupDragHandler.ts | 20 ++++ packages/dockview-core/src/dnd/overlay.ts | 110 +++++++++++------- .../components/titlebar/tabsContainer.ts | 14 ++- .../src/dockview/dockviewComponent.ts | 7 +- .../src/dockview/dockviewGroupPanelModel.ts | 6 +- packages/dockview-core/src/dom.ts | 23 +++- .../src/gridview/gridviewComponent.ts | 2 +- 9 files changed, 172 insertions(+), 58 deletions(-) diff --git a/packages/dockview-core/src/__tests__/dnd/overlay.spec.ts b/packages/dockview-core/src/__tests__/dnd/overlay.spec.ts index c2cd8df2f..1d65d90ad 100644 --- a/packages/dockview-core/src/__tests__/dnd/overlay.spec.ts +++ b/packages/dockview-core/src/__tests__/dnd/overlay.spec.ts @@ -13,8 +13,46 @@ describe('overlay', () => { width: 100, left: 10, top: 20, - minX: 0, - minY: 0, + minimumInViewportWidth: 0, + minimumInViewportHeight: 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('#1', () => { + 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: -1000, + top: -1000, + minimumInViewportWidth: 0, + minimumInViewportHeight: 0, container, content, }); @@ -48,8 +86,8 @@ describe('overlay', () => { width: 500, left: 100, top: 200, - minX: 0, - minY: 0, + minimumInViewportWidth: 0, + minimumInViewportHeight: 0, container, content, }); diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts index 2de54bf0c..626bb66bd 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -8,7 +8,7 @@ import { PanelUpdateEvent } from '../../panel/types'; import { Orientation } from '../../splitview/splitview'; import { CompositeDisposable } from '../../lifecycle'; import { Emitter } from '../../events'; -import { DockviewPanel, IDockviewPanel } from '../../dockview/dockviewPanel'; +import { IDockviewPanel } from '../../dockview/dockviewPanel'; import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel'; class PanelContentPartTest implements IContentRenderer { diff --git a/packages/dockview-core/src/dnd/groupDragHandler.ts b/packages/dockview-core/src/dnd/groupDragHandler.ts index 12b2667fd..794118498 100644 --- a/packages/dockview-core/src/dnd/groupDragHandler.ts +++ b/packages/dockview-core/src/dnd/groupDragHandler.ts @@ -1,4 +1,6 @@ import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel'; +import { quasiPreventDefault } from '../dom'; +import { addDisposableListener } from '../events'; import { IDisposable } from '../lifecycle'; import { DragHandler } from './abstractDragHandler'; import { LocalSelectionTransfer, PanelTransfer } from './dataTransfer'; @@ -14,6 +16,24 @@ export class GroupDragHandler extends DragHandler { private readonly group: DockviewGroupPanel ) { super(element); + + this.addDisposables( + addDisposableListener( + element, + 'mousedown', + (e) => { + if (e.shiftKey) { + /** + * You cannot call e.preventDefault() because that will prevent drag events from firing + * but we also need to stop any group overlay drag events from occuring + * Use a custom event marker that can be checked by the overlay drag events + */ + quasiPreventDefault(e); + } + }, + true + ) + ); } override isCancelled(_event: DragEvent): boolean { diff --git a/packages/dockview-core/src/dnd/overlay.ts b/packages/dockview-core/src/dnd/overlay.ts index 819178ea2..16ac21558 100644 --- a/packages/dockview-core/src/dnd/overlay.ts +++ b/packages/dockview-core/src/dnd/overlay.ts @@ -1,4 +1,4 @@ -import { toggleClass } from '../dom'; +import { quasiDefaultPrevented, toggleClass } from '../dom'; import { Emitter, Event, @@ -7,6 +7,7 @@ import { } from '../events'; import { CompositeDisposable, MutableDisposable } from '../lifecycle'; import { clamp } from '../math'; +import { getPaneData, getPanelData } from './dataTransfer'; const bringElementToFront = (() => { let previous: HTMLElement | null = null; @@ -29,6 +30,9 @@ export class Overlay extends CompositeDisposable { private readonly _onDidChange = new Emitter(); readonly onDidChange: Event = this._onDidChange.event; + private static MINIMUM_HEIGHT = 20; + private static MINIMUM_WIDTH = 20; + constructor( private readonly options: { height: number; @@ -37,8 +41,8 @@ export class Overlay extends CompositeDisposable { top: number; container: HTMLElement; content: HTMLElement; - minX: number; - minY: number; + minimumInViewportWidth: number; + minimumInViewportHeight: number; } ) { super(); @@ -57,6 +61,9 @@ export class Overlay extends CompositeDisposable { this._element.appendChild(this.options.content); this.options.container.appendChild(this._element); + + // if input bad resize within acceptable boundaries + this.renderWithinBoundaryConditions(); } toJSON(): { top: number; left: number; height: number; width: number } { @@ -93,7 +100,7 @@ export class Overlay extends CompositeDisposable { addDisposableListener(resizeHandleElement, 'mousedown', (e) => { e.preventDefault(); - let offset: { + let startPosition: { originalY: number; originalHeight: number; originalX: number; @@ -102,19 +109,21 @@ export class Overlay extends CompositeDisposable { move.value = new CompositeDisposable( addDisposableWindowListener(window, 'mousemove', (e) => { - const rect = + const containerRect = this.options.container.getBoundingClientRect(); - const y = e.clientY - rect.top; - const x = e.clientX - rect.left; + const overlayRect = + this._element.getBoundingClientRect(); - const rect2 = this._element.getBoundingClientRect(); + const y = e.clientY - containerRect.top; + const x = e.clientX - containerRect.left; - if (offset === null) { - offset = { + if (startPosition === null) { + // record the initial dimensions since as all subsequence moves are relative to this + startPosition = { originalY: y, - originalHeight: rect2.height, + originalHeight: overlayRect.height, originalX: x, - originalWidth: rect2.width, + originalWidth: overlayRect.width, }; } @@ -123,37 +132,36 @@ export class Overlay extends CompositeDisposable { let left: number | null = null; let width: number | null = null; - const MIN_HEIGHT = 20; - const MIN_WIDTH = 20; - function moveTop() { top = clamp( y, 0, Math.max( 0, - offset!.originalY + - offset!.originalHeight - - MIN_HEIGHT + startPosition!.originalY + + startPosition!.originalHeight - + Overlay.MINIMUM_HEIGHT ) ); height = - offset!.originalY + - offset!.originalHeight - + startPosition!.originalY + + startPosition!.originalHeight - top; } function moveBottom() { - top = offset!.originalY - offset!.originalHeight; + top = + startPosition!.originalY - + startPosition!.originalHeight; height = clamp( y - top, - MIN_HEIGHT, + Overlay.MINIMUM_HEIGHT, Math.max( 0, - rect.height - - offset!.originalY + - offset!.originalHeight + containerRect.height - + startPosition!.originalY + + startPosition!.originalHeight ) ); } @@ -164,27 +172,29 @@ export class Overlay extends CompositeDisposable { 0, Math.max( 0, - offset!.originalX + - offset!.originalWidth - - MIN_WIDTH + startPosition!.originalX + + startPosition!.originalWidth - + Overlay.MINIMUM_WIDTH ) ); width = - offset!.originalX + - offset!.originalWidth - + startPosition!.originalX + + startPosition!.originalWidth - left; } function moveRight() { - left = offset!.originalX - offset!.originalWidth; + left = + startPosition!.originalX - + startPosition!.originalWidth; width = clamp( x - left, - MIN_WIDTH, + Overlay.MINIMUM_WIDTH, Math.max( 0, - rect.width - - offset!.originalX + - offset!.originalWidth + containerRect.width - + startPosition!.originalX + + startPosition!.originalWidth ) ); } @@ -283,11 +293,12 @@ export class Overlay extends CompositeDisposable { const xOffset = Math.max( 0, - overlayRect.width - this.options.minX + overlayRect.width - this.options.minimumInViewportWidth ); const yOffset = Math.max( 0, - overlayRect.height - this.options.minY + overlayRect.height - + this.options.minimumInViewportHeight ); const left = clamp( @@ -332,6 +343,12 @@ export class Overlay extends CompositeDisposable { return; } + // if somebody has marked this event then treat as a defaultPrevented + // without actually calling event.preventDefault() + if (quasiDefaultPrevented(event)) { + return; + } + track(); }), addDisposableListener( @@ -342,6 +359,12 @@ export class Overlay extends CompositeDisposable { return; } + // if somebody has marked this event then treat as a defaultPrevented + // without actually calling event.preventDefault() + if (quasiDefaultPrevented(event)) { + return; + } + if (event.shiftKey) { track(); } @@ -368,8 +391,17 @@ export class Overlay extends CompositeDisposable { const containerRect = this.options.container.getBoundingClientRect(); const overlayRect = this._element.getBoundingClientRect(); - const xOffset = Math.max(0, overlayRect.width - this.options.minX); - const yOffset = Math.max(0, overlayRect.height - this.options.minY); + // a minimum width of minimumViewportWidth must be inside the viewport + const xOffset = Math.max( + 0, + overlayRect.width - this.options.minimumInViewportWidth + ); + + // a minimum height of minimumViewportHeight must be inside the viewport + const yOffset = Math.max( + 0, + overlayRect.height - this.options.minimumInViewportHeight + ); const left = clamp( this.options.left, diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts index 050addb6d..840be83cd 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts @@ -191,7 +191,14 @@ export class TabsContainer this.voidContainer.element, 'mousedown', (event) => { - if (event.shiftKey && !this.group.isFloating) { + const isFloatingGroupsEnabled = + !this.accessor.options.disableFloatingGroups; + + if ( + isFloatingGroupsEnabled && + event.shiftKey && + !this.group.isFloating + ) { event.preventDefault(); const { top, left } = @@ -282,7 +289,10 @@ export class TabsContainer const disposable = CompositeDisposable.from( tabToAdd.onChanged((event) => { - if (event.shiftKey) { + const isFloatingGroupsEnabled = + !this.accessor.options.disableFloatingGroups; + + if (isFloatingGroupsEnabled && event.shiftKey) { event.preventDefault(); const panel = this.accessor.getGroupPanel(tabToAdd.panelId); diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 9b926d038..2006dfd12 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -345,8 +345,8 @@ export class DockviewComponent width: coord?.width ?? 300, left: overlayLeft, top: overlayTop, - minX: 100, - minY: 100, + minimumInViewportWidth: 100, + minimumInViewportHeight: 100, }); const el = group.element.querySelector('#dv-group-float-drag-handle'); @@ -439,6 +439,7 @@ export class DockviewComponent if (this.floatingGroups) { for (const floating of this.floatingGroups) { + // ensure floting groups stay within visible boundaries floating.overlay.renderWithinBoundaryConditions(); } } @@ -588,7 +589,7 @@ export class DockviewComponent }, }); - this.layout(width, height); + this.layout(width, height, true); const serializedFloatingGroups = data.floatingGroups ?? []; diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index 69b5f782d..05adc5dfd 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -261,12 +261,12 @@ export class DockviewGroupPanelModel return false; } - if (event.shiftKey && !this.isFloating) { + const data = getPanelData(); + + if (!data && event.shiftKey && !this.isFloating) { return false; } - const data = getPanelData(); - if (data && data.viewId === this.accessor.id) { if (data.groupId === this.id) { if (position === 'center') { diff --git a/packages/dockview-core/src/dom.ts b/packages/dockview-core/src/dom.ts index 4a36f4bde..a12b50742 100644 --- a/packages/dockview-core/src/dom.ts +++ b/packages/dockview-core/src/dom.ts @@ -1,5 +1,5 @@ import { - Event, + Event as DockviewEvent, Emitter, addDisposableListener, addDisposableWindowListener, @@ -87,8 +87,8 @@ export function getElementsByTagName(tag: string): HTMLElement[] { } export interface IFocusTracker extends IDisposable { - readonly onDidFocus: Event; - readonly onDidBlur: Event; + readonly onDidFocus: DockviewEvent; + readonly onDidBlur: DockviewEvent; refreshState?(): void; } @@ -101,10 +101,10 @@ export function trackFocus(element: HTMLElement | Window): IFocusTracker { */ class FocusTracker extends CompositeDisposable implements IFocusTracker { private readonly _onDidFocus = new Emitter(); - public readonly onDidFocus: Event = this._onDidFocus.event; + public readonly onDidFocus: DockviewEvent = this._onDidFocus.event; private readonly _onDidBlur = new Emitter(); - public readonly onDidBlur: Event = this._onDidBlur.event; + public readonly onDidBlur: DockviewEvent = this._onDidBlur.event; private _refreshStateHandler: () => void; @@ -172,3 +172,16 @@ class FocusTracker extends CompositeDisposable implements IFocusTracker { this._refreshStateHandler(); } } + +// quasi: apparently, but not really; seemingly +const QUASI_PREVENT_DEFAULT_KEY = 'dv-quasiPreventDefault'; + +// mark an event directly for other listeners to check +export function quasiPreventDefault(event: Event): void { + (event as any)[QUASI_PREVENT_DEFAULT_KEY] = true; +} + +// check if this event has been marked +export function quasiDefaultPrevented(event: Event): boolean { + return (event as any)[QUASI_PREVENT_DEFAULT_KEY]; +} diff --git a/packages/dockview-core/src/gridview/gridviewComponent.ts b/packages/dockview-core/src/gridview/gridviewComponent.ts index 40dea53aa..12729b599 100644 --- a/packages/dockview-core/src/gridview/gridviewComponent.ts +++ b/packages/dockview-core/src/gridview/gridviewComponent.ts @@ -219,7 +219,7 @@ export class GridviewComponent }, }); - this.layout(width, height); + this.layout(width, height, true); queue.forEach((f) => f());