mirror of
https://github.com/mathuo/dockview
synced 2025-03-12 08:52:05 +00:00
feat: floating groups
This commit is contained in:
parent
f364bb70a6
commit
12b4a0d27b
@ -13,8 +13,46 @@ describe('overlay', () => {
|
|||||||
width: 100,
|
width: 100,
|
||||||
left: 10,
|
left: 10,
|
||||||
top: 20,
|
top: 20,
|
||||||
minX: 0,
|
minimumInViewportWidth: 0,
|
||||||
minY: 0,
|
minimumInViewportHeight: 0,
|
||||||
|
container,
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.spyOn(
|
||||||
|
container.childNodes.item(0) as HTMLElement,
|
||||||
|
'getBoundingClientRect'
|
||||||
|
).mockImplementation(() => {
|
||||||
|
return { left: 80, top: 100, width: 40, height: 50 } as any;
|
||||||
|
});
|
||||||
|
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
|
||||||
|
() => {
|
||||||
|
return { left: 20, top: 30, width: 100, height: 100 } as any;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(cut.toJSON()).toEqual({
|
||||||
|
top: 70,
|
||||||
|
left: 60,
|
||||||
|
width: 40,
|
||||||
|
height: 50,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('#1', () => {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const content = document.createElement('div');
|
||||||
|
|
||||||
|
document.body.appendChild(container);
|
||||||
|
container.appendChild(content);
|
||||||
|
|
||||||
|
const cut = new Overlay({
|
||||||
|
height: 200,
|
||||||
|
width: 100,
|
||||||
|
left: -1000,
|
||||||
|
top: -1000,
|
||||||
|
minimumInViewportWidth: 0,
|
||||||
|
minimumInViewportHeight: 0,
|
||||||
container,
|
container,
|
||||||
content,
|
content,
|
||||||
});
|
});
|
||||||
@ -48,8 +86,8 @@ describe('overlay', () => {
|
|||||||
width: 500,
|
width: 500,
|
||||||
left: 100,
|
left: 100,
|
||||||
top: 200,
|
top: 200,
|
||||||
minX: 0,
|
minimumInViewportWidth: 0,
|
||||||
minY: 0,
|
minimumInViewportHeight: 0,
|
||||||
container,
|
container,
|
||||||
content,
|
content,
|
||||||
});
|
});
|
||||||
|
@ -8,7 +8,7 @@ import { PanelUpdateEvent } from '../../panel/types';
|
|||||||
import { Orientation } from '../../splitview/splitview';
|
import { Orientation } from '../../splitview/splitview';
|
||||||
import { CompositeDisposable } from '../../lifecycle';
|
import { CompositeDisposable } from '../../lifecycle';
|
||||||
import { Emitter } from '../../events';
|
import { Emitter } from '../../events';
|
||||||
import { DockviewPanel, IDockviewPanel } from '../../dockview/dockviewPanel';
|
import { IDockviewPanel } from '../../dockview/dockviewPanel';
|
||||||
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
|
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
|
||||||
|
|
||||||
class PanelContentPartTest implements IContentRenderer {
|
class PanelContentPartTest implements IContentRenderer {
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel';
|
import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel';
|
||||||
|
import { quasiPreventDefault } from '../dom';
|
||||||
|
import { addDisposableListener } from '../events';
|
||||||
import { IDisposable } from '../lifecycle';
|
import { IDisposable } from '../lifecycle';
|
||||||
import { DragHandler } from './abstractDragHandler';
|
import { DragHandler } from './abstractDragHandler';
|
||||||
import { LocalSelectionTransfer, PanelTransfer } from './dataTransfer';
|
import { LocalSelectionTransfer, PanelTransfer } from './dataTransfer';
|
||||||
@ -14,6 +16,24 @@ export class GroupDragHandler extends DragHandler {
|
|||||||
private readonly group: DockviewGroupPanel
|
private readonly group: DockviewGroupPanel
|
||||||
) {
|
) {
|
||||||
super(element);
|
super(element);
|
||||||
|
|
||||||
|
this.addDisposables(
|
||||||
|
addDisposableListener(
|
||||||
|
element,
|
||||||
|
'mousedown',
|
||||||
|
(e) => {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
/**
|
||||||
|
* You cannot call e.preventDefault() because that will prevent drag events from firing
|
||||||
|
* but we also need to stop any group overlay drag events from occuring
|
||||||
|
* Use a custom event marker that can be checked by the overlay drag events
|
||||||
|
*/
|
||||||
|
quasiPreventDefault(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
override isCancelled(_event: DragEvent): boolean {
|
override isCancelled(_event: DragEvent): boolean {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { toggleClass } from '../dom';
|
import { quasiDefaultPrevented, toggleClass } from '../dom';
|
||||||
import {
|
import {
|
||||||
Emitter,
|
Emitter,
|
||||||
Event,
|
Event,
|
||||||
@ -7,6 +7,7 @@ import {
|
|||||||
} from '../events';
|
} from '../events';
|
||||||
import { CompositeDisposable, MutableDisposable } from '../lifecycle';
|
import { CompositeDisposable, MutableDisposable } from '../lifecycle';
|
||||||
import { clamp } from '../math';
|
import { clamp } from '../math';
|
||||||
|
import { getPaneData, getPanelData } from './dataTransfer';
|
||||||
|
|
||||||
const bringElementToFront = (() => {
|
const bringElementToFront = (() => {
|
||||||
let previous: HTMLElement | null = null;
|
let previous: HTMLElement | null = null;
|
||||||
@ -29,6 +30,9 @@ export class Overlay extends CompositeDisposable {
|
|||||||
private readonly _onDidChange = new Emitter<void>();
|
private readonly _onDidChange = new Emitter<void>();
|
||||||
readonly onDidChange: Event<void> = this._onDidChange.event;
|
readonly onDidChange: Event<void> = this._onDidChange.event;
|
||||||
|
|
||||||
|
private static MINIMUM_HEIGHT = 20;
|
||||||
|
private static MINIMUM_WIDTH = 20;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly options: {
|
private readonly options: {
|
||||||
height: number;
|
height: number;
|
||||||
@ -37,8 +41,8 @@ export class Overlay extends CompositeDisposable {
|
|||||||
top: number;
|
top: number;
|
||||||
container: HTMLElement;
|
container: HTMLElement;
|
||||||
content: HTMLElement;
|
content: HTMLElement;
|
||||||
minX: number;
|
minimumInViewportWidth: number;
|
||||||
minY: number;
|
minimumInViewportHeight: number;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
@ -57,6 +61,9 @@ export class Overlay extends CompositeDisposable {
|
|||||||
|
|
||||||
this._element.appendChild(this.options.content);
|
this._element.appendChild(this.options.content);
|
||||||
this.options.container.appendChild(this._element);
|
this.options.container.appendChild(this._element);
|
||||||
|
|
||||||
|
// if input bad resize within acceptable boundaries
|
||||||
|
this.renderWithinBoundaryConditions();
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON(): { top: number; left: number; height: number; width: number } {
|
toJSON(): { top: number; left: number; height: number; width: number } {
|
||||||
@ -93,7 +100,7 @@ export class Overlay extends CompositeDisposable {
|
|||||||
addDisposableListener(resizeHandleElement, 'mousedown', (e) => {
|
addDisposableListener(resizeHandleElement, 'mousedown', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
let offset: {
|
let startPosition: {
|
||||||
originalY: number;
|
originalY: number;
|
||||||
originalHeight: number;
|
originalHeight: number;
|
||||||
originalX: number;
|
originalX: number;
|
||||||
@ -102,19 +109,21 @@ export class Overlay extends CompositeDisposable {
|
|||||||
|
|
||||||
move.value = new CompositeDisposable(
|
move.value = new CompositeDisposable(
|
||||||
addDisposableWindowListener(window, 'mousemove', (e) => {
|
addDisposableWindowListener(window, 'mousemove', (e) => {
|
||||||
const rect =
|
const containerRect =
|
||||||
this.options.container.getBoundingClientRect();
|
this.options.container.getBoundingClientRect();
|
||||||
const y = e.clientY - rect.top;
|
const overlayRect =
|
||||||
const x = e.clientX - rect.left;
|
this._element.getBoundingClientRect();
|
||||||
|
|
||||||
const rect2 = this._element.getBoundingClientRect();
|
const y = e.clientY - containerRect.top;
|
||||||
|
const x = e.clientX - containerRect.left;
|
||||||
|
|
||||||
if (offset === null) {
|
if (startPosition === null) {
|
||||||
offset = {
|
// record the initial dimensions since as all subsequence moves are relative to this
|
||||||
|
startPosition = {
|
||||||
originalY: y,
|
originalY: y,
|
||||||
originalHeight: rect2.height,
|
originalHeight: overlayRect.height,
|
||||||
originalX: x,
|
originalX: x,
|
||||||
originalWidth: rect2.width,
|
originalWidth: overlayRect.width,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,37 +132,36 @@ export class Overlay extends CompositeDisposable {
|
|||||||
let left: number | null = null;
|
let left: number | null = null;
|
||||||
let width: number | null = null;
|
let width: number | null = null;
|
||||||
|
|
||||||
const MIN_HEIGHT = 20;
|
|
||||||
const MIN_WIDTH = 20;
|
|
||||||
|
|
||||||
function moveTop() {
|
function moveTop() {
|
||||||
top = clamp(
|
top = clamp(
|
||||||
y,
|
y,
|
||||||
0,
|
0,
|
||||||
Math.max(
|
Math.max(
|
||||||
0,
|
0,
|
||||||
offset!.originalY +
|
startPosition!.originalY +
|
||||||
offset!.originalHeight -
|
startPosition!.originalHeight -
|
||||||
MIN_HEIGHT
|
Overlay.MINIMUM_HEIGHT
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
height =
|
height =
|
||||||
offset!.originalY +
|
startPosition!.originalY +
|
||||||
offset!.originalHeight -
|
startPosition!.originalHeight -
|
||||||
top;
|
top;
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveBottom() {
|
function moveBottom() {
|
||||||
top = offset!.originalY - offset!.originalHeight;
|
top =
|
||||||
|
startPosition!.originalY -
|
||||||
|
startPosition!.originalHeight;
|
||||||
|
|
||||||
height = clamp(
|
height = clamp(
|
||||||
y - top,
|
y - top,
|
||||||
MIN_HEIGHT,
|
Overlay.MINIMUM_HEIGHT,
|
||||||
Math.max(
|
Math.max(
|
||||||
0,
|
0,
|
||||||
rect.height -
|
containerRect.height -
|
||||||
offset!.originalY +
|
startPosition!.originalY +
|
||||||
offset!.originalHeight
|
startPosition!.originalHeight
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -164,27 +172,29 @@ export class Overlay extends CompositeDisposable {
|
|||||||
0,
|
0,
|
||||||
Math.max(
|
Math.max(
|
||||||
0,
|
0,
|
||||||
offset!.originalX +
|
startPosition!.originalX +
|
||||||
offset!.originalWidth -
|
startPosition!.originalWidth -
|
||||||
MIN_WIDTH
|
Overlay.MINIMUM_WIDTH
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
width =
|
width =
|
||||||
offset!.originalX +
|
startPosition!.originalX +
|
||||||
offset!.originalWidth -
|
startPosition!.originalWidth -
|
||||||
left;
|
left;
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveRight() {
|
function moveRight() {
|
||||||
left = offset!.originalX - offset!.originalWidth;
|
left =
|
||||||
|
startPosition!.originalX -
|
||||||
|
startPosition!.originalWidth;
|
||||||
width = clamp(
|
width = clamp(
|
||||||
x - left,
|
x - left,
|
||||||
MIN_WIDTH,
|
Overlay.MINIMUM_WIDTH,
|
||||||
Math.max(
|
Math.max(
|
||||||
0,
|
0,
|
||||||
rect.width -
|
containerRect.width -
|
||||||
offset!.originalX +
|
startPosition!.originalX +
|
||||||
offset!.originalWidth
|
startPosition!.originalWidth
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -283,11 +293,12 @@ export class Overlay extends CompositeDisposable {
|
|||||||
|
|
||||||
const xOffset = Math.max(
|
const xOffset = Math.max(
|
||||||
0,
|
0,
|
||||||
overlayRect.width - this.options.minX
|
overlayRect.width - this.options.minimumInViewportWidth
|
||||||
);
|
);
|
||||||
const yOffset = Math.max(
|
const yOffset = Math.max(
|
||||||
0,
|
0,
|
||||||
overlayRect.height - this.options.minY
|
overlayRect.height -
|
||||||
|
this.options.minimumInViewportHeight
|
||||||
);
|
);
|
||||||
|
|
||||||
const left = clamp(
|
const left = clamp(
|
||||||
@ -332,6 +343,12 @@ export class Overlay extends CompositeDisposable {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if somebody has marked this event then treat as a defaultPrevented
|
||||||
|
// without actually calling event.preventDefault()
|
||||||
|
if (quasiDefaultPrevented(event)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
track();
|
track();
|
||||||
}),
|
}),
|
||||||
addDisposableListener(
|
addDisposableListener(
|
||||||
@ -342,6 +359,12 @@ export class Overlay extends CompositeDisposable {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if somebody has marked this event then treat as a defaultPrevented
|
||||||
|
// without actually calling event.preventDefault()
|
||||||
|
if (quasiDefaultPrevented(event)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (event.shiftKey) {
|
if (event.shiftKey) {
|
||||||
track();
|
track();
|
||||||
}
|
}
|
||||||
@ -368,8 +391,17 @@ export class Overlay extends CompositeDisposable {
|
|||||||
const containerRect = this.options.container.getBoundingClientRect();
|
const containerRect = this.options.container.getBoundingClientRect();
|
||||||
const overlayRect = this._element.getBoundingClientRect();
|
const overlayRect = this._element.getBoundingClientRect();
|
||||||
|
|
||||||
const xOffset = Math.max(0, overlayRect.width - this.options.minX);
|
// a minimum width of minimumViewportWidth must be inside the viewport
|
||||||
const yOffset = Math.max(0, overlayRect.height - this.options.minY);
|
const xOffset = Math.max(
|
||||||
|
0,
|
||||||
|
overlayRect.width - this.options.minimumInViewportWidth
|
||||||
|
);
|
||||||
|
|
||||||
|
// a minimum height of minimumViewportHeight must be inside the viewport
|
||||||
|
const yOffset = Math.max(
|
||||||
|
0,
|
||||||
|
overlayRect.height - this.options.minimumInViewportHeight
|
||||||
|
);
|
||||||
|
|
||||||
const left = clamp(
|
const left = clamp(
|
||||||
this.options.left,
|
this.options.left,
|
||||||
|
@ -191,7 +191,14 @@ export class TabsContainer
|
|||||||
this.voidContainer.element,
|
this.voidContainer.element,
|
||||||
'mousedown',
|
'mousedown',
|
||||||
(event) => {
|
(event) => {
|
||||||
if (event.shiftKey && !this.group.isFloating) {
|
const isFloatingGroupsEnabled =
|
||||||
|
!this.accessor.options.disableFloatingGroups;
|
||||||
|
|
||||||
|
if (
|
||||||
|
isFloatingGroupsEnabled &&
|
||||||
|
event.shiftKey &&
|
||||||
|
!this.group.isFloating
|
||||||
|
) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const { top, left } =
|
const { top, left } =
|
||||||
@ -282,7 +289,10 @@ export class TabsContainer
|
|||||||
|
|
||||||
const disposable = CompositeDisposable.from(
|
const disposable = CompositeDisposable.from(
|
||||||
tabToAdd.onChanged((event) => {
|
tabToAdd.onChanged((event) => {
|
||||||
if (event.shiftKey) {
|
const isFloatingGroupsEnabled =
|
||||||
|
!this.accessor.options.disableFloatingGroups;
|
||||||
|
|
||||||
|
if (isFloatingGroupsEnabled && event.shiftKey) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const panel = this.accessor.getGroupPanel(tabToAdd.panelId);
|
const panel = this.accessor.getGroupPanel(tabToAdd.panelId);
|
||||||
|
@ -345,8 +345,8 @@ export class DockviewComponent
|
|||||||
width: coord?.width ?? 300,
|
width: coord?.width ?? 300,
|
||||||
left: overlayLeft,
|
left: overlayLeft,
|
||||||
top: overlayTop,
|
top: overlayTop,
|
||||||
minX: 100,
|
minimumInViewportWidth: 100,
|
||||||
minY: 100,
|
minimumInViewportHeight: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
const el = group.element.querySelector('#dv-group-float-drag-handle');
|
const el = group.element.querySelector('#dv-group-float-drag-handle');
|
||||||
@ -439,6 +439,7 @@ export class DockviewComponent
|
|||||||
|
|
||||||
if (this.floatingGroups) {
|
if (this.floatingGroups) {
|
||||||
for (const floating of this.floatingGroups) {
|
for (const floating of this.floatingGroups) {
|
||||||
|
// ensure floting groups stay within visible boundaries
|
||||||
floating.overlay.renderWithinBoundaryConditions();
|
floating.overlay.renderWithinBoundaryConditions();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -588,7 +589,7 @@ export class DockviewComponent
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.layout(width, height);
|
this.layout(width, height, true);
|
||||||
|
|
||||||
const serializedFloatingGroups = data.floatingGroups ?? [];
|
const serializedFloatingGroups = data.floatingGroups ?? [];
|
||||||
|
|
||||||
|
@ -261,12 +261,12 @@ export class DockviewGroupPanelModel
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.shiftKey && !this.isFloating) {
|
const data = getPanelData();
|
||||||
|
|
||||||
|
if (!data && event.shiftKey && !this.isFloating) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = getPanelData();
|
|
||||||
|
|
||||||
if (data && data.viewId === this.accessor.id) {
|
if (data && data.viewId === this.accessor.id) {
|
||||||
if (data.groupId === this.id) {
|
if (data.groupId === this.id) {
|
||||||
if (position === 'center') {
|
if (position === 'center') {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
Event,
|
Event as DockviewEvent,
|
||||||
Emitter,
|
Emitter,
|
||||||
addDisposableListener,
|
addDisposableListener,
|
||||||
addDisposableWindowListener,
|
addDisposableWindowListener,
|
||||||
@ -87,8 +87,8 @@ export function getElementsByTagName(tag: string): HTMLElement[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IFocusTracker extends IDisposable {
|
export interface IFocusTracker extends IDisposable {
|
||||||
readonly onDidFocus: Event<void>;
|
readonly onDidFocus: DockviewEvent<void>;
|
||||||
readonly onDidBlur: Event<void>;
|
readonly onDidBlur: DockviewEvent<void>;
|
||||||
refreshState?(): void;
|
refreshState?(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,10 +101,10 @@ export function trackFocus(element: HTMLElement | Window): IFocusTracker {
|
|||||||
*/
|
*/
|
||||||
class FocusTracker extends CompositeDisposable implements IFocusTracker {
|
class FocusTracker extends CompositeDisposable implements IFocusTracker {
|
||||||
private readonly _onDidFocus = new Emitter<void>();
|
private readonly _onDidFocus = new Emitter<void>();
|
||||||
public readonly onDidFocus: Event<void> = this._onDidFocus.event;
|
public readonly onDidFocus: DockviewEvent<void> = this._onDidFocus.event;
|
||||||
|
|
||||||
private readonly _onDidBlur = new Emitter<void>();
|
private readonly _onDidBlur = new Emitter<void>();
|
||||||
public readonly onDidBlur: Event<void> = this._onDidBlur.event;
|
public readonly onDidBlur: DockviewEvent<void> = this._onDidBlur.event;
|
||||||
|
|
||||||
private _refreshStateHandler: () => void;
|
private _refreshStateHandler: () => void;
|
||||||
|
|
||||||
@ -172,3 +172,16 @@ class FocusTracker extends CompositeDisposable implements IFocusTracker {
|
|||||||
this._refreshStateHandler();
|
this._refreshStateHandler();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// quasi: apparently, but not really; seemingly
|
||||||
|
const QUASI_PREVENT_DEFAULT_KEY = 'dv-quasiPreventDefault';
|
||||||
|
|
||||||
|
// mark an event directly for other listeners to check
|
||||||
|
export function quasiPreventDefault(event: Event): void {
|
||||||
|
(event as any)[QUASI_PREVENT_DEFAULT_KEY] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if this event has been marked
|
||||||
|
export function quasiDefaultPrevented(event: Event): boolean {
|
||||||
|
return (event as any)[QUASI_PREVENT_DEFAULT_KEY];
|
||||||
|
}
|
||||||
|
@ -219,7 +219,7 @@ export class GridviewComponent
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.layout(width, height);
|
this.layout(width, height, true);
|
||||||
|
|
||||||
queue.forEach((f) => f());
|
queue.forEach((f) => f());
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user