feat: floating groups

This commit is contained in:
mathuo 2023-07-06 21:13:18 +01:00
parent f364bb70a6
commit 12b4a0d27b
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
9 changed files with 172 additions and 58 deletions

View File

@ -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,
}); });

View File

@ -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 {

View File

@ -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 {

View File

@ -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,

View File

@ -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);

View File

@ -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 ?? [];

View File

@ -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') {

View File

@ -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];
}

View File

@ -219,7 +219,7 @@ export class GridviewComponent
}, },
}); });
this.layout(width, height); this.layout(width, height, true);
queue.forEach((f) => f()); queue.forEach((f) => f());