diff --git a/packages/dockview-core/src/api/component.api.ts b/packages/dockview-core/src/api/component.api.ts index d4c072351..d527ef6af 100644 --- a/packages/dockview-core/src/api/component.api.ts +++ b/packages/dockview-core/src/api/component.api.ts @@ -329,10 +329,6 @@ 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/array.ts b/packages/dockview-core/src/array.ts index 227744927..44b1003f3 100644 --- a/packages/dockview-core/src/array.ts +++ b/packages/dockview-core/src/array.ts @@ -61,3 +61,13 @@ export function firstIndex( return -1; } + +export function remove(array: T[], value: T): boolean { + const index = array.findIndex((t) => t === value); + + if (index > -1) { + array.splice(index, 1); + return true; + } + return false; +} diff --git a/packages/dockview-core/src/dnd/abstractDragHandler.ts b/packages/dockview-core/src/dnd/abstractDragHandler.ts index 2120b8fc5..b1483f347 100644 --- a/packages/dockview-core/src/dnd/abstractDragHandler.ts +++ b/packages/dockview-core/src/dnd/abstractDragHandler.ts @@ -21,10 +21,21 @@ export abstract class DragHandler extends CompositeDisposable { abstract getData(dataTransfer?: DataTransfer | null): IDisposable; + protected isCancelled(_event: DragEvent): boolean { + return false; + } + private configure(): void { this.addDisposables( this._onDragStart, addDisposableListener(this.el, 'dragstart', (event) => { + if (this.isCancelled(event)) { + event.preventDefault(); + return; + } + + this.disposable.value = this.getData(event.dataTransfer); + this.iframes = [ ...getElementsByTagName('iframe'), ...getElementsByTagName('webview'), @@ -37,8 +48,6 @@ export abstract class DragHandler extends CompositeDisposable { this.el.classList.add('dv-dragged'); setTimeout(() => this.el.classList.remove('dv-dragged'), 0); - this.disposable.value = this.getData(event.dataTransfer); - if (event.dataTransfer) { event.dataTransfer.effectAllowed = 'move'; diff --git a/packages/dockview-core/src/dnd/droptarget.ts b/packages/dockview-core/src/dnd/droptarget.ts index cac6d30ad..ba3f21c82 100644 --- a/packages/dockview-core/src/dnd/droptarget.ts +++ b/packages/dockview-core/src/dnd/droptarget.ts @@ -58,6 +58,7 @@ export class Droptarget extends CompositeDisposable { private targetElement: HTMLElement | undefined; private overlayElement: HTMLElement | undefined; private _state: Position | undefined; + private _acceptedTargetZonesSet: Set; private readonly _onDrop = new Emitter(); readonly onDrop: Event = this._onDrop.event; @@ -83,7 +84,7 @@ export class Droptarget extends CompositeDisposable { super(); // use a set to take advantage of #.has - const acceptedTargetZonesSet = new Set( + this._acceptedTargetZonesSet = new Set( this.options.acceptedTargetZones ); @@ -106,7 +107,7 @@ export class Droptarget extends CompositeDisposable { const y = e.clientY - rect.top; const quadrant = this.calculateQuadrant( - acceptedTargetZonesSet, + this._acceptedTargetZonesSet, x, y, width, @@ -175,6 +176,10 @@ export class Droptarget extends CompositeDisposable { ); } + setTargetZones(acceptedTargetZones: Position[]): void { + this._acceptedTargetZonesSet = new Set(acceptedTargetZones); + } + public dispose(): void { this.removeDropTarget(); } diff --git a/packages/dockview-core/src/dnd/groupDragHandler.ts b/packages/dockview-core/src/dnd/groupDragHandler.ts index e7f99e062..2acb296f0 100644 --- a/packages/dockview-core/src/dnd/groupDragHandler.ts +++ b/packages/dockview-core/src/dnd/groupDragHandler.ts @@ -16,6 +16,13 @@ export class GroupDragHandler extends DragHandler { super(element); } + override isCancelled(_event: DragEvent): boolean { + if (this.group.model.isFloating) { + return true; + } + return false; + } + getData(dataTransfer: DataTransfer | null): IDisposable { this.panelTransfer.setData( [new PanelTransfer(this.accessorId, this.group.id, null)], diff --git a/packages/dockview-core/src/dnd/overlay.scss b/packages/dockview-core/src/dnd/overlay.scss index fd33fa47d..92ceb5044 100644 --- a/packages/dockview-core/src/dnd/overlay.scss +++ b/packages/dockview-core/src/dnd/overlay.scss @@ -1,11 +1,43 @@ +.dv-debug { + .dv-resize-container { + .dv-resize-handle-top { + background-color: red; + } + + .dv-resize-handle-bottom { + background-color: green; + } + + .dv-resize-handle-left { + background-color: yellow; + } + + .dv-resize-handle-right { + background-color: blue; + } + + .dv-resize-handle-topleft, + .dv-resize-handle-topright, + .dv-resize-handle-bottomleft, + .dv-resize-handle-bottomright { + background-color: cyan; + } + } +} + .dv-resize-container { position: absolute; - z-index: 9998; + z-index: 9997; - background-color: white; + &.dv-resize-container-priority { + z-index: 9998; + } + + border: 1px solid var(--dv-tab-divider-color); + box-shadow: var(--dv-floating-box-shadow); &.dv-resize-container-dragging { - opacity: 0.2; + opacity: 0.5; } .dv-resize-handle-top { @@ -16,8 +48,6 @@ z-index: 9999; position: absolute; cursor: ns-resize; - - background-color: red; } .dv-resize-handle-bottom { @@ -28,8 +58,6 @@ z-index: 9999; position: absolute; cursor: ns-resize; - - background-color: green; } .dv-resize-handle-left { @@ -40,8 +68,6 @@ z-index: 9999; position: absolute; cursor: ew-resize; - - background-color: yellow; } .dv-resize-handle-right { @@ -52,8 +78,6 @@ z-index: 9999; position: absolute; cursor: ew-resize; - - background-color: blue; } .dv-resize-handle-topleft { @@ -64,8 +88,6 @@ z-index: 9999; position: absolute; cursor: nw-resize; - - background-color: cyan; } .dv-resize-handle-topright { @@ -76,8 +98,6 @@ z-index: 9999; position: absolute; cursor: ne-resize; - - background-color: cyan; } .dv-resize-handle-bottomleft { @@ -88,8 +108,6 @@ z-index: 9999; position: absolute; cursor: sw-resize; - - background-color: cyan; } .dv-resize-handle-bottomright { @@ -100,7 +118,5 @@ 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 index 98891efc4..b10f5fa64 100644 --- a/packages/dockview-core/src/dnd/overlay.ts +++ b/packages/dockview-core/src/dnd/overlay.ts @@ -3,23 +3,40 @@ import { addDisposableListener, addDisposableWindowListener } from '../events'; import { CompositeDisposable, MutableDisposable } from '../lifecycle'; import { clamp } from '../math'; +const bringElementToFront = (() => { + let previous: HTMLElement | null = null; + + function pushToTop(element: HTMLElement) { + if (previous !== element && previous !== null) { + toggleClass(previous, 'dv-resize-container-priority', false); + } + + toggleClass(element, 'dv-resize-container-priority', true); + previous = element; + } + + return pushToTop; +})(); + 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; + container: HTMLElement; + content: HTMLElement; + minX: number; + minY: number; } ) { super(); this.setupOverlay(); - this.setupDrag(); + // this.setupDrag(true,this._element); this.setupResize('top'); this.setupResize('bottom'); this.setupResize('left'); @@ -29,8 +46,10 @@ export class Overlay extends CompositeDisposable { this.setupResize('bottomleft'); this.setupResize('bottomright'); - this._element.appendChild(content); - this.container.appendChild(this._element); + this._element.appendChild(this.options.content); + this.options.container.appendChild(this._element); + + // this.renderWithinBoundaryConditions(); } private setupResize( @@ -52,8 +71,8 @@ export class Overlay extends CompositeDisposable { this.addDisposables( move, - addDisposableListener(resizeHandleElement, 'mousedown', (_) => { - _.preventDefault(); + addDisposableListener(resizeHandleElement, 'mousedown', (e) => { + e.preventDefault(); let offset: { originalY: number; @@ -64,7 +83,8 @@ export class Overlay extends CompositeDisposable { move.value = new CompositeDisposable( addDisposableWindowListener(window, 'mousemove', (e) => { - const rect = this.container.getBoundingClientRect(); + const rect = + this.options.container.getBoundingClientRect(); const y = e.clientY - rect.top; const x = e.clientX - rect.left; @@ -91,9 +111,12 @@ export class Overlay extends CompositeDisposable { top = clamp( y, 0, - offset!.originalY + - offset!.originalHeight - - MIN_HEIGHT + Math.max( + 0, + offset!.originalY + + offset!.originalHeight - + MIN_HEIGHT + ) ); height = offset!.originalY + @@ -107,9 +130,12 @@ export class Overlay extends CompositeDisposable { height = clamp( y - top, MIN_HEIGHT, - rect.height - - offset!.originalY + - offset!.originalHeight + Math.max( + 0, + rect.height - + offset!.originalY + + offset!.originalHeight + ) ); } @@ -117,9 +143,12 @@ export class Overlay extends CompositeDisposable { left = clamp( x, 0, - offset!.originalX + - offset!.originalWidth - - MIN_WIDTH + Math.max( + 0, + offset!.originalX + + offset!.originalWidth - + MIN_WIDTH + ) ); width = offset!.originalX + @@ -132,9 +161,12 @@ export class Overlay extends CompositeDisposable { width = clamp( x - left, MIN_WIDTH, - rect.width - - offset!.originalX + - offset!.originalWidth + Math.max( + 0, + rect.width - + offset!.originalX + + offset!.originalWidth + ) ); } @@ -199,68 +231,128 @@ export class Overlay extends CompositeDisposable { this._element.className = 'dv-resize-container'; } - private setupDrag(): void { + setupDrag(connect: boolean, dragTarget: HTMLElement): void { const move = new MutableDisposable(); + const track = () => { + let offset: { x: number; y: number } | null = null; + + move.value = new CompositeDisposable( + addDisposableWindowListener(window, 'mousemove', (e) => { + const containerRect = + this.options.container.getBoundingClientRect(); + const x = e.clientX - containerRect.left; + const y = e.clientY - containerRect.top; + + toggleClass( + this._element, + 'dv-resize-container-dragging', + true + ); + + const overlayRect = this._element.getBoundingClientRect(); + if (offset === null) { + offset = { + x: e.clientX - overlayRect.left, + y: e.clientY - overlayRect.top, + }; + } + + const xOffset = Math.max( + 0, + overlayRect.width - this.options.minX + ); + const yOffset = Math.max( + 0, + overlayRect.height - this.options.minY + ); + + const left = clamp( + x - offset.x, + -xOffset, + Math.max( + 0, + containerRect.width - overlayRect.width + xOffset + ) + ); + + const top = clamp( + y - offset.y, + -yOffset, + Math.max( + 0, + containerRect.height - overlayRect.height + yOffset + ) + ); + + 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(); + }) + ); + }; + this.addDisposables( move, - addDisposableListener(this._element, 'mousedown', (_) => { + addDisposableListener(dragTarget, '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(); - }) - ); - }) + track(); + }), + addDisposableListener(this.options.content, 'mousedown', (_) => { + if (_.shiftKey) { + track(); + } + }), + addDisposableListener( + this.options.content, + 'mousedown', + () => { + bringElementToFront(this._element); + }, + true + ) ); + + if (connect) { + track(); + } } - dispose(): void { + renderWithinBoundaryConditions(): void { + const rect = this.options.container.getBoundingClientRect(); + const rect2 = this._element.getBoundingClientRect(); + + const left = clamp( + Math.max(this.options.left, 0), + 0, + Math.max(0, rect.width - rect2.width) + ); + + const top = clamp( + Math.max(this.options.top, 0), + 0, + Math.max(0, rect.height - rect2.height) + ); + + console.log(new Error().stack); + + this._element.style.left = `${left}px`; + this._element.style.top = `${top}px`; + } + + override dispose(): void { this._element.remove(); + super.dispose(); } } diff --git a/packages/dockview-core/src/dockview/components/tab/tab.ts b/packages/dockview-core/src/dockview/components/tab/tab.ts index 550752e53..d05eef298 100644 --- a/packages/dockview-core/src/dockview/components/tab/tab.ts +++ b/packages/dockview-core/src/dockview/components/tab/tab.ts @@ -11,6 +11,7 @@ import { DockviewDropTargets, ITabRenderer } from '../../types'; import { DockviewGroupPanel } from '../../dockviewGroupPanel'; import { DroptargetEvent, Droptarget } from '../../../dnd/droptarget'; import { DragHandler } from '../../../dnd/abstractDragHandler'; +import { DockviewPanel } from '../../dockviewPanel'; export interface ITab { readonly panelId: string; @@ -80,6 +81,19 @@ export class Tab extends CompositeDisposable implements ITab { this.addDisposables( addDisposableListener(this._element, 'mousedown', (event) => { + if (event.shiftKey) { + event.preventDefault(); + + const panel = this.accessor.getGroupPanel(this.panelId); + + const { top, left } = this.element.getBoundingClientRect(); + + this.accessor.addFloating(panel as DockviewPanel, { + x: left, + y: top, + }); + } + if (event.defaultPrevented) { return; } diff --git a/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts index 1737c87aa..62b1f5ec2 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts @@ -28,6 +28,7 @@ export class VoidContainer extends CompositeDisposable { this._element = document.createElement('div'); this._element.className = 'void-container'; + this._element.id = 'dv-group-float-drag-handle'; this._element.tabIndex = 0; this._element.draggable = true; @@ -68,6 +69,16 @@ export class VoidContainer extends CompositeDisposable { this.addDisposables( handler, + addDisposableListener(this._element, 'mousedown', (event) => { + if (event.shiftKey && !this.group.model.isFloating) { + event.preventDefault(); + this.accessor.addFloating(this.group, { + x: event.clientX + 20, + y: event.clientY + 20, + }); + } + }), + this.voidDropTarget.onDrop((event) => { this._onDrop.fire(event); }), diff --git a/packages/dockview-core/src/dockview/dockviewComponent.scss b/packages/dockview-core/src/dockview/dockviewComponent.scss index db272fc48..f07025428 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.scss +++ b/packages/dockview-core/src/dockview/dockviewComponent.scss @@ -1,14 +1,15 @@ .dv-dockview { - position: relative; - background-color: var(--dv-group-view-background-color); + position: relative; + background-color: var(--dv-group-view-background-color); - .dv-watermark-container { - position: absolute; - top: 0px; - left: 0px; - height: 100%; - width: 100%; - } + .dv-watermark-container { + position: absolute; + top: 0px; + left: 0px; + height: 100%; + width: 100%; + z-index: 9999; + } } .groupview { diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index e4065844c..218cb7207 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -5,9 +5,9 @@ import { ISerializedLeafNode, } from '../gridview/gridview'; import { directionToPosition, Droptarget, Position } from '../dnd/droptarget'; -import { tail, sequenceEquals } from '../array'; +import { tail, sequenceEquals, remove } from '../array'; import { DockviewPanel, IDockviewPanel } from './dockviewPanel'; -import { CompositeDisposable } from '../lifecycle'; +import { CompositeDisposable, IDisposable } from '../lifecycle'; import { Event, Emitter } from '../events'; import { Watermark } from './components/watermark/watermark'; import { @@ -45,6 +45,7 @@ import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel'; import { DockviewPanelModel } from './dockviewPanelModel'; import { getPanelData } from '../dnd/dataTransfer'; import { Overlay } from '../dnd/overlay'; +import { toggleClass } from '../dom'; export interface PanelReference { update: (event: { params: { [key: string]: any } }) => void; @@ -116,7 +117,10 @@ export interface IDockviewComponent extends IBaseGrid { readonly onDidAddPanel: Event; readonly onDidLayoutFromJSON: Event; readonly onDidActivePanelChange: Event; - addFloating(): void; + addFloating( + item: DockviewPanel | DockviewGroupPanel, + coord?: { x: number; y: number } + ): void; } export class DockviewComponent @@ -148,6 +152,12 @@ export class DockviewComponent readonly onDidActivePanelChange: Event = this._onDidActivePanelChange.event; + private readonly floatingGroups: { + instance: DockviewGroupPanel; + disposable: IDisposable; + render: () => void; + }[] = []; + get orientation(): Orientation { return this.gridview.orientation; } @@ -182,7 +192,7 @@ export class DockviewComponent parentElement: options.parentElement, }); - this.element.classList.add('dv-dockview'); + toggleClass(this.gridview.element, 'dv-dockview', true); this.addDisposables( this._onDidDrop, @@ -277,8 +287,68 @@ export class DockviewComponent this._api = new DockviewApi(this); this.updateWatermark(); + } - this.element.style.position = 'relative'; + addFloating( + item: DockviewPanel | DockviewGroupPanel, + coord?: { x: number; y: number } + ): void { + let group: DockviewGroupPanel; + + if (item instanceof DockviewPanel) { + group = this.createGroup(); + + this.removePanel(item, { + removeEmptyGroup: true, + skipDispose: true, + }); + + group.model.openPanel(item); + } else { + group = item; + this.doRemoveGroup(item, { skipDispose: true }); + } + + group.model.isFloating = true; + + const { left, top } = this.element.getBoundingClientRect(); + + const overlayLeft = + typeof coord?.x === 'number' ? Math.max(coord.x - left, 0) : 100; + const overlayTop = + typeof coord?.y === 'number' ? Math.max(0, coord.y - top) : 100; + + const overlay = new Overlay({ + container: this.gridview.element, + content: group.element, + height: 300, + width: 300, + left: overlayLeft, + top: overlayTop, + minX: 100, + minY: 100, + }); + + const el = group.element.querySelector('#dv-group-float-drag-handle'); + + if (el) { + overlay.setupDrag(true, el as HTMLElement); + } + + const instance = { + instance: group, + render: () => { + overlay.renderWithinBoundaryConditions(); + }, + disposable: new CompositeDisposable(overlay, { + dispose: () => { + group.model.isFloating = false; + remove(this.floatingGroups, instance); + }, + }), + }; + + this.floatingGroups.push(instance); } private orthogonalize(position: Position): DockviewGroupPanel { @@ -329,6 +399,20 @@ export class DockviewComponent this.layout(this.gridview.width, this.gridview.height, true); } + override layout( + width: number, + height: number, + forceResize?: boolean | undefined + ): void { + super.layout(width, height, forceResize); + + if (this.floatingGroups) { + for (const floating of this.floatingGroups) { + floating.render(); + } + } + } + focus(): void { this.activeGroup?.focus(); } @@ -477,7 +561,7 @@ export class DockviewComponent for (const group of groups) { // remove the group will automatically remove the panels - this.removeGroup(group, true); + this.removeGroup(group, { skipActive: true }); } if (hasActiveGroup) { @@ -591,7 +675,9 @@ export class DockviewComponent group.model.removePanel(panel); - panel.dispose(); + if (!options.skipDispose) { + panel.dispose(); + } if (group.size === 0 && options.removeEmptyGroup) { this.removeGroup(group); @@ -625,7 +711,7 @@ export class DockviewComponent watermarkContainer.className = 'dv-watermark-container'; watermarkContainer.appendChild(this.watermark.element); - this.element.appendChild(watermarkContainer); + this.gridview.element.appendChild(watermarkContainer); } } else if (this.watermark) { this.watermark.element.parentElement!.remove(); @@ -695,17 +781,49 @@ export class DockviewComponent } } - removeGroup(group: DockviewGroupPanel, skipActive = false): void { + removeGroup( + group: DockviewGroupPanel, + options?: + | { + skipActive?: boolean | undefined; + skipDispose?: boolean | undefined; + } + | undefined + ): void { const panels = [...group.panels]; // reassign since group panels will mutate for (const panel of panels) { this.removePanel(panel, { removeEmptyGroup: false, - skipDispose: false, + skipDispose: options?.skipDispose ?? false, }); } - super.doRemoveGroup(group, { skipActive }); + this.doRemoveGroup(group, options); + } + + protected override doRemoveGroup( + group: DockviewGroupPanel, + options?: + | { + skipActive?: boolean | undefined; + skipDispose?: boolean | undefined; + } + | undefined + ): DockviewGroupPanel { + const floatingGroup = this.floatingGroups.find( + (_) => _.instance === group + ); + + if (floatingGroup) { + if (!options?.skipDispose) { + floatingGroup.instance.dispose(); + } + floatingGroup.disposable.dispose(); + return floatingGroup.instance; + } + + return super.doRemoveGroup(group, options); } moveGroupOrPanel( @@ -721,7 +839,7 @@ export class DockviewComponent if (itemId === undefined) { if (sourceGroup) { - this.moveGroup(sourceGroup, referenceGroup, target); + this.moveGroup(sourceGroup, referenceGroup, target); } return; } @@ -750,34 +868,44 @@ export class DockviewComponent if (sourceGroup && sourceGroup.size < 2) { const [targetParentLocation, to] = tail(targetLocation); - const sourceLocation = getGridLocation(sourceGroup.element); - const [sourceParentLocation, from] = tail(sourceLocation); - if ( - sequenceEquals(sourceParentLocation, targetParentLocation) - ) { - // special case when 'swapping' two views within same grid location - // if a group has one tab - we are essentially moving the 'group' - // which is equivalent to swapping two views in this case - this.gridview.moveView(sourceParentLocation, from, to); - } else { - // source group will become empty so delete the group - const targetGroup = this.doRemoveGroup(sourceGroup, { - skipActive: true, - skipDispose: true, - }); + const isFloating = this.floatingGroups.find( + (x) => x.instance === sourceGroup + ); - // after deleting the group we need to re-evaulate the ref location - const updatedReferenceLocation = getGridLocation( - referenceGroup.element - ); - const location = getRelativeLocation( - this.gridview.orientation, - updatedReferenceLocation, - target - ); - this.doAddGroup(targetGroup, location); + if (!isFloating) { + const sourceLocation = getGridLocation(sourceGroup.element); + const [sourceParentLocation, from] = tail(sourceLocation); + + if ( + sequenceEquals( + sourceParentLocation, + targetParentLocation + ) + ) { + // special case when 'swapping' two views within same grid location + // if a group has one tab - we are essentially moving the 'group' + // which is equivalent to swapping two views in this case + this.gridview.moveView(sourceParentLocation, from, to); + } } + + // source group will become empty so delete the group + const targetGroup = this.doRemoveGroup(sourceGroup, { + skipActive: true, + skipDispose: true, + }); + + // after deleting the group we need to re-evaulate the ref location + const updatedReferenceLocation = getGridLocation( + referenceGroup.element + ); + const location = getRelativeLocation( + this.gridview.orientation, + updatedReferenceLocation, + target + ); + this.doAddGroup(targetGroup, location); } else { const groupItem: IDockviewPanel | undefined = sourceGroup?.model.removePanel(itemId) || @@ -821,7 +949,17 @@ export class DockviewComponent }); } } else { - this.gridview.removeView(getGridLocation(sourceGroup.element)); + const floatingGroup = this.floatingGroups.find( + (x) => x.instance === sourceGroup + ); + + if (floatingGroup) { + floatingGroup.disposable.dispose(); + } else { + this.gridview.removeView( + getGridLocation(sourceGroup.element) + ); + } const referenceLocation = getGridLocation( referenceGroup.element @@ -963,60 +1101,4 @@ 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, - }); - } } diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanel.ts index b8391306c..6f722021e 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanel.ts @@ -26,7 +26,7 @@ export class DockviewGroupPanel extends GridviewPanel implements IDockviewGroupPanel { - private readonly _model: IDockviewGroupPanelModel; + private readonly _model: DockviewGroupPanelModel; get panels(): IDockviewPanel[] { return this._model.panels; @@ -40,7 +40,7 @@ export class DockviewGroupPanel return this._model.size; } - get model(): IDockviewGroupPanelModel { + get model(): DockviewGroupPanelModel { return this._model; } diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index 5e9773a73..365fc9613 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -138,6 +138,7 @@ export class DockviewGroupPanelModel private _isGroupActive = false; private _locked = false; private _control: IGroupControlRenderer | undefined; + private _isFloating = false; private mostRecentlyUsed: IDockviewPanel[] = []; @@ -223,6 +224,20 @@ export class DockviewGroupPanelModel ); } + get isFloating(): boolean { + return this._isFloating; + } + + set isFloating(value: boolean) { + this._isFloating = value; + + this.dropTarget.setTargetZones( + value ? ['center'] : ['top', 'bottom', 'left', 'right', 'center'] + ); + + toggleClass(this.container, 'dv-groupview-floating', value); + } + constructor( private readonly container: HTMLElement, private accessor: DockviewComponent, @@ -232,7 +247,7 @@ export class DockviewGroupPanelModel ) { super(); - this.container.classList.add('groupview'); + toggleClass(this.container, 'groupview', true); this.tabsContainer = new TabsContainer(this.accessor, this.groupPanel); diff --git a/packages/dockview-core/src/theme.scss b/packages/dockview-core/src/theme.scss index daa0ffaef..6a10a1a35 100644 --- a/packages/dockview-core/src/theme.scss +++ b/packages/dockview-core/src/theme.scss @@ -7,6 +7,7 @@ --dv-drag-over-border-color: white; --dv-tabs-container-scrollbar-color: #888; --dv-icon-hover-background-color: rgba(90, 93, 94, 0.31); + --dv-floating-box-shadow: 8px 8px 8px 0px rgba(83, 89, 93, 0.5); } @mixin dockview-theme-dark-mixin { @@ -225,3 +226,124 @@ .dockview-theme-dracula { @include dockview-theme-dracula-mixin(); } + +@mixin dockview-design-replit-mixin { + &.dv-dockview { + padding: 3px; + } + + .view:has(> .groupview) { + padding: 3px; + } + + .dv-resize-container:has(> .groupview) { + border-radius: 8px; + } + + .groupview { + overflow: hidden; + border-radius: 10px; + + .tabs-and-actions-container { + .tab { + margin: 4px; + border-radius: 8px; + + .dockview-svg { + height: 8px; + width: 8px; + } + + &:hover { + background-color: #e4e5e6 !important; + } + } + border-bottom: 1px solid rgba(128, 128, 128, 0.35); + } + + .content-container { + background-color: #fcfcfc; + } + + &.active-group { + border: 1px solid rgba(128, 128, 128, 0.35); + } + + &.inactive-group { + border: 1px solid transparent; + } + } + + .vertical > .sash-container > .sash { + &::after { + content: ''; + height: 4px; + width: 40px; + border-radius: 2px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: var(--dv-separator-handle-background-color); + position: absolute; + } + + &:hover { + &::after { + background-color: var( + --dv-separator-handle-hover-background-color + ); + } + } + } + + .horizontal > .sash-container > .sash { + &::after { + content: ''; + height: 40px; + width: 4px; + border-radius: 2px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: var(--dv-separator-handle-background-color); + position: absolute; + } + + &:hover { + &::after { + background-color: var( + --dv-separator-handle-hover-background-color + ); + } + } + } +} + +.dockview-theme-replit { + @include dockview-theme-core-mixin(); + @include dockview-design-replit-mixin(); + // + --dv-group-view-background-color: #ebeced; + // + --dv-tabs-and-actions-container-background-color: #fcfcfc; + // + --dv-activegroup-visiblepanel-tab-background-color: #f0f1f2; + --dv-activegroup-hiddenpanel-tab-background-color: ##fcfcfc; + --dv-inactivegroup-visiblepanel-tab-background-color: #f0f1f2; + --dv-inactivegroup-hiddenpanel-tab-background-color: #fcfcfc; + --dv-tab-divider-color: transparent; + // + --dv-activegroup-visiblepanel-tab-color: rgb(51, 51, 51); + --dv-activegroup-hiddenpanel-tab-color: rgb(51, 51, 51); + --dv-inactivegroup-visiblepanel-tab-color: rgb(51, 51, 51); + --dv-inactivegroup-hiddenpanel-tab-color: rgb(51, 51, 51); + // + --dv-separator-border: transparent; + --dv-paneview-header-border-color: rgb(51, 51, 51); + + --dv-background-color: #ebeced; + + ///// + --dv-separator-handle-background-color: #cfd1d3; + --dv-separator-handle-hover-background-color: #babbbb; +} diff --git a/packages/dockview/src/index.ts b/packages/dockview/src/index.ts index 9e68aacdb..acc7fec37 100644 --- a/packages/dockview/src/index.ts +++ b/packages/dockview/src/index.ts @@ -1,12 +1,6 @@ export * from 'dockview-core'; -export { - IDockviewPanelHeaderProps, - IDockviewPanelProps, - DockviewReadyEvent, - IDockviewReactProps, - DockviewReact, -} from './dockview/dockview'; +export * from './dockview/dockview'; export * from './dockview/defaultTab'; export * from './splitview/splitview'; export * from './gridview/gridview'; diff --git a/packages/docs/sandboxes/demo-dockview/src/app.tsx b/packages/docs/sandboxes/demo-dockview/src/app.tsx index 654cbd3f5..92ef2561a 100644 --- a/packages/docs/sandboxes/demo-dockview/src/app.tsx +++ b/packages/docs/sandboxes/demo-dockview/src/app.tsx @@ -11,6 +11,28 @@ import * as ReactDOM from 'react-dom'; import { v4 } from 'uuid'; import './app.scss'; +function useLocalStorageItem(key: string, defaultValue: string): string { + const [item, setItem] = React.useState( + localStorage.getItem(key) + ); + + React.useEffect(() => { + const listener = (event: StorageEvent) => { + setItem(localStorage.getItem(key)); + }; + + window.addEventListener('storage', listener); + + setItem(localStorage.getItem(key)); + + return () => { + window.removeEventListener('storage', listener); + }; + }, [key]); + + return item === null ? defaultValue : item; +} + const components = { default: (props: IDockviewPanelProps<{ title: string }>) => { return
{props.params.title}
; @@ -196,8 +218,8 @@ const DockviewDemo = () => { title: 'Panel 6', position: { referencePanel: 'panel_4', direction: 'below' }, }); - panel6.group.locked = true; - panel6.group.header.hidden = true; + // panel6.group.locked = true; + // panel6.group.header.hidden = true; event.api.addPanel({ id: 'panel_7', component: 'default', @@ -211,18 +233,23 @@ const DockviewDemo = () => { position: { referencePanel: 'panel_7', direction: 'within' }, }); - event.api.addGroup(); + // event.api.addGroup(); event.api.getPanel('panel_1')!.api.setActive(); }; + const theme = useLocalStorageItem( + 'dv-theme-class-name', + 'dockview-theme-abyss' + ); + return ( ); }; diff --git a/packages/docs/src/components/ui/codeSandboxButton.tsx b/packages/docs/src/components/ui/codeSandboxButton.tsx index d536d0ad0..f17a3dec9 100644 --- a/packages/docs/src/components/ui/codeSandboxButton.tsx +++ b/packages/docs/src/components/ui/codeSandboxButton.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import './codeSandboxButton.scss'; +import { ThemePicker } from './container'; const BASE_SANDBOX_URL = 'https://codesandbox.io/s/github/mathuo/dockview/tree/master/packages/docs/sandboxes'; @@ -40,26 +41,29 @@ export const CodeSandboxButton = (props: { id: string }) => { }, [props.id]); return ( - - {`Open in `} - + + - {`Open in `} + - CodeSandbox - - - - + + CodeSandbox + + + + + ); }; diff --git a/packages/docs/src/components/ui/container.tsx b/packages/docs/src/components/ui/container.tsx index 5ad78067e..4011fb732 100644 --- a/packages/docs/src/components/ui/container.tsx +++ b/packages/docs/src/components/ui/container.tsx @@ -69,6 +69,49 @@ const JavascriptIcon = (props: { height: number; width: number }) => { ); }; +const themes = [ + 'dockview-theme-dark', + 'dockview-theme-light', + 'dockview-theme-vs', + 'dockview-theme-dracula', + 'dockview-theme-replit', +]; + +export const ThemePicker = () => { + const [theme, setTheme] = React.useState( + localStorage.getItem('dv-theme-class-name') || themes[0] + ); + + React.useEffect(() => { + localStorage.setItem('dv-theme-class-name', theme); + window.dispatchEvent(new StorageEvent('storage')); + }, [theme]); + + return ( +
+ {'Theme: '} + +
+ ); +}; + export const MultiFrameworkContainer = (props: { react: React.FC; typescript: (parent: HTMLElement) => { dispose: () => void }; @@ -183,6 +226,7 @@ export const MultiFrameworkContainer = (props: { +