diff --git a/packages/dockview-core/src/dnd/abstractDragHandler.ts b/packages/dockview-core/src/dnd/abstractDragHandler.ts index 84345c160..7ba701034 100644 --- a/packages/dockview-core/src/dnd/abstractDragHandler.ts +++ b/packages/dockview-core/src/dnd/abstractDragHandler.ts @@ -67,7 +67,7 @@ export abstract class DragHandler extends CompositeDisposable { * For example: in react-dnd if dataTransfer.types is not set then the dragStart event will be cancelled * through .preventDefault(). Since this is applied globally to all drag events this would break dockviews * dnd logic. You can see the code at - * https://github.com/react-dnd/react-dnd/blob/main/packages/backend-html5/src/HTML5BackendImpl.ts#L542 + P * https://github.com/react-dnd/react-dnd/blob/main/packages/backend-html5/src/HTML5BackendImpl.ts#L542 */ event.dataTransfer.setData('text/plain', ''); } @@ -75,7 +75,9 @@ export abstract class DragHandler extends CompositeDisposable { }), addDisposableListener(this.el, 'dragend', () => { this.pointerEventsDisposable.dispose(); - this.dataDisposable.dispose(); + setTimeout(() => { + this.dataDisposable.dispose(); // allow the data to be read by other handlers before disposing + }, 0); }) ); } diff --git a/packages/dockview-core/src/dnd/dropTragetAnchorContainer.ts b/packages/dockview-core/src/dnd/dropTragetAnchorContainer.ts new file mode 100644 index 000000000..5652a2424 --- /dev/null +++ b/packages/dockview-core/src/dnd/dropTragetAnchorContainer.ts @@ -0,0 +1,66 @@ +import { DropTargetTargetModel } from './droptarget'; + +export class DropTargetAnchorContainer { + private _model: + | { root: HTMLElement; overlay: HTMLElement; changed: boolean } + | undefined; + + private _outline: HTMLElement | undefined; + + get model(): DropTargetTargetModel { + return { + clear: () => { + this._model?.root.remove(); + this._model = undefined; + }, + exists: () => { + return !!this._model; + }, + getElements: (event?: DragEvent, outline?: HTMLElement) => { + const changed = this._outline !== outline; + this._outline = outline; + + if (this._model) { + this._model.changed = changed; + return this._model; + } + + const container = this.createContainer(); + const anchor = this.createAnchor(); + + this._model = { root: container, overlay: anchor, changed }; + + container.appendChild(anchor); + this.element.appendChild(container); + + if (event?.target instanceof HTMLElement) { + const targetBox = event.target.getBoundingClientRect(); + const box = this.element.getBoundingClientRect(); + console.log(box, targetBox); + + anchor.style.left = `${targetBox.left - box.left}px`; + anchor.style.top = `${targetBox.top - box.top}px`; + } + + return this._model; + }, + }; + } + + constructor(readonly element: HTMLElement) {} + + private createContainer(): HTMLElement { + const el = document.createElement('div'); + el.className = 'dv-drop-target-container'; + + return el; + } + + private createAnchor(): HTMLElement { + const el = document.createElement('div'); + el.className = 'dv-drop-target-anchor'; + el.style.visibility = 'hidden'; + + return el; + } +} diff --git a/packages/dockview-core/src/dnd/droptarget.scss b/packages/dockview-core/src/dnd/droptarget.scss index f23f318f7..b444c8d73 100644 --- a/packages/dockview-core/src/dnd/droptarget.scss +++ b/packages/dockview-core/src/dnd/droptarget.scss @@ -1,5 +1,6 @@ .dv-drop-target { position: relative; + --dv-transition-duration: 70ms; > .dv-drop-target-dropzone { position: absolute; @@ -15,10 +16,13 @@ box-sizing: border-box; height: 100%; width: 100%; + border: var(--dv-drag-over-border); background-color: var(--dv-drag-over-background-color); - transition: top 70ms ease-out, left 70ms ease-out, - width 70ms ease-out, height 70ms ease-out, - opacity 0.15s ease-out; + transition: top var(--dv-transition-duration) ease-out, + left var(--dv-transition-duration) ease-out, + width var(--dv-transition-duration) ease-out, + height var(--dv-transition-duration) ease-out, + opacity var(--dv-transition-duration) ease-out; will-change: transform; pointer-events: none; @@ -48,3 +52,27 @@ } } } + +.dv-drop-target-container { + position: absolute; + z-index: 9999; + top: 0px; + left: 0px; + height: 100%; + width: 100%; + pointer-events: none; + overflow: hidden; + --dv-transition-duration: 300ms; + + .dv-drop-target-anchor { + position: relative; + border: var(--dv-drag-over-border); + transition: opacity var(--dv-transition-duration) ease-in, + top var(--dv-transition-duration) ease-out, + left var(--dv-transition-duration) ease-out, + width var(--dv-transition-duration) ease-out, + height var(--dv-transition-duration) ease-out; + background-color: var(--dv-drag-over-background-color); + opacity: 1; + } +} diff --git a/packages/dockview-core/src/dnd/droptarget.ts b/packages/dockview-core/src/dnd/droptarget.ts index 702fed867..ca89d4e9b 100644 --- a/packages/dockview-core/src/dnd/droptarget.ts +++ b/packages/dockview-core/src/dnd/droptarget.ts @@ -93,10 +93,26 @@ const DEFAULT_SIZE: MeasuredValue = { const SMALL_WIDTH_BOUNDARY = 100; const SMALL_HEIGHT_BOUNDARY = 100; +export interface DropTargetTargetModel { + getElements( + event?: DragEvent, + outline?: HTMLElement + ): { + root: HTMLElement; + overlay: HTMLElement; + changed: boolean; + }; + exists(): boolean; + clear(): void; +} + export interface DroptargetOptions { canDisplayOverlay: CanDisplayOverlay; acceptedTargetZones: Position[]; overlayModel?: DroptargetOverlayModel; + getOverrideTarget?: () => DropTargetTargetModel; + className?: string; + getOverlayOutline?: () => HTMLElement | null; } export class Droptarget extends CompositeDisposable { @@ -116,6 +132,18 @@ export class Droptarget extends CompositeDisposable { private static USED_EVENT_ID = '__dockview_droptarget_event_is_used__'; + private static ACTUAL_TARGET: Droptarget | undefined; + + private _disabled: boolean; + + get disabled(): boolean { + return this._disabled; + } + + set disabled(value: boolean) { + this._disabled = value; + } + get state(): Position | undefined { return this._state; } @@ -126,21 +154,33 @@ export class Droptarget extends CompositeDisposable { ) { super(); + this._disabled = false; + // use a set to take advantage of #.has this._acceptedTargetZonesSet = new Set( this.options.acceptedTargetZones ); this.dnd = new DragAndDropObserver(this.element, { - onDragEnter: () => undefined, + onDragEnter: () => { + this.options.getOverrideTarget?.()?.getElements(); + }, onDragOver: (e) => { + Droptarget.ACTUAL_TARGET = this; + if (this._acceptedTargetZonesSet.size === 0) { + if (this.options.getOverrideTarget) { + return; + } this.removeDropTarget(); return; } - const width = this.element.clientWidth; - const height = this.element.clientHeight; + const target = + this.options.getOverlayOutline?.() ?? this.element; + + const width = target.clientWidth; + const height = target.clientHeight; if (width === 0 || height === 0) { return; // avoid div!0 @@ -172,6 +212,9 @@ export class Droptarget extends CompositeDisposable { } if (!this.options.canDisplayOverlay(e, quadrant)) { + if (this.options.getOverrideTarget) { + return; + } this.removeDropTarget(); return; } @@ -194,7 +237,9 @@ export class Droptarget extends CompositeDisposable { this.markAsUsed(e); - if (!this.targetElement) { + if (this.options.getOverrideTarget) { + // + } else if (!this.targetElement) { this.targetElement = document.createElement('div'); this.targetElement.className = 'dv-drop-target-dropzone'; this.overlayElement = document.createElement('div'); @@ -202,8 +247,16 @@ export class Droptarget extends CompositeDisposable { this._state = 'center'; this.targetElement.appendChild(this.overlayElement); - this.element.classList.add('dv-drop-target'); - this.element.append(this.targetElement); + target.classList.add('dv-drop-target'); + target.append(this.targetElement); + + this.overlayElement.style.opacity = '0'; + + requestAnimationFrame(() => { + if (this.overlayElement) { + this.overlayElement.style.opacity = ''; + } + }); } this.toggleClasses(quadrant, width, height); @@ -211,10 +264,31 @@ export class Droptarget extends CompositeDisposable { this._state = quadrant; }, onDragLeave: () => { + if (this.options.getOverrideTarget) { + return; + } + this.removeDropTarget(); }, - onDragEnd: () => { + onDragEnd: (e) => { + if ( + this.options.getOverrideTarget && + Droptarget.ACTUAL_TARGET === this + ) { + if (this._state) { + // only stop the propagation of the event if we are dealing with it + // which is only when the target has state + e.stopPropagation(); + this._onDrop.fire({ + position: this._state, + nativeEvent: e, + }); + } + } + this.removeDropTarget(); + + this.options.getOverrideTarget?.().clear(); }, onDrop: (e) => { e.preventDefault(); @@ -223,6 +297,8 @@ export class Droptarget extends CompositeDisposable { this.removeDropTarget(); + this.options.getOverrideTarget?.().clear(); + if (state) { // only stop the propagation of the event if we are dealing with it // which is only when the target has state @@ -268,7 +344,7 @@ export class Droptarget extends CompositeDisposable { width: number, height: number ): void { - if (!this.overlayElement) { + if (!this.options.getOverrideTarget && !this.overlayElement) { return; } @@ -300,6 +376,105 @@ export class Droptarget extends CompositeDisposable { } } + if (this.options.getOverrideTarget) { + const outlineEl = + this.options.getOverlayOutline?.() ?? this.element; + const elBox = outlineEl.getBoundingClientRect(); + + const ta = this.options + .getOverrideTarget?.() + .getElements(undefined, outlineEl); + const el = ta.root; + const overlay = ta.overlay; + + const bigbox = el.getBoundingClientRect(); + + const rootTop = elBox.top - bigbox.top; + const rootLeft = elBox.left - bigbox.left; + + const box = { + top: rootTop, + left: rootLeft, + width: width, + height: height, + }; + + if (rightClass) { + box.left = rootLeft + width * (1 - size); + box.width = width * size; + } else if (leftClass) { + box.width = width * size; + } else if (topClass) { + box.height = height * size; + } else if (bottomClass) { + box.top = rootTop + height * (1 - size); + box.height = height * size; + } + + if (isSmallX && isLeft) { + box.width = 4; + } + if (isSmallX && isRight) { + box.left = rootLeft + width - 4; + box.width = 4; + } + + const topPx = `${Math.round(box.top)}px`; + const leftPx = `${Math.round(box.left)}px`; + const widthPx = `${Math.round(box.width)}px`; + const heightPx = `${Math.round(box.height)}px`; + + if ( + overlay.style.top === topPx && + overlay.style.left === leftPx && + overlay.style.width === widthPx && + overlay.style.height === heightPx + ) { + return; + } + + overlay.style.top = topPx; + overlay.style.left = leftPx; + overlay.style.width = widthPx; + overlay.style.height = heightPx; + overlay.style.visibility = 'visible'; + + overlay.className = `dv-drop-target-anchor${ + this.options.className ? ` ${this.options.className}` : '' + }`; + + toggleClass(overlay, 'dv-drop-target-left', isLeft); + toggleClass(overlay, 'dv-drop-target-right', isRight); + toggleClass(overlay, 'dv-drop-target-top', isTop); + toggleClass(overlay, 'dv-drop-target-bottom', isBottom); + toggleClass( + overlay, + 'dv-drop-target-center', + quadrant === 'center' + ); + + if (ta.changed) { + toggleClass( + overlay, + 'dv-drop-target-anchor-container-changed', + true + ); + setTimeout(() => { + toggleClass( + overlay, + 'dv-drop-target-anchor-container-changed', + false + ); + }, 10); + } + + return; + } + + if (!this.overlayElement) { + return; + } + const box = { top: '0px', left: '0px', width: '100%', height: '100%' }; /** @@ -396,10 +571,12 @@ export class Droptarget extends CompositeDisposable { private removeDropTarget(): void { if (this.targetElement) { this._state = undefined; - this.element.removeChild(this.targetElement); + this.targetElement.parentElement?.classList.remove( + 'dv-drop-target' + ); + this.targetElement.remove(); this.targetElement = undefined; this.overlayElement = undefined; - this.element.classList.remove('dv-drop-target'); } } } diff --git a/packages/dockview-core/src/dnd/ghost.ts b/packages/dockview-core/src/dnd/ghost.ts index 2ff9c569f..df976c7cf 100644 --- a/packages/dockview-core/src/dnd/ghost.ts +++ b/packages/dockview-core/src/dnd/ghost.ts @@ -2,13 +2,14 @@ import { addClasses, removeClasses } from '../dom'; export function addGhostImage( dataTransfer: DataTransfer, - ghostElement: HTMLElement + ghostElement: HTMLElement, + options?: { x?: number; y?: number } ): void { // class dockview provides to force ghost image to be drawn on a different layer and prevent weird rendering issues addClasses(ghostElement, 'dv-dragged'); document.body.appendChild(ghostElement); - dataTransfer.setDragImage(ghostElement, 0, 0); + dataTransfer.setDragImage(ghostElement, options?.x ?? 0, options?.y ?? 0); setTimeout(() => { removeClasses(ghostElement, 'dv-dragged'); diff --git a/packages/dockview-core/src/dnd/groupDragHandler.ts b/packages/dockview-core/src/dnd/groupDragHandler.ts index bdda2be3b..2e3c9d281 100644 --- a/packages/dockview-core/src/dnd/groupDragHandler.ts +++ b/packages/dockview-core/src/dnd/groupDragHandler.ts @@ -72,9 +72,11 @@ export class GroupDragHandler extends DragHandler { ghostElement.style.lineHeight = '20px'; ghostElement.style.borderRadius = '12px'; ghostElement.style.position = 'absolute'; + ghostElement.style.pointerEvents = 'none'; + ghostElement.style.top = '-9999px'; ghostElement.textContent = `Multiple Panels (${this.group.size})`; - addGhostImage(dataTransfer, ghostElement); + addGhostImage(dataTransfer, ghostElement, { y: -10, x: 30 }); } return { diff --git a/packages/dockview-core/src/dockview/components/panel/content.ts b/packages/dockview-core/src/dockview/components/panel/content.ts index 4f66b03d3..1b29da08b 100644 --- a/packages/dockview-core/src/dockview/components/panel/content.ts +++ b/packages/dockview-core/src/dockview/components/panel/content.ts @@ -55,7 +55,13 @@ export class ContentContainer this.addDisposables(this._onDidFocus, this._onDidBlur); + const target = group.dropTargetContainer; + this.dropTarget = new Droptarget(this.element, { + getOverlayOutline: () => { + return target ? this.element.parentElement : null; + }, + className: 'dv-drop-target-content', acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'], canDisplayOverlay: (event, position) => { if ( @@ -76,26 +82,12 @@ export class ContentContainer } if (data && data.viewId === this.accessor.id) { - if (data.groupId === this.group.id) { - if (position === 'center') { - // don't allow to drop on self for center position - return false; - } - if (data.panelId === null) { - // don't allow group move to drop anywhere on self - return false; - } - } - - const groupHasOnePanelAndIsActiveDragElement = - this.group.panels.length === 1 && - data.groupId === this.group.id; - - return !groupHasOnePanelAndIsActiveDragElement; + return true; } return this.group.canDisplayOverlay(event, position, 'content'); }, + getOverrideTarget: target ? () => target.model : undefined, }); this.addDisposables(this.dropTarget); diff --git a/packages/dockview-core/src/dockview/components/tab/defaultTab.scss b/packages/dockview-core/src/dockview/components/tab/defaultTab.scss index 0fdf53d78..3d2865583 100644 --- a/packages/dockview-core/src/dockview/components/tab/defaultTab.scss +++ b/packages/dockview-core/src/dockview/components/tab/defaultTab.scss @@ -58,15 +58,13 @@ position: relative; height: 100%; display: flex; - min-width: 80px; align-items: center; - padding: 0px 8px; white-space: nowrap; text-overflow: ellipsis; .dv-default-tab-content { - padding: 0px 8px; flex-grow: 1; + margin-right: 4px; } .dv-default-tab-action { diff --git a/packages/dockview-core/src/dockview/components/tab/tab.ts b/packages/dockview-core/src/dockview/components/tab/tab.ts index 1eb1174d8..849b550a9 100644 --- a/packages/dockview-core/src/dockview/components/tab/tab.ts +++ b/packages/dockview-core/src/dockview/components/tab/tab.ts @@ -16,6 +16,7 @@ import { } from '../../../dnd/droptarget'; import { DragHandler } from '../../../dnd/abstractDragHandler'; import { IDockviewPanel } from '../../dockviewPanel'; +import { addGhostImage } from '../../../dnd/ghost'; class TabDragHandler extends DragHandler { private readonly panelTransfer = @@ -85,8 +86,11 @@ export class Tab extends CompositeDisposable { this.panel ); + const target = group.model.dropTargetContainer; + this.dropTarget = new Droptarget(this._element, { - acceptedTargetZones: ['center'], + acceptedTargetZones: ['left', 'right'], + overlayModel: { activationSize: { value: 50, type: 'percentage' } }, canDisplayOverlay: (event, position) => { if (this.group.locked) { return false; @@ -95,15 +99,7 @@ export class Tab extends CompositeDisposable { const data = getPanelData(); if (data && this.accessor.id === data.viewId) { - if ( - data.panelId === null && - data.groupId === this.group.id - ) { - // don't allow group move to drop on self - return false; - } - - return this.panel.id !== data.panelId; + return true; } return this.group.model.canDisplayOverlay( @@ -112,6 +108,7 @@ export class Tab extends CompositeDisposable { 'tab' ); }, + getOverrideTarget: target ? () => target.model : undefined, }); this.onWillShowOverlay = this.dropTarget.onWillShowOverlay; @@ -121,6 +118,23 @@ export class Tab extends CompositeDisposable { this._onDropped, this._onDragStart, dragHandler.onDragStart((event) => { + if (event.dataTransfer) { + const style = getComputedStyle(this.element); + const newNode = this.element.cloneNode(true) as HTMLElement; + Array.from(style).forEach((key) => + newNode.style.setProperty( + key, + style.getPropertyValue(key), + style.getPropertyPriority(key) + ) + ); + newNode.style.position = 'absolute'; + + addGhostImage(event.dataTransfer, newNode, { + y: -10, + x: 30, + }); + } this._onDragStart.fire(event); }), dragHandler, diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabs.scss b/packages/dockview-core/src/dockview/components/titlebar/tabs.scss new file mode 100644 index 000000000..e9faba197 --- /dev/null +++ b/packages/dockview-core/src/dockview/components/titlebar/tabs.scss @@ -0,0 +1,64 @@ +.dv-tabs-container { + display: flex; + overflow-x: overlay; + overflow-y: hidden; + + scrollbar-width: thin; // firefox + + &::-webkit-scrollbar { + height: 3px; + } + + /* Track */ + &::-webkit-scrollbar-track { + background: transparent; + } + + /* Handle */ + &::-webkit-scrollbar-thumb { + background: var(--dv-tabs-container-scrollbar-color); + } + + .dv-tab { + -webkit-user-drag: element; + outline: none; + padding: 0.25rem 0.5rem; + cursor: pointer; + position: relative; + box-sizing: border-box; + + &:not(:first-child)::before { + content: ' '; + position: absolute; + top: 0; + left: 0; + z-index: 5; + pointer-events: none; + background-color: var(--dv-tab-divider-color); + width: 1px; + height: 100%; + } + } + + &.dv-tabs-overflow-container { + flex-direction: column; + height: unset; + + .dv-tab { + height: var(--dv-tabs-and-actions-container-height); + } + + .dv-active-tab { + background-color: var( + --dv-activegroup-visiblepanel-tab-background-color + ); + color: var(--dv-activegroup-visiblepanel-tab-color); + } + .dv-inactive-tab { + background-color: var( + --dv-activegroup-hiddenpanel-tab-background-color + ); + color: var(--dv-activegroup-hiddenpanel-tab-color); + } + } +} diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.scss b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.scss index fef520e03..c4df44574 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.scss +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.scss @@ -7,17 +7,17 @@ font-size: var(--dv-tabs-and-actions-container-font-size); &.dv-single-tab.dv-full-width-single-tab { - .dv-tabs-container { - flex-grow: 1; - - .dv-tab { + .dv-tabs-container { flex-grow: 1; - } - } - .dv-void-container { - flex-grow: 0; - } + .dv-tab { + flex-grow: 1; + } + } + + .dv-void-container { + flex-grow: 0; + } } .dv-void-container { @@ -50,7 +50,7 @@ .dv-tab { -webkit-user-drag: element; outline: none; - min-width: 75px; + padding: 0.25rem 0.5rem; cursor: pointer; position: relative; box-sizing: border-box; diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts index d3bd0568b..ea3b7fb33 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts @@ -10,7 +10,10 @@ import { VoidContainer } from './voidContainer'; import { toggleClass } from '../../../dom'; import { DockviewPanel, IDockviewPanel } from '../../dockviewPanel'; import { DockviewComponent } from '../../dockviewComponent'; -import { WillShowOverlayLocationEvent } from '../../dockviewGroupPanelModel'; +import { + DockviewGroupPanelModel, + WillShowOverlayLocationEvent, +} from '../../dockviewGroupPanelModel'; import { getPanelData } from '../../../dnd/dataTransfer'; export interface TabDropIndexEvent { @@ -171,7 +174,8 @@ export class TabsContainer constructor( private readonly accessor: DockviewComponent, - private readonly group: DockviewGroupPanel + private readonly group: DockviewGroupPanel, + model: DockviewGroupPanelModel ) { super(); @@ -196,7 +200,11 @@ export class TabsContainer this.tabContainer = document.createElement('div'); this.tabContainer.className = 'dv-tabs-container'; - this.voidContainer = new VoidContainer(this.accessor, this.group); + this.voidContainer = new VoidContainer( + this.accessor, + this.group, + model + ); this._element.appendChild(this.preActionsContainer); this._element.appendChild(this.tabContainer); diff --git a/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts index 6e9ea0c47..3b8d96e24 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts @@ -1,4 +1,3 @@ -import { last } from '../../../array'; import { getPanelData } from '../../../dnd/dataTransfer'; import { Droptarget, @@ -10,6 +9,7 @@ import { DockviewComponent } from '../../dockviewComponent'; import { addDisposableListener, Emitter, Event } from '../../../events'; import { CompositeDisposable } from '../../../lifecycle'; import { DockviewGroupPanel } from '../../dockviewGroupPanel'; +import { DockviewGroupPanelModel } from '../../dockviewGroupPanelModel'; export class VoidContainer extends CompositeDisposable { private readonly _element: HTMLElement; @@ -29,7 +29,8 @@ export class VoidContainer extends CompositeDisposable { constructor( private readonly accessor: DockviewComponent, - private readonly group: DockviewGroupPanel + private readonly group: DockviewGroupPanel, + model: DockviewGroupPanelModel ) { super(); @@ -48,22 +49,15 @@ export class VoidContainer extends CompositeDisposable { const handler = new GroupDragHandler(this._element, accessor, group); + const target = model.dropTargetContainer; + this.dropTraget = new Droptarget(this._element, { acceptedTargetZones: ['center'], canDisplayOverlay: (event, position) => { const data = getPanelData(); if (data && this.accessor.id === data.viewId) { - if ( - data.panelId === null && - data.groupId === this.group.id - ) { - // don't allow group move to drop on self - return false; - } - - // don't show the overlay if the tab being dragged is the last panel of this group - return last(this.group.panels)?.id !== data.panelId; + return true; } return group.model.canDisplayOverlay( @@ -72,6 +66,7 @@ export class VoidContainer extends CompositeDisposable { 'header_space' ); }, + getOverrideTarget: target ? () => target.model : undefined, }); this.onWillShowOverlay = this.dropTraget.onWillShowOverlay; diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 0dde93454..eb46079ca 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -9,6 +9,7 @@ import { directionToPosition, Droptarget, DroptargetOverlayModel, + DropTargetTargetModel, Position, } from '../dnd/droptarget'; import { tail, sequenceEquals, remove } from '../array'; @@ -74,6 +75,7 @@ import { } from '../overlay/overlayRenderContainer'; import { PopoutWindow } from '../popoutWindow'; import { StrictEventsSequencing } from './strictEventsSequencing'; +import { DropTargetAnchorContainer } from '../dnd/dropTragetAnchorContainer'; const DEFAULT_ROOT_OVERLAY_MODEL: DroptargetOverlayModel = { activationSize: { type: 'pixels', value: 10 }, @@ -368,6 +370,12 @@ export class DockviewComponent return this._floatingGroups; } + private _rootDropTargetContainer: DropTargetAnchorContainer | null = null; + + get rootDropTargetContainer(): DropTargetAnchorContainer | null { + return this._rootDropTargetContainer; + } + constructor(container: HTMLElement, options: DockviewComponentOptions) { super(container, { proportionalLayout: true, @@ -381,6 +389,8 @@ export class DockviewComponent className: options.className, }); + this.updateDropTargetModel(options); + this.overlayRenderContainer = new OverlayRenderContainer( this.gridview.element, this @@ -465,7 +475,10 @@ export class DockviewComponent this._options = options; + const target = this.rootDropTargetContainer; + this._rootDropTarget = new Droptarget(this.element, { + className: 'dv-drop-target-edge', canDisplayOverlay: (event, position) => { const data = getPanelData(); @@ -506,6 +519,7 @@ export class DockviewComponent acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'], overlayModel: this.options.rootOverlayModel ?? DEFAULT_ROOT_OVERLAY_MODEL, + getOverrideTarget: target ? () => target.model : undefined, }); this.addDisposables( @@ -756,6 +770,14 @@ export class DockviewComponent popoutContainer.appendChild(group.element); + const anchor = document.createElement('div'); + const dropTragetContainer = new DropTargetAnchorContainer( + anchor + ); + popoutContainer.appendChild(anchor); + + group.model.dropTargetContainer = dropTragetContainer; + group.model.location = { type: 'popout', getWindow: () => _window.window!, @@ -844,6 +866,8 @@ export class DockviewComponent } else if (this.getPanel(group.id)) { group.model.renderContainer = this.overlayRenderContainer; + group.model.dropTargetContainer = + this.rootDropTargetContainer; returnedGroup = group; const alreadyRemoved = !this._popoutGroups.find( @@ -1158,11 +1182,7 @@ export class DockviewComponent } } - if ('rootOverlayModel' in options) { - this._rootDropTarget.setOverlayModel( - options.rootOverlayModel ?? DEFAULT_ROOT_OVERLAY_MODEL - ); - } + this.updateDropTargetModel(options); if ('gap' in options) { this.gridview.margin = options.gap ?? 0; @@ -2404,9 +2424,11 @@ export class DockviewComponent if (this._moving) { return; } + if (event.panel !== this.activePanel) { return; } + if (this._onDidActivePanelChange.value !== event.panel) { this._onDidActivePanelChange.fire(event.panel); } @@ -2489,4 +2511,39 @@ export class DockviewComponent ? rootOrientation : orthogonal(rootOrientation); } + + private updateDropTargetModel(options: Partial) { + if ('dndEdges' in options) { + this._rootDropTarget.disabled = + typeof options.dndEdges === 'boolean' && + options.dndEdges === false; + + if (typeof options.dndEdges === 'object') { + this._rootDropTarget.setOverlayModel(options.dndEdges); + } else { + this._rootDropTarget.setOverlayModel( + DEFAULT_ROOT_OVERLAY_MODEL + ); + } + } + + if ('rootOverlayModel' in options) { + this._rootDropTarget.setOverlayModel( + options.rootOverlayModel ?? DEFAULT_ROOT_OVERLAY_MODEL + ); + } + + if ('dndOverlayMode' in options) { + switch (options.dndOverlayMode) { + case 'static': + this._rootDropTargetContainer = null; + break; + case 'transitional': + default: + this._rootDropTargetContainer = + new DropTargetAnchorContainer(this.element); + break; + } + } + } } diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index a34d5ef10..7c5e8ae56 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -39,6 +39,7 @@ import { import { OverlayRenderContainer } from '../overlay/overlayRenderContainer'; import { TitleEvent } from '../api/dockviewPanelApi'; import { Contraints } from '../gridview/gridviewPanel'; +import { DropTargetAnchorContainer } from '../dnd/dropTragetAnchorContainer'; interface GroupMoveEvent { groupId: string; @@ -265,6 +266,8 @@ export class DockviewGroupPanelModel private mostRecentlyUsed: IDockviewPanel[] = []; private _overwriteRenderContainer: OverlayRenderContainer | null = null; + private _overwriteDropTargetContainer: DropTargetAnchorContainer | null = + null; private readonly _onDidChange = new Emitter(); readonly onDidChange: Event = @@ -441,7 +444,11 @@ export class DockviewGroupPanelModel this._api = new DockviewApi(this.accessor); - this.tabsContainer = new TabsContainer(this.accessor, this.groupPanel); + this.tabsContainer = new TabsContainer( + this.accessor, + this.groupPanel, + this + ); this.contentContainer = new ContentContainer(this.accessor, this); @@ -535,6 +542,17 @@ export class DockviewGroupPanelModel ); } + set dropTargetContainer(value: DropTargetAnchorContainer | null) { + this._overwriteDropTargetContainer = value; + } + + get dropTargetContainer(): DropTargetAnchorContainer | null { + return ( + this._overwriteDropTargetContainer ?? + this.accessor.rootDropTargetContainer + ); + } + initialize(): void { if (this.options.panels) { this.options.panels.forEach((panel) => { @@ -1049,6 +1067,29 @@ export class DockviewGroupPanelModel const data = getPanelData(); if (data && data.viewId === this.accessor.id) { + if (type === 'content') { + if (data.groupId === this.id) { + // don't allow to drop on self for center position + + if (position === 'center') { + return; + } + + if (data.panelId === null) { + // don't allow group move to drop anywhere on self + return; + } + } + } + + if (type === 'header') { + if (data.groupId === this.id) { + if (data.panelId === null) { + return; + } + } + } + if (data.panelId === null) { // this is a group move dnd event const { groupId } = data; diff --git a/packages/dockview-core/src/dockview/options.ts b/packages/dockview-core/src/dockview/options.ts index 3f7b94367..046efbc81 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -52,9 +52,18 @@ export interface DockviewOptions { popoutUrl?: string; defaultRenderer?: DockviewPanelRenderer; debug?: boolean; + // #start dnd + dndEdges?: false | DroptargetOverlayModel; + dndOverlayMode?: 'transitional' | 'static'; + dndContentOverlayIncludesHeader?: boolean; + /** + * @deprecated use `dndEdges` instead. Will be removed in a future version. + * */ rootOverlayModel?: DroptargetOverlayModel; - locked?: boolean; disableDnd?: boolean; + // #end dnd + locked?: boolean; + className?: string; /** * Pixel gap between groups @@ -109,6 +118,9 @@ export const PROPERTY_KEYS_DOCKVIEW: (keyof DockviewOptions)[] = (() => { gap: undefined, className: undefined, noPanelsOverlay: undefined, + dndEdges: undefined, + dndOverlayMode: undefined, + dndContentOverlayIncludesHeader: undefined, }; return Object.keys(properties) as (keyof DockviewOptions)[]; diff --git a/packages/dockview-core/src/theme.scss b/packages/dockview-core/src/theme.scss index 5eb3f7442..63a019cdc 100644 --- a/packages/dockview-core/src/theme.scss +++ b/packages/dockview-core/src/theme.scss @@ -10,8 +10,20 @@ --dv-overlay-z-index: 999; } +@mixin dockview-drop-target-no-travel { + .dv-drop-target-container { + .dv-drop-target-anchor { + &.dv-drop-target-anchor-container-changed { + opacity: 0; + transition: none; + } + } + } +} + @mixin dockview-theme-dark-mixin { @include dockview-theme-core-mixin(); + @include dockview-drop-target-no-travel(); // --dv-group-view-background-color: #1e1e1e; @@ -35,6 +47,8 @@ @mixin dockview-theme-light-mixin { @include dockview-theme-core-mixin(); + @include dockview-drop-target-no-travel(); + // --dv-group-view-background-color: white; // @@ -131,6 +145,8 @@ @mixin dockview-theme-abyss-mixin { @include dockview-theme-core-mixin(); + @include dockview-drop-target-no-travel(); + // --dv-group-view-background-color: #000c18; // @@ -155,6 +171,8 @@ @mixin dockview-theme-dracula-mixin { @include dockview-theme-core-mixin(); + @include dockview-drop-target-no-travel(); + // --dv-group-view-background-color: #282a36; // @@ -229,6 +247,8 @@ } @mixin dockview-design-replit-mixin { + @include dockview-drop-target-no-travel(); + .dv-resize-container:has(> .dv-groupview) { border-radius: 8px; } @@ -342,3 +362,130 @@ --dv-separator-handle-background-color: #cfd1d3; --dv-separator-handle-hover-background-color: #babbbb; } + +@mixin dockview-design-kraken-mixin { + .dv-resize-container:has(> .dv-groupview) { + border-radius: 8px; + } + + .dv-drop-target-anchor { + &.dv-drop-target-content { + border-radius: 20px; + } + } + + .dv-groupview { + overflow: hidden; + border-radius: 20px; + + .dv-tabs-and-actions-container { + .dv-tab { + border-radius: 8px; + font-size: 12px; + + margin: 0.5rem 0; + height: 1.75rem; + + &:first { + margin-left: 0.5rem; + } + + &:not(:nth-last-child(1)) { + margin-right: 0.25rem; + } + .dv-svg { + height: 8px; + width: 8px; + } + } + } + + .dv-content-container { + background-color: #16121f; + } + } + + .vertical > .sash-container > .sash { + &:not(.disabled) { + &::after { + content: ''; + height: 4px; + width: 100%; + border-radius: 2px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: transparent; + position: absolute; + } + + &:hover { + &::after { + background-color: var( + --dv-separator-handle-hover-background-color + ); + } + } + } + } + + .dv-horizontal > .dv-sash-container > .dv-sash { + &:not(.disabled) { + &::after { + content: ''; + height: 100%; + 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-kraken { + @include dockview-theme-core-mixin(); + @include dockview-design-kraken-mixin(); + + // + --dv-drag-over-border: 2px solid rgb(91, 30, 207); + --dv-drag-over-background-color: ''; + // + --dv-tabs-and-actions-container-height: 44px; + // + --dv-group-view-background-color: rgb(11, 6, 17); + // + --dv-tabs-and-actions-container-background-color: #16121f; + // + --dv-activegroup-visiblepanel-tab-background-color: #2a2837; + --dv-activegroup-hiddenpanel-tab-background-color: #201d2b; + --dv-inactivegroup-visiblepanel-tab-background-color: #2a2837; + --dv-inactivegroup-hiddenpanel-tab-background-color: #201d2b; + --dv-tab-divider-color: transparent; + // + --dv-activegroup-visiblepanel-tab-color: white; + --dv-activegroup-hiddenpanel-tab-color: rgb(148, 151, 169); + --dv-inactivegroup-visiblepanel-tab-color: white; + --dv-inactivegroup-hiddenpanel-tab-color: rgb(148, 151, 169); + // + --dv-separator-border: transparent; + --dv-paneview-header-border-color: rgb(51, 51, 51); + + ///// + --dv-separator-handle-background-color: transparent; + --dv-separator-handle-hover-background-color: rgb(91, 30, 207); + + padding: 10px; + background-color: rgb(11, 6, 17); +} diff --git a/packages/dockview/src/svg.tsx b/packages/dockview/src/svg.tsx index 76143411a..eccf52bb6 100644 --- a/packages/dockview/src/svg.tsx +++ b/packages/dockview/src/svg.tsx @@ -7,7 +7,7 @@ export const CloseButton = () => ( viewBox="0 0 28 28" aria-hidden={'false'} focusable={false} - className="dockview-svg" + className="dv-svg" > @@ -21,7 +21,7 @@ export const ExpandMore = () => { viewBox="0 0 24 15" aria-hidden={'false'} focusable={false} - className="dockview-svg" + className="dv-svg" > diff --git a/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.scss b/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.scss index 57549c075..2f7a940fc 100644 --- a/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.scss +++ b/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.scss @@ -11,6 +11,7 @@ &:hover { border-radius: 2px; + color: var(--dv-activegroup-visiblepanel-tab-color); background-color: var(--dv-icon-hover-background-color); } } diff --git a/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.tsx b/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.tsx index e1ccaef0f..4727fbff9 100644 --- a/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.tsx +++ b/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.tsx @@ -389,6 +389,7 @@ const DockviewDemo = (props: { theme?: string }) => { watermarkComponent={ watermark ? WatermarkComponent : undefined } + gap={10} onReady={onReady} className={props.theme || 'dockview-theme-abyss'} /> diff --git a/packages/docs/sandboxes/react/dockview/demo-dockview/src/controls.tsx b/packages/docs/sandboxes/react/dockview/demo-dockview/src/controls.tsx index 63032b5f4..c9fd5e19f 100644 --- a/packages/docs/sandboxes/react/dockview/demo-dockview/src/controls.tsx +++ b/packages/docs/sandboxes/react/dockview/demo-dockview/src/controls.tsx @@ -81,7 +81,7 @@ export const RightControls = (props: IDockviewHeaderActionsProps) => { alignItems: 'center', padding: '0px 8px', height: '100%', - color: 'var(--dv-activegroup-visiblepanel-tab-color)', + color: 'var(--dv-activegroup-hiddenpanel-tab-color)', }} > {props.isGroupActive && } diff --git a/packages/docs/sandboxes/react/dockview/demo-dockview/src/gridActions.tsx b/packages/docs/sandboxes/react/dockview/demo-dockview/src/gridActions.tsx index 40e57b2fa..c0c95721c 100644 --- a/packages/docs/sandboxes/react/dockview/demo-dockview/src/gridActions.tsx +++ b/packages/docs/sandboxes/react/dockview/demo-dockview/src/gridActions.tsx @@ -151,10 +151,17 @@ export const GridActions = (props: { props.api?.addGroup(); }; - const [gap, setGap] = React.useState(0); + const [gap, setGap] = React.useState(undefined); React.useEffect(() => { - props.api?.setGap(gap); + if (!props.api) { + return; + } + if (typeof gap === 'number') { + props.api.setGap(gap); + } else { + setGap(props.api.gap); + } }, [gap, props.api]); return ( @@ -212,7 +219,7 @@ export const GridActions = (props: { min={0} max={99} step={1} - value={gap} + value={gap ?? 0} onChange={(event) => setGap(Number(event.target.value))} /> diff --git a/packages/docs/src/components/ui/codeSandboxButton.scss b/packages/docs/src/components/ui/codeSandboxButton.scss index 8c31be3b8..5fda1291b 100644 --- a/packages/docs/src/components/ui/codeSandboxButton.scss +++ b/packages/docs/src/components/ui/codeSandboxButton.scss @@ -28,7 +28,7 @@ } } -.dockview-svg { +.dv-svg { display: inline-block; fill: currentcolor; line-height: 1; diff --git a/packages/docs/src/components/ui/codeSandboxButton.tsx b/packages/docs/src/components/ui/codeSandboxButton.tsx index a68aa34d7..c2c6e02ff 100644 --- a/packages/docs/src/components/ui/codeSandboxButton.tsx +++ b/packages/docs/src/components/ui/codeSandboxButton.tsx @@ -17,7 +17,7 @@ const createSvgElementFromPath = (params: { width={params.width} viewBox={params.viewbox} focusable={false} - className={'dockview-svg'} + className={'dv-svg'} > @@ -54,7 +54,7 @@ export const CodeSandboxButton = (props: { { return ( { const JavascriptIcon = (props: { height: number; width: number }) => { return ( { return (