UNPKG

dockview-core

Version:

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

1,171 lines (1,170 loc) 47.4 kB
import { DockviewApi } from '../api/component.api'; import { getPanelData } from '../dnd/dataTransfer'; import { addClasses, isAncestor, removeClasses, toggleClass } from '../dom'; import { addDisposableListener, DockviewEvent, Emitter, } from '../events'; import { DockviewWillShowOverlayLocationEvent, } from './events'; import { CompositeDisposable, MutableDisposable, } from '../lifecycle'; import { ContentContainer, } from './components/panel/content'; import { TabsContainer, } from './components/titlebar/tabsContainer'; import { DockviewUnhandledDragOverEvent, } from './options'; import { TabGroup, } from './tabGroup'; export class DockviewDidDropEvent extends DockviewEvent { /** * `PointerEvent` for touch drags has no `dataTransfer`; use * `getData()` for the dockview payload regardless of input method. */ get nativeEvent() { return this.options.nativeEvent; } get position() { return this.options.position; } get panel() { return this.options.panel; } get group() { return this.options.group; } get api() { return this.options.api; } constructor(options) { super(); this.options = options; } getData() { return this.options.getData(); } } export class DockviewWillDropEvent extends DockviewDidDropEvent { get kind() { return this._kind; } constructor(options) { super(options); this._kind = options.kind; } } export class DockviewGroupPanelModel extends CompositeDisposable { get tabGroups() { return this._tabGroups; } get element() { throw new Error('dockview: not supported'); } get activePanel() { return this._activePanel; } get locked() { return this._locked; } set locked(value) { this._locked = value; toggleClass(this.container, 'dv-locked-groupview', value === 'no-drop-target' || value); } get isActive() { return this._isGroupActive; } get panels() { return this._panels; } get size() { return this._panels.length; } get isEmpty() { return this._panels.length === 0; } get hasWatermark() { return !!(this.watermark && this.container.contains(this.watermark.element)); } get header() { return this.tabsContainer; } get isContentFocused() { if (!document.activeElement) { return false; } return isAncestor(document.activeElement, this.contentContainer.element); } get headerPosition() { var _a; return (_a = this._headerPosition) !== null && _a !== void 0 ? _a : 'top'; } set headerPosition(value) { var _a; this._headerPosition = value; removeClasses(this.container, 'dv-groupview-header-top', 'dv-groupview-header-bottom', 'dv-groupview-header-left', 'dv-groupview-header-right'); addClasses(this.container, `dv-groupview-header-${value}`); const direction = value === 'top' || value === 'bottom' ? 'horizontal' : 'vertical'; this.tabsContainer.direction = direction; this.header.direction = direction; // resize the active panel to fit the new header direction // if not, the panel will overflow the tabs container if ((_a = this._activePanel) === null || _a === void 0 ? void 0 : _a.layout) { this._activePanel.layout(this._width, this._height); } if (this._leftHeaderActions || this._rightHeaderActions || this._prefixHeaderActions) { this.updateHeaderActions(); } } get location() { return this._location; } set location(value) { this._location = value; toggleClass(this.container, 'dv-groupview-floating', false); toggleClass(this.container, 'dv-groupview-popout', false); toggleClass(this.container, 'dv-groupview-edge', false); // Mouse and touch drop targets must agree on accepted zones. const applyZones = (zones) => { this.contentContainer.dropTarget.setTargetZones(zones); this.contentContainer.pointerDropTarget.setTargetZones(zones); }; switch (value.type) { case 'grid': applyZones(['top', 'bottom', 'left', 'right', 'center']); break; case 'floating': applyZones(['center']); applyZones(value ? ['center'] : ['top', 'bottom', 'left', 'right', 'center']); toggleClass(this.container, 'dv-groupview-floating', true); break; case 'popout': applyZones(['center']); toggleClass(this.container, 'dv-groupview-popout', true); break; case 'edge': applyZones(['center']); toggleClass(this.container, 'dv-groupview-edge', true); break; } this.groupPanel.api._onDidLocationChange.fire({ location: this.location, }); } constructor(container, accessor, id, options, groupPanel) { var _a, _b; super(); this.container = container; this.accessor = accessor; this.id = id; this.options = options; this.groupPanel = groupPanel; this._isGroupActive = false; this._locked = false; this._rightHeaderActionsDisposable = new MutableDisposable(); this._leftHeaderActionsDisposable = new MutableDisposable(); this._prefixHeaderActionsDisposable = new MutableDisposable(); this._location = { type: 'grid' }; this.mostRecentlyUsed = []; this._overwriteRenderContainer = null; this._overwriteDropTargetContainer = null; this._onDidChange = new Emitter(); this.onDidChange = this._onDidChange.event; this._width = 0; this._height = 0; this._panels = []; this._panelDisposables = new Map(); this._tabGroupDisposables = new Map(); this._pendingMicrotaskDisposables = new Set(); this._onMove = new Emitter(); this.onMove = this._onMove.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._onTabDragStart = new Emitter(); this.onTabDragStart = this._onTabDragStart.event; this._onGroupDragStart = new Emitter(); this.onGroupDragStart = this._onGroupDragStart.event; this._onDidAddPanel = new Emitter(); this.onDidAddPanel = this._onDidAddPanel.event; this._onDidPanelTitleChange = new Emitter(); this.onDidPanelTitleChange = this._onDidPanelTitleChange.event; this._onDidPanelParametersChange = new Emitter(); this.onDidPanelParametersChange = this._onDidPanelParametersChange.event; this._onDidRemovePanel = new Emitter(); this.onDidRemovePanel = this._onDidRemovePanel.event; this._onDidActivePanelChange = new Emitter(); this.onDidActivePanelChange = this._onDidActivePanelChange.event; this._onUnhandledDragOverEvent = new Emitter(); this.onUnhandledDragOverEvent = this._onUnhandledDragOverEvent.event; this._tabGroups = []; this._tabGroupMap = new Map(); this._panelToTabGroup = new Map(); this._tabGroupIdCounter = 0; this._pendingTabGroupUpdate = false; 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; toggleClass(this.container, 'dv-groupview', true); this._api = new DockviewApi(this.accessor); this.tabsContainer = new TabsContainer(this.accessor, this.groupPanel); this.contentContainer = new ContentContainer(this.accessor, this); container.append(this.tabsContainer.element, this.contentContainer.element); this.header.hidden = !!options.hideHeader; this.locked = (_a = options.locked) !== null && _a !== void 0 ? _a : false; this.headerPosition = (_b = options.headerPosition) !== null && _b !== void 0 ? _b : accessor.defaultHeaderPosition; this.addDisposables(this._onTabDragStart, this._onGroupDragStart, this._onWillShowOverlay, this._rightHeaderActionsDisposable, this._leftHeaderActionsDisposable, this._prefixHeaderActionsDisposable, this.tabsContainer.onTabDragStart((event) => { this._onTabDragStart.fire(event); }), this.tabsContainer.onGroupDragStart((event) => { this._onGroupDragStart.fire(event); }), this.tabsContainer.onDrop((event) => { var _a; // Capture panel data before handleDropEvent (which may trigger moves) const dragData = getPanelData(); const draggedPanelId = (_a = dragData === null || dragData === void 0 ? void 0 : dragData.panelId) !== null && _a !== void 0 ? _a : null; this.handleDropEvent('header', event.event, 'center', event.index); // Update tab group membership after the move completes if (draggedPanelId && event.targetTabGroupId) { // Compute the local index within the target tab group // from the global panel index so the panel is inserted // at the correct position, not just appended. const tabGroup = this._tabGroupMap.get(event.targetTabGroupId); let localIndex; if (tabGroup) { const globalIdx = this._panels.findIndex((p) => p.id === draggedPanelId); if (globalIdx !== -1) { // Count how many of this group's panels // appear before the dragged panel localIndex = 0; for (const pid of tabGroup.panelIds) { const pidIdx = this._panels.findIndex((p) => p.id === pid); if (pidIdx < globalIdx) { localIndex++; } } } } this.addPanelToTabGroup(event.targetTabGroupId, draggedPanelId, localIndex); } else if (draggedPanelId && event.targetTabGroupId === null) { // Dropped outside any group — remove from current group this.removePanelFromTabGroup(draggedPanelId); } }), this.contentContainer.onDidFocus(() => { this.accessor.doSetGroupActive(this.groupPanel); }), this.contentContainer.onDidBlur(() => { // noop }), this.contentContainer.dropTarget.onDrop((event) => { this.handleDropEvent('content', event.nativeEvent, event.position); }), this.contentContainer.pointerDropTarget.onDrop((event) => { this.handleDropEvent('content', event.nativeEvent, event.position); }), this.tabsContainer.onWillShowOverlay((event) => { this._onWillShowOverlay.fire(event); }), this.contentContainer.dropTarget.onWillShowOverlay((event) => { this._onWillShowOverlay.fire(new DockviewWillShowOverlayLocationEvent(event, { kind: 'content', panel: this.activePanel, api: this._api, group: this.groupPanel, getData: getPanelData, })); }), this.contentContainer.pointerDropTarget.onWillShowOverlay((event) => { this._onWillShowOverlay.fire(new DockviewWillShowOverlayLocationEvent(event, { kind: 'content', panel: this.activePanel, api: this._api, group: this.groupPanel, getData: getPanelData, })); }), this._onMove, this._onDidChange, this._onDidDrop, this._onWillDrop, this._onDidAddPanel, this._onDidRemovePanel, this._onDidActivePanelChange, this._onUnhandledDragOverEvent, this._onDidPanelTitleChange, this._onDidPanelParametersChange, this._onDidCreateTabGroup, this._onDidDestroyTabGroup, this._onDidAddPanelToTabGroup, this._onDidRemovePanelFromTabGroup, this._onDidTabGroupChange, this._onDidTabGroupCollapsedChange, this._onDidCreateTabGroup.event(() => { this._scheduleTabGroupUpdate(); }), this._onDidDestroyTabGroup.event(() => { this._scheduleTabGroupUpdate(); }), this._onDidAddPanelToTabGroup.event(() => { this._scheduleTabGroupUpdate(); }), this._onDidRemovePanelFromTabGroup.event(() => { this._scheduleTabGroupUpdate(); }), this._onDidTabGroupChange.event(() => { this._scheduleTabGroupUpdate(); }), this._onDidTabGroupCollapsedChange.event(() => { this._scheduleTabGroupUpdate(); })); } _scheduleTabGroupUpdate() { if (this._pendingTabGroupUpdate) { return; } this._pendingTabGroupUpdate = true; queueMicrotask(() => { this._pendingTabGroupUpdate = false; if (!this.isDisposed) { this.tabsContainer.updateTabGroups(); } }); } createTabGroup(options) { var _a; const id = (_a = options === null || options === void 0 ? void 0 : options.id) !== null && _a !== void 0 ? _a : `tg-${this.id}-${this._tabGroupIdCounter++}`; const tabGroup = new TabGroup(id, { label: options === null || options === void 0 ? void 0 : options.label, color: options === null || options === void 0 ? void 0 : options.color, collapsed: options === null || options === void 0 ? void 0 : options.collapsed, componentParams: options === null || options === void 0 ? void 0 : options.componentParams, }); this._tabGroups.push(tabGroup); this._tabGroupMap.set(id, tabGroup); this._tabGroupDisposables.set(id, new CompositeDisposable(tabGroup.onDidChange(() => { this._onDidTabGroupChange.fire({ tabGroup }); }), tabGroup.onDidCollapseChange((isCollapsed) => { if (isCollapsed) { this._handleGroupCollapse(tabGroup); } else { this._handleGroupExpand(tabGroup); } this._onDidTabGroupCollapsedChange.fire({ tabGroup, }); }), tabGroup.onDidDestroy(() => { this._removeTabGroupInternal(tabGroup); }))); this._onDidCreateTabGroup.fire({ tabGroup }); return tabGroup; } dissolveTabGroup(tabGroupId) { const tabGroup = this._tabGroupMap.get(tabGroupId); if (!tabGroup) { return; } // Remove all panels from the group (they stay in the flat panel list) const panelIds = [...tabGroup.panelIds]; for (const panelId of panelIds) { tabGroup.removePanel(panelId); this._panelToTabGroup.delete(panelId); this._onDidRemovePanelFromTabGroup.fire({ tabGroup, panelId }); } tabGroup.dispose(); } addPanelToTabGroup(tabGroupId, panelId, index) { const tabGroup = this._tabGroupMap.get(tabGroupId); if (!tabGroup) { return; } // Ensure the panel actually exists in this group model if (!this._panels.some((p) => p.id === panelId)) { return; } // Remove from any existing group first const existingGroup = this.getTabGroupForPanel(panelId); if (existingGroup) { if (existingGroup.id === tabGroupId) { return; // already in this group } this.removePanelFromTabGroup(panelId); } tabGroup.addPanel(panelId, index); this._panelToTabGroup.set(panelId, tabGroup); // Enforce contiguity: move the panel in the flat _panels array // to the correct global position matching its group-local index this._enforceContiguity(tabGroup, panelId); this._onDidAddPanelToTabGroup.fire({ tabGroup, panelId }); } /** * Move a panel to a new index within its tab group. * Updates both the group's panelIds order and the flat _panels array. */ movePanelWithinGroup(tabGroupId, panelId, newIndex) { const tabGroup = this._tabGroupMap.get(tabGroupId); if (!tabGroup || !tabGroup.containsPanel(panelId)) { return; } // Remove and re-add at new index within the group tabGroup.removePanel(panelId); tabGroup.addPanel(panelId, newIndex); // Re-enforce contiguity in the flat array this._enforceContiguity(tabGroup, panelId); this.tabsContainer.updateTabGroups(); } /** * Move a panel from one tab group to another. */ movePanelBetweenGroups(sourcePanelId, targetTabGroupId, targetIndex) { const sourceGroup = this._findTabGroupForPanel(sourcePanelId); const targetGroup = this._tabGroupMap.get(targetTabGroupId); if (!targetGroup) { return; } if (sourceGroup) { sourceGroup.removePanel(sourcePanelId); this._panelToTabGroup.delete(sourcePanelId); this._onDidRemovePanelFromTabGroup.fire({ tabGroup: sourceGroup, panelId: sourcePanelId, }); // Auto-destroy empty source group if (sourceGroup.isEmpty) { sourceGroup.dispose(); } } targetGroup.addPanel(sourcePanelId, targetIndex); this._panelToTabGroup.set(sourcePanelId, targetGroup); this._enforceContiguity(targetGroup, sourcePanelId); this._onDidAddPanelToTabGroup.fire({ tabGroup: targetGroup, panelId: sourcePanelId, }); } /** * Move an entire tab group to a new position in the tab bar. * The group's internal panel order is preserved. */ moveTabGroup(tabGroupId, targetIndex) { const tabGroup = this._tabGroupMap.get(tabGroupId); if (!tabGroup || tabGroup.panelIds.length === 0) { return; } // Collect group panels in their current order const groupPanelIds = new Set(tabGroup.panelIds); const groupPanels = tabGroup.panelIds .map((pid) => this._panels.find((p) => p.id === pid)) .filter((p) => p !== undefined); if (groupPanels.length === 0) { return; } // Count how many group panels sit before the target index so // we can compensate after removing them from the array. let groupPanelsBefore = 0; for (let i = 0; i < Math.min(targetIndex, this._panels.length); i++) { if (groupPanelIds.has(this._panels[i].id)) { groupPanelsBefore++; } } // Remove group panels from the flat array for (const panel of groupPanels) { const idx = this._panels.indexOf(panel); if (idx !== -1) { this._panels.splice(idx, 1); } } // Adjust target index to account for removed panels const adjustedIndex = targetIndex - groupPanelsBefore; // Clamp target index to valid range after removal const insertAt = Math.max(0, Math.min(adjustedIndex, this._panels.length)); // Insert group panels at the target position this._panels.splice(insertAt, 0, ...groupPanels); // Rebuild the tabs container to match new order for (const panel of this._panels) { this.tabsContainer.delete(panel.id); } for (let i = 0; i < this._panels.length; i++) { this.tabsContainer.openPanel(this._panels[i], i); } this.tabsContainer.updateTabGroups(); } /** * Ensure a panel is at the correct global index in _panels * to maintain contiguity of its tab group members. */ _enforceContiguity(tabGroup, panelId) { const panel = this._panels.find((p) => p.id === panelId); if (!panel) { return; } const localIndex = tabGroup.indexOfPanel(panelId); const globalIndex = this._computeGlobalIndex(tabGroup, localIndex); const currentIndex = this._panels.indexOf(panel); if (currentIndex === globalIndex) { return; } // Move panel in the flat array this._panels.splice(currentIndex, 1); const adjustedIndex = globalIndex > currentIndex ? globalIndex - 1 : globalIndex; this._panels.splice(adjustedIndex, 0, panel); // Reorder in the tabs container to match this.tabsContainer.delete(panelId); this.tabsContainer.openPanel(panel, adjustedIndex); } /** * Compute the global index in _panels for a group-local index. * Finds where the group's panels start in the flat array and offsets. */ _computeGlobalIndex(tabGroup, localIndex) { const groupPanelIds = tabGroup.panelIds; if (groupPanelIds.length <= 1) { // Only one panel (the one being added), keep current position const panel = this._panels.find((p) => p.id === groupPanelIds[0]); return panel ? this._panels.indexOf(panel) : this._panels.length; } // Find the first existing group member (other than the one at localIndex) // to anchor the group position for (let i = 0; i < groupPanelIds.length; i++) { if (i === localIndex) { continue; } const existingPanel = this._panels.find((p) => p.id === groupPanelIds[i]); if (existingPanel) { const existingGlobalIndex = this._panels.indexOf(existingPanel); // Offset based on relative position within group return Math.max(0, existingGlobalIndex + (localIndex - i)); } } return this._panels.length; } removePanelFromTabGroup(panelId) { const tabGroup = this._findTabGroupForPanel(panelId); if (!tabGroup) { return; } tabGroup.removePanel(panelId); this._panelToTabGroup.delete(panelId); this._onDidRemovePanelFromTabGroup.fire({ tabGroup, panelId }); // Auto-destroy empty groups if (tabGroup.isEmpty) { tabGroup.dispose(); } } getTabGroups() { return this._tabGroups; } updateTabGroups() { this.tabsContainer.updateTabGroups(); } refreshTabGroupAccent() { this.tabsContainer.refreshTabGroupAccent(); } refreshWatermark() { var _a, _b; if (this.watermark) { this.watermark.element.remove(); (_b = (_a = this.watermark).dispose) === null || _b === void 0 ? void 0 : _b.call(_a); this.watermark = undefined; } this.updateContainer(); } getTabGroupForPanel(panelId) { return this._findTabGroupForPanel(panelId); } _findTabGroupForPanel(panelId) { return this._panelToTabGroup.get(panelId); } _removeTabGroupInternal(tabGroup) { const index = this._tabGroups.indexOf(tabGroup); if (index !== -1) { this._tabGroups.splice(index, 1); this._tabGroupMap.delete(tabGroup.id); for (const panelId of tabGroup.panelIds) { this._panelToTabGroup.delete(panelId); } this._onDidDestroyTabGroup.fire({ tabGroup }); // Dispose the external listeners (onDidChange, onDidCollapseChange) // we registered on this group. We cannot dispose synchronously // here because this method runs inside the onDidDestroy fire // loop — disposing the CompositeDisposable that holds the // onDidDestroy subscription would splice listeners mid-iteration. // Schedule cleanup on the next microtask instead. const tabGroupDisposable = this._tabGroupDisposables.get(tabGroup.id); this._tabGroupDisposables.delete(tabGroup.id); if (tabGroupDisposable) { this._pendingMicrotaskDisposables.add(tabGroupDisposable); queueMicrotask(() => { this._pendingMicrotaskDisposables.delete(tabGroupDisposable); tabGroupDisposable.dispose(); }); } } } _handleGroupCollapse(tabGroup) { if (!this._activePanel) { return; } // Only act if the active panel belongs to the collapsed group if (!tabGroup.containsPanel(this._activePanel.id)) { return; } const activePanelIndex = this._panels.indexOf(this._activePanel); // Search right first, then left, for a visible (non-collapsed-group) panel for (let i = activePanelIndex + 1; i < this._panels.length; i++) { const candidate = this._panels[i]; const candidateGroup = this._findTabGroupForPanel(candidate.id); if (!candidateGroup || !candidateGroup.collapsed) { this.doSetActivePanel(candidate); this.updateContainer(); return; } } for (let i = activePanelIndex - 1; i >= 0; i--) { const candidate = this._panels[i]; const candidateGroup = this._findTabGroupForPanel(candidate.id); if (!candidateGroup || !candidateGroup.collapsed) { this.doSetActivePanel(candidate); this.updateContainer(); return; } } // All tabs are in collapsed groups — show watermark this.contentContainer.closePanel(); this.doSetActivePanel(undefined); this.updateContainer(); } _handleGroupExpand(tabGroup) { if (this._activePanel) { return; } // Watermark is showing because all groups were collapsed. // Activate the first panel in the newly expanded group. const firstPanelId = tabGroup.panelIds[0]; if (firstPanelId) { const panel = this._panels.find((p) => p.id === firstPanelId); if (panel) { this.doSetActivePanel(panel); this.updateContainer(); } } } /** Restore tab groups from serialized data (used by fromJSON) */ restoreTabGroups(serializedGroups) { // Bump counter past any restored numeric suffixes to avoid ID collisions for (const data of serializedGroups) { const match = data.id.match(/-(\d+)$/); if (match) { const num = parseInt(match[1], 10) + 1; if (num > this._tabGroupIdCounter) { this._tabGroupIdCounter = num; } } } for (const data of serializedGroups) { const tabGroup = this.createTabGroup({ id: data.id, label: data.label, color: data.color, componentParams: data.componentParams, }); const concreteGroup = this._tabGroupMap.get(tabGroup.id); for (const panelId of data.panelIds) { // Only add panels that actually exist in this group model if (this._panels.some((p) => p.id === panelId)) { tabGroup.addPanel(panelId); this._panelToTabGroup.set(panelId, concreteGroup); this._enforceContiguity(concreteGroup, panelId); } } if (data.collapsed) { tabGroup.collapse(); } // Auto-destroy if no valid panels were added if (tabGroup.isEmpty) { tabGroup.dispose(); } } } focusContent() { this.contentContainer.element.focus(); } set renderContainer(value) { this.panels.forEach((panel) => { this.renderContainer.detatch(panel); }); this._overwriteRenderContainer = value; this.panels.forEach((panel) => { this.rerender(panel); }); } get renderContainer() { var _a; return ((_a = this._overwriteRenderContainer) !== null && _a !== void 0 ? _a : this.accessor.overlayRenderContainer); } set dropTargetContainer(value) { this._overwriteDropTargetContainer = value; } get dropTargetContainer() { var _a; return ((_a = this._overwriteDropTargetContainer) !== null && _a !== void 0 ? _a : this.accessor.rootDropTargetContainer); } initialize() { if (this.options.panels) { this.options.panels.forEach((panel) => { this.doAddPanel(panel); }); } if (this.options.activePanel) { this.openPanel(this.options.activePanel); } // must be run after the constructor otherwise this.parent may not be // correctly initialized this.setActive(this.isActive, true); this.updateContainer(); this.updateHeaderActions(); } updateHeaderActions() { if (this.accessor.options.createRightHeaderActionComponent) { this._rightHeaderActions = this.accessor.options.createRightHeaderActionComponent(this.groupPanel); this._rightHeaderActionsDisposable.value = this._rightHeaderActions; this._rightHeaderActions.init({ containerApi: this._api, api: this.groupPanel.api, group: this.groupPanel, }); this.tabsContainer.setRightActionsElement(this._rightHeaderActions.element); } else { this._rightHeaderActions = undefined; this._rightHeaderActionsDisposable.dispose(); this.tabsContainer.setRightActionsElement(undefined); } if (this.accessor.options.createLeftHeaderActionComponent) { this._leftHeaderActions = this.accessor.options.createLeftHeaderActionComponent(this.groupPanel); this._leftHeaderActionsDisposable.value = this._leftHeaderActions; this._leftHeaderActions.init({ containerApi: this._api, api: this.groupPanel.api, group: this.groupPanel, }); this.tabsContainer.setLeftActionsElement(this._leftHeaderActions.element); } else { this._leftHeaderActions = undefined; this._leftHeaderActionsDisposable.dispose(); this.tabsContainer.setLeftActionsElement(undefined); } if (this.accessor.options.createPrefixHeaderActionComponent) { this._prefixHeaderActions = this.accessor.options.createPrefixHeaderActionComponent(this.groupPanel); this._prefixHeaderActionsDisposable.value = this._prefixHeaderActions; this._prefixHeaderActions.init({ containerApi: this._api, api: this.groupPanel.api, group: this.groupPanel, }); this.tabsContainer.setPrefixActionsElement(this._prefixHeaderActions.element); } else { this._prefixHeaderActions = undefined; this._prefixHeaderActionsDisposable.dispose(); this.tabsContainer.setPrefixActionsElement(undefined); } } rerender(panel) { this.contentContainer.renderPanel(panel, { asActive: false }); } indexOf(panel) { return this.tabsContainer.indexOf(panel.id); } toJSON() { var _a; const result = { views: this.tabsContainer.panels, activeView: (_a = this._activePanel) === null || _a === void 0 ? void 0 : _a.id, id: this.id, }; if (this.locked !== false) { result.locked = this.locked; } if (this.header.hidden) { result.hideHeader = true; } if (this.headerPosition !== 'top') { result.headerPosition = this.headerPosition; } if (this._tabGroups.length > 0) { result.tabGroups = this._tabGroups.map((tg) => tg.toJSON()); } return result; } moveToNext(options) { if (!options) { options = {}; } if (!options.panel) { options.panel = this.activePanel; } const index = options.panel ? this.panels.indexOf(options.panel) : -1; let normalizedIndex; if (index < this.panels.length - 1) { normalizedIndex = index + 1; } else if (!options.suppressRoll) { normalizedIndex = 0; } else { return; } this.openPanel(this.panels[normalizedIndex]); } moveToPrevious(options) { if (!options) { options = {}; } if (!options.panel) { options.panel = this.activePanel; } if (!options.panel) { return; } const index = this.panels.indexOf(options.panel); let normalizedIndex; if (index > 0) { normalizedIndex = index - 1; } else if (!options.suppressRoll) { normalizedIndex = this.panels.length - 1; } else { return; } this.openPanel(this.panels[normalizedIndex]); } containsPanel(panel) { return this.panels.includes(panel); } init(_params) { //noop } update(_params) { //noop } focus() { var _a; (_a = this._activePanel) === null || _a === void 0 ? void 0 : _a.focus(); } openPanel(panel, options = {}) { /** * set the panel group * add the panel * check if group active * check if panel active */ if (typeof options.index !== 'number' || options.index > this.panels.length) { options.index = this.panels.length; } const skipSetActive = !!options.skipSetActive; // ensure the group is updated before we fire any events panel.updateParentGroup(this.groupPanel, { skipSetActive: options.skipSetActive, }); this.doAddPanel(panel, options.index, { skipSetActive: skipSetActive, }); if (this._activePanel === panel) { this.contentContainer.renderPanel(panel, { asActive: true }); return; } if (!skipSetActive) { this.doSetActivePanel(panel); } if (!options.skipSetGroupActive) { this.accessor.doSetGroupActive(this.groupPanel); } if (!options.skipSetActive) { this.updateContainer(); } } removePanel(groupItemOrId, options = { skipSetActive: false, }) { const id = typeof groupItemOrId === 'string' ? groupItemOrId : groupItemOrId.id; const panelToRemove = this._panels.find((panel) => panel.id === id); if (!panelToRemove) { throw new Error('invalid operation'); } return this._removePanel(panelToRemove, options); } closeAllPanels() { if (this.panels.length > 0) { // take a copy since we will be edting the array as we iterate through const arrPanelCpy = [...this.panels]; for (const panel of arrPanelCpy) { this.doClose(panel); } } else { this.accessor.removeGroup(this.groupPanel); } } closePanel(panel) { this.doClose(panel); } doClose(panel) { const isLast = this.panels.length === 1 && this.accessor.groups.length === 1; this.accessor.removePanel(panel, isLast && this.accessor.options.noPanelsOverlay === 'emptyGroup' ? { removeEmptyGroup: false } : undefined); } isPanelActive(panel) { return this._activePanel === panel; } updateActions(element) { this.tabsContainer.setRightActionsElement(element); } setActive(isGroupActive, force = false) { if (!force && this.isActive === isGroupActive) { return; } this._isGroupActive = isGroupActive; toggleClass(this.container, 'dv-active-group', isGroupActive); toggleClass(this.container, 'dv-inactive-group', !isGroupActive); this.tabsContainer.setActive(this.isActive); if (!this._activePanel && this.panels.length > 0) { const candidate = this._panels.find((p) => { const tg = this._findTabGroupForPanel(p.id); return !tg || !tg.collapsed; }); if (candidate) { this.doSetActivePanel(candidate); } } this.updateContainer(); } layout(width, height) { var _a; this._width = width; this._height = height; this.contentContainer.layout(this._width, this._height); if ((_a = this._activePanel) === null || _a === void 0 ? void 0 : _a.layout) { this._activePanel.layout(this._width, this._height); } } _removePanel(panel, options) { const isActivePanel = this._activePanel === panel; this.doRemovePanel(panel); if (isActivePanel && this.panels.length > 0) { const nextPanel = this.mostRecentlyUsed[0]; this.openPanel(nextPanel, { skipSetActive: options.skipSetActive, skipSetGroupActive: options.skipSetActiveGroup, }); } if (this._activePanel && this.panels.length === 0) { this.doSetActivePanel(undefined); } if (!options.skipSetActive) { this.updateContainer(); } return panel; } doRemovePanel(panel) { const index = this.panels.indexOf(panel); if (this._activePanel === panel) { this.contentContainer.closePanel(); } this.tabsContainer.delete(panel.id); this._panels.splice(index, 1); if (this.mostRecentlyUsed.includes(panel)) { const index = this.mostRecentlyUsed.indexOf(panel); this.mostRecentlyUsed.splice(index, 1); } const disposable = this._panelDisposables.get(panel.id); if (disposable) { disposable.dispose(); this._panelDisposables.delete(panel.id); } // Remove panel from its tab group (auto-destroys empty groups) this.removePanelFromTabGroup(panel.id); this._onDidRemovePanel.fire({ panel }); } doAddPanel(panel, index = this.panels.length, options = { skipSetActive: false }) { const existingPanel = this._panels.indexOf(panel); const hasExistingPanel = existingPanel > -1; this.tabsContainer.show(); this.contentContainer.show(); this.tabsContainer.openPanel(panel, index); if (!options.skipSetActive) { this.contentContainer.openPanel(panel); } else if (panel.api.renderer === 'always') { this.contentContainer.renderPanel(panel, { asActive: false }); } if (hasExistingPanel) { // TODO - need to ensure ordering hasn't changed and if it has need to re-order this.panels return; } this.updateMru(panel); this.panels.splice(index, 0, panel); this._panelDisposables.set(panel.id, new CompositeDisposable(panel.api.onDidTitleChange((event) => this._onDidPanelTitleChange.fire(event)), panel.api.onDidParametersChange((event) => this._onDidPanelParametersChange.fire(event)))); this._onDidAddPanel.fire({ panel }); } doSetActivePanel(panel) { if (this._activePanel === panel) { return; } this._activePanel = panel; if (panel) { this.tabsContainer.setActivePanel(panel); this.contentContainer.openPanel(panel); panel.layout(this._width, this._height); this.updateMru(panel); // Refresh focus state to handle programmatic activation without DOM focus change this.contentContainer.refreshFocusState(); this._onDidActivePanelChange.fire({ panel, }); } } updateMru(panel) { if (this.mostRecentlyUsed.includes(panel)) { this.mostRecentlyUsed.splice(this.mostRecentlyUsed.indexOf(panel), 1); } this.mostRecentlyUsed = [panel, ...this.mostRecentlyUsed]; } updateContainer() { var _a, _b; this.panels.forEach((panel) => panel.runEvents()); const shouldShowWatermark = this.isEmpty || !this._activePanel; if (shouldShowWatermark && !this.watermark) { const watermark = this.accessor.createWatermarkComponent(); watermark.init({ containerApi: this._api, group: this.groupPanel, }); this.watermark = watermark; addDisposableListener(this.watermark.element, 'pointerdown', () => { if (!this.isActive) { this.accessor.doSetGroupActive(this.groupPanel); } }); this.contentContainer.element.appendChild(this.watermark.element); } if (!shouldShowWatermark && this.watermark) { this.watermark.element.remove(); (_b = (_a = this.watermark).dispose) === null || _b === void 0 ? void 0 : _b.call(_a); this.watermark = undefined; } } canDisplayOverlay(event, position, target) { const firedEvent = new DockviewUnhandledDragOverEvent(event, target, position, getPanelData, this.accessor.getPanel(this.id)); this._onUnhandledDragOverEvent.fire(firedEvent); return firedEvent.isAccepted; } handleDropEvent(type, event, position, index) { if (this.locked === 'no-drop-target') { return; } function getKind() { switch (type) { case 'header': return typeof index === 'number' ? 'tab' : 'header_space'; case 'content': return 'content'; } } const panel = typeof index === 'number' ? this.panels[index] : undefined; const willDropEvent = new DockviewWillDropEvent({ nativeEvent: event, position, panel, getData: () => getPanelData(), kind: getKind(), group: this.groupPanel, api: this._api, }); this._onWillDrop.fire(willDropEvent); if (willDropEvent.defaultPrevented) { return; } const data = getPanelData(); if (data && data.viewId === this.accessor.id) { if (type === 'content') { if (data.groupId === this.id) { // don't allow to drop on self for center position if (position === 'center') { return; } if (data.panelId === null && !data.tabGroupId) { // Full-group drops on self are a no-op. // Tab-group drags are partial moves: an edge drop // splits the layout and creates a new group. return; } } } if (type === 'header') { if (data.groupId === this.id) { if (data.panelId === null && !data.tabGroupId) { return; } } } if (data.panelId === null) { // this is a group move dnd event const { groupId } = data; this._onMove.fire({ target: position, groupId: groupId, index, tabGroupId: data.tabGroupId, }); return; } const fromSameGroup = this.tabsContainer.indexOf(data.panelId) !== -1; if (fromSameGroup && this.tabsContainer.size === 1) { return; } const { groupId, panelId } = data; const isSameGroup = this.id === groupId; if (isSameGroup && !position) { const oldIndex = this.tabsContainer.indexOf(panelId); if (oldIndex === index) { return; } } this._onMove.fire({ target: position, groupId: data.groupId, itemId: data.panelId, index, }); } else { this._onDidDrop.fire(new DockviewDidDropEvent({ nativeEvent: event, position, panel, getData: () => getPanelData(), group: this.groupPanel, api: this._api, })); } } updateDragAndDropState() { this.tabsContainer.updateDragAndDropState(); } dispose() { var _a, _b, _c; super.dispose(); (_a = this.watermark) === null || _a === void 0 ? void 0 : _a.element.remove(); (_c = (_b = this.watermark) === null || _b === void 0 ? void 0 : _b.dispose) === null || _c === void 0 ? void 0 : _c.call(_b); this.watermark = undefined; // Dispose all tab groups for (const tabGroup of [...this._tabGroups]) { tabGroup.dispose(); } for (const disposable of this._tabGroupDisposables.values()) { disposable.dispose(); } this._tabGroupDisposables.clear(); // Dispose any microtask-deferred disposables that haven't run yet for (const disposable of this._pendingMicrotaskDisposables) { disposable.dispose(); } this._pendingMicrotaskDisposables.clear(); for (const panel of this.panels) { panel.dispose(); } this.tabsContainer.dispose(); this.contentContainer.dispose(); } }