mirror of
https://github.com/mathuo/dockview
synced 2025-01-22 17:35:57 +00:00
Merge pull request #421 from mathuo/397-gready-rendering-mode
feat: gready rendering
This commit is contained in:
commit
4b616c5578
@ -1,5 +1,14 @@
|
||||
import * as React from 'react';
|
||||
|
||||
/**
|
||||
* useful utility type to erase readonly signatures for testing purposes
|
||||
*
|
||||
* @see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#readonly-mapped-type-modifiers-and-readonly-arrays
|
||||
*/
|
||||
export type Writable<T> = T extends object
|
||||
? { -readonly [K in keyof T]: Writable<T[K]> }
|
||||
: T;
|
||||
|
||||
export function setMockRefElement(node: Partial<HTMLElement>): void {
|
||||
const mockRef = {
|
||||
get current() {
|
||||
@ -25,3 +34,13 @@ export function createOffsetDragOverEvent(params: {
|
||||
Object.defineProperty(event, 'clientY', { get: () => params.clientY });
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* `jest.runAllTicks` doesn't seem to exhaust all events in the micro-task queue so
|
||||
* as a **hacky** alternative we'll wait for an empty Promise to complete which runs
|
||||
* on the micro-task queue so will force a run-to-completion emptying the queue
|
||||
* of any pending micro-task
|
||||
*/
|
||||
export function exhaustMicrotaskQueue(): Promise<void> {
|
||||
return new Promise<void>((resolve) => resolve());
|
||||
}
|
||||
|
@ -0,0 +1,145 @@
|
||||
import { Droptarget } from '../dnd/droptarget';
|
||||
import { IDockviewPanel } from '../dockview/dockviewPanel';
|
||||
import { Emitter } from '../events';
|
||||
import { IRenderable, OverlayRenderContainer } from '../overlayRenderContainer';
|
||||
import { fromPartial } from '@total-typescript/shoehorn';
|
||||
import { Writable, exhaustMicrotaskQueue } from './__test_utils__/utils';
|
||||
|
||||
describe('overlayRenderContainer', () => {
|
||||
test('add a view that is not currently in the DOM', async () => {
|
||||
const parentContainer = document.createElement('div');
|
||||
|
||||
const cut = new OverlayRenderContainer(parentContainer);
|
||||
|
||||
const panelContentEl = document.createElement('div');
|
||||
|
||||
const onDidVisibilityChange = new Emitter<any>();
|
||||
const onDidDimensionsChange = new Emitter<any>();
|
||||
|
||||
const panel = fromPartial<IDockviewPanel>({
|
||||
api: {
|
||||
id: 'test_panel_id',
|
||||
onDidVisibilityChange: onDidVisibilityChange.event,
|
||||
onDidDimensionsChange: onDidDimensionsChange.event,
|
||||
isVisible: true,
|
||||
},
|
||||
view: {
|
||||
content: {
|
||||
element: panelContentEl,
|
||||
},
|
||||
},
|
||||
group: {
|
||||
api: {
|
||||
location: 'grid',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const dropTarget = fromPartial<Droptarget>({});
|
||||
|
||||
const refContainerEl = document.createElement('div');
|
||||
|
||||
const referenceContainer: IRenderable = {
|
||||
element: refContainerEl,
|
||||
dropTarget,
|
||||
};
|
||||
|
||||
(parentContainer as jest.Mocked<HTMLDivElement>).getBoundingClientRect =
|
||||
jest
|
||||
.fn<DOMRect, []>()
|
||||
.mockReturnValueOnce(
|
||||
fromPartial<DOMRect>({
|
||||
left: 100,
|
||||
top: 200,
|
||||
width: 1000,
|
||||
height: 500,
|
||||
})
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
fromPartial<DOMRect>({
|
||||
left: 101,
|
||||
top: 201,
|
||||
width: 1000,
|
||||
height: 500,
|
||||
})
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
fromPartial<DOMRect>({
|
||||
left: 100,
|
||||
top: 200,
|
||||
width: 1000,
|
||||
height: 500,
|
||||
})
|
||||
);
|
||||
|
||||
(refContainerEl as jest.Mocked<HTMLDivElement>).getBoundingClientRect =
|
||||
jest
|
||||
.fn<DOMRect, []>()
|
||||
.mockReturnValueOnce(
|
||||
fromPartial<DOMRect>({
|
||||
left: 150,
|
||||
top: 300,
|
||||
width: 100,
|
||||
height: 200,
|
||||
})
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
fromPartial<DOMRect>({
|
||||
left: 150,
|
||||
top: 300,
|
||||
width: 101,
|
||||
height: 201,
|
||||
})
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
fromPartial<DOMRect>({
|
||||
left: 150,
|
||||
top: 300,
|
||||
width: 100,
|
||||
height: 200,
|
||||
})
|
||||
);
|
||||
|
||||
const container = cut.setReferenceContentContainer(
|
||||
panel,
|
||||
referenceContainer
|
||||
);
|
||||
|
||||
await exhaustMicrotaskQueue();
|
||||
|
||||
expect(panelContentEl.parentElement).toBe(container);
|
||||
expect(container.parentElement).toBe(parentContainer);
|
||||
|
||||
expect(container.style.display).toBe('');
|
||||
|
||||
expect(container.style.left).toBe('50px');
|
||||
expect(container.style.top).toBe('100px');
|
||||
expect(container.style.width).toBe('100px');
|
||||
expect(container.style.height).toBe('200px');
|
||||
expect(refContainerEl.getBoundingClientRect).toHaveBeenCalledTimes(1);
|
||||
|
||||
onDidDimensionsChange.fire({});
|
||||
expect(container.style.display).toBe('');
|
||||
|
||||
expect(container.style.left).toBe('49px');
|
||||
expect(container.style.top).toBe('99px');
|
||||
expect(container.style.width).toBe('101px');
|
||||
expect(container.style.height).toBe('201px');
|
||||
expect(refContainerEl.getBoundingClientRect).toHaveBeenCalledTimes(2);
|
||||
|
||||
(panel as Writable<IDockviewPanel>).api.isVisible = false;
|
||||
onDidVisibilityChange.fire({});
|
||||
expect(container.style.display).toBe('none');
|
||||
expect(refContainerEl.getBoundingClientRect).toHaveBeenCalledTimes(2);
|
||||
|
||||
(panel as Writable<IDockviewPanel>).api.isVisible = true;
|
||||
onDidVisibilityChange.fire({});
|
||||
expect(container.style.display).toBe('');
|
||||
|
||||
expect(container.style.left).toBe('50px');
|
||||
expect(container.style.top).toBe('100px');
|
||||
expect(container.style.width).toBe('100px');
|
||||
expect(container.style.height).toBe('200px');
|
||||
expect(refContainerEl.getBoundingClientRect).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
@ -5,7 +5,7 @@ import { MutableDisposable } from '../lifecycle';
|
||||
import { DockviewPanel } from '../dockview/dockviewPanel';
|
||||
import { DockviewComponent } from '../dockview/dockviewComponent';
|
||||
import { Position } from '../dnd/droptarget';
|
||||
import { DockviewPanelRenderer } from '../dockview/components/greadyRenderContainer';
|
||||
import { DockviewPanelRenderer } from '../overlayRenderContainer';
|
||||
|
||||
export interface TitleEvent {
|
||||
readonly title: string;
|
||||
|
@ -56,12 +56,6 @@ export class ContentContainer
|
||||
|
||||
this.addDisposables(this._onDidFocus, this._onDidBlur);
|
||||
|
||||
// for hosted containers
|
||||
// 1) register a drop target on the host
|
||||
// 2) register window dragStart events to disable pointer events
|
||||
// 3) register dragEnd events
|
||||
// 4) register mouseMove events (if no buttons are present we take this as a dragEnd event)
|
||||
|
||||
this.dropTarget = new Droptarget(this.element, {
|
||||
acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'],
|
||||
canDisplayOverlay: (event, position) => {
|
||||
@ -127,7 +121,7 @@ export class ContentContainer
|
||||
|
||||
switch (panel.api.renderer) {
|
||||
case 'onlyWhenVisibile':
|
||||
this.accessor.greadyRenderContainer.remove(panel);
|
||||
this.accessor.overlayRenderContainer.remove(panel);
|
||||
if (isActive) {
|
||||
if (this.panel) {
|
||||
this._element.appendChild(
|
||||
@ -144,7 +138,7 @@ export class ContentContainer
|
||||
this._element.removeChild(panel.view.content.element);
|
||||
}
|
||||
container =
|
||||
this.accessor.greadyRenderContainer.setReferenceContentContainer(
|
||||
this.accessor.overlayRenderContainer.setReferenceContentContainer(
|
||||
panel,
|
||||
this
|
||||
);
|
||||
@ -203,7 +197,7 @@ export class ContentContainer
|
||||
switch (renderer) {
|
||||
case 'always':
|
||||
container =
|
||||
this.accessor.greadyRenderContainer.setReferenceContentContainer(
|
||||
this.accessor.overlayRenderContainer.setReferenceContentContainer(
|
||||
panel,
|
||||
this
|
||||
);
|
||||
|
@ -11,7 +11,7 @@
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dv-gready-render-container {
|
||||
.dv-overlay-render-container {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
@ -53,15 +53,12 @@ import {
|
||||
TabDragEvent,
|
||||
} from './components/titlebar/tabsContainer';
|
||||
import { Box } from '../types';
|
||||
import {
|
||||
GreadyRenderContainer,
|
||||
DockviewPanelRenderer,
|
||||
} from './components/greadyRenderContainer';
|
||||
import { DockviewPopoutGroupPanel } from './dockviewPopoutGroupPanel';
|
||||
import {
|
||||
DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE,
|
||||
DEFAULT_FLOATING_GROUP_POSITION,
|
||||
} from '../constants';
|
||||
import { DockviewPanelRenderer, OverlayRenderContainer } from '../overlayRenderContainer';
|
||||
|
||||
function getTheme(element: HTMLElement): string | undefined {
|
||||
function toClassList(element: HTMLElement) {
|
||||
@ -289,7 +286,7 @@ export class DockviewComponent
|
||||
private _options: Exclude<DockviewComponentOptions, 'orientation'>;
|
||||
private watermark: IWatermarkRenderer | null = null;
|
||||
|
||||
readonly greadyRenderContainer: GreadyRenderContainer;
|
||||
readonly overlayRenderContainer: OverlayRenderContainer;
|
||||
|
||||
private readonly _onWillDragPanel = new Emitter<TabDragEvent>();
|
||||
readonly onWillDragPanel: Event<TabDragEvent> = this._onWillDragPanel.event;
|
||||
@ -360,16 +357,16 @@ export class DockviewComponent
|
||||
});
|
||||
|
||||
const gready = document.createElement('div');
|
||||
gready.className = 'dv-gready-render-container';
|
||||
gready.className = 'dv-overlay-render-container';
|
||||
this.gridview.element.appendChild(gready);
|
||||
|
||||
this.greadyRenderContainer = new GreadyRenderContainer(gready);
|
||||
this.overlayRenderContainer = new OverlayRenderContainer(gready);
|
||||
|
||||
toggleClass(this.gridview.element, 'dv-dockview', true);
|
||||
toggleClass(this.element, 'dv-debug', !!options.debug);
|
||||
|
||||
this.addDisposables(
|
||||
this.greadyRenderContainer,
|
||||
this.overlayRenderContainer,
|
||||
this._onWillDragPanel,
|
||||
this._onWillDragGroup,
|
||||
this._onDidActivePanelChange,
|
||||
@ -1200,7 +1197,7 @@ export class DockviewComponent
|
||||
group.model.removePanel(panel);
|
||||
|
||||
if (!options.skipDispose) {
|
||||
this.greadyRenderContainer.remove(panel);
|
||||
this.overlayRenderContainer.remove(panel);
|
||||
panel.dispose();
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ import { CompositeDisposable, IDisposable } from '../lifecycle';
|
||||
import { IPanel, PanelUpdateEvent, Parameters } from '../panel/types';
|
||||
import { IDockviewPanelModel } from './dockviewPanelModel';
|
||||
import { DockviewComponent } from './dockviewComponent';
|
||||
import { DockviewPanelRenderer } from './components/greadyRenderContainer';
|
||||
import { DockviewPanelRenderer } from '../overlayRenderContainer';
|
||||
|
||||
export interface IDockviewPanel extends IDisposable, IPanel {
|
||||
readonly view: IDockviewPanelModel;
|
||||
|
@ -20,7 +20,7 @@ import {
|
||||
FrameworkFactory,
|
||||
} from '../panel/componentFactory';
|
||||
import { DockviewGroupPanelApi } from '../api/dockviewGroupPanelApi';
|
||||
import { DockviewPanelRenderer } from './components/greadyRenderContainer';
|
||||
import { DockviewPanelRenderer } from '../overlayRenderContainer';
|
||||
|
||||
export interface IHeaderActionsRenderer extends IDisposable {
|
||||
readonly element: HTMLElement;
|
||||
|
@ -5,7 +5,7 @@ import { DockviewApi } from '../api/component.api';
|
||||
import { Event } from '../events';
|
||||
import { Optional } from '../types';
|
||||
import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel';
|
||||
import { DockviewPanelRenderer } from './components/greadyRenderContainer';
|
||||
import { DockviewPanelRenderer } from '../overlayRenderContainer';
|
||||
|
||||
export enum DockviewDropTargets {
|
||||
Tab,
|
||||
|
@ -49,7 +49,7 @@ export * from './splitview/splitviewPanel';
|
||||
export * from './paneview/paneviewPanel';
|
||||
export * from './dockview/types';
|
||||
|
||||
export { DockviewPanelRenderer } from './dockview/components/greadyRenderContainer';
|
||||
export { DockviewPanelRenderer } from './overlayRenderContainer';
|
||||
|
||||
export {
|
||||
Position,
|
||||
|
162
packages/dockview-core/src/overlayRenderContainer.ts
Normal file
162
packages/dockview-core/src/overlayRenderContainer.ts
Normal file
@ -0,0 +1,162 @@
|
||||
import { DragAndDropObserver } from './dnd/dnd';
|
||||
import { Droptarget } from './dnd/droptarget';
|
||||
import { getDomNodePagePosition, toggleClass } from './dom';
|
||||
import { CompositeDisposable, Disposable, IDisposable } from './lifecycle';
|
||||
import { IDockviewPanel } from './dockview/dockviewPanel';
|
||||
|
||||
export type DockviewPanelRenderer = 'onlyWhenVisibile' | 'always';
|
||||
|
||||
export interface IRenderable {
|
||||
readonly element: HTMLElement;
|
||||
readonly dropTarget: Droptarget;
|
||||
}
|
||||
|
||||
function createFocusableElement(): HTMLDivElement {
|
||||
const element = document.createElement('div');
|
||||
element.tabIndex = -1;
|
||||
return element;
|
||||
}
|
||||
|
||||
export class OverlayRenderContainer extends CompositeDisposable {
|
||||
private readonly map: Record<
|
||||
string,
|
||||
{ disposable: IDisposable; element: HTMLElement }
|
||||
> = {};
|
||||
|
||||
get allIds(): string[] {
|
||||
return Object.keys(this.map);
|
||||
}
|
||||
|
||||
constructor(private readonly element: HTMLElement) {
|
||||
super();
|
||||
|
||||
this.addDisposables({
|
||||
dispose: () => {
|
||||
for (const value of Object.values(this.map)) {
|
||||
value.disposable.dispose();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
remove(panel: IDockviewPanel): boolean {
|
||||
if (this.map[panel.api.id]) {
|
||||
this.map[panel.api.id].disposable.dispose();
|
||||
delete this.map[panel.api.id];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
setReferenceContentContainer(
|
||||
panel: IDockviewPanel,
|
||||
referenceContainer: IRenderable
|
||||
): HTMLElement {
|
||||
if (!this.map[panel.api.id]) {
|
||||
const element = createFocusableElement();
|
||||
element.className = 'dv-render-overlay';
|
||||
|
||||
this.map[panel.api.id] = {
|
||||
disposable: Disposable.NONE,
|
||||
element,
|
||||
};
|
||||
}
|
||||
|
||||
this.map[panel.api.id]?.disposable.dispose();
|
||||
const focusContainer = this.map[panel.api.id].element;
|
||||
|
||||
if (panel.view.content.element.parentElement !== focusContainer) {
|
||||
focusContainer.appendChild(panel.view.content.element);
|
||||
}
|
||||
|
||||
if (focusContainer.parentElement !== this.element) {
|
||||
this.element.appendChild(focusContainer);
|
||||
}
|
||||
|
||||
const resize = () => {
|
||||
// TODO propagate position to avoid getDomNodePagePosition calls, possible performance bottleneck?
|
||||
const box = getDomNodePagePosition(referenceContainer.element);
|
||||
const box2 = getDomNodePagePosition(this.element);
|
||||
focusContainer.style.left = `${box.left - box2.left}px`;
|
||||
focusContainer.style.top = `${box.top - box2.top}px`;
|
||||
focusContainer.style.width = `${box.width}px`;
|
||||
focusContainer.style.height = `${box.height}px`;
|
||||
|
||||
toggleClass(
|
||||
focusContainer,
|
||||
'dv-render-overlay-float',
|
||||
panel.group.api.location === 'floating'
|
||||
);
|
||||
};
|
||||
|
||||
const visibilityChanged = () => {
|
||||
if (panel.api.isVisible) {
|
||||
resize();
|
||||
}
|
||||
|
||||
focusContainer.style.display = panel.api.isVisible ? '' : 'none';
|
||||
};
|
||||
|
||||
const disposable = new CompositeDisposable(
|
||||
/**
|
||||
* since container is positioned absoutely we must explicitly forward
|
||||
* the dnd events for the expect behaviours to continue to occur in terms of dnd
|
||||
*/
|
||||
new DragAndDropObserver(focusContainer, {
|
||||
onDragEnd: (e) => {
|
||||
referenceContainer.dropTarget.dnd.onDragEnd(e);
|
||||
},
|
||||
onDragEnter: (e) => {
|
||||
referenceContainer.dropTarget.dnd.onDragEnter(e);
|
||||
},
|
||||
onDragLeave: (e) => {
|
||||
referenceContainer.dropTarget.dnd.onDragLeave(e);
|
||||
},
|
||||
onDrop: (e) => {
|
||||
referenceContainer.dropTarget.dnd.onDrop(e);
|
||||
},
|
||||
onDragOver: (e) => {
|
||||
referenceContainer.dropTarget.dnd.onDragOver(e);
|
||||
},
|
||||
}),
|
||||
panel.api.onDidVisibilityChange((event) => {
|
||||
/**
|
||||
* Control the visibility of the content, however even when not visible (display: none)
|
||||
* the content is still maintained within the DOM hence DOM specific attributes
|
||||
* such as scroll position are maintained when next made visible.
|
||||
*/
|
||||
visibilityChanged();
|
||||
}),
|
||||
panel.api.onDidDimensionsChange(() => {
|
||||
if (!panel.api.isVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
resize();
|
||||
}),
|
||||
{
|
||||
dispose: () => {
|
||||
focusContainer.removeChild(panel.view.content.element);
|
||||
this.element.removeChild(focusContainer);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
queueMicrotask(() => {
|
||||
if (this.isDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* wait until everything has finished in the current stack-frame call before
|
||||
* calling the first resize as other size-altering events may still occur before
|
||||
* the end of the stack-frame.
|
||||
*/
|
||||
visibilityChanged();
|
||||
});
|
||||
|
||||
this.map[panel.api.id].disposable = disposable;
|
||||
|
||||
return focusContainer;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user