dockview
Version:
Zero dependency layout manager supporting tabs, grids and splitviews with ReactJS support
534 lines (533 loc) • 22.9 kB
JavaScript
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();
}
}