diff --git a/packages/dockview-core/src/__tests__/dockview/components/tab.spec.ts b/packages/dockview-core/src/__tests__/dockview/components/tab.spec.ts index cb246b598..049fa1c5c 100644 --- a/packages/dockview-core/src/__tests__/dockview/components/tab.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/components/tab.spec.ts @@ -8,6 +8,7 @@ import { DockviewGroupPanel } from '../../../dockview/dockviewGroupPanel'; import { DockviewGroupPanelModel } from '../../../dockview/dockviewGroupPanelModel'; import { Tab } from '../../../dockview/components/tab/tab'; import { IDockviewPanel } from '../../../dockview/dockviewPanel'; +import { fromPartial } from '@total-typescript/shoehorn'; describe('tab', () => { test('that empty tab has inactive-tab class', () => { @@ -46,15 +47,10 @@ describe('tab', () => { id: 'testcomponentid', }; }); - const groupviewMock = jest.fn, []>( - () => { - return { - canDisplayOverlay: jest.fn(), - }; - } - ); - const groupView = new groupviewMock() as DockviewGroupPanelModel; + const groupView = fromPartial({ + canDisplayOverlay: jest.fn(), + }); const groupPanelMock = jest.fn, []>(() => { return { @@ -82,28 +78,23 @@ describe('tab', () => { fireEvent.dragEnter(cut.element); fireEvent.dragOver(cut.element); - expect(groupView.canDisplayOverlay).toBeCalled(); + expect(groupView.canDisplayOverlay).toHaveBeenCalled(); expect( cut.element.getElementsByClassName('dv-drop-target-dropzone').length ).toBe(0); }); - test('that if you drag over yourself no drop target is shown', () => { + test('that if you drag over yourself a drop target is shown', () => { const accessorMock = jest.fn, []>(() => { return { id: 'testcomponentid', }; }); - const groupviewMock = jest.fn, []>( - () => { - return { - canDisplayOverlay: jest.fn(), - }; - } - ); - const groupView = new groupviewMock() as DockviewGroupPanelModel; + const groupView = fromPartial({ + canDisplayOverlay: jest.fn(), + }); const groupPanelMock = jest.fn, []>(() => { return { @@ -136,11 +127,11 @@ describe('tab', () => { fireEvent.dragEnter(cut.element); fireEvent.dragOver(cut.element); - expect(groupView.canDisplayOverlay).toBeCalledTimes(0); + expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0); expect( cut.element.getElementsByClassName('dv-drop-target-dropzone').length - ).toBe(0); + ).toBe(1); }); test('that if you drag over another tab a drop target is shown', () => { diff --git a/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts b/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts index eee78a588..b90fc0bef 100644 --- a/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts @@ -73,15 +73,14 @@ describe('tabsContainer', () => { options: {}, }); - const groupviewMock = jest.fn, []>( - () => { - return { - canDisplayOverlay: jest.fn(), - }; - } - ); + const dropTargetContainer = document.createElement('div'); - const groupView = new groupviewMock() as DockviewGroupPanelModel; + const groupView = fromPartial({ + canDisplayOverlay: jest.fn(), + // dropTargetContainer: new DropTargetAnchorContainer( + // dropTargetContainer + // ), + }); const groupPanelMock = jest.fn, []>(() => { return { @@ -129,6 +128,10 @@ describe('tabsContainer', () => { expect( cut.element.getElementsByClassName('dv-drop-target-dropzone').length ).toBe(1); + // expect( + // dropTargetContainer.getElementsByClassName('dv-drop-target-anchor') + // .length + // ).toBe(1); }); test('that dropping over the empty space should render a drop target', () => { diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts index 0d00d1a31..54426cb87 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -133,11 +133,11 @@ describe('dockviewComponent', () => { }, className: 'test-a test-b', }); - expect(dockview.element.className).toBe('test-a test-b'); + expect(dockview.element.className).toBe('test-a test-b dockview-theme-abyss'); dockview.updateOptions({ className: 'test-b test-c' }); - expect(dockview.element.className).toBe('test-b test-c'); + expect(dockview.element.className).toBe('dockview-theme-abyss test-b test-c'); }); describe('memory leakage', () => { @@ -6652,36 +6652,4 @@ describe('dockviewComponent', () => { expect(api.panels.length).toBe(3); expect(api.groups.length).toBe(3); }); - - describe('updateOptions', () => { - test('gap', () => { - const container = document.createElement('div'); - - const dockview = new DockviewComponent(container, { - createComponent(options) { - switch (options.name) { - case 'default': - return new PanelContentPartTest( - options.id, - options.name - ); - default: - throw new Error(`unsupported`); - } - }, - gap: 6, - }); - - expect(dockview.gap).toBe(6); - - dockview.updateOptions({ gap: 10 }); - expect(dockview.gap).toBe(10); - - dockview.updateOptions({}); - expect(dockview.gap).toBe(10); - - dockview.updateOptions({ gap: 15 }); - expect(dockview.gap).toBe(15); - }); - }); }); diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts index 55aed39ec..a6bd4c013 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts @@ -792,7 +792,7 @@ describe('dockviewGroupPanelModel', () => { fireEvent.dragEnd(element); }); - test('that should not show drop target if dropping on self', () => { + test('that should show drop target if dropping on self', () => { const accessor = fromPartial({ id: 'testcomponentid', options: {}, @@ -806,15 +806,9 @@ describe('dockviewGroupPanelModel', () => { ), }); - const groupviewMock = jest.fn, []>( - () => { - return { - canDisplayOverlay: jest.fn(), - }; - } - ); - - const groupView = new groupviewMock() as DockviewGroupPanelModel; + const groupView = fromPartial({ + canDisplayOverlay: jest.fn(), + }); const groupPanelMock = jest.fn, []>(() => { return { @@ -861,10 +855,10 @@ describe('dockviewGroupPanelModel', () => { expect( element.getElementsByClassName('dv-drop-target-dropzone').length - ).toBe(0); + ).toBe(1); }); - test('that should not allow drop when dropping on self for same component id', () => { + test('that should allow drop when dropping on self for same component id', () => { const accessor = fromPartial({ id: 'testcomponentid', options: {}, @@ -934,7 +928,7 @@ describe('dockviewGroupPanelModel', () => { expect( element.getElementsByClassName('dv-drop-target-dropzone').length - ).toBe(0); + ).toBe(1); }); test('that should not allow drop when not dropping for different component id', () => { diff --git a/packages/dockview-core/src/api/component.api.ts b/packages/dockview-core/src/api/component.api.ts index 6884c93aa..0b0850ed0 100644 --- a/packages/dockview-core/src/api/component.api.ts +++ b/packages/dockview-core/src/api/component.api.ts @@ -629,10 +629,20 @@ export class DockviewApi implements CommonApi { return this.component.totalPanels; } + /** + * @deprecated dockview: dockviewComponent.gap has been deprecated. Use `theme` instead. This will be removed in a future version. + */ get gap(): number { return this.component.gap; } + /** + * @deprecated dockview: dockviewComponent.setGap has been deprecated. Use `theme` instead. This will be removed in a future version. + */ + setGap(gap: number | undefined): void { + this.component.updateOptions({ gap: gap }); + } + /** * Invoked when the active group changes. May be undefined if no group is active. */ @@ -914,10 +924,6 @@ export class DockviewApi implements CommonApi { return this.component.addPopoutGroup(item, options); } - setGap(gap: number | undefined): void { - this.component.updateOptions({ gap }); - } - updateOptions(options: Partial) { this.component.updateOptions(options); } 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/dropTargetAnchorContainer.scss b/packages/dockview-core/src/dnd/dropTargetAnchorContainer.scss new file mode 100644 index 000000000..0fd3dc5a5 --- /dev/null +++ b/packages/dockview-core/src/dnd/dropTargetAnchorContainer.scss @@ -0,0 +1,23 @@ +.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/dropTargetAnchorContainer.ts b/packages/dockview-core/src/dnd/dropTargetAnchorContainer.ts new file mode 100644 index 000000000..e43989c9e --- /dev/null +++ b/packages/dockview-core/src/dnd/dropTargetAnchorContainer.ts @@ -0,0 +1,102 @@ +import { CompositeDisposable, Disposable } from '../lifecycle'; +import { DropTargetTargetModel } from './droptarget'; + +export class DropTargetAnchorContainer extends CompositeDisposable { + private _model: + | { root: HTMLElement; overlay: HTMLElement; changed: boolean } + | undefined; + + private _outline: HTMLElement | undefined; + + private _disabled = false; + + get disabled(): boolean { + return this._disabled; + } + + set disabled(value: boolean) { + if (this.disabled === value) { + return; + } + + this._disabled = value; + + if (value) { + this.model?.clear(); + } + } + + get model(): DropTargetTargetModel | undefined { + if (this.disabled) { + return undefined; + } + + return { + clear: () => { + if (this._model) { + this._model.root.parentElement?.removeChild( + this._model.root + ); + } + 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(); + + anchor.style.left = `${targetBox.left - box.left}px`; + anchor.style.top = `${targetBox.top - box.top}px`; + } + + return this._model; + }, + }; + } + + constructor(readonly element: HTMLElement, options: { disabled: boolean }) { + super(); + + this._disabled = options.disabled; + + this.addDisposables( + Disposable.from(() => { + this.model?.clear(); + }) + ); + } + + 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..7f2c8cb8b 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; diff --git a/packages/dockview-core/src/dnd/droptarget.ts b/packages/dockview-core/src/dnd/droptarget.ts index 702fed867..60b7f66f8 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 | undefined; + 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,35 @@ 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; + + const overrideTraget = this.options.getOverrideTarget?.(); + if (this._acceptedTargetZonesSet.size === 0) { + if (overrideTraget) { + 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 @@ -149,8 +191,8 @@ export class Droptarget extends CompositeDisposable { const rect = ( e.currentTarget as HTMLElement ).getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; + const x = (e.clientX ?? 0) - rect.left; + const y = (e.clientY ?? 0) - rect.top; const quadrant = this.calculateQuadrant( this._acceptedTargetZonesSet, @@ -172,6 +214,9 @@ export class Droptarget extends CompositeDisposable { } if (!this.options.canDisplayOverlay(e, quadrant)) { + if (overrideTraget) { + return; + } this.removeDropTarget(); return; } @@ -194,7 +239,9 @@ export class Droptarget extends CompositeDisposable { this.markAsUsed(e); - if (!this.targetElement) { + if (overrideTraget) { + // + } else if (!this.targetElement) { this.targetElement = document.createElement('div'); this.targetElement.className = 'dv-drop-target-dropzone'; this.overlayElement = document.createElement('div'); @@ -202,8 +249,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 +266,32 @@ export class Droptarget extends CompositeDisposable { this._state = quadrant; }, onDragLeave: () => { + const target = this.options.getOverrideTarget?.(); + + if (target) { + return; + } + this.removeDropTarget(); }, - onDragEnd: () => { + onDragEnd: (e) => { + const target = this.options.getOverrideTarget?.(); + + if (target && 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(); + + target?.clear(); }, onDrop: (e) => { e.preventDefault(); @@ -223,6 +300,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 +347,9 @@ export class Droptarget extends CompositeDisposable { width: number, height: number ): void { - if (!this.overlayElement) { + const target = this.options.getOverrideTarget?.(); + + if (!target && !this.overlayElement) { return; } @@ -300,6 +381,103 @@ export class Droptarget extends CompositeDisposable { } } + if (target) { + const outlineEl = + this.options.getOverlayOutline?.() ?? this.element; + const elBox = outlineEl.getBoundingClientRect(); + + const ta = target.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 +574,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..08703179d 100644 --- a/packages/dockview-core/src/dockview/components/panel/content.ts +++ b/packages/dockview-core/src/dockview/components/panel/content.ts @@ -55,7 +55,15 @@ export class ContentContainer this.addDisposables(this._onDidFocus, this._onDidBlur); + const target = group.dropTargetContainer; + this.dropTarget = new Droptarget(this.element, { + getOverlayOutline: () => { + return accessor.options.theme?.includeHeaderWhenHoverOverContent + ? this.element.parentElement + : null; + }, + className: 'dv-drop-target-content', acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'], canDisplayOverlay: (event, position) => { if ( @@ -76,26 +84,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..9b1975d4a 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 = @@ -86,7 +87,8 @@ export class Tab extends CompositeDisposable { ); 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 +97,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 +106,7 @@ export class Tab extends CompositeDisposable { 'tab' ); }, + getOverrideTarget: () => group.model.dropTargetContainer?.model, }); this.onWillShowOverlay = this.dropTarget.onWillShowOverlay; @@ -121,6 +116,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..c6af57973 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 { diff --git a/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts index 6e9ea0c47..29e31b9b6 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; @@ -54,16 +54,7 @@ export class VoidContainer 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; - } - - // 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 +63,7 @@ export class VoidContainer extends CompositeDisposable { 'header_space' ); }, + getOverrideTarget: () => group.model.dropTargetContainer?.model, }); 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..86465c015 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -54,6 +54,7 @@ import { Parameters } from '../panel/types'; import { Overlay } from '../overlay/overlay'; import { addTestId, + Classnames, getDockviewTheme, toggleClass, watchElementResize, @@ -74,6 +75,8 @@ import { } from '../overlay/overlayRenderContainer'; import { PopoutWindow } from '../popoutWindow'; import { StrictEventsSequencing } from './strictEventsSequencing'; +import { DropTargetAnchorContainer } from '../dnd/dropTargetAnchorContainer'; +import { DockviewTheme, themeAbyss } from './theme'; const DEFAULT_ROOT_OVERLAY_MODEL: DroptargetOverlayModel = { activationSize: { type: 'pixels', value: 10 }, @@ -191,6 +194,9 @@ export interface IDockviewComponent extends IBaseGrid { readonly totalPanels: number; readonly panels: IDockviewPanel[]; readonly orientation: Orientation; + /** + * @deprecated use `theme` instead. This will be removed in a future version + */ readonly gap: number; readonly onDidDrop: Event; readonly onWillDrop: Event; @@ -253,9 +259,11 @@ export class DockviewComponent private readonly _deserializer = new DefaultDockviewDeserialzier(this); private readonly _api: DockviewApi; private _options: Exclude; - private watermark: IWatermarkRenderer | null = null; + private _watermark: IWatermarkRenderer | null = null; + private readonly _themeClassnames: Classnames; readonly overlayRenderContainer: OverlayRenderContainer; + readonly rootDropTargetContainer: DropTargetAnchorContainer; private readonly _onWillDragPanel = new Emitter(); readonly onWillDragPanel: Event = this._onWillDragPanel.event; @@ -361,6 +369,9 @@ export class DockviewComponent } get gap(): number { + console.warn( + 'dockview: dockviewComponent.gap has been deprecated. Use `theme` instead. This will be removed in a future version.' + ); return this.gridview.margin; } @@ -377,10 +388,18 @@ export class DockviewComponent : undefined, disableAutoResizing: options.disableAutoResizing, locked: options.locked, - margin: options.gap, + margin: options.theme?.gap ?? 0, className: options.className, }); + this.updateDropTargetModel(options); + + this._themeClassnames = new Classnames(this.element); + + this.rootDropTargetContainer = new DropTargetAnchorContainer( + this.element, + { disabled: true } + ); this.overlayRenderContainer = new OverlayRenderContainer( this.gridview.element, this @@ -394,6 +413,7 @@ export class DockviewComponent } this.addDisposables( + this.rootDropTargetContainer, this.overlayRenderContainer, this._onWillDragPanel, this._onWillDragGroup, @@ -464,8 +484,10 @@ export class DockviewComponent ); this._options = options; + this.updateTheme(); this._rootDropTarget = new Droptarget(this.element, { + className: 'dv-drop-target-edge', canDisplayOverlay: (event, position) => { const data = getPanelData(); @@ -506,6 +528,7 @@ export class DockviewComponent acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'], overlayModel: this.options.rootOverlayModel ?? DEFAULT_ROOT_OVERLAY_MODEL, + getOverrideTarget: () => this.rootDropTargetContainer?.model, }); this.addDisposables( @@ -756,6 +779,15 @@ export class DockviewComponent popoutContainer.appendChild(group.element); + const anchor = document.createElement('div'); + const dropTargetContainer = new DropTargetAnchorContainer( + anchor, + { disabled: this.rootDropTargetContainer.disabled } + ); + popoutContainer.appendChild(anchor); + + group.model.dropTargetContainer = dropTargetContainer; + group.model.location = { type: 'popout', getWindow: () => _window.window!, @@ -844,6 +876,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( @@ -1134,6 +1168,13 @@ export class DockviewComponent override updateOptions(options: Partial): void { super.updateOptions(options); + if ('gap' in options) { + console.warn( + 'dockview: dockviewComponent.setGap has been deprecated. Use `theme` instead. This will be removed in a future version.' + ); + this.gridview.margin = options.gap ?? 0; + } + if ('floatingGroupBounds' in options) { for (const group of this._floatingGroups) { switch (options.floatingGroupBounds) { @@ -1158,18 +1199,14 @@ export class DockviewComponent } } - if ('rootOverlayModel' in options) { - this._rootDropTarget.setOverlayModel( - options.rootOverlayModel ?? DEFAULT_ROOT_OVERLAY_MODEL - ); - } - - if ('gap' in options) { - this.gridview.margin = options.gap ?? 0; - } + this.updateDropTargetModel(options); this._options = { ...this.options, ...options }; + if ('theme' in options) { + this.updateTheme(); + } + this.layout(this.gridview.width, this.gridview.height, true); } @@ -1745,24 +1782,24 @@ export class DockviewComponent (x) => x.api.location.type === 'grid' && x.api.isVisible ).length === 0 ) { - if (!this.watermark) { - this.watermark = this.createWatermarkComponent(); + if (!this._watermark) { + this._watermark = this.createWatermarkComponent(); - this.watermark.init({ + this._watermark.init({ containerApi: new DockviewApi(this), }); const watermarkContainer = document.createElement('div'); watermarkContainer.className = 'dv-watermark-container'; addTestId(watermarkContainer, 'watermark-component'); - watermarkContainer.appendChild(this.watermark.element); + watermarkContainer.appendChild(this._watermark.element); this.gridview.element.appendChild(watermarkContainer); } - } else if (this.watermark) { - this.watermark.element.parentElement!.remove(); - this.watermark.dispose?.(); - this.watermark = null; + } else if (this._watermark) { + this._watermark.element.parentElement!.remove(); + this._watermark.dispose?.(); + this._watermark = null; } } @@ -2404,9 +2441,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 +2528,44 @@ 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' && + options.dndEdges !== null + ) { + this._rootDropTarget.setOverlayModel(options.dndEdges); + } else { + this._rootDropTarget.setOverlayModel( + DEFAULT_ROOT_OVERLAY_MODEL + ); + } + } + + if ('rootOverlayModel' in options) { + this.updateDropTargetModel({ dndEdges: options.dndEdges }); + } + } + + private updateTheme(): void { + const theme = this._options.theme ?? themeAbyss; + this._themeClassnames.setClassNames(theme.className); + + this.gridview.margin = theme.gap ?? 0; + + switch (theme.dndOverlayMounting) { + case 'absolute': + this.rootDropTargetContainer.disabled = false; + break; + case 'relative': + default: + this.rootDropTargetContainer.disabled = true; + break; + } + } } diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index a34d5ef10..c56babf91 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/dropTargetAnchorContainer'; 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 = @@ -535,6 +538,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 +1063,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..4b096c941 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -17,6 +17,7 @@ import { IGroupHeaderProps } from './framework'; import { FloatingGroupOptions } from './dockviewComponent'; import { Contraints } from '../gridview/gridviewPanel'; import { AcceptableEvent, IAcceptableEvent } from '../events'; +import { DockviewTheme } from './theme'; export interface IHeaderActionsRenderer extends IDisposable { readonly element: HTMLElement; @@ -51,19 +52,26 @@ export interface DockviewOptions { }; popoutUrl?: string; defaultRenderer?: DockviewPanelRenderer; + /** + * @deprecated dockview: dockviewComponent.gap has been deprecated. Use `theme` instead. This will be removed in a future version. + */ + gap?: number; debug?: boolean; + // #start dnd + dndEdges?: false | DroptargetOverlayModel; + /** + * @deprecated use `dndEdges` instead. To be removed in a future version. + * */ rootOverlayModel?: DroptargetOverlayModel; - locked?: boolean; disableDnd?: boolean; + // #end dnd + locked?: boolean; className?: string; - /** - * Pixel gap between groups - */ - gap?: number; /** * Define the behaviour of the dock when there are no panels to display. Defaults to `watermark`. */ noPanelsOverlay?: 'emptyGroup' | 'watermark'; + theme?: DockviewTheme; } export interface DockviewDndOverlayEvent extends IAcceptableEvent { @@ -106,9 +114,11 @@ export const PROPERTY_KEYS_DOCKVIEW: (keyof DockviewOptions)[] = (() => { rootOverlayModel: undefined, locked: undefined, disableDnd: undefined, - gap: undefined, className: undefined, noPanelsOverlay: undefined, + dndEdges: undefined, + theme: undefined, + gap: undefined, }; return Object.keys(properties) as (keyof DockviewOptions)[]; diff --git a/packages/dockview-core/src/dockview/theme.ts b/packages/dockview-core/src/dockview/theme.ts new file mode 100644 index 000000000..e6188f5b4 --- /dev/null +++ b/packages/dockview-core/src/dockview/theme.ts @@ -0,0 +1,54 @@ +export interface DockviewTheme { + name: string; + className: string; + gap?: number; + dndOverlayMounting?: 'absolute' | 'relative'; + includeHeaderWhenHoverOverContent?: boolean; +} + +export const themeDark: DockviewTheme = { + name: 'dark', + className: 'dockview-theme-dark', +}; + +export const themeLight: DockviewTheme = { + name: 'light', + className: 'dockview-theme-light', +}; + +export const themeVisualStudio: DockviewTheme = { + name: 'visualStudio', + className: 'dockview-theme-vs', +}; + +export const themeAbyss: DockviewTheme = { + name: 'abyss', + className: 'dockview-theme-abyss', +}; + +export const themeDracula: DockviewTheme = { + name: 'dracula', + className: 'dockview-theme-dracula', +}; + +export const themeReplit: DockviewTheme = { + name: 'replit', + className: 'dockview-theme-replit', + gap: 10, +}; + +export const themeKrakenDark: DockviewTheme = { + name: 'krakenDark', + className: 'dockview-theme-kraken', + gap: 10, + dndOverlayMounting: 'absolute', + includeHeaderWhenHoverOverContent: true, +}; + +export const themeKrakenLight: DockviewTheme = { + name: 'krakenLight', + className: 'dockview-theme-kraken-light', + gap: 10, + dndOverlayMounting: 'absolute', + includeHeaderWhenHoverOverContent: true, +}; diff --git a/packages/dockview-core/src/index.ts b/packages/dockview-core/src/index.ts index 9764f658a..d9f66a787 100644 --- a/packages/dockview-core/src/index.ts +++ b/packages/dockview-core/src/index.ts @@ -64,6 +64,7 @@ export { } from './dockview/framework'; export * from './dockview/options'; +export * from './dockview/theme'; export * from './dockview/dockviewPanel'; export { DefaultTab } from './dockview/components/tab/defaultTab'; export { diff --git a/packages/dockview-core/src/splitview/splitview.ts b/packages/dockview-core/src/splitview/splitview.ts index 77fbb9cf5..c511d54f6 100644 --- a/packages/dockview-core/src/splitview/splitview.ts +++ b/packages/dockview-core/src/splitview/splitview.ts @@ -219,6 +219,8 @@ export class Splitview { set margin(value: number) { this._margin = value; + + toggleClass(this.element, 'dv-splitview-has-margin', value !== 0); } constructor( diff --git a/packages/dockview-core/src/theme.scss b/packages/dockview-core/src/theme.scss index 5eb3f7442..a94ba4002 100644 --- a/packages/dockview-core/src/theme.scss +++ b/packages/dockview-core/src/theme.scss @@ -3,15 +3,27 @@ --dv-tabs-and-actions-container-font-size: 13px; --dv-tabs-and-actions-container-height: 35px; --dv-drag-over-background-color: rgba(83, 89, 93, 0.5); - --dv-drag-over-border-color: white; + --dv-drag-over-border-color: transparent; --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); --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,10 +247,17 @@ } @mixin dockview-design-replit-mixin { + @include dockview-drop-target-no-travel(); + .dv-resize-container:has(> .dv-groupview) { border-radius: 8px; } + .dv-resize-container { + border-radius: 10px !important; + border: none; + } + .dv-groupview { overflow: hidden; border-radius: 10px; @@ -319,6 +344,10 @@ .dockview-theme-replit { @include dockview-theme-core-mixin(); @include dockview-design-replit-mixin(); + + padding: 10px; + background-color: #ebeced; + // --dv-group-view-background-color: #ebeced; // @@ -342,3 +371,196 @@ --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-sash { + border-radius: 4px; + } + + .dv-drop-target-anchor { + border-radius: 5px; + &.dv-drop-target-content { + border-radius: 20px; + } + } + + .dv-resize-container { + border-radius: 20px !important; + border: none; + } + + .dv-groupview { + overflow: hidden; + border-radius: 20px; + + .dv-tabs-and-actions-container { + padding: 0px 10px; + + .dv-tab { + border-radius: 8px; + font-size: 12px; + + margin: 0.5rem 0; + height: 1.75rem; + + &:first-child { + margin-left: 0.5rem; + } + + &:not(:nth-last-child(1)) { + margin-right: 0.25rem; + } + .dv-svg { + height: 8px; + width: 8px; + } + } + } + + .dv-content-container { + background-color: var( + --dv-tabs-and-actions-container-background-color + ); + } + } + + .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-active-sash-color: rgb(91, 30, 207); + // + --dv-floating-box-shadow: 8px 8px 8px 0px rgba(0, 0, 0, 0.5); + + padding: 10px; + background-color: rgb(11, 6, 17); + + .dv-resize-container { + .dv-groupview { + border: 2px solid rgb(11, 6, 17); + } + } +} + +.dockview-theme-kraken-light { + @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: #f6f5f9; + // + --dv-tabs-and-actions-container-background-color: white; + // + --dv-activegroup-visiblepanel-tab-background-color: #ededf0; + --dv-activegroup-hiddenpanel-tab-background-color: #f9f9fa; + --dv-inactivegroup-visiblepanel-tab-background-color: #ededf0; + --dv-inactivegroup-hiddenpanel-tab-background-color: #f9f9fa; + --dv-tab-divider-color: transparent; + // + --dv-activegroup-visiblepanel-tab-color: rgb(104, 107, 130); + --dv-activegroup-hiddenpanel-tab-color: rgb(148, 151, 169); + --dv-inactivegroup-visiblepanel-tab-color: rgb(104, 107, 130); + --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-active-sash-color: rgb(91, 30, 207); + // + --dv-floating-box-shadow: 8px 8px 8px 0px rgba(0, 0, 0, 0.1); + + padding: 10px; + background-color: #f6f5f9; + + .dv-resize-container { + .dv-groupview { + border: 2px solid rgb(104, 107, 130); + } + } +} 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..a69632ebf 100644 --- a/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.tsx +++ b/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.tsx @@ -5,6 +5,7 @@ import { IDockviewPanelHeaderProps, IDockviewPanelProps, DockviewApi, + DockviewTheme, } from 'dockview'; import * as React from 'react'; import './app.scss'; @@ -80,6 +81,7 @@ const components = { ); }, nested: (props: IDockviewPanelProps) => { + const theme = React.useContext(ThemeContext); return ( ); }, @@ -141,7 +143,9 @@ const WatermarkComponent = () => { return
custom watermark
; }; -const DockviewDemo = (props: { theme?: string }) => { +const ThemeContext = React.createContext(undefined); + +const DockviewDemo = (props: { theme?: DockviewTheme }) => { const [logLines, setLogLines] = React.useState< { text: string; timestamp?: Date; backgroundColor?: string }[] >([]); @@ -380,18 +384,22 @@ const DockviewDemo = (props: { theme?: string }) => { }} > - + + + 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..ec16135ed 100644 --- a/packages/docs/sandboxes/react/dockview/demo-dockview/src/gridActions.tsx +++ b/packages/docs/sandboxes/react/dockview/demo-dockview/src/gridActions.tsx @@ -151,11 +151,20 @@ 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); - }, [gap, props.api]); + const [overlayMode, setOverlayMode] = React.useState(false); + + // React.useEffect(() => { + // if (!props.api) { + // return; + // } + // if (typeof gap === 'number') { + // props.api.setGap(gap); + // } else { + // setGap(props.api.gap); + // } + // }, [gap, props.api]); return (
@@ -191,6 +200,23 @@ export const GridActions = (props: { Use Custom Watermark + {/* + + */} @@ -204,7 +230,7 @@ export const GridActions = (props: { Reset -
+ {/*
Grid Gap 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 (
{ diff --git a/packages/docs/src/config/theme.config.ts b/packages/docs/src/config/theme.config.ts index 2e8b9741f..100da739d 100644 --- a/packages/docs/src/config/theme.config.ts +++ b/packages/docs/src/config/theme.config.ts @@ -1,33 +1,48 @@ +import { + themeAbyss, + themeDark, + themeDracula, + themeKraken, + themeLight, + themeReplit, + themeVisualStudio, +} from 'dockview'; + export const themeConfig = [ { - id: 'dockview-theme-dark', + id: themeDark, key: '**[dockview-theme-dark](/demo?theme=dockview-theme-dark)**', text: '', }, { - id: 'dockview-theme-light', + id: themeLight, key: '**[dockview-theme-light](/demo?theme=dockview-theme-light)**', text: '', }, { - id: 'dockview-theme-vs', + id: themeVisualStudio, key: '**[dockview-theme-vs](/demo?theme=dockview-theme-vs)**', text: 'Based on [Visual Studio](https://visualstudio.microsoft.com)', }, { - id: 'dockview-theme-abyss', + id: themeAbyss, key: '**[dockview-theme-abyss](/demo?theme=dockview-theme-abyss)**', text: 'Based on [Visual Studio Code](https://code.visualstudio.com/docs/getstarted/themes) abyss theme', }, { - id: 'dockview-theme-dracula', + id: themeDracula, key: '**[dockview-theme-dracula](/demo?theme=dockview-theme-dracula)**', text: 'Based on [Visual Studio Code](https://code.visualstudio.com/docs/getstarted/themes) dracula theme', }, { - id: 'dockview-theme-replit', + id: themeReplit, key: '**[dockview-theme-replit](/demo?theme=dockview-theme-replit)**', text: 'Based on [Replit](https://replit.com)', }, + { + id: themeKraken, + key: '**[dockview-theme-replit](/demo?theme=dockview-theme-kraken)**', + text: '', + }, ]; diff --git a/packages/docs/src/pages/demo.tsx b/packages/docs/src/pages/demo.tsx index cab33d7ef..76a053946 100644 --- a/packages/docs/src/pages/demo.tsx +++ b/packages/docs/src/pages/demo.tsx @@ -3,11 +3,29 @@ import Layout from '@theme/Layout'; import { themeConfig } from '../config/theme.config'; import ExampleFrame from '../components/ui/exampleFrame'; import BrowserOnly from '@docusaurus/BrowserOnly'; +import { DockviewTheme, themeAbyss } from 'dockview'; + +const updateTheme = (theme: DockviewTheme) => { + const urlParams = new URLSearchParams(window.location.search); + + urlParams.set('theme', theme.name); + + const newUrl = window.location.pathname + '?' + urlParams.toString(); + + window.history.pushState({ path: newUrl }, '', newUrl); +}; const ThemeToggle: React.FC = () => { - const [theme, setTheme] = React.useState( - new URLSearchParams(location.search).get('theme') ?? themeConfig[3].id - ); + const [theme, setTheme] = React.useState(themeAbyss); + + React.useEffect(() => { + const urlParams = new URLSearchParams(window.location.search); + const themeName = urlParams.get('theme'); + const newTheme = + themeConfig.find((c) => c.id.name === themeName)?.id ?? themeAbyss; + setTheme(newTheme); + updateTheme(newTheme); + }, []); return ( <> @@ -20,14 +38,18 @@ const ThemeToggle: React.FC = () => { >