mirror of
https://github.com/mathuo/dockview
synced 2025-01-22 17:35:57 +00:00
feat: window popout enhancements
This commit is contained in:
parent
6274708acb
commit
8f9d225c61
@ -111,6 +111,13 @@ describe('dockviewComponent', () => {
|
||||
});
|
||||
|
||||
describe('memory leakage', () => {
|
||||
beforeEach(() => {
|
||||
window.open = () => fromPartial<Window>({
|
||||
addEventListener: jest.fn(),
|
||||
close: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
test('event leakage', () => {
|
||||
Emitter.setLeakageMonitorEnabled(true);
|
||||
|
||||
@ -4415,6 +4422,15 @@ describe('dockviewComponent', () => {
|
||||
});
|
||||
|
||||
describe('popout group', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(window, 'open').mockReturnValue(
|
||||
fromPartial<Window>({
|
||||
addEventListener: jest.fn(),
|
||||
close: jest.fn(),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('that can remove a popout group', () => {
|
||||
const container = document.createElement('div');
|
||||
|
||||
|
@ -833,7 +833,7 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
|
||||
onDidOpen?: (event: { id: string; window: Window }) => void;
|
||||
onWillClose?: (event: { id: string; window: Window }) => void;
|
||||
}
|
||||
): void {
|
||||
this.component.addPopoutGroup(item, options);
|
||||
): Promise<boolean> {
|
||||
return this.component.addPopoutGroup(item, options);
|
||||
}
|
||||
}
|
||||
|
@ -9,12 +9,9 @@ export interface DockviewGroupPanelApi extends GridviewPanelApi {
|
||||
readonly onDidLocationChange: Event<DockviewGroupPanelFloatingChangeEvent>;
|
||||
readonly location: DockviewGroupLocation;
|
||||
/**
|
||||
*
|
||||
* If you require the documents Window object you can call `document.defaultView`.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Document/defaultView
|
||||
* If you require the Window object
|
||||
*/
|
||||
getDocument(): Document;
|
||||
getWindow(): Window;
|
||||
moveTo(options: { group?: DockviewGroupPanel; position?: Position }): void;
|
||||
maximize(): void;
|
||||
isMaximized(): boolean;
|
||||
@ -49,10 +46,10 @@ export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl {
|
||||
this.addDisposables(this._onDidLocationChange);
|
||||
}
|
||||
|
||||
getDocument(): Document {
|
||||
getWindow(): Window {
|
||||
return this.location.type === 'popout'
|
||||
? this.location.getWindow().document
|
||||
: window.document;
|
||||
? this.location.getWindow()
|
||||
: window;
|
||||
}
|
||||
|
||||
moveTo(options: { group?: DockviewGroupPanel; position?: Position }): void {
|
||||
|
@ -44,12 +44,9 @@ export interface DockviewPanelApi
|
||||
isMaximized(): boolean;
|
||||
exitMaximized(): void;
|
||||
/**
|
||||
*
|
||||
* If you require the documents Window object you can call `document.defaultView`.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Document/defaultView
|
||||
* If you require the Window object
|
||||
*/
|
||||
getDocument(): Document;
|
||||
getWindow(): Window;
|
||||
}
|
||||
|
||||
export class DockviewPanelApiImpl
|
||||
@ -145,8 +142,8 @@ export class DockviewPanelApiImpl
|
||||
);
|
||||
}
|
||||
|
||||
getDocument(): Document {
|
||||
return this.group.api.getDocument();
|
||||
getWindow(): Window {
|
||||
return this.group.api.getWindow();
|
||||
}
|
||||
|
||||
moveTo(options: {
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
} from '../dnd/droptarget';
|
||||
import { tail, sequenceEquals, remove } from '../array';
|
||||
import { DockviewPanel, IDockviewPanel } from './dockviewPanel';
|
||||
import { CompositeDisposable, Disposable } from '../lifecycle';
|
||||
import { CompositeDisposable, Disposable, IDisposable } from '../lifecycle';
|
||||
import { Event, Emitter } from '../events';
|
||||
import { Watermark } from './components/watermark/watermark';
|
||||
import {
|
||||
@ -74,7 +74,7 @@ const DEFAULT_ROOT_OVERLAY_MODEL: DroptargetOverlayModel = {
|
||||
size: { type: 'pixels', value: 20 },
|
||||
};
|
||||
|
||||
function getTheme(element: HTMLElement): string | undefined {
|
||||
function getDockviewTheme(element: HTMLElement): string | undefined {
|
||||
function toClassList(element: HTMLElement) {
|
||||
const list: string[] = [];
|
||||
|
||||
@ -290,7 +290,7 @@ export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
|
||||
onDidOpen?: (event: { id: string; window: Window }) => void;
|
||||
onWillClose?: (event: { id: string; window: Window }) => void;
|
||||
}
|
||||
): void;
|
||||
): Promise<boolean>;
|
||||
}
|
||||
|
||||
export class DockviewComponent
|
||||
@ -332,7 +332,11 @@ export class DockviewComponent
|
||||
this._onDidActivePanelChange.event;
|
||||
|
||||
private readonly _floatingGroups: DockviewFloatingGroupPanel[] = [];
|
||||
private readonly _popoutGroups: DockviewPopoutGroupPanel[] = [];
|
||||
private readonly _popoutGroups: {
|
||||
window: PopoutWindow;
|
||||
group: DockviewGroupPanel;
|
||||
disposable: IDisposable;
|
||||
}[] = [];
|
||||
private readonly _rootDropTarget: Droptarget;
|
||||
|
||||
get orientation(): Orientation {
|
||||
@ -413,7 +417,7 @@ export class DockviewComponent
|
||||
|
||||
// iterate over a copy of the array since .dispose() mutates the original array
|
||||
for (const group of [...this._popoutGroups]) {
|
||||
group.dispose();
|
||||
group.disposable.dispose();
|
||||
}
|
||||
})
|
||||
);
|
||||
@ -510,7 +514,7 @@ export class DockviewComponent
|
||||
this.updateWatermark();
|
||||
}
|
||||
|
||||
addPopoutGroup(
|
||||
async addPopoutGroup(
|
||||
item: DockviewPanel | DockviewGroupPanel,
|
||||
options?: {
|
||||
skipRemoveGroup?: boolean;
|
||||
@ -519,72 +523,108 @@ export class DockviewComponent
|
||||
onDidOpen?: (event: { id: string; window: Window }) => void;
|
||||
onWillClose?: (event: { id: string; window: Window }) => void;
|
||||
}
|
||||
): void {
|
||||
let group: DockviewGroupPanel;
|
||||
let box: Box | undefined = options?.position;
|
||||
): Promise<boolean> {
|
||||
const theme = getDockviewTheme(this.gridview.element);
|
||||
|
||||
if (item instanceof DockviewPanel) {
|
||||
group = this.createGroup();
|
||||
|
||||
this.removePanel(item, {
|
||||
removeEmptyGroup: true,
|
||||
skipDispose: true,
|
||||
});
|
||||
|
||||
group.model.openPanel(item);
|
||||
|
||||
if (!box) {
|
||||
box = this.element.getBoundingClientRect();
|
||||
}
|
||||
} else {
|
||||
group = item;
|
||||
|
||||
if (!box) {
|
||||
box = group.element.getBoundingClientRect();
|
||||
const getBox: () => Box = () => {
|
||||
if (options?.position) {
|
||||
return options.position;
|
||||
}
|
||||
|
||||
const skip =
|
||||
typeof options?.skipRemoveGroup === 'boolean' &&
|
||||
options.skipRemoveGroup;
|
||||
|
||||
if (!skip) {
|
||||
this.doRemoveGroup(item, { skipDispose: true });
|
||||
if (item instanceof DockviewGroupPanel) {
|
||||
return item.element.getBoundingClientRect();
|
||||
}
|
||||
}
|
||||
|
||||
const theme = getTheme(this.gridview.element);
|
||||
if (item.group) {
|
||||
return item.group.element.getBoundingClientRect();
|
||||
}
|
||||
return this.element.getBoundingClientRect();
|
||||
};
|
||||
|
||||
const popoutWindow = new DockviewPopoutGroupPanel(
|
||||
`${this.id}-${group.id}`, // globally unique within dockview
|
||||
group,
|
||||
const box: Box = getBox();
|
||||
|
||||
const groupId =
|
||||
item instanceof DockviewGroupPanel
|
||||
? item.id
|
||||
: this.getNextGroupId();
|
||||
|
||||
const _window = new PopoutWindow(
|
||||
`${this.id}-${groupId}`, // globally unique within dockview
|
||||
theme ?? '',
|
||||
{
|
||||
className: theme ?? '',
|
||||
popoutUrl: options?.popoutUrl ?? '/popout.html',
|
||||
box: {
|
||||
left: window.screenX + box.left,
|
||||
top: window.screenY + box.top,
|
||||
width: box.width,
|
||||
height: box.height,
|
||||
},
|
||||
url: options?.popoutUrl ?? '/popout.html',
|
||||
left: window.screenX + box.left,
|
||||
top: window.screenY + box.top,
|
||||
width: box.width,
|
||||
height: box.height,
|
||||
onDidOpen: options?.onDidOpen,
|
||||
onWillClose: options?.onWillClose,
|
||||
}
|
||||
);
|
||||
|
||||
popoutWindow.addDisposables(
|
||||
{
|
||||
dispose: () => {
|
||||
remove(this._popoutGroups, popoutWindow);
|
||||
this.updateWatermark();
|
||||
},
|
||||
},
|
||||
popoutWindow.window.onDidClose(() => {
|
||||
this.doAddGroup(group, [0]);
|
||||
const disposables = new CompositeDisposable(
|
||||
_window,
|
||||
_window.onDidClose(() => {
|
||||
disposables.dispose();
|
||||
})
|
||||
);
|
||||
|
||||
this._popoutGroups.push(popoutWindow);
|
||||
this.updateWatermark();
|
||||
const popoutContainer = await _window.open();
|
||||
|
||||
if (popoutContainer) {
|
||||
let group: DockviewGroupPanel;
|
||||
|
||||
if (item instanceof DockviewPanel) {
|
||||
group = this.createGroup({ id: groupId });
|
||||
|
||||
this.removePanel(item, {
|
||||
removeEmptyGroup: true,
|
||||
skipDispose: true,
|
||||
});
|
||||
|
||||
group.model.openPanel(item);
|
||||
} else {
|
||||
group = item;
|
||||
|
||||
const skip =
|
||||
typeof options?.skipRemoveGroup === 'boolean' &&
|
||||
options.skipRemoveGroup;
|
||||
|
||||
if (!skip) {
|
||||
this.doRemoveGroup(item, { skipDispose: true });
|
||||
}
|
||||
}
|
||||
|
||||
popoutContainer.appendChild(group.element);
|
||||
|
||||
group.model.location = {
|
||||
type: 'popout',
|
||||
getWindow: () => _window.window!,
|
||||
};
|
||||
|
||||
const value = { window: _window, group, disposable: disposables };
|
||||
|
||||
disposables.addDisposables(
|
||||
{
|
||||
dispose: () => {
|
||||
group.model.location = { type: 'grid' };
|
||||
|
||||
remove(this._popoutGroups, value);
|
||||
this.updateWatermark();
|
||||
},
|
||||
},
|
||||
_window.onDidClose(() => {
|
||||
this.doAddGroup(group, [0]);
|
||||
})
|
||||
);
|
||||
|
||||
this._popoutGroups.push(value);
|
||||
this.updateWatermark();
|
||||
return true;
|
||||
} else {
|
||||
disposables.dispose();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
addFloatingGroup(
|
||||
@ -1428,7 +1468,7 @@ export class DockviewComponent
|
||||
this._onDidRemoveGroup.fire(group);
|
||||
}
|
||||
|
||||
selectedGroup.dispose();
|
||||
selectedGroup.disposable.dispose();
|
||||
|
||||
if (!options?.skipActive && this._activeGroup === group) {
|
||||
const groups = Array.from(this._groups.values());
|
||||
@ -1595,7 +1635,7 @@ export class DockviewComponent
|
||||
if (!selectedPopoutGroup) {
|
||||
throw new Error('failed to find popout group');
|
||||
}
|
||||
selectedPopoutGroup.dispose();
|
||||
selectedPopoutGroup.disposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1629,6 +1669,15 @@ export class DockviewComponent
|
||||
}
|
||||
}
|
||||
|
||||
private getNextGroupId(): string {
|
||||
let id = this.nextGroupId.next();
|
||||
while (this._groups.has(id)) {
|
||||
id = this.nextGroupId.next();
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
createGroup(options?: GroupOptions): DockviewGroupPanel {
|
||||
if (!options) {
|
||||
options = {};
|
||||
|
@ -1,14 +1,12 @@
|
||||
import { CompositeDisposable } from '../lifecycle';
|
||||
import { PopoutWindow } from '../popoutWindow';
|
||||
import { Box } from '../types';
|
||||
import { DockviewGroupPanel } from './dockviewGroupPanel';
|
||||
|
||||
export class DockviewPopoutGroupPanel extends CompositeDisposable {
|
||||
readonly window: PopoutWindow;
|
||||
|
||||
constructor(
|
||||
readonly id: string,
|
||||
readonly group: DockviewGroupPanel,
|
||||
private readonly options: {
|
||||
className: string;
|
||||
popoutUrl: string;
|
||||
@ -29,23 +27,17 @@ export class DockviewPopoutGroupPanel extends CompositeDisposable {
|
||||
onWillClose: this.options.onWillClose,
|
||||
});
|
||||
|
||||
group.model.location = {
|
||||
type: 'popout',
|
||||
getWindow: () => this.window.window!,
|
||||
};
|
||||
|
||||
this.addDisposables(
|
||||
this.window,
|
||||
{
|
||||
dispose: () => {
|
||||
group.model.location = { type: 'grid' };
|
||||
},
|
||||
},
|
||||
this.window.onDidClose(() => {
|
||||
this.dispose();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this.window.open(group.element);
|
||||
open(): Promise<HTMLElement | null> {
|
||||
const didOpen = this.window.open();
|
||||
|
||||
return didOpen;
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ export class PopoutWindow extends CompositeDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
open(content: HTMLElement): void {
|
||||
async open(): Promise<HTMLElement | null> {
|
||||
if (this._window) {
|
||||
throw new Error('instance of popout window is already open');
|
||||
}
|
||||
@ -88,9 +88,13 @@ export class PopoutWindow extends CompositeDisposable {
|
||||
const externalWindow = window.open(url, this.target, features);
|
||||
|
||||
if (!externalWindow) {
|
||||
return;
|
||||
/**
|
||||
* Popup blocked
|
||||
*/
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
const disposable = new CompositeDisposable();
|
||||
|
||||
this._window = { value: externalWindow, disposable };
|
||||
@ -104,36 +108,41 @@ export class PopoutWindow extends CompositeDisposable {
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
|
||||
*/
|
||||
this.close();
|
||||
})
|
||||
);
|
||||
|
||||
externalWindow.addEventListener('load', () => {
|
||||
/**
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event
|
||||
*/
|
||||
|
||||
const externalDocument = externalWindow.document;
|
||||
externalDocument.title = document.title;
|
||||
|
||||
const container = this.createPopoutWindowContainer();
|
||||
container.classList.add(this.className);
|
||||
container.appendChild(content);
|
||||
|
||||
// externalDocument.body.replaceChildren(container);
|
||||
externalDocument.body.appendChild(container);
|
||||
externalDocument.body.classList.add(this.className);
|
||||
|
||||
addStyles(externalDocument, window.document.styleSheets);
|
||||
|
||||
externalWindow.addEventListener('beforeunload', () => {
|
||||
}),
|
||||
addDisposableWindowListener(externalWindow, 'beforeunload', () => {
|
||||
/**
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
|
||||
*/
|
||||
this.close();
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const container = this.createPopoutWindowContainer();
|
||||
container.classList.add(this.className);
|
||||
|
||||
this.options.onDidOpen?.({
|
||||
id: this.target,
|
||||
window: externalWindow,
|
||||
});
|
||||
|
||||
this.options.onDidOpen?.({ id: this.target, window: externalWindow });
|
||||
return new Promise<HTMLElement | null>((resolve) => {
|
||||
externalWindow.addEventListener('load', () => {
|
||||
/**
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event
|
||||
*/
|
||||
|
||||
const externalDocument = externalWindow.document;
|
||||
externalDocument.title = document.title;
|
||||
|
||||
// externalDocument.body.replaceChildren(container);
|
||||
externalDocument.body.appendChild(container);
|
||||
externalDocument.body.classList.add(this.className);
|
||||
|
||||
addStyles(externalDocument, window.document.styleSheets);
|
||||
|
||||
resolve(container);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private createPopoutWindowContainer(): HTMLElement {
|
||||
|
@ -34,6 +34,7 @@
|
||||
"dockview": "^1.9.2",
|
||||
"prism-react-renderer": "^2.3.1",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-laag": "^2.0.5",
|
||||
"recoil": "^0.7.7",
|
||||
"source-map-loader": "^4.0.2",
|
||||
"uuid": "^9.0.1"
|
||||
|
@ -6,49 +6,30 @@ import {
|
||||
IDockviewPanelProps,
|
||||
SerializedDockview,
|
||||
DockviewPanelApi,
|
||||
DockviewGroupLocation,
|
||||
} from 'dockview';
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { Icon } from './utils';
|
||||
import { PopoverMenu } from './popover';
|
||||
|
||||
function usePopoutWindowContext(api: DockviewPanelApi): Window {
|
||||
const [location, setLocation] = React.useState<DockviewGroupLocation>(
|
||||
api.location
|
||||
);
|
||||
function usePanelWindowObject(api: DockviewPanelApi): Window {
|
||||
const [document, setDocument] = React.useState<Window>(api.getWindow());
|
||||
|
||||
React.useEffect(() => {
|
||||
const disposable = api.onDidLocationChange((event) => {
|
||||
setLocation(event.location);
|
||||
setDocument(api.getWindow());
|
||||
});
|
||||
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
});
|
||||
}, [api]);
|
||||
|
||||
const windowContext = React.useMemo(() => {
|
||||
if (location.type === 'popout') {
|
||||
return location.getWindow();
|
||||
}
|
||||
return window;
|
||||
}, [location]);
|
||||
|
||||
return windowContext;
|
||||
return document;
|
||||
}
|
||||
|
||||
const components = {
|
||||
default: (props: IDockviewPanelProps<{ title: string }>) => {
|
||||
const windowContext = usePopoutWindowContext(props.api);
|
||||
|
||||
React.useEffect(() => {
|
||||
setTimeout(() => {
|
||||
const a = windowContext.document.createElement('div');
|
||||
a.className = 'aaa';
|
||||
windowContext.document.body.appendChild(a);
|
||||
}, 5000);
|
||||
}, [windowContext]);
|
||||
const _window = usePanelWindowObject(props.api);
|
||||
|
||||
const [reset, setReset] = React.useState<boolean>(false);
|
||||
|
||||
@ -62,7 +43,7 @@ const components = {
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
console.log(windowContext);
|
||||
console.log(_window);
|
||||
setReset(true);
|
||||
setTimeout(() => {
|
||||
setReset(false);
|
||||
@ -71,7 +52,7 @@ const components = {
|
||||
>
|
||||
Print
|
||||
</button>
|
||||
{!reset && <PopoverMenu api={props.api} />}
|
||||
{!reset && <PopoverMenu window={_window} />}
|
||||
{props.api.title}
|
||||
</div>
|
||||
);
|
||||
@ -258,12 +239,12 @@ const LeftComponent = (props: IDockviewHeaderActionsProps) => {
|
||||
|
||||
const RightComponent = (props: IDockviewHeaderActionsProps) => {
|
||||
const [popout, setPopout] = React.useState<boolean>(
|
||||
props.api.location === 'popout'
|
||||
props.api.location.type === 'popout'
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const disposable = props.group.api.onDidLocationChange((event) => [
|
||||
setPopout(event.location === 'popout'),
|
||||
setPopout(event.location.type === 'popout'),
|
||||
]);
|
||||
|
||||
return () => {
|
||||
|
@ -3,7 +3,7 @@ import { motion, AnimatePresence } from 'framer-motion';
|
||||
import * as React from 'react';
|
||||
import { DockviewPanelApi } from 'dockview';
|
||||
|
||||
export function PopoverMenu(props: { api: DockviewPanelApi }) {
|
||||
export function PopoverMenu(props: { window: Window }) {
|
||||
const [isOpen, setOpen] = React.useState(false);
|
||||
|
||||
// helper function to close the menu
|
||||
@ -11,11 +11,6 @@ export function PopoverMenu(props: { api: DockviewPanelApi }) {
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
const _window =
|
||||
props.api.location.type === 'popout'
|
||||
? props.api.location.getWindow()
|
||||
: undefined;
|
||||
|
||||
const { renderLayer, triggerProps, layerProps, arrowProps } = useLayer({
|
||||
isOpen,
|
||||
onOutsideClick: close, // close the menu when the user clicks outside
|
||||
@ -26,15 +21,14 @@ export function PopoverMenu(props: { api: DockviewPanelApi }) {
|
||||
triggerOffset: 12, // keep some distance to the trigger
|
||||
containerOffset: 16, // give the menu some room to breath relative to the container
|
||||
arrowOffset: 16, // let the arrow have some room to breath also,
|
||||
environment: _window,
|
||||
container: _window
|
||||
environment: props.window,
|
||||
container: props.window
|
||||
? () => {
|
||||
const el = _window.document.body;
|
||||
const el = props.window.document.body;
|
||||
Object.setPrototypeOf(el, HTMLElement.prototype);
|
||||
return el;
|
||||
}
|
||||
: undefined,
|
||||
// container: props.window.document.body
|
||||
});
|
||||
|
||||
// Again, we're using framer-motion for the transition effect
|
||||
|
Loading…
Reference in New Issue
Block a user