dockview-core
Version:
Zero dependency layout manager supporting tabs, grids and splitviews
1,064 lines • 77.4 kB
JavaScript
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