UNPKG

dockview-core

Version:

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

1,013 lines 121 kB
import { getRelativeLocation, getGridLocation, orthogonal, } from '../gridview/gridview'; import { directionToPosition, } from '../dnd/droptarget'; import { html5Backend, pointerBackend } from '../dnd/backend'; import { tail, sequenceEquals, remove } 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 } 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 { addTestId, Classnames, getDockviewTheme, onDidWindowResizeEnd, onDidWindowMoveEnd, toggleClass, watchElementResize, } from '../dom'; import { DockviewFloatingGroupPanel } from './dockviewFloatingGroupPanel'; 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 { ContextMenuController } from './contextMenu'; import { DropTargetAnchorContainer } from '../dnd/dropTargetAnchorContainer'; import { themeAbyss } from './theme'; import { ShellManager, } from './dockviewShell'; import { DEFAULT_TAB_GROUP_COLORS, TabGroupColorPalette, } from './tabGroupAccent'; const DEFAULT_ROOT_OVERLAY_MODEL = { activationSize: { type: 'pixels', value: 10 }, size: { type: 'pixels', value: 20 }, }; 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, }); }); } export class DockviewComponent extends BaseGrid { 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() { return this._floatingGroups; } /** * 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() { return this._popoutRestorationPromise; } constructor(container, options) { var _a, _b, _c, _d, _e, _f, _g; 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._watermark = null; this._popoutPopupServices = new Map(); 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; this._onWillShowOverlay = new Emitter(); this.onWillShowOverlay = this._onWillShowOverlay.event; this._onUnhandledDragOverEvent = new Emitter(); this.onUnhandledDragOverEvent = this._onUnhandledDragOverEvent.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._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._edgeGroups = new Map(); this._edgeGroupDisposables = new Map(); this._floatingGroups = []; this._popoutGroups = []; this._popoutRestorationPromise = Promise.resolve(); this._popoutRestorationCleanups = new Set(); 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); this.popupService = new PopupService(this.element); this.contextMenuController = new ContextMenuController(this); 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); const rootCanDisplayOverlay = (event, position) => { const data = getPanelData(); if (data) { if (data.viewId !== this.id) { return false; } if (position === 'center') { // center drop target is only allowed if there are no panels in the grid // floating panels are allowed return this.gridview.length === 0; } return true; } if (position === 'center' && this.gridview.length !== 0) { /** * for external events only show the four-corner drag overlays, disable * the center position so that external drag events can fall through to the group * and panel drop target handlers */ return false; } const firedEvent = new DockviewUnhandledDragOverEvent(event, 'edge', position, getPanelData); this._onUnhandledDragOverEvent.fire(firedEvent); return firedEvent.isAccepted; }; this._rootDropTarget = html5Backend.createDropTarget(this.element, { className: 'dv-drop-target-edge', canDisplayOverlay: rootCanDisplayOverlay, acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'], overlayModel: (_f = options.rootOverlayModel) !== null && _f !== void 0 ? _f : DEFAULT_ROOT_OVERLAY_MODEL, getOverrideTarget: () => { var _a; return (_a = this.rootDropTargetContainer) === null || _a === void 0 ? void 0 : _a.model; }, }); this._rootPointerDropTarget = pointerBackend.createDropTarget(this.element, { className: 'dv-drop-target-edge', canDisplayOverlay: rootCanDisplayOverlay, acceptedTargetZones: [ 'top', 'bottom', 'left', 'right', 'center', ], overlayModel: (_g = options.rootOverlayModel) !== null && _g !== void 0 ? _g : DEFAULT_ROOT_OVERLAY_MODEL, getOverrideTarget: () => { var _a; return (_a = this.rootDropTargetContainer) === null || _a === void 0 ? void 0 : _a.model; }, }); this.updateDropTargetModel(options); toggleClass(this.gridview.element, 'dv-dockview', true); toggleClass(this.element, 'dv-debug', !!options.debug); this.updateTheme(); this.updateWatermark(); 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._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._onUnhandledDragOverEvent, this._onDidMaximizedGroupChange, this._onDidOptionsChange, this._onDidPopoutGroupSizeChange, this._onDidPopoutGroupPositionChange, this._onDidOpenPopoutWindowFail, this._onDidCreateTabGroup, this._onDidDestroyTabGroup, this._onDidAddPanelToTabGroup, this._onDidRemovePanelFromTabGroup, this._onDidTabGroupChange, this._onDidTabGroupCollapsedChange, this.onDidViewVisibilityChangeMicroTaskQueue(() => { this.updateWatermark(); }), 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.onDidAdd, this.onDidRemove)(() => { this.updateWatermark(); }), Event.any(this.onDidAddPanel, this.onDidRemovePanel, this.onDidAddGroup, this.onDidRemove, this.onDidRemoveGroup, this.onDidMovePanel, this.onDidActivePanelChange, this.onDidPopoutGroupPositionChange, this.onDidPopoutGroupSizeChange, this.onDidCreateTabGroup, this.onDidDestroyTabGroup, this.onDidAddPanelToTabGroup, this.onDidRemovePanelFromTabGroup, this.onDidTabGroupChange, this.onDidTabGroupCollapsedChange)(() => { this._bufferOnDidLayoutChange.fire(); }), Disposable.from(() => { var _a; // Cancel any pending popout-restoration timers scheduled by // fromJSON so they don't open new browser windows after // dispose, and resolve their promises so callers awaiting // popoutRestorationPromise don't hang. See issue #851. for (const cleanup of [...this._popoutRestorationCleanups]) { cleanup(); } this._popoutRestorationCleanups.clear(); // iterate over a copy of the array since .dispose() mutates the original array for (const group of [...this._floatingGroups]) { group.dispose(); } // iterate over a copy of the array since .dispose() mutates the original array for (const group of [...this._popoutGroups]) { group.disposable.dispose(); } (_a = this._shellManager) === null || _a === void 0 ? void 0 : _a.dispose(); for (const d of this._edgeGroupDisposables.values()) { d.dispose(); } this._edgeGroupDisposables.clear(); }), this._rootDropTarget, this._rootPointerDropTarget, Event.any(this._rootDropTarget.onWillShowOverlay, this._rootPointerDropTarget.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, })); }), Event.any(this._rootDropTarget.onDrop, this._rootPointerDropTarget.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, })); } })); } 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; return (_a = this._popoutPopupServices.get(group.id)) !== null && _a !== void 0 ? _a : this.popupService; } addPopoutGroup(itemToPopout, options) { var _a, _b, _c, _d, _e, _f; 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(); const _window = new PopoutWindow(`${this.id}-${groupId}`, // unique id theme !== null && theme !== void 0 ? theme : '', { url: (_e = (_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) !== null && _e !== void 0 ? _e : '/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: (_f = this.options) === null || _f === void 0 ? void 0 : _f.nonce, }); const popoutWindowDisposable = new CompositeDisposable(_window, _window.onDidClose(() => { popoutWindowDisposable.dispose(); })); return _window .open() .then((popoutContainer) => { var _a; 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 (!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) { console.error('dockview: failed to create popout. perhaps you need to allow pop-ups for this website'); popoutWindowDisposable.dispose(); this._onDidOpenPopoutWindowFail.fire(); // if the popout window was blocked, we need to move the group back to the reference group // and set it to visible this.movingLock(() => moveGroupWithoutDestroying({ from: group, to: referenceGroup, })); if (!referenceGroup.api.isVisible) { referenceGroup.api.setVisible(true); } return false; } const gready = document.createElement('div'); gready.className = 'dv-overlay-render-container'; const overlayRenderContainer = new OverlayRenderContainer(gready, this); group.model.renderContainer = overlayRenderContainer; group.layout(_window.window.innerWidth, _window.window.innerHeight); let floatingBox; if (!(options === null || options === void 0 ? void 0 : options.overridePopoutGroup) && 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 = (_a = this._floatingGroups .find((value) => value.group.api.id === itemToPopout.api.id)) === null || _a === void 0 ? void 0 : _a.overlay.toJSON(); this.removeGroup(referenceGroup); break; } } } popoutContainer.classList.add('dv-dockview'); popoutContainer.style.overflow = 'hidden'; popoutContainer.appendChild(gready); popoutContainer.appendChild(group.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); this._popoutPopupServices.set(group.id, popoutPopupService); popoutWindowDisposable.addDisposables(popoutPopupService, Disposable.from(() => { this._popoutPopupServices.delete(group.id); })); group.model.location = { type: 'popout', getWindow: () => _window.window, popoutUrl: options === null || options === void 0 ? void 0 : options.popoutUrl, }; if (isGroupAddedToDom && itemToPopout.api.location.type === 'grid') { itemToPopout.api.setVisible(false); } this.doSetGroupAndPanelActive(group); 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(); })); let returnedGroup; const isValidReferenceGroup = isGroupAddedToDom && referenceGroup && this.getPanel(referenceGroup.id); const value = { window: _window, popoutGroup: group, referenceGroup: isValidReferenceGroup ? referenceGroup.id : undefined, disposable: { dispose: () => { popoutWindowDisposable.dispose(); return 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, }); }), /** * ResizeObserver seems slow here, I do not know why but we don't need it * since we can reply on the window resize event as we will occupy the full * window dimensions */ addDisposableListener(_window.window, 'resize', () => { group.layout(_window.window.innerWidth, _window.window.innerHeight); }), overlayRenderContainer, Disposable.from(() => { if (this.isDisposed) { return; // cleanup may run after instance is disposed } if (isGroupAddedToDom && this.getPanel(referenceGroup.id)) { this.movingLock(() => moveGroupWithoutDestroying({ from: group, to: referenceGroup, })); if (!referenceGroup.api.isVisible) { referenceGroup.api.setVisible(true); } if (this.getPanel(group.id)) { this.doRemoveGroup(group, { skipPopoutAssociated: true, }); } } else if (this.getPanel(group.id)) { group.model.renderContainer = this.overlayRenderContainer; group.model.dropTargetContainer = this.rootDropTargetContainer; returnedGroup = group; const alreadyRemoved = !this._popoutGroups.find((p) => p.popoutGroup === group); if (alreadyRemoved) { /** * If this popout group was explicitly removed then we shouldn't run the additional * steps. To tell if the running of this disposable is the result of this popout group * being explicitly removed we can check if this popout group is still referenced in * the `this._popoutGroups` list. */ return; } if (floatingBox) { this.addFloatingGroup(group, { height: floatingBox.height, width: floatingBox.width, position: floatingBox, }); } else { this.doRemoveGroup(group, { skipDispose: true, skipActive: true, skipPopoutReturn: true, }); group.model.location = { type: 'grid' }; this.movingLock(() => { // suppress group add events since the group already exists this.doAddGroup(group, [0]); }); } this.doSetGroupAndPanelActive(group); } })); this._popoutGroups.push(value); this.updateWatermark(); return true; }) .catch((err) => { console.error('dockview: failed to create popout.', err); return false; }); } addFloatingGroup(item, options) { var _a, _b, _c, _d, _e, _f; if (item instanceof DockviewGroupPanel && item.model.location.type === 'edge') { // edge groups are permanent structural elements and cannot be floated return; } let group; if (item instanceof DockviewPanel) { group = this.createGroup(); this._onDidAddGroup.fire(group); this.movingLock(() => this.removePanel(item, { removeEmptyGroup: true, skipDispose: true, skipSetActiveGroup: true, })); this.movingLock(() => group.model.openPanel(item, { skipSetGroupActive: true })); } else { group = item; const popoutReferenceGroupId = (_a = this._popoutGroups.find((_) => _.popoutGroup === group)) === null || _a === void 0 ? void 0 : _a.referenceGroup; const popoutReferenceGroup = popoutReferenceGroupId ? this.getPanel(popoutReferenceGroupId) : undefined; const skip = typeof (options === null || options === void 0 ? void 0 : options.skipRemoveGroup) === 'boolean' && options.skipRemoveGroup; if (!skip) { if (popoutReferenceGroup) { this.movingLock(() => moveGroupWithoutDestroying({ from: item, to: popoutReferenceGroup, })); this.doRemoveGroup(item, { skipPopoutReturn: true, skipPopoutAssociated: true, }); this.doRemoveGroup(popoutReferenceGroup, { skipDispose: true, }); group = popoutReferenceGroup; } else { this.doRemoveGroup(item, { skipDispose: true, skipPopoutReturn: true, skipPopoutAssociated: false, }); } } } function getAnchoredBox() { if (options === null || options === void 0 ? void 0 : options.position) { const result = {}; if ('left' in options.position) { result.left = Math.max(options.position.left, 0); } else if ('right' in options.position) { result.right = Math.max(options.position.right, 0); } else { result.left = DEFAULT_FLOATING_GROUP_POSITION.left; } if ('top' in options.position) { result.top = Math.max(options.position.top, 0); } else if ('bottom' in options.position) { result.bottom = Math.max(options.position.bottom, 0); } else { result.top = DEFAULT_FLOATING_GROUP_POSITION.top; } if (typeof options.width === 'number') { result.width = Math.max(options.width, 0); } else { result.width = DEFAULT_FLOATING_GROUP_POSITION.width; } if (typeof options.height === 'number') { result.height = Math.max(options.height, 0); } else { result.height = DEFAULT_FLOATING_GROUP_POSITION.height; } return result; } return { left: typeof (options === null || options === void 0 ? void 0 : options.x) === 'number' ? Math.max(options.x, 0) : DEFAULT_FLOATING_GROUP_POSITION.left, top: typeof (options === null || options === void 0 ? void 0 : options.y) === 'number' ? Math.max(options.y, 0) : DEFAULT_FLOATING_GROUP_POSITION.top, width: typeof (options === null || options === void 0 ? void 0 : options.width) === 'number' ? Math.max(options.width, 0) : DEFAULT_FLOATING_GROUP_POSITION.width, height: typeof (options === null || options === void 0 ? void 0 : options.height) === 'number' ? Math.max(options.height, 0) : DEFAULT_FLOATING_GROUP_POSITION.height, }; } const anchoredBox = getAnchoredBox(); const overlay = new Overlay(Object.assign(Object.assign({ container: (_b = this._floatingOverlayHost) !== null && _b !== void 0 ? _b : this.gridview.element, content: group.element }, anchoredBox), { minimumInViewportWidth: this.options.floatingGroupBounds === 'boundedWithinViewport' ? undefined : ((_d = (_c = this.options.floatingGroupBounds) === null || _c === void 0 ? void 0 : _c.minimumWidthWithinViewport) !== null && _d !== void 0 ? _d : DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE), minimumInViewportHeight: this.options.floatingGroupBounds === 'boundedWithinViewport' ? undefined : ((_f = (_e = this.options.floatingGroupBounds) === null || _e === void 0 ? void 0 : _e.minimumHeightWithinViewport) !== null && _f !== void 0 ? _f : DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE) })); const el = group.element.querySelector('.dv-void-container'); if (!el) { throw new Error('dockview: failed to find drag handle'); } overlay.setupDrag(el, { inDragMode: typeof (options === null || options === void 0 ? void 0 : options.inDragMode) === 'boolean' ? options.inDragMode : false, }); const floatingGroupPanel = new DockviewFloatingGroupPanel(group, overlay); const disposable = new CompositeDisposable(group.api.onDidActiveChange((event) => { if (event.isActive) { overlay.bringToFront(); } }), (() => { let lastWidth = -1; let lastHeight = -1; return watchElementResize(group.element, (entry) => { const width = Math.round(entry.contentRect.width); const height = Math.round(entry.contentRect.height); if (width === lastWidth && height === lastHeight) { return; } lastWidth = width; lastHeight = height; group.layout(width, height); // let the group know it's size is changing so it can fire events to the panel }); })()); floatingGroupPanel.addDisposables(overlay.onDidChange(() => { // this is either a resize or a move // to inform the panels .layout(...) the group with it's current size // don't care about resize since the above watcher handles that group.layout(group.width, group.height); }), overlay.onDidChangeEnd(() => { this._bufferOnDidLayoutChange.fire(); }), group.onDidChange((event) => { overlay.setBounds({ height: event === null || event === void 0 ? void 0 : event.height, width: event === null || event === void 0 ? void 0 : event.width, }); }), { dispose: () => { disposable.dispose(); remove(this._floatingGroups, floatingGroupPanel); group.model.location = { type: 'grid' }; this.updateWatermark(); }, }); this._floatingGroups.push(floatingGroupPanel); group.model.location = { type: 'floating' }; if (!(options === null || options === void 0 ? void 0 : options.skipActiveGroup)) { this.doSetGroupAndPanelActive(group); } this.updateWatermark(); } orthogonalize(position, options) { this.gridview.normalize(); switch (position) { case 'top': case 'bottom': if (this.gridview.orientation === Orientation.HORIZONTAL) { // we need to add to a vertical splitview but the current root is a horizontal splitview. // insert a vertical splitview at the root level and add the existing view as a child this.gridview.insertOrthogonalSplitviewAtRoot(); } break; case 'left': case 'right': if (this.gridview.orientation === Orientation.VERTICAL) { // we need to add to a horizontal splitview but the current root is a vertical splitview. // insert a horiziontal splitview at the root level and add the existing view as a child this.gridview.insertOrthogonalSplitviewAtRoot(); } break; default: break; } switch (position) { case 'top': case 'left': case 'center': return this.createGroupAtLocation([0], undefined, options); // insert into first position case 'bottom': case 'right': return this.createGroupAtLocation([this.gridview.length], undefined, options); // insert into last position default: throw new Error(`dockview: unsupported position ${position}`); } } updateOptions(options) { var _a, _b, _c, _d, _e; super.updateOptions(options); if ('floatingGroupBounds' in options) { for (const group of this._floatingGroups) { switch (options.floatingGroupBounds) { case 'boundedWithinViewport': group.overlay.minimumInViewportHeight = undefined; group.overlay.minimumInViewportWidth = undefined; break; case undefined: group.overlay.minimumInViewportHeight = DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE; group.overlay.minimumInViewportWidth = DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE; break; default: group.overlay.minimumInViewportHeight = (_a = options.floatingGroupBounds) === null || _a === void 0 ? void 0 : _a.minimumHeightWithinViewport; group.overlay.minimumInViewportWidth = (_b = options.floatingGroupBounds) === null || _b === void 0 ? void 0 : _b.minimumWidthWithinViewport; } group.overlay.setBounds(); } } this.updateDropTargetModel(options); const oldDisableDnd = this.options.disableDnd; const oldDndStrategy = this.options.dndStrategy; this._options = Object.assign(Object.assign({}, this.options), options); const newDisableDnd = this.options.disableDnd; const newDndStrategy = this.options.dndStrategy; if (oldDisableDnd !== newDisableDnd || oldDndStrategy !== newDndStrategy) { this.updateDragAndDropState(); } if ('theme' in options) { this.updateTheme(); } if ('createRightHeaderActionComponent' in options || 'createLeftHeaderActionComponent' in options || 'createPrefixHeaderActionComponent' in options) { for (const group of this.groups) { group.model.updateHeaderActions(); } } if ('createWatermarkComponent' in options) { if (this._watermark) { this._watermark.element.parentElement.remove(); (_d = (_c = this._watermark).dispose) === null || _d === void 0 ? void 0 : _d.call(_c); this._watermark = null; } this.updateWatermark(); for (const group of this.groups) { group.model.refreshWatermark(); } } if ('tabGroupColors' in options || 'tabGroupAccent' in options) { this._tabGroupColorPalette.setEntries((_e = this._options.tabGroupColors) !== null && _e !== void 0 ? _e : DEFAULT_TAB_GROUP_COLORS); this._tabGroupColorPalette.enabled = this._options.tabGroupAccent !== 'off'; for (const group of this.groups) { group.model.refreshTabGroupAccent(); } } this._onDidOptionsChange.fire(); this._layoutFromShell(this.gridview.width, this.gridview.height); } layout(width, height, forceResize) { if (this._shellManager && !this._inShellLayout) { this._shellManager.layout(width, height); } else { super.layout(width, height, forceResize); } this._syncFloatingOverlayHost(); if (this._floatingGroups) { for (const floating of this._floatingGroups) { // ensure floting groups stay within visible boundaries floating.overlay.setBounds(); } } } _syncFloatingOverlayHost() { if (!this._floatingOverlayHost || !this._shellManager) { return; } const shellRect = this._shellManager.element.getBoundingClientRect(); const gridRect = this.element.getBoundingClientRect(); const host = this._floatingOverlayHost; host.style.left = `${gridRect.left - shellRect.left}px`; host.style.top = `${gridRect.top - shellRect.top}px`; host.style.width = `${gridRect.width}px`; host.style.height = `${gridRect.height}px`; } _layoutFromShell(width, height) { this._inShellLayout = true; this.layout(width, height, true); this._inShellLayout = false; } forceRelayout() { if (this._shellManager) { this._layoutFromShell(this.width, this.height); } else { super.forceRelayout(); } } addEdgeGroup(position, options) { if (this._edgeGroups.has(position)) { throw new Error(`dockview: edge group already exists at position '${position}'`); } const group = this.createGroup({ id: options.id }); group.model.location = { type: 'edge', position }; group.model.headerPosition = position; this._edgeGroups.set(position, group); this._onDidAddGroup.fire(group); // Collapse when the group becomes empty const autoCollapseDisposable = group.model.onDidRemovePanel(() => { if (group.model.isEmpty) { this.setEdgeGroupCollapsed(group, true); } }); this._edgeGroupDisposables.set(position, autoCollapseDisposable); this._shellManager.addEdgeView(position, options, group); return group.api; } getEdgeGroup(position) { var _a; return (_a = this._edgeGroups.get(position)) === null || _a === void 0 ? void 0 : _a.api; } setEdgeGroupVisible(position, visible) { this._shellManager.setEdgeGroupVisible(position, visible); } isEdgeGroupVisible(position) { return this._shellManager.isEdgeGroupVisible(position); } removeEdgeGroup(position) { var _a; const group = this._edgeGroups.get(position); if (!group) { throw new Error(`dockview: no edge group exists at position '${position}'`); } // Remove panels inside the group first for (const panel of [...group.panels]) { this.removePanel(panel, { removeEmptyGroup: false, skipDispose: false, }); } // Dispose the auto-collapse listener (_a = this._edgeGroupDisposables.get(position)) === null || _a === void 0 ? void 0 : _a.dispose(); this._edgeGroupDisposables.delete(position); // Remove from the shell splitview this._shellManager.removeEdgeView(position); // Clean up group state this._edgeGroups.delete(position); group.dispose(); this._groups.delete(group.id); this._onDidRemoveGroup.fire(group); } setEdgeGroupCollapsed(group, collapsed) { for (const [position, edgeGroup] of this._edgeGroups) { if (edgeGroup === group) { if (this._shellManager.isEdgeGroupCollapsed(position) === collapsed) { // Skip the splitview resize on a no-op: with non-zero // theme gap, redundan