From ffd5db273e119741233e21da3d790ba03b613546 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Sat, 1 Apr 2023 20:57:41 +0100 Subject: [PATCH] feature: floating groups --- .../dockview-core/src/api/component.api.ts | 4 + packages/dockview-core/src/dnd/overlay.scss | 106 +++++++ packages/dockview-core/src/dnd/overlay.ts | 266 ++++++++++++++++++ .../src/dockview/dockviewComponent.ts | 60 ++++ 4 files changed, 436 insertions(+) create mode 100644 packages/dockview-core/src/dnd/overlay.scss create mode 100644 packages/dockview-core/src/dnd/overlay.ts diff --git a/packages/dockview-core/src/api/component.api.ts b/packages/dockview-core/src/api/component.api.ts index 4de792690..66b10e7fa 100644 --- a/packages/dockview-core/src/api/component.api.ts +++ b/packages/dockview-core/src/api/component.api.ts @@ -329,6 +329,10 @@ export class GridviewApi implements CommonApi { } export class DockviewApi implements CommonApi { + addFloating() { + return this.component.addFloating(); + } + get id(): string { return this.component.id; } diff --git a/packages/dockview-core/src/dnd/overlay.scss b/packages/dockview-core/src/dnd/overlay.scss new file mode 100644 index 000000000..fd33fa47d --- /dev/null +++ b/packages/dockview-core/src/dnd/overlay.scss @@ -0,0 +1,106 @@ +.dv-resize-container { + position: absolute; + z-index: 9998; + + background-color: white; + + &.dv-resize-container-dragging { + opacity: 0.2; + } + + .dv-resize-handle-top { + height: 4px; + width: calc(100% - 8px); + left: 4px; + top: -2px; + z-index: 9999; + position: absolute; + cursor: ns-resize; + + background-color: red; + } + + .dv-resize-handle-bottom { + height: 4px; + width: calc(100% - 8px); + left: 4px; + bottom: -2px; + z-index: 9999; + position: absolute; + cursor: ns-resize; + + background-color: green; + } + + .dv-resize-handle-left { + height: calc(100% - 8px); + width: 4px; + left: -2px; + top: 4px; + z-index: 9999; + position: absolute; + cursor: ew-resize; + + background-color: yellow; + } + + .dv-resize-handle-right { + height: calc(100% - 8px); + width: 4px; + right: -2px; + top: 4px; + z-index: 9999; + position: absolute; + cursor: ew-resize; + + background-color: blue; + } + + .dv-resize-handle-topleft { + height: 4px; + width: 4px; + top: -2px; + left: -2px; + z-index: 9999; + position: absolute; + cursor: nw-resize; + + background-color: cyan; + } + + .dv-resize-handle-topright { + height: 4px; + width: 4px; + right: -2px; + top: -2px; + z-index: 9999; + position: absolute; + cursor: ne-resize; + + background-color: cyan; + } + + .dv-resize-handle-bottomleft { + height: 4px; + width: 4px; + left: -2px; + bottom: -2px; + z-index: 9999; + position: absolute; + cursor: sw-resize; + + background-color: cyan; + } + + .dv-resize-handle-bottomright { + height: 4px; + width: 4px; + right: -2px; + bottom: -2px; + z-index: 9999; + position: absolute; + cursor: se-resize; + + background-color: cyan; + } +} diff --git a/packages/dockview-core/src/dnd/overlay.ts b/packages/dockview-core/src/dnd/overlay.ts new file mode 100644 index 000000000..98891efc4 --- /dev/null +++ b/packages/dockview-core/src/dnd/overlay.ts @@ -0,0 +1,266 @@ +import { toggleClass } from '../dom'; +import { addDisposableListener, addDisposableWindowListener } from '../events'; +import { CompositeDisposable, MutableDisposable } from '../lifecycle'; +import { clamp } from '../math'; + +export class Overlay extends CompositeDisposable { + private _element: HTMLElement = document.createElement('div'); + + constructor( + private readonly container: HTMLElement, + private readonly content: HTMLElement, + private readonly options: { + height: number; + width: number; + left: number; + top: number; + } + ) { + super(); + + this.setupOverlay(); + this.setupDrag(); + this.setupResize('top'); + this.setupResize('bottom'); + this.setupResize('left'); + this.setupResize('right'); + this.setupResize('topleft'); + this.setupResize('topright'); + this.setupResize('bottomleft'); + this.setupResize('bottomright'); + + this._element.appendChild(content); + this.container.appendChild(this._element); + } + + private setupResize( + direction: + | 'top' + | 'bottom' + | 'left' + | 'right' + | 'topleft' + | 'topright' + | 'bottomleft' + | 'bottomright' + ): void { + const resizeHandleElement = document.createElement('div'); + resizeHandleElement.className = `dv-resize-handle-${direction}`; + this._element.appendChild(resizeHandleElement); + + const move = new MutableDisposable(); + + this.addDisposables( + move, + addDisposableListener(resizeHandleElement, 'mousedown', (_) => { + _.preventDefault(); + + let offset: { + originalY: number; + originalHeight: number; + originalX: number; + originalWidth: number; + } | null = null; + + move.value = new CompositeDisposable( + addDisposableWindowListener(window, 'mousemove', (e) => { + const rect = this.container.getBoundingClientRect(); + const y = e.clientY - rect.top; + const x = e.clientX - rect.left; + + const rect2 = this._element.getBoundingClientRect(); + + if (offset === null) { + offset = { + originalY: y, + originalHeight: rect2.height, + originalX: x, + originalWidth: rect2.width, + }; + } + + let top: number | null = null; + let height: number | null = null; + let left: number | null = null; + let width: number | null = null; + + const MIN_HEIGHT = 20; + const MIN_WIDTH = 20; + + function moveTop() { + top = clamp( + y, + 0, + offset!.originalY + + offset!.originalHeight - + MIN_HEIGHT + ); + height = + offset!.originalY + + offset!.originalHeight - + top; + } + + function moveBottom() { + top = offset!.originalY - offset!.originalHeight; + + height = clamp( + y - top, + MIN_HEIGHT, + rect.height - + offset!.originalY + + offset!.originalHeight + ); + } + + function moveLeft() { + left = clamp( + x, + 0, + offset!.originalX + + offset!.originalWidth - + MIN_WIDTH + ); + width = + offset!.originalX + + offset!.originalWidth - + left; + } + + function moveRight() { + left = offset!.originalX - offset!.originalWidth; + width = clamp( + x - left, + MIN_WIDTH, + rect.width - + offset!.originalX + + offset!.originalWidth + ); + } + + switch (direction) { + case 'top': + moveTop(); + break; + case 'bottom': + moveBottom(); + break; + case 'left': + moveLeft(); + break; + case 'right': + moveRight(); + break; + case 'topleft': + moveTop(); + moveLeft(); + break; + case 'topright': + moveTop(); + moveRight(); + break; + case 'bottomleft': + moveBottom(); + moveLeft(); + break; + case 'bottomright': + moveBottom(); + moveRight(); + break; + } + + if (height !== null) { + this._element.style.height = `${height}px`; + } + if (top !== null) { + this._element.style.top = `${top}px`; + } + if (left !== null) { + this._element.style.left = `${left}px`; + } + if (width !== null) { + this._element.style.width = `${width}px`; + } + }), + addDisposableWindowListener(window, 'mouseup', () => { + move.dispose(); + }) + ); + }) + ); + } + + private setupOverlay(): void { + this._element.style.height = `${this.options.height}px`; + 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'; + } + + private setupDrag(): void { + const move = new MutableDisposable(); + + this.addDisposables( + move, + addDisposableListener(this._element, 'mousedown', (_) => { + if (_.defaultPrevented) { + return; + } + + let offset: { x: number; y: number } | null = null; + + move.value = new CompositeDisposable( + addDisposableWindowListener(window, 'mousemove', (e) => { + const rect = this.container.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + toggleClass( + this._element, + 'dv-resize-container-dragging', + true + ); + + const rect2 = this._element.getBoundingClientRect(); + if (offset === null) { + offset = { + x: e.clientX - rect2.left, + y: e.clientY - rect2.top, + }; + } + + const left = clamp( + Math.max(0, x - offset.x), + 0, + rect.width - rect2.width + ); + + const top = clamp( + Math.max(0, y - offset.y), + 0, + rect.height - rect2.height + ); + + this._element.style.left = `${left}px`; + this._element.style.top = `${top}px`; + }), + addDisposableWindowListener(window, 'mouseup', () => { + toggleClass( + this._element, + 'dv-resize-container-dragging', + false + ); + + move.dispose(); + }) + ); + }) + ); + } + + dispose(): void { + this._element.remove(); + } +} diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 13eb455aa..3a0131c1b 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -44,6 +44,7 @@ import { import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel'; import { DockviewPanelModel } from './dockviewPanelModel'; import { getPanelData } from '../dnd/dataTransfer'; +import { Overlay } from '../dnd/overlay'; export interface PanelReference { update: (event: { params: { [key: string]: any } }) => void; @@ -117,6 +118,7 @@ export interface IDockviewComponent extends IBaseGrid { readonly onDidAddPanel: Event; readonly onDidLayoutFromJSON: Event; readonly onDidActivePanelChange: Event; + addFloating(): void; } export class DockviewComponent @@ -288,6 +290,8 @@ export class DockviewComponent this._api = new DockviewApi(this); this.updateWatermark(); + + this.element.style.position = 'relative'; } private orthogonalize(position: Position): DockviewGroupPanel { @@ -984,4 +988,60 @@ export class DockviewComponent this._onDidRemovePanel.dispose(); this._onDidLayoutFromJSON.dispose(); } + + // + + addFloating() { + const parentDockview = this; + + const floatingDockview = new DockviewComponent({ + ...this.options, + parentElement: undefined, + showDndOverlay: (event) => { + const data = event.getData(); + + if (data && data.viewId === parentDockview.id) { + return true; + } + + return false; + }, + }); + + floatingDockview.onDidDrop((event) => { + const data = event.getData(); + + if (!data || data.viewId !== parentDockview.id) { + return; + } + + if (data.panelId === null) { + const group = parentDockview.removeGroup( + parentDockview.getPanel(data.groupId)! + ); + } else { + const panel = parentDockview.removePanel( + parentDockview.getGroupPanel(data.panelId)! + ); + + parentDockview.moveGroupOrPanel() + } + }); + + floatingDockview.addPanel({ + id: '__test__', + component: 'default', + }); + floatingDockview.addPanel({ + id: '__test__2__', + component: 'default', + }); + + const overlay = new Overlay(this.element, floatingDockview.element, { + height: 300, + width: 300, + left: 100, + top: 100, + }); + } }