UNPKG

dockview-core

Version:

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

993 lines 155 kB
import { getRelativeLocation, getGridLocation, orthogonal, Gridview, } from '../gridview/gridview'; import { directionToPosition, } from '../dnd/droptarget'; import { tail, sequenceEquals } from '../array'; import { DockviewPanel } from './dockviewPanel'; import { CompositeDisposable, Disposable } from '../lifecycle'; import { Event, Emitter, addDisposableListener } from '../events'; import { Watermark } from './components/watermark/watermark'; import { sequentialNumberGenerator } from '../math'; import { DefaultDockviewDeserialzier } from './deserializer'; import { DockviewUnhandledDragOverEvent, isGroupOptionsWithGroup, isGroupOptionsWithPanel, isPanelOptionsWithGroup, isPanelOptionsWithPanel, } from './options'; import { BaseGrid, toTarget, } from '../gridview/baseComponentGridview'; import { DockviewApi } from '../api/component.api'; import { Orientation, Sizing } from '../splitview/splitview'; import { DockviewDidDropEvent, DockviewWillDropEvent, } from './dockviewGroupPanelModel'; import { DockviewWillShowOverlayLocationEvent, } from './events'; import { DockviewGroupPanel } from './dockviewGroupPanel'; import { DockviewPanelModel } from './dockviewPanelModel'; import { getPanelData } from '../dnd/dataTransfer'; import { Overlay } from '../overlay/overlay'; import { Classnames, getDockviewTheme, onDidWindowResizeEnd, onDidWindowMoveEnd, toggleClass, } from '../dom'; import { FloatingTitleBar } from './components/titlebar/floatingTitleBar'; import { assertModule, getRegisteredModules, isDockviewPackageLoaded, ModuleRegistry, } from './modules'; import { AllModules } from './allModules'; import { DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE, DEFAULT_FLOATING_GROUP_POSITION, DESERIALIZATION_POPOUT_DELAY_MS, } from '../constants'; import { OverlayRenderContainer, } from '../overlay/overlayRenderContainer'; import { PopoutWindow } from '../popoutWindow'; import { StrictEventsSequencing } from './strictEventsSequencing'; import { PopupService } from './components/popupService'; import { DropTargetAnchorContainer } from '../dnd/dropTargetAnchorContainer'; import { themeAbyss } from './theme'; import { ShellManager, } from './dockviewShell'; import { DEFAULT_TAB_GROUP_COLORS, TabGroupColorPalette, } from './tabGroupAccent'; function buildTabGroupColorPalette(options) { var _a; const entries = (_a = options.tabGroupColors) !== null && _a !== void 0 ? _a : DEFAULT_TAB_GROUP_COLORS; const enabled = options.tabGroupAccent !== 'off'; return new TabGroupColorPalette(entries, enabled); } function moveGroupWithoutDestroying(options) { const activePanel = options.from.activePanel; const panels = [...options.from.panels].map((panel) => { const removedPanel = options.from.model.removePanel(panel); options.from.model.renderContainer.detatch(panel); return removedPanel; }); panels.forEach((panel) => { options.to.model.openPanel(panel, { skipSetActive: activePanel !== panel, skipSetGroupActive: true, }); }); } let _hasWarnedUsingCoreDirectly = false; /** * `dockview-core` is an internal package. The public `dockview` package calls * `markDockviewPackageLoaded()` on import; if that marker is absent the consumer * is using `dockview-core` directly, so emit a one-time console warning * steering them to `dockview`. * * Suppressed in production builds: it is a development-time nudge and most * bundlers inline `process.env.NODE_ENV` so the branch is dropped entirely. The * `typeof process` guard keeps this safe in plain browser/UMD contexts where * `process` is undefined. */ function warnIfUsingCoreDirectly() { if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'production') { return; } if (_hasWarnedUsingCoreDirectly || isDockviewPackageLoaded()) { return; } _hasWarnedUsingCoreDirectly = true; console.warn('dockview: do not use "dockview-core" directly — it is an internal ' + 'package. Use the "dockview" package, the JavaScript version of ' + 'dockview, instead. This notice is shown once.'); } export class DockviewComponent extends BaseGrid { fireDidCreateTabGroup(event) { this._onDidCreateTabGroup.fire(event); } fireDidDestroyTabGroup(event) { this._onDidDestroyTabGroup.fire(event); } fireDidAddPanelToTabGroup(event) { this._onDidAddPanelToTabGroup.fire(event); } fireDidRemovePanelFromTabGroup(event) { this._onDidRemovePanelFromTabGroup.fire(event); } fireDidTabGroupChange(event) { this._onDidTabGroupChange.fire(event); } fireDidTabGroupCollapsedChange(event) { this._onDidTabGroupCollapsedChange.fire(event); } get orientation() { return this.gridview.orientation; } get totalPanels() { return this.panels.length; } get panels() { return this.groups.flatMap((group) => group.panels); } get options() { return this._options; } get tabGroupColorPalette() { return this._tabGroupColorPalette; } get activePanel() { const activeGroup = this.activeGroup; if (!activeGroup) { return undefined; } return activeGroup.activePanel; } get renderer() { var _a; return (_a = this.options.defaultRenderer) !== null && _a !== void 0 ? _a : 'onlyWhenVisible'; } get defaultHeaderPosition() { var _a; return (_a = this.options.defaultHeaderPosition) !== null && _a !== void 0 ? _a : 'top'; } get api() { return this._api; } get floatingGroups() { var _a, _b, _c; return ((_c = (_b = (_a = this._moduleRegistry) === null || _a === void 0 ? void 0 : _a.services.floatingGroupService) === null || _b === void 0 ? void 0 : _b.floatingGroups) !== null && _c !== void 0 ? _c : []); } /** * Boxes of the floating groups other than `exclude`, in coordinates * relative to the floating overlay container. Supplied to a * `transformFloatingGroupDrag` callback as `context.others` so it can * align the dragged float against its siblings. */ _gatherFloatingGroupBoxes(exclude) { var _a; const container = (_a = this._floatingOverlayHost) !== null && _a !== void 0 ? _a : this.gridview.element; const containerRect = container.getBoundingClientRect(); return this.floatingGroups .filter((floating) => floating.group !== exclude) .map((floating) => { const rect = floating.overlay.element.getBoundingClientRect(); return { left: rect.left - containerRect.left, top: rect.top - containerRect.top, width: rect.width, height: rect.height, }; }); } get _floatingGroupService() { return this._moduleRegistry.services.floatingGroupService; } get _popoutWindowService() { return this._moduleRegistry.services.popoutWindowService; } get _watermarkService() { // Tier 1 module — optional. Callers must `?.`-guard so the module // can be removed from AllModules without crashing the component. return this._moduleRegistry.services.watermarkService; } get _edgeGroupService() { return this._moduleRegistry.services.edgeGroupService; } get _rootDropTargetService() { // Optional like every other module service — RootDropTargetModule can be // removed from the registered set without crashing the component. return this._moduleRegistry.services.rootDropTargetService; } get _advancedDnDService() { // Optional — callers `?.`-guard so the module can be removed from // AllModules. Absent ⇒ the onWill* hooks simply don't fire (≡ no // subscriber), which is invisible to apps not customising DnD. return this._moduleRegistry.services.advancedDnDService; } get headerActionsService() { return this._moduleRegistry.services.headerActionsService; } isGridEmpty() { return this.gridview.length === 0; } rootDropTargetOverrideTarget() { var _a; return (_a = this.rootDropTargetContainer) === null || _a === void 0 ? void 0 : _a.model; } dispatchUnhandledDragOver(nativeEvent, position) { const event = new DockviewUnhandledDragOverEvent(nativeEvent, 'edge', position, getPanelData); this._onUnhandledDragOver.fire(event); return event.isAccepted; } // IAdvancedDnDHost — the emitters stay here so the public onWill* event // shape is unchanged; AdvancedDnDService routes the per-group fires // through these. Engine guards (e.g. disableDnd) run on the component // ahead of the dispatch. fireWillDragPanel(event) { this._onWillDragPanel.fire(event); } fireWillDragGroup(event) { this._onWillDragGroup.fire(event); } fireWillDrop(event) { this._onWillDrop.fire(event); } fireWillShowOverlay(event) { this._onWillShowOverlay.fire(event); } /** * Resolve the custom group drag ghost (via the AdvancedDnD module), or * `undefined` to fall back to the default chip. Returns `undefined` when * the module is absent — the default ghost then renders. */ buildGroupDragGhost(group) { var _a; return (_a = this._advancedDnDService) === null || _a === void 0 ? void 0 : _a.buildGroupDragGhost(group); } /** * Resolve the app-supplied drop overlay model (via the AdvancedDnD module) * for a group drop target, or `undefined` to keep the target's default. */ resolveDropOverlayModel(location, group) { var _a; return (_a = this._advancedDnDService) === null || _a === void 0 ? void 0 : _a.resolveOverlayModel(location, group); } // IAccessibilityHost — keyboard docking reaches the AdvancedDnD preview + // LiveRegion announcer through these so the service stays decoupled. /** Outermost element — the shell (incl. edge groups) once built, else the gridview. */ get rootElement() { var _a, _b; return (_b = (_a = this._shellManager) === null || _a === void 0 ? void 0 : _a.element) !== null && _b !== void 0 ? _b : this.element; } /** * The next / previous group in gridview (spatial) order, wrapping round. * The keyboard accessibility module's focus navigation is built on this * primitive — the only piece that needs the grid internals; the rest of * the navigation logic lives in the AccessibilityService. */ adjacentGroup(group, reverse) { var _a; // gridview traversal only covers grid groups; a floating/popout group // isn't in the grid, so there's no adjacent grid group to step to. if (group.api.location.type !== 'grid') { return undefined; } const location = getGridLocation(group.element); return ((_a = (reverse ? this.gridview.previous(location) : this.gridview.next(location))) === null || _a === void 0 ? void 0 : _a.view); } /** * The nearest grid group in a spatial direction from `group`, by * comparing group centre points. Floating and popout groups sit outside * the grid's geometry and are ignored. Returns `undefined` when there is * no group in that direction. */ adjacentGroupInDirection(group, direction) { if (group.api.location.type !== 'grid') { return undefined; } const from = group.element.getBoundingClientRect(); const fromX = from.left + from.width / 2; const fromY = from.top + from.height / 2; let best; let bestDistance = Number.POSITIVE_INFINITY; for (const candidate of this.groups) { if (candidate === group || candidate.api.location.type !== 'grid') { continue; } const rect = candidate.element.getBoundingClientRect(); const dx = rect.left + rect.width / 2 - fromX; const dy = rect.top + rect.height / 2 - fromY; // Require the candidate to sit predominantly in the asked-for // direction (dominant axis), so 'left' ignores a group that's // mostly above/below. const inDirection = direction === 'left' ? dx < 0 && Math.abs(dx) >= Math.abs(dy) : direction === 'right' ? dx > 0 && Math.abs(dx) >= Math.abs(dy) : direction === 'up' ? dy < 0 && Math.abs(dy) >= Math.abs(dx) : dy > 0 && Math.abs(dy) >= Math.abs(dx); if (!inDirection) { continue; } const distance = dx * dx + dy * dy; if (distance < bestDistance) { bestDistance = distance; best = candidate; } } return best; } showDropPreview(group, position) { var _a, _b; return ((_b = (_a = this._advancedDnDService) === null || _a === void 0 ? void 0 : _a.showPreviewOverlay(group, position)) !== null && _b !== void 0 ? _b : Disposable.NONE); } announce(message) { var _a; (_a = this._moduleRegistry.services.liveRegionService) === null || _a === void 0 ? void 0 : _a.announce(message); } dockPanel(panel, group, position) { this.moveGroupOrPanel({ from: { groupId: panel.group.id, panelId: panel.id }, to: { group, position }, }); } get contextMenuService() { // Owned by ContextMenuModule — undefined when the module is not // registered, so callers must `?.`-guard. return this._moduleRegistry.services.contextMenuService; } get mountElement() { return this.gridview.element; } hasVisibleGridGroup() { return this.groups.some((group) => group.api.location.type === 'grid' && group.api.isVisible); } fireLayoutChange() { this._bufferOnDidLayoutChange.fire(); } /** * Promise that resolves when all popout groups from the last fromJSON call are restored. * Useful for tests that need to wait for delayed popout creation. */ get popoutRestorationPromise() { var _a, _b; return ((_b = (_a = this._popoutWindowService) === null || _a === void 0 ? void 0 : _a.restorationPromise) !== null && _b !== void 0 ? _b : Promise.resolve()); } constructor(container, options) { var _a, _b, _c, _d, _e; super(container, { proportionalLayout: true, orientation: Orientation.HORIZONTAL, styles: options.hideBorders ? { separatorBorder: 'transparent' } : undefined, disableAutoResizing: options.disableAutoResizing, locked: options.locked, margin: (_b = (_a = options.theme) === null || _a === void 0 ? void 0 : _a.gap) !== null && _b !== void 0 ? _b : 0, className: options.className, }); this.nextGroupId = sequentialNumberGenerator(); this._deserializer = new DefaultDockviewDeserialzier(this); this._moduleRegistry = new ModuleRegistry(); this._onWillDragPanel = new Emitter(); this.onWillDragPanel = this._onWillDragPanel.event; this._onWillDragGroup = new Emitter(); this.onWillDragGroup = this._onWillDragGroup.event; this._onDidDrop = new Emitter(); this.onDidDrop = this._onDidDrop.event; this._onWillDrop = new Emitter(); this.onWillDrop = this._onWillDrop.event; // Transaction boundary bracketing each top-level structural mutation. // Compound operations (e.g. a drag that relocates a panel) nest via the // depth counter and bracket as a single transaction. See `mutation()`. this._mutationDepth = 0; // Current operation origin. Defaults to `'user'`; the DockviewApi boundary // flips it to `'api'` for the duration of a programmatic call via // `withOrigin`. Nested operations inherit the outermost origin (tracked by // `_originDepth`, independent of mutation bracketing so it also covers // active-panel changes that are not structural mutations). this._origin = 'user'; this._originDepth = 0; this._onWillMutateLayout = new Emitter(); this.onWillMutateLayout = this._onWillMutateLayout.event; this._onDidMutateLayout = new Emitter(); this.onDidMutateLayout = this._onDidMutateLayout.event; this._onWillShowOverlay = new Emitter(); this.onWillShowOverlay = this._onWillShowOverlay.event; this._onUnhandledDragOver = new Emitter(); this.onUnhandledDragOver = this._onUnhandledDragOver.event; this._onDidRemovePanel = new Emitter(); this.onDidRemovePanel = this._onDidRemovePanel.event; this._onDidAddPanel = new Emitter(); this.onDidAddPanel = this._onDidAddPanel.event; this._onDidPopoutGroupSizeChange = new Emitter(); this.onDidPopoutGroupSizeChange = this._onDidPopoutGroupSizeChange.event; this._onDidPopoutGroupPositionChange = new Emitter(); this.onDidPopoutGroupPositionChange = this._onDidPopoutGroupPositionChange.event; this._onDidAddPopoutGroup = new Emitter(); this.onDidAddPopoutGroup = this._onDidAddPopoutGroup.event; this._onDidRemovePopoutGroup = new Emitter(); this.onDidRemovePopoutGroup = this._onDidRemovePopoutGroup.event; this._onDidOpenPopoutWindowFail = new Emitter(); this.onDidOpenPopoutWindowFail = this._onDidOpenPopoutWindowFail.event; this._onDidLayoutFromJSON = new Emitter(); this.onDidLayoutFromJSON = this._onDidLayoutFromJSON.event; this._onDidActivePanelChange = new Emitter({ replay: true }); this.onDidActivePanelChange = this._onDidActivePanelChange.event; this._onDidMovePanel = new Emitter(); this.onDidMovePanel = this._onDidMovePanel.event; this._onDidCreateTabGroup = new Emitter(); this.onDidCreateTabGroup = this._onDidCreateTabGroup.event; this._onDidDestroyTabGroup = new Emitter(); this.onDidDestroyTabGroup = this._onDidDestroyTabGroup.event; this._onDidAddPanelToTabGroup = new Emitter(); this.onDidAddPanelToTabGroup = this._onDidAddPanelToTabGroup.event; this._onDidRemovePanelFromTabGroup = new Emitter(); this.onDidRemovePanelFromTabGroup = this._onDidRemovePanelFromTabGroup.event; this._onDidTabGroupChange = new Emitter(); this.onDidTabGroupChange = this._onDidTabGroupChange.event; this._onDidTabGroupCollapsedChange = new Emitter(); this.onDidTabGroupCollapsedChange = this._onDidTabGroupCollapsedChange.event; this._onDidMaximizedGroupChange = new Emitter(); this.onDidMaximizedGroupChange = this._onDidMaximizedGroupChange.event; this._inShellLayout = false; this._onDidRemoveGroup = new Emitter(); this.onDidRemoveGroup = this._onDidRemoveGroup.event; this._onDidAddGroup = new Emitter(); this.onDidAddGroup = this._onDidAddGroup.event; this._onDidOptionsChange = new Emitter(); this.onDidOptionsChange = this._onDidOptionsChange.event; this._onDidActiveGroupChange = new Emitter(); this.onDidActiveGroupChange = this._onDidActiveGroupChange.event; this._moving = false; this._options = options; this._tabGroupColorPalette = buildTabGroupColorPalette(options); // Internal seam: defaults to the full set, but tests can supply a // subset to assert every module is independently removable (and the // opt-in module API will build on this later). Not part of the public // options surface. const explicitModules = options .modules; const modules = explicitModules !== null && explicitModules !== void 0 ? explicitModules : [ ...AllModules, ...getRegisteredModules(), ]; for (const module of modules) { this._moduleRegistry.register(module); } this._moduleRegistry.initialize(this); // Surface popout removal symmetrically with `onDidAddPopoutGroup`. The // service is the single point every removal path funnels through — a // genuine window close and an explicit removal alike — and it suppresses // the event during component teardown. const popoutWindowService = this._popoutWindowService; if (popoutWindowService) { this.addDisposables(popoutWindowService.onDidRemove((entry) => { this._onDidRemovePopoutGroup.fire({ id: entry.popoutGroup.id, group: entry.popoutGroup, window: entry.getWindow(), }); })); } // Purely a developer warning (no functional effect): nudge anyone using // the internal `dockview-core` package directly towards `dockview`. warnIfUsingCoreDirectly(); this.popupService = new PopupService(this.element); this._api = new DockviewApi(this); // The shell always wraps the dockview element so edge groups can be // added at any time via addEdgeGroup() without re-parenting the DOM. this.disableResizing = true; container.removeChild(this.element); this._shellManager = new ShellManager(container, this.element, (w, h) => this._layoutFromShell(w, h), (_d = (_c = options.theme) === null || _c === void 0 ? void 0 : _c.gap) !== null && _d !== void 0 ? _d : 0, (_e = options.theme) === null || _e === void 0 ? void 0 : _e.edgeGroupCollapsedSize); // The shell wraps the dockview element, so move the popup anchor // into the shell so overflow dropdowns in edge groups position correctly this.popupService.updateRoot(this._shellManager.element); this._shellThemeClassnames = new Classnames(this._shellManager.element); // Anchor the overlay container to the shell element so that edge groups // (which live outside this.element in the shell layout) are covered when // dndOverlayMounting is 'absolute'. this.rootDropTargetContainer = new DropTargetAnchorContainer(this._shellManager.element, { disabled: true }); this.overlayRenderContainer = new OverlayRenderContainer(this._shellManager.element, this); // Hosted in the shell (not inside `.dv-dockview`) so floating overlays // share a stacking context with `dv-render-overlay` panels; sized to // mirror the gridview rect so saved positions remain valid. this._floatingOverlayHost = document.createElement('div'); this._floatingOverlayHost.className = 'dv-floating-overlay-host'; this._shellManager.element.appendChild(this._floatingOverlayHost); toggleClass(this.gridview.element, 'dv-dockview', true); toggleClass(this.element, 'dv-debug', !!options.debug); this.updateTheme(); if (options.debug) { this.addDisposables(new StrictEventsSequencing(this)); } this.addDisposables(this.rootDropTargetContainer, this.overlayRenderContainer, this._onWillDragPanel, this._onWillDragGroup, this._onWillShowOverlay, this._onDidActivePanelChange, this._onDidAddPanel, this._onDidRemovePanel, this._onDidLayoutFromJSON, this._onDidDrop, this._onWillDrop, this._onWillMutateLayout, this._onDidMutateLayout, this._onDidMovePanel, this._onDidMovePanel.event(() => { /** * Update overlay positions after DOM layout completes to prevent 0×0 dimensions. * With defaultRenderer="always" this results in panel content not showing after move operations. * Debounced to avoid multiple calls when moving groups with multiple panels. */ this.debouncedUpdateAllPositions(); }), this._onDidAddGroup, this._onDidRemoveGroup, this._onDidActiveGroupChange, this._onUnhandledDragOver, this._onDidMaximizedGroupChange, this._onDidPopoutGroupSizeChange, this._onDidPopoutGroupPositionChange, this._onDidAddPopoutGroup, this._onDidRemovePopoutGroup, this._onDidOpenPopoutWindowFail, this._onDidCreateTabGroup, this._onDidDestroyTabGroup, this._onDidAddPanelToTabGroup, this._onDidRemovePanelFromTabGroup, this._onDidTabGroupChange, this._onDidTabGroupCollapsedChange, Event.any(this.onDidPopoutGroupSizeChange, this.onDidPopoutGroupPositionChange, this.onDidCreateTabGroup, this.onDidDestroyTabGroup, this.onDidAddPanelToTabGroup, this.onDidRemovePanelFromTabGroup, this.onDidTabGroupChange, this.onDidTabGroupCollapsedChange)(() => { // Popout size/position and tab-group mutations persist as layout changes. this.fireLayoutChange(); }), this._onDidOptionsChange, this.onDidAdd((event) => { if (!this._moving) { this._onDidAddGroup.fire(event); } }), this.onDidRemove((event) => { if (!this._moving) { this._onDidRemoveGroup.fire(event); } }), this.onDidActiveChange((event) => { if (!this._moving) { this._onDidActiveGroupChange.fire(event); } }), this.onDidMaximizedChange((event) => { this._onDidMaximizedGroupChange.fire({ group: event.panel, isMaximized: event.isMaximized, }); }), Event.any(this.onDidAddPanel, this.onDidRemovePanel, this.onDidAddGroup, this.onDidRemove, this.onDidRemoveGroup, this.onDidMovePanel, this.onDidActivePanelChange)(() => { this._bufferOnDidLayoutChange.fire(); }), Disposable.from(() => { var _a; // Registry disposes init() subscriptions then every module // service that implements IDisposable. The order matters so // init handlers stop firing into services about to be torn // down. Includes popout-restoration timer cancellation that // resolves promises so awaiters of popoutRestorationPromise // don't hang. See issue #851. this._moduleRegistry.dispose(); (_a = this._shellManager) === null || _a === void 0 ? void 0 : _a.dispose(); })); // Root edge-drop wiring lives with its (optional) module — guard it so // the module is independently removable. const rootDropTarget = this._rootDropTargetService; if (rootDropTarget) { this.addDisposables(rootDropTarget.onWillShowOverlay((event) => { if (this.gridview.length > 0 && event.position === 'center') { // option only available when no panels in primary grid return; } this._onWillShowOverlay.fire(new DockviewWillShowOverlayLocationEvent(event, { kind: 'edge', panel: undefined, api: this._api, group: undefined, getData: getPanelData, })); }), rootDropTarget.onDrop((event) => { var _a; const willDropEvent = new DockviewWillDropEvent({ nativeEvent: event.nativeEvent, position: event.position, panel: undefined, api: this._api, group: undefined, getData: getPanelData, kind: 'edge', }); this._onWillDrop.fire(willDropEvent); if (willDropEvent.defaultPrevented) { return; } const data = getPanelData(); if (data) { this.moveGroupOrPanel({ from: { groupId: data.groupId, panelId: (_a = data.panelId) !== null && _a !== void 0 ? _a : undefined, }, to: { group: this.orthogonalize(event.position), position: 'center', }, }); } else { this._onDidDrop.fire(new DockviewDidDropEvent({ nativeEvent: event.nativeEvent, position: event.position, panel: undefined, api: this._api, group: undefined, getData: getPanelData, })); } })); } // Final module wiring: runs after the host is fully constructed. // Modules subscribe to host events here so the component doesn't // need to manually invoke them at scattered call sites. this._moduleRegistry.postConstruct(this); } setVisible(panel, visible) { switch (panel.api.location.type) { case 'grid': super.setVisible(panel, visible); break; case 'floating': { const item = this.floatingGroups.find((floatingGroup) => floatingGroup.group === panel); if (item) { item.overlay.setVisible(visible); panel.api._onDidVisibilityChange.fire({ isVisible: visible, }); } break; } case 'popout': console.warn('dockview: You cannot hide a group that is in a popout window'); break; case 'edge': // Edge group visibility is managed via setEdgeGroupVisible break; } } /** * Returns the {@link PopupService} that should host popovers (context * menus, tab overflow menus) for the given group. Popout groups have their * own service rooted in their popout window so the popover renders there * and dismisses on events from that window. */ getPopupServiceForGroup(group) { var _a, _b; return ((_b = (_a = this._popoutWindowService) === null || _a === void 0 ? void 0 : _a.getPopupService(group.id)) !== null && _b !== void 0 ? _b : this.popupService); } addPopoutGroup(itemToPopout, options) { // The transaction brackets the synchronous structural change; the // popout window opens asynchronously after it resolves. return this.mutation('popout', () => this._doAddPopoutGroup(itemToPopout, options)); } /** Enumerate the popout groups currently open in their own windows. */ getPopouts() { var _a, _b; return ((_b = (_a = this._popoutWindowService) === null || _a === void 0 ? void 0 : _a.entries.map((entry) => ({ id: entry.popoutGroup.id, group: entry.popoutGroup, window: entry.getWindow(), }))) !== null && _b !== void 0 ? _b : []); } _doAddPopoutGroup(itemToPopout, options) { var _a, _b, _c, _d, _e; const service = assertModule(this._popoutWindowService, 'PopoutWindow', 'api.addPopoutGroup'); if (!service) { return Promise.resolve(false); } if (itemToPopout instanceof DockviewGroupPanel && itemToPopout.model.location.type === 'edge') { // edge groups are permanent structural elements and cannot be popped out return Promise.resolve(false); } if (itemToPopout instanceof DockviewPanel && itemToPopout.group.size === 1) { return this.addPopoutGroup(itemToPopout.group, options); } const theme = getDockviewTheme(this.gridview.element); const element = this.element; function getBox() { if (options === null || options === void 0 ? void 0 : options.position) { return options.position; } if (itemToPopout instanceof DockviewGroupPanel) { return itemToPopout.element.getBoundingClientRect(); } if (itemToPopout.group) { return itemToPopout.group.element.getBoundingClientRect(); } return element.getBoundingClientRect(); } const box = getBox(); const groupId = (_b = (_a = options === null || options === void 0 ? void 0 : options.overridePopoutGroup) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : this.getNextGroupId(); // Resolve the configured popout URL once (per-call override → global // option). Recorded on the entry / group locations so it survives // serialization; the '/popout.html' default is applied only when // actually opening the window, not baked into saved layouts. const resolvedPopoutUrl = (_c = options === null || options === void 0 ? void 0 : options.popoutUrl) !== null && _c !== void 0 ? _c : (_d = this.options) === null || _d === void 0 ? void 0 : _d.popoutUrl; const _window = new PopoutWindow(`${this.id}-${groupId}`, // unique id theme !== null && theme !== void 0 ? theme : '', { url: resolvedPopoutUrl !== null && resolvedPopoutUrl !== void 0 ? resolvedPopoutUrl : '/popout.html', left: window.screenX + box.left, top: window.screenY + box.top, width: box.width, height: box.height, onDidOpen: options === null || options === void 0 ? void 0 : options.onDidOpen, onWillClose: options === null || options === void 0 ? void 0 : options.onWillClose, nonce: (_e = this.options) === null || _e === void 0 ? void 0 : _e.nonce, }); const popoutWindowDisposable = new CompositeDisposable(_window, _window.onDidClose(() => { popoutWindowDisposable.dispose(); })); return _window .open() .then((popoutContainer) => { var _a, _b, _c; if (_window.isDisposed) { return false; } const referenceGroup = (options === null || options === void 0 ? void 0 : options.referenceGroup) ? options.referenceGroup : itemToPopout instanceof DockviewPanel ? itemToPopout.group : itemToPopout; const referenceLocation = itemToPopout.api.location.type; /** * The group that is being added doesn't already exist within the DOM, the most likely occurrence * of this case is when being called from the `fromJSON(...)` method */ const isGroupAddedToDom = referenceGroup.element.parentElement !== null; let group; if (options === null || options === void 0 ? void 0 : options.overridePopoutGridview) { // Restoring a multi-group window: the anchor group is // already built inside the supplied gridview. group = (_a = options.overridePopoutGroup) !== null && _a !== void 0 ? _a : referenceGroup; } else if (!isGroupAddedToDom) { group = referenceGroup; } else if (options === null || options === void 0 ? void 0 : options.overridePopoutGroup) { group = options.overridePopoutGroup; } else { group = this.createGroup({ id: groupId }); if (popoutContainer) { this._onDidAddGroup.fire(group); } } if (popoutContainer === null) { this.handleBlockedPopout({ group, referenceGroup, options, popoutWindowDisposable, }); return false; } const gready = document.createElement('div'); gready.className = 'dv-overlay-render-container'; const overlayRenderContainer = new OverlayRenderContainer(gready, this); group.model.renderContainer = overlayRenderContainer; // The popout window hosts its own gridview so it can grow into // a nested splitview layout. The window starts with the single // anchor group; further groups arrive via drag-and-drop. On // restore a pre-populated gridview is supplied instead. const popoutGridview = (_b = options === null || options === void 0 ? void 0 : options.overridePopoutGridview) !== null && _b !== void 0 ? _b : this.createNestedGridview(); if (!(options === null || options === void 0 ? void 0 : options.overridePopoutGridview)) { popoutGridview.addView(group, Sizing.Distribute, [0]); } // Fill the popout window. Unlike the main grid (explicit px) and // floating windows (CSS inside .dv-resize-container), the popout // gridview has no sizing context, so without this it collapses // to 0 height and nothing renders. popoutGridview.element.style.width = '100%'; popoutGridview.element.style.height = '100%'; popoutGridview.layout(_window.window.innerWidth, _window.window.innerHeight); // Guarded so the teardown's re-entrant paths (window close // re-enters via the anchor's doRemoveGroup) never double-dispose. let popoutGridviewDisposed = false; const disposePopoutGridview = () => { if (!popoutGridviewDisposed) { popoutGridviewDisposed = true; popoutGridview.dispose(); } }; let floatingBox; if (!(options === null || options === void 0 ? void 0 : options.overridePopoutGroup) && !(options === null || options === void 0 ? void 0 : options.overridePopoutGridview) && isGroupAddedToDom) { if (itemToPopout instanceof DockviewPanel) { this.movingLock(() => { const panel = referenceGroup.model.removePanel(itemToPopout); group.model.openPanel(panel); }); } else { this.movingLock(() => moveGroupWithoutDestroying({ from: referenceGroup, to: group, })); switch (referenceLocation) { case 'grid': referenceGroup.api.setVisible(false); break; case 'floating': case 'popout': floatingBox = (_c = this.floatingGroups .find((value) => value.group.api.id === itemToPopout.api.id)) === null || _c === void 0 ? void 0 : _c.overlay.toJSON(); this.removeGroup(referenceGroup); break; } } } popoutContainer.classList.add('dv-dockview'); popoutContainer.style.overflow = 'hidden'; popoutContainer.appendChild(gready); popoutContainer.appendChild(popoutGridview.element); const anchor = document.createElement('div'); const dropTargetContainer = new DropTargetAnchorContainer(anchor, { disabled: this.rootDropTargetContainer.disabled }); popoutContainer.appendChild(anchor); group.model.dropTargetContainer = dropTargetContainer; // Each popout group needs its own popover service so that // tab context menus, chip menus, and tab overflow menus // render in the popout window (not the main window) and // their pointerdown/keydown listeners fire on the right // window for outside-click and Escape dismissal. const popoutPopupService = new PopupService(popoutContainer, _window.window); service.setPopupService(group.id, popoutPopupService); popoutWindowDisposable.addDisposables(popoutPopupService, Disposable.from(() => { service.deletePopupService(group.id); })); group.model.location = { type: 'popout', getWindow: () => _window.window, popoutUrl: resolvedPopoutUrl, }; if (options === null || options === void 0 ? void 0 : options.overridePopoutGridview) { // Restored multi-group window. Wire every member (including // the anchor) to this window's containers and popout // location now that the gridview is attached and laid out — // re-setting renderContainer forces a re-render at the right // time so 'always'-rendered content positions in this // window rather than where it was first created. const members = this.groups.filter((candidate) => popoutGridview.element.contains(candidate.element)); for (const member of members) { member.model.renderContainer = overlayRenderContainer; member.model.dropTargetContainer = dropTargetContainer; member.model.location = { type: 'popout', getWindow: () => _window.window, popoutUrl: resolvedPopoutUrl, }; } } if (isGroupAddedToDom && itemToPopout.api.location.type === 'grid') { itemToPopout.api.setVisible(false); } this.doSetGroupAndPanelActive(group); const resizeObserverDisposable = service.observeGridviewSize(_window, popoutGridview, overlayRenderContainer); if (resizeObserverDisposable) { popoutWindowDisposable.addDisposables(resizeObserverDisposable); } popoutWindowDisposable.addDisposables(group.api.onDidActiveChange((event) => { var _a; if (event.isActive) { (_a = _window.window) === null || _a === void 0 ? void 0 : _a.focus(); } }), group.api.onWillFocus(() => { var _a; (_a = _window.window) === null || _a === void 0 ? void 0 : _a.focus(); })); // Holder so the close teardown (extracted below) can publish // the group that was returned to the main grid back to the // entry's `dispose()` contract. const closeResult = {}; const isValidReferenceGroup = isGroupAddedToDom && referenceGroup && this.getPanel(referenceGroup.id); const value = { window: _window, popoutGroup: group, gridview: popoutGridview, overlayRenderContainer, dropTargetContainer, getWindow: () => _window.window, popoutUrl: resolvedPopoutUrl, referenceGroup: isValidReferenceGroup ? referenceGroup.id : undefined, disposable: { dispose: () => { popoutWindowDisposable.dispose(); return closeResult.returnedGroup; }, }, }; const _onDidWindowPositionChange = onDidWindowMoveEnd(_window.window); popoutWindowDisposable.addDisposables(_onDidWindowPositionChange, onDidWindowResizeEnd(_window.window, () => { this._onDidPopoutGroupSizeChange.fire({ width: _window.window.innerWidth, height: _window.window.innerHeight, group, }); }), _onDidWindowPositionChange.event(() => { this._onDidPopoutGroupPositionChange.fire({ screenX: _window.window.screenX, screenY: _window.window.screenX, group, }); }), addDisposableListener(_window.window, 'resize', () => { popoutGridview.layout(_window.window.innerWidth, _window.window.innerHeight); }), overlayRenderContainer, Disposable.from(() => this.disposePopoutWindow({ group, referenceGroup, popoutGridview, isGroupAddedToDom, floatingBox, disposePopoutGridview, closeResult, }))); service.add(value); this._onDidAddPopoutGroup.fire({ id: value.popoutGroup.id, group: value.popoutGroup, window: value.getWindow(), }); return true; }) .catch((err) => { console.error('dockview: failed to create popout.', err); return false; }); } /** * The popout window was blocked (e.g. by the browser's popup blocker — * common when restoring popouts on load). Fall back gracefully so the * group(s) end up valid and visible in the main grid rather than as * orphans that later crash clear()/remove(). */ handleBlockedPopout(params) { const { group, referenceGroup, options, popoutWindowDisposable } = params; console.error('dockview: failed to create popout. perhaps you need to allow pop-ups for this website'); popoutWindowDisposable.dispose(); this._onDidOpenPopoutWindowFail.fire(); if (options === null || options === void 0 ? void 0 : options.overridePopoutGridview) { // Restoring a multi-group popout window: its nested gridview was // built up-front but never attached to a window. Dock every member // into the main grid so no group is lost, then discard the // detached gridview. const blockedGridview = options.overridePopoutGridview; const members = this.groups.filter((candidate) => blockedGridview.element.contains(candidate.element)); for (const member of members) { this.movingLock(() => { blockedGridview.remove(member); this.redockGroupToMainGrid(member); }); } blockedGridview.dispose(); if (referenceGroup && !referenceGroup.api.isVisible) { referenceGroup.api.setVisible(true); } return; } if (group === referenceGroup) { // No separate grid group to return to (e.g. restoring a popout // straight from JSON) — dock this group into the main grid. if (!this.gridview.element.contains(group.element)) { this.movingLock(() => this.doAddGroup(group, [0])); group.model.location = { type: 'grid' }; } } else { // A fresh group was created for the popout — return its panels to // the reference group and discard the now-empty popout group so it // doesn't linger as an orphan. this.movingLock(() => moveGroupWithoutDestroying({ from: group, to: referenceGroup, })); if (group.model.size === 0 && this._groups.has(group.id)) { group.dispose(); this._groups.delete(group.id); this._onDidRemoveGroup.fire(group); } } if (!referenceGroup.api.isVisible) { referenceGroup.api.setVisible(true); } } /** * Wire a group that has been displaced from a floating / popout window back * to the main grid's render & drop-target containers and dock it at the * root. The caller is responsible for first detaching it from its old * gridview — the detach strategy differs between the window-teardown path * (`doRemoveGroup`) and the blocked-window path (`gridview.remove`). */ redockGroupToMainGrid(group) { group.model.renderContainer = this.overlayRenderContainer; group.model.dropTargetContainer = this.rootDropTargetContainer; group.model.location = { type: 'grid' }; this.doAddGroup(group, [0]); } /** * Teardown for a popout window's `popoutWindowDisposable`. Runs when the * window closes (by user, by `removeGroup`, or by component disposal): * relocates every member group back to the main grid (or to a floating * window when the anchor came from one), then disposes the nested gridview. * `closeResult.returnedGroup` is read by the entry's `dispose()` contract. */ disposePopoutWindow(params) { var _a; const { group, referenceGroup, popoutGridview, isGroupAddedToDom, floatingBox, disposePopoutGridview, closeResult, } = params; if (this.isDisposed) { // cleanup may run after instance is disposed; just tear down the // nested gridview. disposePopoutGridview(); return; } // Distinguish a genuine window close from an explicit `removeGroup`: // the explicit-removal paths remove the service entry *before* this // disposable runs. Key off the stable `popoutGridview` rather than the // captured `group`, which may no longer be the window's anchor (it can // have been dr