UNPKG

dockview-core

Version:

Zero dependency layout manager supporting tabs, groups, grids and splitviews for vanilla TypeScript

176 lines (175 loc) 6.97 kB
import { Emitter } from '../events'; import { remove } from '../array'; import { defineModule } from './modules'; export class PopoutWindowService { constructor(host) { this._entries = []; this._popupServices = new Map(); this._restorationCleanups = new Set(); this._restorationPromise = Promise.resolve(); this._onDidRemove = new Emitter(); this.onDidRemove = this._onDidRemove.event; this._host = host; } get entries() { return this._entries; } get restorationPromise() { return this._restorationPromise; } add(entry) { this._entries.push(entry); } remove(entry) { // Fire only on a genuine removal, and not while the host component is // tearing down (consumers don't want popout-removed events during // dispose). if (remove(this._entries, entry) && !this._host.isDisposed) { this._onDidRemove.fire(entry); } } findByGroup(group) { // A popout window may host several groups in a nested gridview, so // match by membership (DOM containment) rather than only the anchor. return this._entries.find((entry) => entry.popoutGroup === group || entry.gridview.element.contains(group.element)); } findReferenceGroupId(group) { var _a; return (_a = this._entries.find((entry) => entry.popoutGroup === group)) === null || _a === void 0 ? void 0 : _a.referenceGroup; } /** * The popout window's innerWidth/innerHeight are often 0/stale until it has * painted, and the nested gridview lays its children out to the size passed * to layout() (a plain group fills via CSS instead). To stop content * rendering into a zero box until a manual resize — and to avoid the race a * fixed number of animation frames had — observe the gridview element with * a ResizeObserver created in the POPOUT window's OWN realm. A parent-realm * observer fires unreliably across the window boundary; a same-realm one * fires reliably, including the initial observation once the window is * sized. * * @returns a disposable that disconnects the observer, or `undefined` when * the popout realm has no ResizeObserver (e.g. jsdom). */ observeGridviewSize(popoutWindow, gridview, overlayRenderContainer) { var _a; const PopoutResizeObserver = (_a = popoutWindow.window) === null || _a === void 0 ? void 0 : _a.ResizeObserver; if (!PopoutResizeObserver) { return undefined; } let lastWidth = -1; let lastHeight = -1; const relayout = () => { const win = popoutWindow.window; if (this._host.isDisposed || !win || win.closed) { return; } const width = Math.round(gridview.element.clientWidth); const height = Math.round(gridview.element.clientHeight); if (width === lastWidth && height === lastHeight) { return; } lastWidth = width; lastHeight = height; if (width > 0 && height > 0) { gridview.layout(width, height); } overlayRenderContainer.updateAllPositions(); }; const observer = new PopoutResizeObserver(() => { var _a; // Defer out of the observer callback into the popout's own frame to // size against the settled layout and to avoid resize-loop warnings. const raf = (_a = popoutWindow.window) === null || _a === void 0 ? void 0 : _a.requestAnimationFrame; if (raf) { raf.call(popoutWindow.window, relayout); } else { relayout(); } }); observer.observe(gridview.element); return { dispose: () => observer.disconnect() }; } getPopupService(groupId) { return this._popupServices.get(groupId); } setPopupService(groupId, service) { this._popupServices.set(groupId, service); } deletePopupService(groupId) { this._popupServices.delete(groupId); } scheduleRestoration(delayMs, work, onCancel) { return new Promise((resolve) => { const cleanup = () => { this._restorationCleanups.delete(cleanup); clearTimeout(handle); onCancel === null || onCancel === void 0 ? void 0 : onCancel(); resolve(); }; const handle = setTimeout(() => { this._restorationCleanups.delete(cleanup); // Guard against the component being disposed before this // timer fires. Under React StrictMode the component is // mounted -> disposed -> remounted, and without this guard // the first instance's queued restoration would open a // second popout window. See issue #851. if (this._host.isDisposed) { resolve(); return; } work(); resolve(); }, delayMs); this._restorationCleanups.add(cleanup); }); } finishRestoration(promises) { this._restorationPromise = Promise.all(promises).then(() => void 0); } cancelPendingRestorations() { for (const cleanup of [...this._restorationCleanups]) { cleanup(); } this._restorationCleanups.clear(); } serialize() { return this._entries.map((entry) => { const grid = entry.gridview.serialize(); const root = grid.root; const url = entry.popoutGroup.api.location.type === 'popout' ? entry.popoutGroup.api.location.popoutUrl : undefined; const base = { gridReferenceGroup: entry.referenceGroup, position: entry.window.dimensions(), url, }; // Single-group window keeps the legacy `data` shape so layouts // round-trip byte-stably and older readers keep working. if (root.type === 'branch' && root.data.length === 1 && root.data[0].type === 'leaf') { return Object.assign(Object.assign({}, base), { data: root.data[0].data }); } return Object.assign(Object.assign({}, base), { grid }); }); } disposeAll() { for (const entry of [...this._entries]) { entry.disposable.dispose(); } } dispose() { this.cancelPendingRestorations(); this.disposeAll(); this._onDidRemove.dispose(); } } export const PopoutWindowModule = defineModule({ name: 'PopoutWindow', serviceKey: 'popoutWindowService', create: (host) => new PopoutWindowService(host), });