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,
left: 10,
top: 20,
minX: 0,
minY: 0,
minimumInViewportWidth: 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,
content,
});
@ -48,8 +86,8 @@ describe('overlay', () => {
width: 500,
left: 100,
top: 200,
minX: 0,
minY: 0,
minimumInViewportWidth: 0,
minimumInViewportHeight: 0,
container,
content,
});

View File

@ -8,7 +8,7 @@ import { PanelUpdateEvent } from '../../panel/types';
import { Orientation } from '../../splitview/splitview';
import { CompositeDisposable } from '../../lifecycle';
import { Emitter } from '../../events';
import { DockviewPanel, IDockviewPanel } from '../../dockview/dockviewPanel';
import { IDockviewPanel } from '../../dockview/dockviewPanel';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
class PanelContentPartTest implements IContentRenderer {

View File

@ -1,4 +1,6 @@
import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel';
import { quasiPreventDefault } from '../dom';
import { addDisposableListener } from '../events';
import { IDisposable } from '../lifecycle';
import { DragHandler } from './abstractDragHandler';
import { LocalSelectionTransfer, PanelTransfer } from './dataTransfer';
@ -14,6 +16,24 @@ export class GroupDragHandler extends DragHandler {
private readonly group: DockviewGroupPanel
) {
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 {

View File

@ -1,4 +1,4 @@
import { toggleClass } from '../dom';
import { quasiDefaultPrevented, toggleClass } from '../dom';
import {
Emitter,
Event,
@ -7,6 +7,7 @@ import {
} from '../events';
import { CompositeDisposable, MutableDisposable } from '../lifecycle';
import { clamp } from '../math';
import { getPaneData, getPanelData } from './dataTransfer';
const bringElementToFront = (() => {
let previous: HTMLElement | null = null;
@ -29,6 +30,9 @@ export class Overlay extends CompositeDisposable {
private readonly _onDidChange = new Emitter<void>();
readonly onDidChange: Event<void> = this._onDidChange.event;
private static MINIMUM_HEIGHT = 20;
private static MINIMUM_WIDTH = 20;
constructor(
private readonly options: {
height: number;
@ -37,8 +41,8 @@ export class Overlay extends CompositeDisposable {
top: number;
container: HTMLElement;
content: HTMLElement;
minX: number;
minY: number;
minimumInViewportWidth: number;
minimumInViewportHeight: number;
}
) {
super();
@ -57,6 +61,9 @@ export class Overlay extends CompositeDisposable {
this._element.appendChild(this.options.content);
this.options.container.appendChild(this._element);
// if input bad resize within acceptable boundaries
this.renderWithinBoundaryConditions();
}
toJSON(): { top: number; left: number; height: number; width: number } {
@ -93,7 +100,7 @@ export class Overlay extends CompositeDisposable {
addDisposableListener(resizeHandleElement, 'mousedown', (e) => {
e.preventDefault();
let offset: {
let startPosition: {
originalY: number;
originalHeight: number;
originalX: number;
@ -102,19 +109,21 @@ export class Overlay extends CompositeDisposable {
move.value = new CompositeDisposable(
addDisposableWindowListener(window, 'mousemove', (e) => {
const rect =
const containerRect =
this.options.container.getBoundingClientRect();
const y = e.clientY - rect.top;
const x = e.clientX - rect.left;
const overlayRect =
this._element.getBoundingClientRect();
const rect2 = this._element.getBoundingClientRect();
const y = e.clientY - containerRect.top;
const x = e.clientX - containerRect.left;
if (offset === null) {
offset = {
if (startPosition === null) {
// record the initial dimensions since as all subsequence moves are relative to this
startPosition = {
originalY: y,
originalHeight: rect2.height,
originalHeight: overlayRect.height,
originalX: x,
originalWidth: rect2.width,
originalWidth: overlayRect.width,
};
}
@ -123,37 +132,36 @@ export class Overlay extends CompositeDisposable {
let left: number | null = null;
let width: number | null = null;
const MIN_HEIGHT = 20;
const MIN_WIDTH = 20;
function moveTop() {
top = clamp(
y,
0,
Math.max(
0,
offset!.originalY +
offset!.originalHeight -
MIN_HEIGHT
startPosition!.originalY +
startPosition!.originalHeight -
Overlay.MINIMUM_HEIGHT
)
);
height =
offset!.originalY +
offset!.originalHeight -
startPosition!.originalY +
startPosition!.originalHeight -
top;
}
function moveBottom() {
top = offset!.originalY - offset!.originalHeight;
top =
startPosition!.originalY -
startPosition!.originalHeight;
height = clamp(
y - top,
MIN_HEIGHT,
Overlay.MINIMUM_HEIGHT,
Math.max(
0,
rect.height -
offset!.originalY +
offset!.originalHeight
containerRect.height -
startPosition!.originalY +
startPosition!.originalHeight
)
);
}
@ -164,27 +172,29 @@ export class Overlay extends CompositeDisposable {
0,
Math.max(
0,
offset!.originalX +
offset!.originalWidth -
MIN_WIDTH
startPosition!.originalX +
startPosition!.originalWidth -
Overlay.MINIMUM_WIDTH
)
);
width =
offset!.originalX +
offset!.originalWidth -
startPosition!.originalX +
startPosition!.originalWidth -
left;
}
function moveRight() {
left = offset!.originalX - offset!.originalWidth;
left =
startPosition!.originalX -
startPosition!.originalWidth;
width = clamp(
x - left,
MIN_WIDTH,
Overlay.MINIMUM_WIDTH,
Math.max(
0,
rect.width -
offset!.originalX +
offset!.originalWidth
containerRect.width -
startPosition!.originalX +
startPosition!.originalWidth
)
);
}
@ -283,11 +293,12 @@ export class Overlay extends CompositeDisposable {
const xOffset = Math.max(
0,
overlayRect.width - this.options.minX
overlayRect.width - this.options.minimumInViewportWidth
);
const yOffset = Math.max(
0,
overlayRect.height - this.options.minY
overlayRect.height -
this.options.minimumInViewportHeight
);
const left = clamp(
@ -332,6 +343,12 @@ export class Overlay extends CompositeDisposable {
return;
}
// if somebody has marked this event then treat as a defaultPrevented
// without actually calling event.preventDefault()
if (quasiDefaultPrevented(event)) {
return;
}
track();
}),
addDisposableListener(
@ -342,6 +359,12 @@ export class Overlay extends CompositeDisposable {
return;
}
// if somebody has marked this event then treat as a defaultPrevented
// without actually calling event.preventDefault()
if (quasiDefaultPrevented(event)) {
return;
}
if (event.shiftKey) {
track();
}
@ -368,8 +391,17 @@ export class Overlay extends CompositeDisposable {
const containerRect = this.options.container.getBoundingClientRect();
const overlayRect = this._element.getBoundingClientRect();
const xOffset = Math.max(0, overlayRect.width - this.options.minX);
const yOffset = Math.max(0, overlayRect.height - this.options.minY);
// a minimum width of minimumViewportWidth must be inside the viewport
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(
this.options.left,

View File

@ -191,7 +191,14 @@ export class TabsContainer
this.voidContainer.element,
'mousedown',
(event) => {
if (event.shiftKey && !this.group.isFloating) {
const isFloatingGroupsEnabled =
!this.accessor.options.disableFloatingGroups;
if (
isFloatingGroupsEnabled &&
event.shiftKey &&
!this.group.isFloating
) {
event.preventDefault();
const { top, left } =
@ -282,7 +289,10 @@ export class TabsContainer
const disposable = CompositeDisposable.from(
tabToAdd.onChanged((event) => {
if (event.shiftKey) {
const isFloatingGroupsEnabled =
!this.accessor.options.disableFloatingGroups;
if (isFloatingGroupsEnabled && event.shiftKey) {
event.preventDefault();
const panel = this.accessor.getGroupPanel(tabToAdd.panelId);

View File

@ -345,8 +345,8 @@ export class DockviewComponent
width: coord?.width ?? 300,
left: overlayLeft,
top: overlayTop,
minX: 100,
minY: 100,
minimumInViewportWidth: 100,
minimumInViewportHeight: 100,
});
const el = group.element.querySelector('#dv-group-float-drag-handle');
@ -439,6 +439,7 @@ export class DockviewComponent
if (this.floatingGroups) {
for (const floating of this.floatingGroups) {
// ensure floting groups stay within visible boundaries
floating.overlay.renderWithinBoundaryConditions();
}
}
@ -588,7 +589,7 @@ export class DockviewComponent
},
});
this.layout(width, height);
this.layout(width, height, true);
const serializedFloatingGroups = data.floatingGroups ?? [];

View File

@ -261,12 +261,12 @@ export class DockviewGroupPanelModel
return false;
}
if (event.shiftKey && !this.isFloating) {
const data = getPanelData();
if (!data && event.shiftKey && !this.isFloating) {
return false;
}
const data = getPanelData();
if (data && data.viewId === this.accessor.id) {
if (data.groupId === this.id) {
if (position === 'center') {

View File

@ -1,5 +1,5 @@
import {
Event,
Event as DockviewEvent,
Emitter,
addDisposableListener,
addDisposableWindowListener,
@ -87,8 +87,8 @@ export function getElementsByTagName(tag: string): HTMLElement[] {
}
export interface IFocusTracker extends IDisposable {
readonly onDidFocus: Event<void>;
readonly onDidBlur: Event<void>;
readonly onDidFocus: DockviewEvent<void>;
readonly onDidBlur: DockviewEvent<void>;
refreshState?(): void;
}
@ -101,10 +101,10 @@ export function trackFocus(element: HTMLElement | Window): IFocusTracker {
*/
class FocusTracker extends CompositeDisposable implements IFocusTracker {
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>();
public readonly onDidBlur: Event<void> = this._onDidBlur.event;
public readonly onDidBlur: DockviewEvent<void> = this._onDidBlur.event;
private _refreshStateHandler: () => void;
@ -172,3 +172,16 @@ class FocusTracker extends CompositeDisposable implements IFocusTracker {
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());