UNPKG

dockview-core

Version:

Zero dependency layout manager supporting tabs, grids and splitviews

1,064 lines 77.4 kB
import { getRelativeLocation, getGridLocation, orthogonal, } from '../gridview/gridview'; import { directionToPosition, Droptarget, } from '../dnd/droptarget'; 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, WillShowOverlayLocationEvent, } from './dockviewGroupPanelModel'; 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, } 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'; const DEFAULT_ROOT_OVERLAY_MODEL = { activationSize: { type: 'pixels', value: 10 }, size: { type: 'pixels', value: 20 }, }; 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 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 api() { return this._api; } get floatingGroups() { return this._floatingGroups; } constructor(container, options) { var _a, _b, _c; 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._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._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._onDidMaximizedGroupChange = new Emitter(); this.onDidMaximizedGroupChange = this._onDidMaximizedGroupChange.event; this._floatingGroups = []; this._popoutGroups = []; 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.popupService = new PopupService(this.element); this._themeClassnames = new Classnames(this.element); this._api = new DockviewApi(this); this.rootDropTargetContainer = new DropTargetAnchorContainer(this.element, { disabled: true }); this.overlayRenderContainer = new OverlayRenderContainer(this.gridview.element, this); this._rootDropTarget = new Droptarget(this.element, { className: 'dv-drop-target-edge', canDisplayOverlay: (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; }, acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'], overlayModel: (_c = options.rootOverlayModel) !== null && _c !== void 0 ? _c : 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._onDidAddGroup, this._onDidRemoveGroup, this._onDidActiveGroupChange, this._onUnhandledDragOverEvent, this._onDidMaximizedGroupChange, this._onDidOptionsChange, this._onDidPopoutGroupSizeChange, this._onDidPopoutGroupPositionChange, 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.onDidMovePanel, this.onDidActivePanelChange, this.onDidPopoutGroupPositionChange, this.onDidPopoutGroupSizeChange)(() => { this._bufferOnDidLayoutChange.fire(); }), Disposable.from(() => { // 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(); } }), this._rootDropTarget, this._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 WillShowOverlayLocationEvent(event, { kind: 'edge', panel: undefined, api: this._api, group: undefined, getData: getPanelData, })); }), this._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, })); } }), this._rootDropTarget); } 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; } } addPopoutGroup(itemToPopout, options) { var _a, _b, _c, _d, _e; 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, }); const popoutWindowDisposable = new CompositeDisposable(_window, _window.onDidClose(() => { popoutWindowDisposable.dispose(); })); return _window .open() .then((popoutContainer) => { var _a; if (_window.isDisposed) { return false; } if (popoutContainer === null) { popoutWindowDisposable.dispose(); return false; } const gready = document.createElement('div'); gready.className = 'dv-overlay-render-container'; const overlayRenderContainer = new OverlayRenderContainer(gready, this); 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 occurance * 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 }); this._onDidAddGroup.fire(group); } 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; 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 window', err); return false; }); } addFloatingGroup(item, options) { var _a, _b, _c, _d, _e; 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: this.gridview.element, content: group.element }, anchoredBox), { minimumInViewportWidth: this.options.floatingGroupBounds === 'boundedWithinViewport' ? undefined : (_c = (_b = this.options.floatingGroupBounds) === null || _b === void 0 ? void 0 : _b.minimumWidthWithinViewport) !== null && _c !== void 0 ? _c : DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE, minimumInViewportHeight: this.options.floatingGroupBounds === 'boundedWithinViewport' ? undefined : (_e = (_d = this.options.floatingGroupBounds) === null || _d === void 0 ? void 0 : _d.minimumHeightWithinViewport) !== null && _e !== void 0 ? _e : DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE })); const el = group.element.querySelector('.dv-void-container'); if (!el) { throw new Error('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(); } }), watchElementResize(group.element, (entry) => { const { width, height } = entry.contentRect; 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) { 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(`unsupported position ${position}`); } } updateOptions(options) { var _a, _b; 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); this._options = Object.assign(Object.assign({}, this.options), options); if ('theme' in options) { this.updateTheme(); } this.layout(this.gridview.width, this.gridview.height, true); } layout(width, height, forceResize) { super.layout(width, height, forceResize); if (this._floatingGroups) { for (const floating of this._floatingGroups) { // ensure floting groups stay within visible boundaries floating.overlay.setBounds(); } } } focus() { var _a; (_a = this.activeGroup) === null || _a === void 0 ? void 0 : _a.focus(); } getGroupPanel(id) { return this.panels.find((panel) => panel.id === id); } setActivePanel(panel) { panel.group.model.openPanel(panel); this.doSetGroupAndPanelActive(panel.group); } moveToNext(options = {}) { var _a; if (!options.group) { if (!this.activeGroup) { return; } options.group = this.activeGroup; } if (options.includePanel && options.group) { if (options.group.activePanel !== options.group.panels[options.group.panels.length - 1]) { options.group.model.moveToNext({ suppressRoll: true }); return; } } const location = getGridLocation(options.group.element); const next = (_a = this.gridview.next(location)) === null || _a === void 0 ? void 0 : _a.view; this.doSetGroupAndPanelActive(next); } moveToPrevious(options = {}) { var _a; if (!options.group) { if (!this.activeGroup) { return; } options.group = this.activeGroup; } if (options.includePanel && options.group) { if (options.group.activePanel !== options.group.panels[0]) { options.group.model.moveToPrevious({ suppressRoll: true }); return; } } const location = getGridLocation(options.group.element); const next = (_a = this.gridview.previous(location)) === null || _a === void 0 ? void 0 : _a.view; if (next) { this.doSetGroupAndPanelActive(next); } } /** * Serialize the current state of the layout * * @returns A JSON respresentation of the layout */ toJSON() { var _a; const data = this.gridview.serialize(); const panels = this.panels.reduce((collection, panel) => { collection[panel.id] = panel.toJSON(); return collection; }, {}); const floats = this._floatingGroups.map((group) => { return { data: group.group.toJSON(), position: group.overlay.toJSON(), }; }); const popoutGroups = this._popoutGroups.map((group) => { return { data: group.popoutGroup.toJSON(), gridReferenceGroup: group.referenceGroup, position: group.window.dimensions(), url: group.popoutGroup.api.location.type === 'popout' ? group.popoutGroup.api.location.popoutUrl : undefined, }; }); const result = { grid: data, panels, activeGroup: (_a = this.activeGroup) === null || _a === void 0 ? void 0 : _a.id, }; if (floats.length > 0) { result.floatingGroups = floats; } if (popoutGroups.length > 0) { result.popoutGroups = popoutGroups; } return result; } fromJSON(data) { var _a, _b; this.clear(); if (typeof data !== 'object' || data === null) { throw new Error('serialized layout must be a non-null object'); } const { grid, panels, activeGroup } = data; if (grid.root.type !== 'branch' || !Array.isArray(grid.root.data)) { throw new Error('root must be of type branch'); } try { // take note of the existing dimensions const width = this.width; const height = this.height; const createGroupFromSerializedState = (data) => { const { id, locked, hideHeader, views, activeView } = data; if (typeof id !== 'string') { throw new Error('group id must be of type string'); } const group = this.createGroup({ id, locked: !!locked, hideHeader: !!hideHeader, }); this._onDidAddGroup.fire(group); const createdPanels = []; for (const child of views) { /** * Run the deserializer step seperately since this may fail to due corrupted external state. * In running this section first we avoid firing lots of 'add' events in the event of a failure * due to a corruption of input data. */ const panel = this._deserializer.fromJSON(panels[child], group); createdPanels.push(panel); } for (let i = 0; i < views.length; i++) { const panel = createdPanels[i]; const isActive = typeof activeView === 'string' && activeView === panel.id; group.model.openPanel(panel, { skipSetActive: !isActive, skipSetGroupActive: true, }); } if (!group.activePanel && group.panels.length > 0) { group.model.openPanel(group.panels[group.panels.length - 1], { skipSetGroupActive: true, }); } return group; }; this.gridview.deserialize(grid, { fromJSON: (node) => { return createGroupFromSerializedState(node.data); }, }); this.layout(width, height, true); const serializedFloatingGroups = (_a = data.floatingGroups) !== null && _a !== void 0 ? _a : []; for (const serializedFloatingGroup of serializedFloatingGroups) { const { data, position } = serializedFloatingGroup; const group = createGroupFromSerializedState(data); this.addFloatingGroup(group, { position: position, width: position.width, height: position.height, skipRemoveGroup: true, inDragMode: false, }); } const serializedPopoutGroups = (_b = data.popoutGroups) !== null && _b !== void 0 ? _b : []; for (const serializedPopoutGroup of serializedPopoutGroups) { const { data, position, gridReferenceGroup, url } = serializedPopoutGroup; const group = createGroupFromSerializedState(data); this.addPopoutGroup(group, { position: position !== null && position !== void 0 ? position : undefined, overridePopoutGroup: gridReferenceGroup ? group : undefined, referenceGroup: gridReferenceGroup ? this.getPanel(gridReferenceGroup) : undefined, popoutUrl: url, }); } for (const floatingGroup of this._floatingGroups) { floatingGroup.overlay.setBounds(); } if (typeof activeGroup === 'string') { const panel = this.getPanel(activeGroup); if (panel) { this.doSetGroupAndPanelActive(panel); } } } catch (err) { console.error('dockview: failed to deserialize layout. Reverting changes', err); /** * Takes all the successfully created groups and remove all of their panels. */ for (const group of this.groups) { for (const panel of group.panels) { this.removePanel(panel, { removeEmptyGroup: false, skipDispose: false, }); } } /** * To remove a group we cannot call this.removeGroup(...) since this makes assumptions about * the underlying HTMLElement existing in the Gridview. */ for (const group of this.groups) { group.dispose(); this._groups.delete(group.id); this._onDidRemoveGroup.fire(group); } // iterate over a reassigned array since original array will be modified for (const floatingGroup of [...this._floatingGroups]) { floatingGroup.dispose(); } // fires clean-up events and clears the underlying HTML gridview. this.clear(); /** * even though we have cleaned-up we still want to inform the caller of their error * and we'll do this through re-throwing the original error since afterall you would * expect trying to load a corrupted layout to result in an error and not silently fail... */ throw err; } this.updateWatermark(); this._onDidLayoutFromJSON.fire(); } clear() { const groups = Array.from(this._groups.values()).map((_) => _.value); const hasActiveGroup = !!this.activeGroup; for (const group of groups) { // remove the group will automatically remove the panels this.removeGroup(group, { skipActive: true }); } if (hasActiveGroup) { this.doSetGroupAndPanelActive(undefined); } this.gridview.clear(); } closeAllGroups() { for (const entry of this._groups.entries()) { const [_, group] = entry; group.value.model.closeAllPanels(); } } addPanel(options) { var _a, _b; if (this.panels.find((_) => _.id === options.id)) { throw new Error(`panel with id ${options.id} already exists`); } let referenceGroup; if (options.position && options.floating) { throw new Error('you can only provide one of: position, floating as arguments to .addPanel(...)'); } const initial = { width: options.initialWidth, height: options.initialHeight, }; let index; if (options.position) { if (isPanelOptionsWithPanel(options.position)) { const referencePanel = typeof options.position.referencePanel === 'string' ? this.getGroupPanel(options.position.referencePanel) : options.position.referencePanel; index = options.position.index; if (!referencePanel) { throw new Error(`referencePanel '${options.position.referencePanel}' does not exist`); } referenceGroup = this.findGroup(referencePanel); } else if (isPanelOptionsWithGroup(options.position)) { referenceGroup = typeof options.position.referenceGroup === 'string' ? (_a = this._groups.get(options.position.referenceGroup)) === null || _a === void 0 ? void 0 : _a.value : options.position.referenceGroup; index = options.position.index; if (!referenceGroup) { throw new Error(`referenceGroup '${options.position.referenceGroup}' does not exist`); } } else { const group = this.orthogonalize(directionToPosition(options.position.direction)); const panel = this.createPanel(options, group); group.model.openPanel(panel, { skipSetActive: options.inactive, skipSetGroupActive: options.inactive, index, }); if (!options.inactive) { this.doSetGroupAndPanelActive(group); } group.api.setSize({ height: initial === null || initial === void 0 ? void 0 : initial.height, width: initial === null || initial === void 0 ? void 0 : initial.width, }); return panel; } } else { referenceGroup = this.activeGroup; } let panel; if (referenceGroup) { const target = toTarget(((_b = options.position) === null || _b === void 0 ? void 0 : _b.direction) || 'within'); if (options.floating) { const group = this.createGroup(); this._onDidAddGroup.fire(group); const floatingGroupOptions = typeof options.floating === 'object' && options.floating !== null ? options.floating : {}; this.addFloatingGroup(group, Object.assign(Object.assign({}, floatingGroupOptions), { inDragMode: false, skipRemoveGroup: true, skipActiveGroup: true })); panel = this.createPanel(options, group); group.model.openPanel(panel, { skipSetActive: options.inactive, skipSetGroupActive: options.inactive, index, }); } else if (referenceGroup.api.location.type === 'floating' || target === 'center') { panel = this.createPanel(options, referenceGroup); referenceGroup.model.openPanel(panel, { skipSetActive: options.inactive, skipSetGroupActive: options.inactive, index, }); referenceGroup.api.setSize({ width: initial === null || initial === void 0 ? void 0 : initial.width, height: initial === null || initial === void 0 ? void 0 : initial.height, }); if (!options.inactive) { this.doSetGroupAndPanelActive(referenceGroup); } } else { const location = getGridLocation(referenceGroup.element); const relativeLocation = getRelativeLocation(this.gridview.orientation, location, target); const group = this.createGroupAtLocation(relativeLocation, this.orientationAtLocation(relativeLocation) === Orientation.VERT