feat: experimental floating groups

This commit is contained in:
mathuo 2023-05-31 21:08:23 +01:00
parent 7fdede6952
commit 5b493b95e0
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
18 changed files with 683 additions and 234 deletions

View File

@ -329,10 +329,6 @@ export class GridviewApi implements CommonApi<SerializedGridviewComponent> {
} }
export class DockviewApi implements CommonApi<SerializedDockview> { export class DockviewApi implements CommonApi<SerializedDockview> {
addFloating() {
return this.component.addFloating();
}
get id(): string { get id(): string {
return this.component.id; return this.component.id;
} }

View File

@ -61,3 +61,13 @@ export function firstIndex<T>(
return -1; return -1;
} }
export function remove<T>(array: T[], value: T): boolean {
const index = array.findIndex((t) => t === value);
if (index > -1) {
array.splice(index, 1);
return true;
}
return false;
}

View File

@ -21,10 +21,21 @@ export abstract class DragHandler extends CompositeDisposable {
abstract getData(dataTransfer?: DataTransfer | null): IDisposable; abstract getData(dataTransfer?: DataTransfer | null): IDisposable;
protected isCancelled(_event: DragEvent): boolean {
return false;
}
private configure(): void { private configure(): void {
this.addDisposables( this.addDisposables(
this._onDragStart, this._onDragStart,
addDisposableListener(this.el, 'dragstart', (event) => { addDisposableListener(this.el, 'dragstart', (event) => {
if (this.isCancelled(event)) {
event.preventDefault();
return;
}
this.disposable.value = this.getData(event.dataTransfer);
this.iframes = [ this.iframes = [
...getElementsByTagName('iframe'), ...getElementsByTagName('iframe'),
...getElementsByTagName('webview'), ...getElementsByTagName('webview'),
@ -37,8 +48,6 @@ export abstract class DragHandler extends CompositeDisposable {
this.el.classList.add('dv-dragged'); this.el.classList.add('dv-dragged');
setTimeout(() => this.el.classList.remove('dv-dragged'), 0); setTimeout(() => this.el.classList.remove('dv-dragged'), 0);
this.disposable.value = this.getData(event.dataTransfer);
if (event.dataTransfer) { if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.effectAllowed = 'move';

View File

@ -58,6 +58,7 @@ export class Droptarget extends CompositeDisposable {
private targetElement: HTMLElement | undefined; private targetElement: HTMLElement | undefined;
private overlayElement: HTMLElement | undefined; private overlayElement: HTMLElement | undefined;
private _state: Position | undefined; private _state: Position | undefined;
private _acceptedTargetZonesSet: Set<Position>;
private readonly _onDrop = new Emitter<DroptargetEvent>(); private readonly _onDrop = new Emitter<DroptargetEvent>();
readonly onDrop: Event<DroptargetEvent> = this._onDrop.event; readonly onDrop: Event<DroptargetEvent> = this._onDrop.event;
@ -83,7 +84,7 @@ export class Droptarget extends CompositeDisposable {
super(); super();
// use a set to take advantage of #<set>.has // use a set to take advantage of #<set>.has
const acceptedTargetZonesSet = new Set( this._acceptedTargetZonesSet = new Set(
this.options.acceptedTargetZones this.options.acceptedTargetZones
); );
@ -106,7 +107,7 @@ export class Droptarget extends CompositeDisposable {
const y = e.clientY - rect.top; const y = e.clientY - rect.top;
const quadrant = this.calculateQuadrant( const quadrant = this.calculateQuadrant(
acceptedTargetZonesSet, this._acceptedTargetZonesSet,
x, x,
y, y,
width, width,
@ -175,6 +176,10 @@ export class Droptarget extends CompositeDisposable {
); );
} }
setTargetZones(acceptedTargetZones: Position[]): void {
this._acceptedTargetZonesSet = new Set(acceptedTargetZones);
}
public dispose(): void { public dispose(): void {
this.removeDropTarget(); this.removeDropTarget();
} }

View File

@ -16,6 +16,13 @@ export class GroupDragHandler extends DragHandler {
super(element); super(element);
} }
override isCancelled(_event: DragEvent): boolean {
if (this.group.model.isFloating) {
return true;
}
return false;
}
getData(dataTransfer: DataTransfer | null): IDisposable { getData(dataTransfer: DataTransfer | null): IDisposable {
this.panelTransfer.setData( this.panelTransfer.setData(
[new PanelTransfer(this.accessorId, this.group.id, null)], [new PanelTransfer(this.accessorId, this.group.id, null)],

View File

@ -1,11 +1,43 @@
.dv-debug {
.dv-resize-container {
.dv-resize-handle-top {
background-color: red;
}
.dv-resize-handle-bottom {
background-color: green;
}
.dv-resize-handle-left {
background-color: yellow;
}
.dv-resize-handle-right {
background-color: blue;
}
.dv-resize-handle-topleft,
.dv-resize-handle-topright,
.dv-resize-handle-bottomleft,
.dv-resize-handle-bottomright {
background-color: cyan;
}
}
}
.dv-resize-container { .dv-resize-container {
position: absolute; position: absolute;
z-index: 9998; z-index: 9997;
background-color: white; &.dv-resize-container-priority {
z-index: 9998;
}
border: 1px solid var(--dv-tab-divider-color);
box-shadow: var(--dv-floating-box-shadow);
&.dv-resize-container-dragging { &.dv-resize-container-dragging {
opacity: 0.2; opacity: 0.5;
} }
.dv-resize-handle-top { .dv-resize-handle-top {
@ -16,8 +48,6 @@
z-index: 9999; z-index: 9999;
position: absolute; position: absolute;
cursor: ns-resize; cursor: ns-resize;
background-color: red;
} }
.dv-resize-handle-bottom { .dv-resize-handle-bottom {
@ -28,8 +58,6 @@
z-index: 9999; z-index: 9999;
position: absolute; position: absolute;
cursor: ns-resize; cursor: ns-resize;
background-color: green;
} }
.dv-resize-handle-left { .dv-resize-handle-left {
@ -40,8 +68,6 @@
z-index: 9999; z-index: 9999;
position: absolute; position: absolute;
cursor: ew-resize; cursor: ew-resize;
background-color: yellow;
} }
.dv-resize-handle-right { .dv-resize-handle-right {
@ -52,8 +78,6 @@
z-index: 9999; z-index: 9999;
position: absolute; position: absolute;
cursor: ew-resize; cursor: ew-resize;
background-color: blue;
} }
.dv-resize-handle-topleft { .dv-resize-handle-topleft {
@ -64,8 +88,6 @@
z-index: 9999; z-index: 9999;
position: absolute; position: absolute;
cursor: nw-resize; cursor: nw-resize;
background-color: cyan;
} }
.dv-resize-handle-topright { .dv-resize-handle-topright {
@ -76,8 +98,6 @@
z-index: 9999; z-index: 9999;
position: absolute; position: absolute;
cursor: ne-resize; cursor: ne-resize;
background-color: cyan;
} }
.dv-resize-handle-bottomleft { .dv-resize-handle-bottomleft {
@ -88,8 +108,6 @@
z-index: 9999; z-index: 9999;
position: absolute; position: absolute;
cursor: sw-resize; cursor: sw-resize;
background-color: cyan;
} }
.dv-resize-handle-bottomright { .dv-resize-handle-bottomright {
@ -100,7 +118,5 @@
z-index: 9999; z-index: 9999;
position: absolute; position: absolute;
cursor: se-resize; cursor: se-resize;
background-color: cyan;
} }
} }

View File

@ -3,23 +3,40 @@ import { addDisposableListener, addDisposableWindowListener } from '../events';
import { CompositeDisposable, MutableDisposable } from '../lifecycle'; import { CompositeDisposable, MutableDisposable } from '../lifecycle';
import { clamp } from '../math'; import { clamp } from '../math';
const bringElementToFront = (() => {
let previous: HTMLElement | null = null;
function pushToTop(element: HTMLElement) {
if (previous !== element && previous !== null) {
toggleClass(previous, 'dv-resize-container-priority', false);
}
toggleClass(element, 'dv-resize-container-priority', true);
previous = element;
}
return pushToTop;
})();
export class Overlay extends CompositeDisposable { export class Overlay extends CompositeDisposable {
private _element: HTMLElement = document.createElement('div'); private _element: HTMLElement = document.createElement('div');
constructor( constructor(
private readonly container: HTMLElement,
private readonly content: HTMLElement,
private readonly options: { private readonly options: {
height: number; height: number;
width: number; width: number;
left: number; left: number;
top: number; top: number;
container: HTMLElement;
content: HTMLElement;
minX: number;
minY: number;
} }
) { ) {
super(); super();
this.setupOverlay(); this.setupOverlay();
this.setupDrag(); // this.setupDrag(true,this._element);
this.setupResize('top'); this.setupResize('top');
this.setupResize('bottom'); this.setupResize('bottom');
this.setupResize('left'); this.setupResize('left');
@ -29,8 +46,10 @@ export class Overlay extends CompositeDisposable {
this.setupResize('bottomleft'); this.setupResize('bottomleft');
this.setupResize('bottomright'); this.setupResize('bottomright');
this._element.appendChild(content); this._element.appendChild(this.options.content);
this.container.appendChild(this._element); this.options.container.appendChild(this._element);
// this.renderWithinBoundaryConditions();
} }
private setupResize( private setupResize(
@ -52,8 +71,8 @@ export class Overlay extends CompositeDisposable {
this.addDisposables( this.addDisposables(
move, move,
addDisposableListener(resizeHandleElement, 'mousedown', (_) => { addDisposableListener(resizeHandleElement, 'mousedown', (e) => {
_.preventDefault(); e.preventDefault();
let offset: { let offset: {
originalY: number; originalY: number;
@ -64,7 +83,8 @@ export class Overlay extends CompositeDisposable {
move.value = new CompositeDisposable( move.value = new CompositeDisposable(
addDisposableWindowListener(window, 'mousemove', (e) => { addDisposableWindowListener(window, 'mousemove', (e) => {
const rect = this.container.getBoundingClientRect(); const rect =
this.options.container.getBoundingClientRect();
const y = e.clientY - rect.top; const y = e.clientY - rect.top;
const x = e.clientX - rect.left; const x = e.clientX - rect.left;
@ -90,10 +110,13 @@ export class Overlay extends CompositeDisposable {
function moveTop() { function moveTop() {
top = clamp( top = clamp(
y, y,
0,
Math.max(
0, 0,
offset!.originalY + offset!.originalY +
offset!.originalHeight - offset!.originalHeight -
MIN_HEIGHT MIN_HEIGHT
)
); );
height = height =
offset!.originalY + offset!.originalY +
@ -107,19 +130,25 @@ export class Overlay extends CompositeDisposable {
height = clamp( height = clamp(
y - top, y - top,
MIN_HEIGHT, MIN_HEIGHT,
Math.max(
0,
rect.height - rect.height -
offset!.originalY + offset!.originalY +
offset!.originalHeight offset!.originalHeight
)
); );
} }
function moveLeft() { function moveLeft() {
left = clamp( left = clamp(
x, x,
0,
Math.max(
0, 0,
offset!.originalX + offset!.originalX +
offset!.originalWidth - offset!.originalWidth -
MIN_WIDTH MIN_WIDTH
)
); );
width = width =
offset!.originalX + offset!.originalX +
@ -132,9 +161,12 @@ export class Overlay extends CompositeDisposable {
width = clamp( width = clamp(
x - left, x - left,
MIN_WIDTH, MIN_WIDTH,
Math.max(
0,
rect.width - rect.width -
offset!.originalX + offset!.originalX +
offset!.originalWidth offset!.originalWidth
)
); );
} }
@ -199,23 +231,18 @@ export class Overlay extends CompositeDisposable {
this._element.className = 'dv-resize-container'; this._element.className = 'dv-resize-container';
} }
private setupDrag(): void { setupDrag(connect: boolean, dragTarget: HTMLElement): void {
const move = new MutableDisposable(); const move = new MutableDisposable();
this.addDisposables( const track = () => {
move,
addDisposableListener(this._element, 'mousedown', (_) => {
if (_.defaultPrevented) {
return;
}
let offset: { x: number; y: number } | null = null; let offset: { x: number; y: number } | null = null;
move.value = new CompositeDisposable( move.value = new CompositeDisposable(
addDisposableWindowListener(window, 'mousemove', (e) => { addDisposableWindowListener(window, 'mousemove', (e) => {
const rect = this.container.getBoundingClientRect(); const containerRect =
const x = e.clientX - rect.left; this.options.container.getBoundingClientRect();
const y = e.clientY - rect.top; const x = e.clientX - containerRect.left;
const y = e.clientY - containerRect.top;
toggleClass( toggleClass(
this._element, this._element,
@ -223,24 +250,39 @@ export class Overlay extends CompositeDisposable {
true true
); );
const rect2 = this._element.getBoundingClientRect(); const overlayRect = this._element.getBoundingClientRect();
if (offset === null) { if (offset === null) {
offset = { offset = {
x: e.clientX - rect2.left, x: e.clientX - overlayRect.left,
y: e.clientY - rect2.top, y: e.clientY - overlayRect.top,
}; };
} }
const left = clamp( const xOffset = Math.max(
Math.max(0, x - offset.x),
0, 0,
rect.width - rect2.width overlayRect.width - this.options.minX
);
const yOffset = Math.max(
0,
overlayRect.height - this.options.minY
);
const left = clamp(
x - offset.x,
-xOffset,
Math.max(
0,
containerRect.width - overlayRect.width + xOffset
)
); );
const top = clamp( const top = clamp(
Math.max(0, y - offset.y), y - offset.y,
-yOffset,
Math.max(
0, 0,
rect.height - rect2.height containerRect.height - overlayRect.height + yOffset
)
); );
this._element.style.left = `${left}px`; this._element.style.left = `${left}px`;
@ -256,11 +298,61 @@ export class Overlay extends CompositeDisposable {
move.dispose(); move.dispose();
}) })
); );
}) };
);
this.addDisposables(
move,
addDisposableListener(dragTarget, 'mousedown', (_) => {
if (_.defaultPrevented) {
return;
} }
dispose(): void { track();
}),
addDisposableListener(this.options.content, 'mousedown', (_) => {
if (_.shiftKey) {
track();
}
}),
addDisposableListener(
this.options.content,
'mousedown',
() => {
bringElementToFront(this._element);
},
true
)
);
if (connect) {
track();
}
}
renderWithinBoundaryConditions(): void {
const rect = this.options.container.getBoundingClientRect();
const rect2 = this._element.getBoundingClientRect();
const left = clamp(
Math.max(this.options.left, 0),
0,
Math.max(0, rect.width - rect2.width)
);
const top = clamp(
Math.max(this.options.top, 0),
0,
Math.max(0, rect.height - rect2.height)
);
console.log(new Error().stack);
this._element.style.left = `${left}px`;
this._element.style.top = `${top}px`;
}
override dispose(): void {
this._element.remove(); this._element.remove();
super.dispose();
} }
} }

View File

@ -11,6 +11,7 @@ import { DockviewDropTargets, ITabRenderer } from '../../types';
import { DockviewGroupPanel } from '../../dockviewGroupPanel'; import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import { DroptargetEvent, Droptarget } from '../../../dnd/droptarget'; import { DroptargetEvent, Droptarget } from '../../../dnd/droptarget';
import { DragHandler } from '../../../dnd/abstractDragHandler'; import { DragHandler } from '../../../dnd/abstractDragHandler';
import { DockviewPanel } from '../../dockviewPanel';
export interface ITab { export interface ITab {
readonly panelId: string; readonly panelId: string;
@ -80,6 +81,19 @@ export class Tab extends CompositeDisposable implements ITab {
this.addDisposables( this.addDisposables(
addDisposableListener(this._element, 'mousedown', (event) => { addDisposableListener(this._element, 'mousedown', (event) => {
if (event.shiftKey) {
event.preventDefault();
const panel = this.accessor.getGroupPanel(this.panelId);
const { top, left } = this.element.getBoundingClientRect();
this.accessor.addFloating(panel as DockviewPanel, {
x: left,
y: top,
});
}
if (event.defaultPrevented) { if (event.defaultPrevented) {
return; return;
} }

View File

@ -28,6 +28,7 @@ export class VoidContainer extends CompositeDisposable {
this._element = document.createElement('div'); this._element = document.createElement('div');
this._element.className = 'void-container'; this._element.className = 'void-container';
this._element.id = 'dv-group-float-drag-handle';
this._element.tabIndex = 0; this._element.tabIndex = 0;
this._element.draggable = true; this._element.draggable = true;
@ -68,6 +69,16 @@ export class VoidContainer extends CompositeDisposable {
this.addDisposables( this.addDisposables(
handler, handler,
addDisposableListener(this._element, 'mousedown', (event) => {
if (event.shiftKey && !this.group.model.isFloating) {
event.preventDefault();
this.accessor.addFloating(this.group, {
x: event.clientX + 20,
y: event.clientY + 20,
});
}
}),
this.voidDropTarget.onDrop((event) => { this.voidDropTarget.onDrop((event) => {
this._onDrop.fire(event); this._onDrop.fire(event);
}), }),

View File

@ -8,6 +8,7 @@
left: 0px; left: 0px;
height: 100%; height: 100%;
width: 100%; width: 100%;
z-index: 9999;
} }
} }

View File

@ -5,9 +5,9 @@ import {
ISerializedLeafNode, ISerializedLeafNode,
} from '../gridview/gridview'; } from '../gridview/gridview';
import { directionToPosition, Droptarget, Position } from '../dnd/droptarget'; import { directionToPosition, Droptarget, Position } from '../dnd/droptarget';
import { tail, sequenceEquals } from '../array'; import { tail, sequenceEquals, remove } from '../array';
import { DockviewPanel, IDockviewPanel } from './dockviewPanel'; import { DockviewPanel, IDockviewPanel } from './dockviewPanel';
import { CompositeDisposable } from '../lifecycle'; import { CompositeDisposable, IDisposable } from '../lifecycle';
import { Event, Emitter } from '../events'; import { Event, Emitter } from '../events';
import { Watermark } from './components/watermark/watermark'; import { Watermark } from './components/watermark/watermark';
import { import {
@ -45,6 +45,7 @@ import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel';
import { DockviewPanelModel } from './dockviewPanelModel'; import { DockviewPanelModel } from './dockviewPanelModel';
import { getPanelData } from '../dnd/dataTransfer'; import { getPanelData } from '../dnd/dataTransfer';
import { Overlay } from '../dnd/overlay'; import { Overlay } from '../dnd/overlay';
import { toggleClass } from '../dom';
export interface PanelReference { export interface PanelReference {
update: (event: { params: { [key: string]: any } }) => void; update: (event: { params: { [key: string]: any } }) => void;
@ -116,7 +117,10 @@ export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
readonly onDidAddPanel: Event<IDockviewPanel>; readonly onDidAddPanel: Event<IDockviewPanel>;
readonly onDidLayoutFromJSON: Event<void>; readonly onDidLayoutFromJSON: Event<void>;
readonly onDidActivePanelChange: Event<IDockviewPanel | undefined>; readonly onDidActivePanelChange: Event<IDockviewPanel | undefined>;
addFloating(): void; addFloating(
item: DockviewPanel | DockviewGroupPanel,
coord?: { x: number; y: number }
): void;
} }
export class DockviewComponent export class DockviewComponent
@ -148,6 +152,12 @@ export class DockviewComponent
readonly onDidActivePanelChange: Event<IDockviewPanel | undefined> = readonly onDidActivePanelChange: Event<IDockviewPanel | undefined> =
this._onDidActivePanelChange.event; this._onDidActivePanelChange.event;
private readonly floatingGroups: {
instance: DockviewGroupPanel;
disposable: IDisposable;
render: () => void;
}[] = [];
get orientation(): Orientation { get orientation(): Orientation {
return this.gridview.orientation; return this.gridview.orientation;
} }
@ -182,7 +192,7 @@ export class DockviewComponent
parentElement: options.parentElement, parentElement: options.parentElement,
}); });
this.element.classList.add('dv-dockview'); toggleClass(this.gridview.element, 'dv-dockview', true);
this.addDisposables( this.addDisposables(
this._onDidDrop, this._onDidDrop,
@ -277,8 +287,68 @@ export class DockviewComponent
this._api = new DockviewApi(this); this._api = new DockviewApi(this);
this.updateWatermark(); this.updateWatermark();
}
this.element.style.position = 'relative'; addFloating(
item: DockviewPanel | DockviewGroupPanel,
coord?: { x: number; y: number }
): void {
let group: DockviewGroupPanel;
if (item instanceof DockviewPanel) {
group = this.createGroup();
this.removePanel(item, {
removeEmptyGroup: true,
skipDispose: true,
});
group.model.openPanel(item);
} else {
group = item;
this.doRemoveGroup(item, { skipDispose: true });
}
group.model.isFloating = true;
const { left, top } = this.element.getBoundingClientRect();
const overlayLeft =
typeof coord?.x === 'number' ? Math.max(coord.x - left, 0) : 100;
const overlayTop =
typeof coord?.y === 'number' ? Math.max(0, coord.y - top) : 100;
const overlay = new Overlay({
container: this.gridview.element,
content: group.element,
height: 300,
width: 300,
left: overlayLeft,
top: overlayTop,
minX: 100,
minY: 100,
});
const el = group.element.querySelector('#dv-group-float-drag-handle');
if (el) {
overlay.setupDrag(true, el as HTMLElement);
}
const instance = {
instance: group,
render: () => {
overlay.renderWithinBoundaryConditions();
},
disposable: new CompositeDisposable(overlay, {
dispose: () => {
group.model.isFloating = false;
remove(this.floatingGroups, instance);
},
}),
};
this.floatingGroups.push(instance);
} }
private orthogonalize(position: Position): DockviewGroupPanel { private orthogonalize(position: Position): DockviewGroupPanel {
@ -329,6 +399,20 @@ export class DockviewComponent
this.layout(this.gridview.width, this.gridview.height, true); this.layout(this.gridview.width, this.gridview.height, true);
} }
override layout(
width: number,
height: number,
forceResize?: boolean | undefined
): void {
super.layout(width, height, forceResize);
if (this.floatingGroups) {
for (const floating of this.floatingGroups) {
floating.render();
}
}
}
focus(): void { focus(): void {
this.activeGroup?.focus(); this.activeGroup?.focus();
} }
@ -477,7 +561,7 @@ export class DockviewComponent
for (const group of groups) { for (const group of groups) {
// remove the group will automatically remove the panels // remove the group will automatically remove the panels
this.removeGroup(group, true); this.removeGroup(group, { skipActive: true });
} }
if (hasActiveGroup) { if (hasActiveGroup) {
@ -591,7 +675,9 @@ export class DockviewComponent
group.model.removePanel(panel); group.model.removePanel(panel);
if (!options.skipDispose) {
panel.dispose(); panel.dispose();
}
if (group.size === 0 && options.removeEmptyGroup) { if (group.size === 0 && options.removeEmptyGroup) {
this.removeGroup(group); this.removeGroup(group);
@ -625,7 +711,7 @@ export class DockviewComponent
watermarkContainer.className = 'dv-watermark-container'; watermarkContainer.className = 'dv-watermark-container';
watermarkContainer.appendChild(this.watermark.element); watermarkContainer.appendChild(this.watermark.element);
this.element.appendChild(watermarkContainer); this.gridview.element.appendChild(watermarkContainer);
} }
} else if (this.watermark) { } else if (this.watermark) {
this.watermark.element.parentElement!.remove(); this.watermark.element.parentElement!.remove();
@ -695,17 +781,49 @@ export class DockviewComponent
} }
} }
removeGroup(group: DockviewGroupPanel, skipActive = false): void { removeGroup(
group: DockviewGroupPanel,
options?:
| {
skipActive?: boolean | undefined;
skipDispose?: boolean | undefined;
}
| undefined
): void {
const panels = [...group.panels]; // reassign since group panels will mutate const panels = [...group.panels]; // reassign since group panels will mutate
for (const panel of panels) { for (const panel of panels) {
this.removePanel(panel, { this.removePanel(panel, {
removeEmptyGroup: false, removeEmptyGroup: false,
skipDispose: false, skipDispose: options?.skipDispose ?? false,
}); });
} }
super.doRemoveGroup(group, { skipActive }); this.doRemoveGroup(group, options);
}
protected override doRemoveGroup(
group: DockviewGroupPanel,
options?:
| {
skipActive?: boolean | undefined;
skipDispose?: boolean | undefined;
}
| undefined
): DockviewGroupPanel {
const floatingGroup = this.floatingGroups.find(
(_) => _.instance === group
);
if (floatingGroup) {
if (!options?.skipDispose) {
floatingGroup.instance.dispose();
}
floatingGroup.disposable.dispose();
return floatingGroup.instance;
}
return super.doRemoveGroup(group, options);
} }
moveGroupOrPanel( moveGroupOrPanel(
@ -750,17 +868,28 @@ export class DockviewComponent
if (sourceGroup && sourceGroup.size < 2) { if (sourceGroup && sourceGroup.size < 2) {
const [targetParentLocation, to] = tail(targetLocation); const [targetParentLocation, to] = tail(targetLocation);
const isFloating = this.floatingGroups.find(
(x) => x.instance === sourceGroup
);
if (!isFloating) {
const sourceLocation = getGridLocation(sourceGroup.element); const sourceLocation = getGridLocation(sourceGroup.element);
const [sourceParentLocation, from] = tail(sourceLocation); const [sourceParentLocation, from] = tail(sourceLocation);
if ( if (
sequenceEquals(sourceParentLocation, targetParentLocation) sequenceEquals(
sourceParentLocation,
targetParentLocation
)
) { ) {
// special case when 'swapping' two views within same grid location // special case when 'swapping' two views within same grid location
// if a group has one tab - we are essentially moving the 'group' // if a group has one tab - we are essentially moving the 'group'
// which is equivalent to swapping two views in this case // which is equivalent to swapping two views in this case
this.gridview.moveView(sourceParentLocation, from, to); this.gridview.moveView(sourceParentLocation, from, to);
} else { }
}
// source group will become empty so delete the group // source group will become empty so delete the group
const targetGroup = this.doRemoveGroup(sourceGroup, { const targetGroup = this.doRemoveGroup(sourceGroup, {
skipActive: true, skipActive: true,
@ -777,7 +906,6 @@ export class DockviewComponent
target target
); );
this.doAddGroup(targetGroup, location); this.doAddGroup(targetGroup, location);
}
} else { } else {
const groupItem: IDockviewPanel | undefined = const groupItem: IDockviewPanel | undefined =
sourceGroup?.model.removePanel(itemId) || sourceGroup?.model.removePanel(itemId) ||
@ -821,7 +949,17 @@ export class DockviewComponent
}); });
} }
} else { } else {
this.gridview.removeView(getGridLocation(sourceGroup.element)); const floatingGroup = this.floatingGroups.find(
(x) => x.instance === sourceGroup
);
if (floatingGroup) {
floatingGroup.disposable.dispose();
} else {
this.gridview.removeView(
getGridLocation(sourceGroup.element)
);
}
const referenceLocation = getGridLocation( const referenceLocation = getGridLocation(
referenceGroup.element referenceGroup.element
@ -963,60 +1101,4 @@ export class DockviewComponent
this._onDidRemovePanel.dispose(); this._onDidRemovePanel.dispose();
this._onDidLayoutFromJSON.dispose(); this._onDidLayoutFromJSON.dispose();
} }
//
addFloating() {
const parentDockview = this;
const floatingDockview = new DockviewComponent({
...this.options,
parentElement: undefined,
showDndOverlay: (event) => {
const data = event.getData();
if (data && data.viewId === parentDockview.id) {
return true;
}
return false;
},
});
floatingDockview.onDidDrop((event) => {
const data = event.getData();
if (!data || data.viewId !== parentDockview.id) {
return;
}
if (data.panelId === null) {
const group = parentDockview.removeGroup(
parentDockview.getPanel(data.groupId)!
);
} else {
const panel = parentDockview.removePanel(
parentDockview.getGroupPanel(data.panelId)!
);
parentDockview.moveGroupOrPanel()
}
});
floatingDockview.addPanel({
id: '__test__',
component: 'default',
});
floatingDockview.addPanel({
id: '__test__2__',
component: 'default',
});
const overlay = new Overlay(this.element, floatingDockview.element, {
height: 300,
width: 300,
left: 100,
top: 100,
});
}
} }

View File

@ -26,7 +26,7 @@ export class DockviewGroupPanel
extends GridviewPanel extends GridviewPanel
implements IDockviewGroupPanel implements IDockviewGroupPanel
{ {
private readonly _model: IDockviewGroupPanelModel; private readonly _model: DockviewGroupPanelModel;
get panels(): IDockviewPanel[] { get panels(): IDockviewPanel[] {
return this._model.panels; return this._model.panels;
@ -40,7 +40,7 @@ export class DockviewGroupPanel
return this._model.size; return this._model.size;
} }
get model(): IDockviewGroupPanelModel { get model(): DockviewGroupPanelModel {
return this._model; return this._model;
} }

View File

@ -138,6 +138,7 @@ export class DockviewGroupPanelModel
private _isGroupActive = false; private _isGroupActive = false;
private _locked = false; private _locked = false;
private _control: IGroupControlRenderer | undefined; private _control: IGroupControlRenderer | undefined;
private _isFloating = false;
private mostRecentlyUsed: IDockviewPanel[] = []; private mostRecentlyUsed: IDockviewPanel[] = [];
@ -223,6 +224,20 @@ export class DockviewGroupPanelModel
); );
} }
get isFloating(): boolean {
return this._isFloating;
}
set isFloating(value: boolean) {
this._isFloating = value;
this.dropTarget.setTargetZones(
value ? ['center'] : ['top', 'bottom', 'left', 'right', 'center']
);
toggleClass(this.container, 'dv-groupview-floating', value);
}
constructor( constructor(
private readonly container: HTMLElement, private readonly container: HTMLElement,
private accessor: DockviewComponent, private accessor: DockviewComponent,
@ -232,7 +247,7 @@ export class DockviewGroupPanelModel
) { ) {
super(); super();
this.container.classList.add('groupview'); toggleClass(this.container, 'groupview', true);
this.tabsContainer = new TabsContainer(this.accessor, this.groupPanel); this.tabsContainer = new TabsContainer(this.accessor, this.groupPanel);

View File

@ -7,6 +7,7 @@
--dv-drag-over-border-color: white; --dv-drag-over-border-color: white;
--dv-tabs-container-scrollbar-color: #888; --dv-tabs-container-scrollbar-color: #888;
--dv-icon-hover-background-color: rgba(90, 93, 94, 0.31); --dv-icon-hover-background-color: rgba(90, 93, 94, 0.31);
--dv-floating-box-shadow: 8px 8px 8px 0px rgba(83, 89, 93, 0.5);
} }
@mixin dockview-theme-dark-mixin { @mixin dockview-theme-dark-mixin {
@ -225,3 +226,124 @@
.dockview-theme-dracula { .dockview-theme-dracula {
@include dockview-theme-dracula-mixin(); @include dockview-theme-dracula-mixin();
} }
@mixin dockview-design-replit-mixin {
&.dv-dockview {
padding: 3px;
}
.view:has(> .groupview) {
padding: 3px;
}
.dv-resize-container:has(> .groupview) {
border-radius: 8px;
}
.groupview {
overflow: hidden;
border-radius: 10px;
.tabs-and-actions-container {
.tab {
margin: 4px;
border-radius: 8px;
.dockview-svg {
height: 8px;
width: 8px;
}
&:hover {
background-color: #e4e5e6 !important;
}
}
border-bottom: 1px solid rgba(128, 128, 128, 0.35);
}
.content-container {
background-color: #fcfcfc;
}
&.active-group {
border: 1px solid rgba(128, 128, 128, 0.35);
}
&.inactive-group {
border: 1px solid transparent;
}
}
.vertical > .sash-container > .sash {
&::after {
content: '';
height: 4px;
width: 40px;
border-radius: 2px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--dv-separator-handle-background-color);
position: absolute;
}
&:hover {
&::after {
background-color: var(
--dv-separator-handle-hover-background-color
);
}
}
}
.horizontal > .sash-container > .sash {
&::after {
content: '';
height: 40px;
width: 4px;
border-radius: 2px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--dv-separator-handle-background-color);
position: absolute;
}
&:hover {
&::after {
background-color: var(
--dv-separator-handle-hover-background-color
);
}
}
}
}
.dockview-theme-replit {
@include dockview-theme-core-mixin();
@include dockview-design-replit-mixin();
//
--dv-group-view-background-color: #ebeced;
//
--dv-tabs-and-actions-container-background-color: #fcfcfc;
//
--dv-activegroup-visiblepanel-tab-background-color: #f0f1f2;
--dv-activegroup-hiddenpanel-tab-background-color: ##fcfcfc;
--dv-inactivegroup-visiblepanel-tab-background-color: #f0f1f2;
--dv-inactivegroup-hiddenpanel-tab-background-color: #fcfcfc;
--dv-tab-divider-color: transparent;
//
--dv-activegroup-visiblepanel-tab-color: rgb(51, 51, 51);
--dv-activegroup-hiddenpanel-tab-color: rgb(51, 51, 51);
--dv-inactivegroup-visiblepanel-tab-color: rgb(51, 51, 51);
--dv-inactivegroup-hiddenpanel-tab-color: rgb(51, 51, 51);
//
--dv-separator-border: transparent;
--dv-paneview-header-border-color: rgb(51, 51, 51);
--dv-background-color: #ebeced;
/////
--dv-separator-handle-background-color: #cfd1d3;
--dv-separator-handle-hover-background-color: #babbbb;
}

View File

@ -1,12 +1,6 @@
export * from 'dockview-core'; export * from 'dockview-core';
export { export * from './dockview/dockview';
IDockviewPanelHeaderProps,
IDockviewPanelProps,
DockviewReadyEvent,
IDockviewReactProps,
DockviewReact,
} from './dockview/dockview';
export * from './dockview/defaultTab'; export * from './dockview/defaultTab';
export * from './splitview/splitview'; export * from './splitview/splitview';
export * from './gridview/gridview'; export * from './gridview/gridview';

View File

@ -11,6 +11,28 @@ import * as ReactDOM from 'react-dom';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import './app.scss'; import './app.scss';
function useLocalStorageItem(key: string, defaultValue: string): string {
const [item, setItem] = React.useState<string | null>(
localStorage.getItem(key)
);
React.useEffect(() => {
const listener = (event: StorageEvent) => {
setItem(localStorage.getItem(key));
};
window.addEventListener('storage', listener);
setItem(localStorage.getItem(key));
return () => {
window.removeEventListener('storage', listener);
};
}, [key]);
return item === null ? defaultValue : item;
}
const components = { const components = {
default: (props: IDockviewPanelProps<{ title: string }>) => { default: (props: IDockviewPanelProps<{ title: string }>) => {
return <div style={{ padding: '20px' }}>{props.params.title}</div>; return <div style={{ padding: '20px' }}>{props.params.title}</div>;
@ -196,8 +218,8 @@ const DockviewDemo = () => {
title: 'Panel 6', title: 'Panel 6',
position: { referencePanel: 'panel_4', direction: 'below' }, position: { referencePanel: 'panel_4', direction: 'below' },
}); });
panel6.group.locked = true; // panel6.group.locked = true;
panel6.group.header.hidden = true; // panel6.group.header.hidden = true;
event.api.addPanel({ event.api.addPanel({
id: 'panel_7', id: 'panel_7',
component: 'default', component: 'default',
@ -211,18 +233,23 @@ const DockviewDemo = () => {
position: { referencePanel: 'panel_7', direction: 'within' }, position: { referencePanel: 'panel_7', direction: 'within' },
}); });
event.api.addGroup(); // event.api.addGroup();
event.api.getPanel('panel_1')!.api.setActive(); event.api.getPanel('panel_1')!.api.setActive();
}; };
const theme = useLocalStorageItem(
'dv-theme-class-name',
'dockview-theme-abyss'
);
return ( return (
<DockviewReact <DockviewReact
components={components} components={components}
defaultTabComponent={headerComponents.default} defaultTabComponent={headerComponents.default}
groupControlComponent={GroupControls} groupControlComponent={GroupControls}
onReady={onReady} onReady={onReady}
className="dockview-theme-abyss" className={theme}
/> />
); );
}; };

View File

@ -1,5 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import './codeSandboxButton.scss'; import './codeSandboxButton.scss';
import { ThemePicker } from './container';
const BASE_SANDBOX_URL = const BASE_SANDBOX_URL =
'https://codesandbox.io/s/github/mathuo/dockview/tree/master/packages/docs/sandboxes'; 'https://codesandbox.io/s/github/mathuo/dockview/tree/master/packages/docs/sandboxes';
@ -40,6 +41,8 @@ export const CodeSandboxButton = (props: { id: string }) => {
}, [props.id]); }, [props.id]);
return ( return (
<>
<ThemePicker />
<span <span
className="codesandbox-button" className="codesandbox-button"
style={{ display: 'flex', alignItems: 'center' }} style={{ display: 'flex', alignItems: 'center' }}
@ -61,5 +64,6 @@ export const CodeSandboxButton = (props: { id: string }) => {
<CloseButton /> <CloseButton />
</a> </a>
</span> </span>
</>
); );
}; };

View File

@ -69,6 +69,49 @@ const JavascriptIcon = (props: { height: number; width: number }) => {
); );
}; };
const themes = [
'dockview-theme-dark',
'dockview-theme-light',
'dockview-theme-vs',
'dockview-theme-dracula',
'dockview-theme-replit',
];
export const ThemePicker = () => {
const [theme, setTheme] = React.useState<string>(
localStorage.getItem('dv-theme-class-name') || themes[0]
);
React.useEffect(() => {
localStorage.setItem('dv-theme-class-name', theme);
window.dispatchEvent(new StorageEvent('storage'));
}, [theme]);
return (
<div
style={{
height: '20px',
display: 'flex',
alignItems: 'center',
padding: '0px 0px 0px 4px',
}}
>
<span style={{ paddingRight: '4px' }}>{'Theme: '}</span>
<select
style={{ backgroundColor: 'inherit', color: 'inherit' }}
onChange={(e) => setTheme(e.target.value)}
value={theme}
>
{themes.map((theme) => (
<option key={theme} value={theme}>
{theme}
</option>
))}
</select>
</div>
);
};
export const MultiFrameworkContainer = (props: { export const MultiFrameworkContainer = (props: {
react: React.FC; react: React.FC;
typescript: (parent: HTMLElement) => { dispose: () => void }; typescript: (parent: HTMLElement) => { dispose: () => void };
@ -183,6 +226,7 @@ export const MultiFrameworkContainer = (props: {
</select> </select>
</div> </div>
<span style={{ flexGrow: 1 }} /> <span style={{ flexGrow: 1 }} />
<ThemePicker />
<CodeSandboxButton id={sandboxId} /> <CodeSandboxButton id={sandboxId} />
</div> </div>
</> </>