UNPKG

dockview

Version:

Zero dependency layout manager supporting tabs, grids and splitviews with ReactJS support

534 lines (533 loc) 22.9 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { getRelativeLocation, getGridLocation, } from '../gridview/gridview'; import { Position } from '../dnd/droptarget'; import { tail, sequenceEquals } from '../array'; import { DockviewGroupPanel } from './dockviewGroupPanel'; import { CompositeDisposable } from '../lifecycle'; import { Emitter } from '../events'; import { Watermark } from './components/watermark/watermark'; import { debounce } from '../functions'; import { sequentialNumberGenerator } from '../math'; import { DefaultDeserializer } from './deserializer'; import { createComponent } from '../panel/componentFactory'; import { BaseGrid, GroupChangeKind, toTarget, } from '../gridview/baseComponentGridview'; import { DockviewApi } from '../api/component.api'; import { MouseEventKind } from '../groupview/tab'; import { Orientation } from '../splitview/core/splitview'; import { DefaultTab } from './components/tab/defaultTab'; import { GroupChangeKind2, } from '../groupview/groupview'; import { GroupviewPanel } from '../groupview/groupviewPanel'; import { DefaultGroupPanelView } from './defaultGroupPanelView'; const nextGroupId = sequentialNumberGenerator(); export class DockviewComponent extends BaseGrid { constructor(element, options) { super(element, { proportionalLayout: true, orientation: options.orientation || Orientation.HORIZONTAL, styles: options.styles, }); this._panels = new Map(); this.dirtyPanels = new Set(); this.debouncedDeque = debounce(this.syncConfigs.bind(this), 5000); // events this._onTabInteractionEvent = new Emitter(); this.onTabInteractionEvent = this._onTabInteractionEvent.event; this._onTabContextMenu = new Emitter(); this.onTabContextMenu = this._onTabContextMenu.event; this.panelState = {}; this._options = options; if (!this.options.components) { this.options.components = {}; } if (!this.options.frameworkComponents) { this.options.frameworkComponents = {}; } if (!this.options.frameworkTabComponents) { this.options.frameworkTabComponents = {}; } if (!this.options.tabComponents) { this.options.tabComponents = {}; } if (!this.options.watermarkComponent && !this.options.watermarkFrameworkComponent) { this.options.watermarkComponent = Watermark; } this._api = new DockviewApi(this); } get totalPanels() { return this._panels.size; } get panels() { return Array.from(this._panels.values()).map((_) => _.value); } get deserializer() { return this._deserializer; } set deserializer(value) { this._deserializer = value; } get options() { return this._options; } get activePanel() { const activeGroup = this.activeGroup; if (!activeGroup) { return undefined; } return activeGroup.model.activePanel; } set tabHeight(height) { this.options.tabHeight = height; this._groups.forEach((value) => { value.value.model.tabHeight = height; }); } get tabHeight() { return this.options.tabHeight; } updateOptions(options) { const hasOrientationChanged = typeof options.orientation === 'string' && this.options.orientation !== options.orientation; // TODO support style update // const hasStylesChanged = // typeof options.styles === 'object' && // this.options.styles !== options.styles; this._options = Object.assign(Object.assign({}, this.options), options); if (hasOrientationChanged) { this.gridview.orientation = options.orientation; } this.layout(this.gridview.width, this.gridview.height, true); } focus() { var _a; (_a = this.activeGroup) === null || _a === void 0 ? void 0 : _a.focus(); } getGroupPanel(id) { var _a; return (_a = this._panels.get(id)) === null || _a === void 0 ? void 0 : _a.value; } setActivePanel(panel) { if (!panel.group) { throw new Error(`Panel ${panel.id} has no associated group`); } this.doSetGroupActive(panel.group); panel.group.model.openPanel(panel); } moveToNext(options = {}) { var _a; if (!options.group) { if (!this.activeGroup) { return; } options.group = this.activeGroup; } if (options.includePanel && options.group) { if (options.group.model.activePanel !== options.group.model.panels[options.group.model.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.doSetGroupActive(next); } moveToPrevious(options = {}) { var _a; if (!options.group) { if (!this.activeGroup) { return; } options.group = this.activeGroup; } if (options.includePanel && options.group) { if (options.group.model.activePanel !== options.group.model.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.doSetGroupActive(next); } } registerPanel(panel) { if (this._panels.has(panel.id)) { throw new Error(`panel ${panel.id} already exists`); } const disposable = new CompositeDisposable(panel.onDidStateChange(() => this.addDirtyPanel(panel))); this._panels.set(panel.id, { value: panel, disposable }); } unregisterPanel(panel) { if (!this._panels.has(panel.id)) { throw new Error(`panel ${panel.id} doesn't exist`); } const item = this._panels.get(panel.id); if (item) { item.disposable.dispose(); item.value.dispose(); } this._panels.delete(panel.id); } /** * Serialize the current state of the layout * * @returns A JSON respresentation of the layout */ toJSON() { var _a; this.syncConfigs(); const data = this.gridview.serialize(); const panels = Array.from(this._panels.values()).reduce((collection, panel) => { if (!this.panelState[panel.value.id]) { collection[panel.value.id] = panel.value.toJSON(); } return collection; }, {}); return { grid: data, panels, activeGroup: (_a = this.activeGroup) === null || _a === void 0 ? void 0 : _a.id, options: { tabHeight: this.tabHeight }, }; } fromJSON(data) { this.gridview.clear(); this._panels.forEach((panel) => { panel.disposable.dispose(); panel.value.dispose(); }); this._panels.clear(); this._groups.clear(); if (!this.deserializer) { throw new Error('invalid deserializer'); } const { grid, panels, options, activeGroup } = data; if (typeof (options === null || options === void 0 ? void 0 : options.tabHeight) === 'number') { this.tabHeight = options.tabHeight; } if (!this.deserializer) { throw new Error('no deserializer provided'); } this.gridview.deserialize(grid, new DefaultDeserializer(this, { createPanel: (id) => { const panelData = panels[id]; const panel = this.deserializer.fromJSON(panelData); this.registerPanel(panel); return panel; }, })); if (typeof activeGroup === 'string') { const panel = this.getPanel(activeGroup); if (panel) { this.doSetGroupActive(panel); } } this.gridview.layout(this.width, this.height); this._onGridEvent.fire({ kind: GroupChangeKind.LAYOUT_FROM_JSON }); } closeAllGroups() { return __awaiter(this, void 0, void 0, function* () { for (const entry of this._groups.entries()) { const [_, group] = entry; const didCloseAll = yield group.value.model.closeAllPanels(); if (!didCloseAll) { return false; } } return true; }); } fireMouseEvent(event) { switch (event.kind) { case MouseEventKind.CONTEXT_MENU: if (event.tab && event.panel) { this._onTabContextMenu.fire({ event: event.event, api: this._api, panel: event.panel, }); } break; } } addPanel(options) { var _a, _b; const panel = this._addPanel(options); let referenceGroup; if ((_a = options.position) === null || _a === void 0 ? void 0 : _a.referencePanel) { const referencePanel = this.getGroupPanel(options.position.referencePanel); if (!referencePanel) { throw new Error(`referencePanel ${options.position.referencePanel} does not exist`); } referenceGroup = this.findGroup(referencePanel); } else { referenceGroup = this.activeGroup; } if (referenceGroup) { const target = toTarget(((_b = options.position) === null || _b === void 0 ? void 0 : _b.direction) || 'within'); if (target === Position.Center) { referenceGroup.model.openPanel(panel); } else { const location = getGridLocation(referenceGroup.element); const relativeLocation = getRelativeLocation(this.gridview.orientation, location, target); this.addPanelToNewGroup(panel, relativeLocation); } } else { this.addPanelToNewGroup(panel); } return panel; } removePanel(panel) { this.unregisterPanel(panel); const group = panel.group; if (!group) { throw new Error(`cannot remove panel ${panel.id}. it's missing a group.`); } group.model.removePanel(panel); if (group.model.size === 0) { this.removeGroup(group); } } createWatermarkComponent() { var _a; return createComponent('watermark-id', 'watermark-name', this.options.watermarkComponent ? { 'watermark-name': this.options.watermarkComponent } : {}, this.options.watermarkFrameworkComponent ? { 'watermark-name': this.options.watermarkFrameworkComponent } : {}, (_a = this.options.frameworkComponentFactory) === null || _a === void 0 ? void 0 : _a.watermark); } addEmptyGroup(options) { var _a; const group = this.createGroup(); if (options) { const referencePanel = (_a = this._panels.get(options.referencePanel)) === null || _a === void 0 ? void 0 : _a.value; if (!referencePanel) { throw new Error(`reference panel ${options.referencePanel} does not exist`); } const referenceGroup = this.findGroup(referencePanel); if (!referenceGroup) { throw new Error(`reference group for reference panel ${options.referencePanel} does not exist`); } const target = toTarget(options.direction || 'within'); const location = getGridLocation(referenceGroup.element); const relativeLocation = getRelativeLocation(this.gridview.orientation, location, target); this.doAddGroup(group, relativeLocation); } else { this.doAddGroup(group); } } removeGroup(group) { const panels = [...group.model.panels]; // reassign since group panels will mutate panels.forEach((panel) => { this.removePanel(panel); }); if (this._groups.size === 1) { this._activeGroup = group; return; } super.removeGroup(group); } moveGroupOrPanel(referenceGroup, groupId, itemId, target, index) { var _a, _b, _c; const sourceGroup = groupId ? (_a = this._groups.get(groupId)) === null || _a === void 0 ? void 0 : _a.value : undefined; if (!target || target === Position.Center) { const groupItem = (sourceGroup === null || sourceGroup === void 0 ? void 0 : sourceGroup.model.removePanel(itemId)) || ((_b = this._panels.get(itemId)) === null || _b === void 0 ? void 0 : _b.value); if (!groupItem) { throw new Error(`No panel with id ${itemId}`); } if ((sourceGroup === null || sourceGroup === void 0 ? void 0 : sourceGroup.model.size) === 0) { this.doRemoveGroup(sourceGroup); } referenceGroup.model.openPanel(groupItem, { index }); } else { const referenceLocation = getGridLocation(referenceGroup.element); const targetLocation = getRelativeLocation(this.gridview.orientation, referenceLocation, target); if (sourceGroup && sourceGroup.model.size < 2) { const [targetParentLocation, to] = tail(targetLocation); const sourceLocation = getGridLocation(sourceGroup.element); const [sourceParentLocation, from] = tail(sourceLocation); if (sequenceEquals(sourceParentLocation, targetParentLocation)) { // special case when 'swapping' two views within same grid location // if a group has one tab - we are essentially moving the 'group' // which is equivalent to swapping two views in this case this.gridview.moveView(sourceParentLocation, from, to); } else { // source group will become empty so delete the group const targetGroup = this.doRemoveGroup(sourceGroup, { skipActive: true, skipDispose: true, }); // after deleting the group we need to re-evaulate the ref location const updatedReferenceLocation = getGridLocation(referenceGroup.element); const location = getRelativeLocation(this.gridview.orientation, updatedReferenceLocation, target); this.doAddGroup(targetGroup, location); } } else { const groupItem = (sourceGroup === null || sourceGroup === void 0 ? void 0 : sourceGroup.model.removePanel(itemId)) || ((_c = this._panels.get(itemId)) === null || _c === void 0 ? void 0 : _c.value); if (!groupItem) { throw new Error(`No panel with id ${itemId}`); } const dropLocation = getRelativeLocation(this.gridview.orientation, referenceLocation, target); this.addPanelToNewGroup(groupItem, dropLocation); } } } doSetGroupActive(group, skipFocus) { var _a, _b; const isGroupAlreadyFocused = this._activeGroup === group; super.doSetGroupActive(group, skipFocus); if (!isGroupAlreadyFocused && ((_a = this._activeGroup) === null || _a === void 0 ? void 0 : _a.model.activePanel)) { this._onGridEvent.fire({ kind: GroupChangeKind.PANEL_ACTIVE, panel: (_b = this._activeGroup) === null || _b === void 0 ? void 0 : _b.model.activePanel, }); } } createGroup(options) { if (!options) { options = { tabHeight: this.tabHeight }; } if (typeof options.tabHeight !== 'number') { options.tabHeight = this.tabHeight; } let id = options === null || options === void 0 ? void 0 : options.id; if (id && this._groups.has(options.id)) { console.warn(`Duplicate group id ${options === null || options === void 0 ? void 0 : options.id}. reassigning group id to avoid errors`); id = undefined; } if (!id) { id = nextGroupId.next(); while (this._groups.has(id)) { id = nextGroupId.next(); } } const view = new GroupviewPanel(this, id, options); if (!this._groups.has(view.id)) { const disposable = new CompositeDisposable(view.model.onMove((event) => { const { groupId, itemId, target, index } = event; this.moveGroupOrPanel(view, groupId, itemId, target, index); }), view.model.onDidGroupChange((event) => { switch (event.kind) { case GroupChangeKind2.ADD_PANEL: this._onGridEvent.fire({ kind: GroupChangeKind.ADD_PANEL, panel: event.panel, }); break; case GroupChangeKind2.GROUP_ACTIVE: this._onGridEvent.fire({ kind: GroupChangeKind.GROUP_ACTIVE, panel: event.panel, }); break; case GroupChangeKind2.REMOVE_PANEL: this._onGridEvent.fire({ kind: GroupChangeKind.REMOVE_PANEL, panel: event.panel, }); break; case GroupChangeKind2.PANEL_ACTIVE: this._onGridEvent.fire({ kind: GroupChangeKind.PANEL_ACTIVE, panel: event.panel, }); break; } })); this._groups.set(view.id, { value: view, disposable }); } // TODO: must be called after the above listeners have been setup, // not an ideal pattern view.initialize(); if (typeof this.options.tabHeight === 'number') { view.model.tabHeight = this.options.tabHeight; } return view; } dispose() { super.dispose(); this._onGridEvent.dispose(); } /** * Ensure the local copy of the layout state is up-to-date */ syncConfigs() { const dirtyPanels = Array.from(this.dirtyPanels); if (dirtyPanels.length === 0) { // } this.dirtyPanels.clear(); const partialPanelState = dirtyPanels .map((panel) => this._panels.get(panel.id)) .filter((_) => !!_) .reduce((collection, panel) => { collection[panel.value.id] = panel.value.toJSON(); return collection; }, {}); this.panelState = Object.assign(Object.assign({}, this.panelState), partialPanelState); dirtyPanels .filter((p) => this._panels.has(p.id)) .forEach((panel) => { panel.setDirty(false); }); } _addPanel(options) { const view = new DefaultGroupPanelView({ content: this.createContentComponent(options.id, options.component), tab: this.createTabComponent(options.id, options.tabComponent), }); const panel = new DockviewGroupPanel(options.id, this._api); panel.init({ view, title: options.title || options.id, suppressClosable: options === null || options === void 0 ? void 0 : options.suppressClosable, params: (options === null || options === void 0 ? void 0 : options.params) || {}, }); this.registerPanel(panel); return panel; } createContentComponent(id, componentName) { var _a; return createComponent(id, componentName, this.options.components || {}, this.options.frameworkComponents, (_a = this.options.frameworkComponentFactory) === null || _a === void 0 ? void 0 : _a.content); } createTabComponent(id, componentName) { var _a; return createComponent(id, componentName, this.options.tabComponents || {}, this.options.frameworkTabComponents, (_a = this.options.frameworkComponentFactory) === null || _a === void 0 ? void 0 : _a.tab, () => new DefaultTab()); } addPanelToNewGroup(panel, location = [0]) { const group = this.createGroup(); this.doAddGroup(group, location); group.model.openPanel(panel); } findGroup(panel) { var _a; return (_a = Array.from(this._groups.values()).find((group) => group.value.model.containsPanel(panel))) === null || _a === void 0 ? void 0 : _a.value; } addDirtyPanel(panel) { this.dirtyPanels.add(panel); panel.setDirty(true); this.debouncedDeque(); } }