dockview-core
Version:
Zero dependency layout manager supporting tabs, groups, grids and splitviews for vanilla TypeScript
1,013 lines • 121 kB
JavaScript
import { getRelativeLocation, getGridLocation, orthogonal, } from '../gridview/gridview';
import { directionToPosition, } from '../dnd/droptarget';
import { html5Backend, pointerBackend } from '../dnd/backend';
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, } from './dockviewGroupPanelModel';
import { DockviewWillShowOverlayLocationEvent, } from './events';
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, DESERIALIZATION_POPOUT_DELAY_MS, } from '../constants';
import { OverlayRenderContainer, } from '../overlay/overlayRenderContainer';
import { PopoutWindow } from '../popoutWindow';
import { StrictEventsSequencing } from './strictEventsSequencing';
import { PopupService } from './components/popupService';
import { ContextMenuController } from './contextMenu';
import { DropTargetAnchorContainer } from '../dnd/dropTargetAnchorContainer';
import { themeAbyss } from './theme';
import { ShellManager, } from './dockviewShell';
import { DEFAULT_TAB_GROUP_COLORS, TabGroupColorPalette, } from './tabGroupAccent';
const DEFAULT_ROOT_OVERLAY_MODEL = {
activationSize: { type: 'pixels', value: 10 },
size: { type: 'pixels', value: 20 },
};
function buildTabGroupColorPalette(options) {
var _a;
const entries = (_a = options.tabGroupColors) !== null && _a !== void 0 ? _a : DEFAULT_TAB_GROUP_COLORS;
const enabled = options.tabGroupAccent !== 'off';
return new TabGroupColorPalette(entries, enabled);
}
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 tabGroupColorPalette() {
return this._tabGroupColorPalette;
}
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 defaultHeaderPosition() {
var _a;
return (_a = this.options.defaultHeaderPosition) !== null && _a !== void 0 ? _a : 'top';
}
get api() {
return this._api;
}
get floatingGroups() {
return this._floatingGroups;
}
/**
* Promise that resolves when all popout groups from the last fromJSON call are restored.
* Useful for tests that need to wait for delayed popout creation.
*/
get popoutRestorationPromise() {
return this._popoutRestorationPromise;
}
constructor(container, options) {
var _a, _b, _c, _d, _e, _f, _g;
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._popoutPopupServices = new Map();
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._onDidOpenPopoutWindowFail = new Emitter();
this.onDidOpenPopoutWindowFail = this._onDidOpenPopoutWindowFail.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._onDidCreateTabGroup = new Emitter();
this.onDidCreateTabGroup = this._onDidCreateTabGroup.event;
this._onDidDestroyTabGroup = new Emitter();
this.onDidDestroyTabGroup = this._onDidDestroyTabGroup.event;
this._onDidAddPanelToTabGroup = new Emitter();
this.onDidAddPanelToTabGroup = this._onDidAddPanelToTabGroup.event;
this._onDidRemovePanelFromTabGroup = new Emitter();
this.onDidRemovePanelFromTabGroup = this._onDidRemovePanelFromTabGroup.event;
this._onDidTabGroupChange = new Emitter();
this.onDidTabGroupChange = this._onDidTabGroupChange.event;
this._onDidTabGroupCollapsedChange = new Emitter();
this.onDidTabGroupCollapsedChange = this._onDidTabGroupCollapsedChange.event;
this._onDidMaximizedGroupChange = new Emitter();
this.onDidMaximizedGroupChange = this._onDidMaximizedGroupChange.event;
this._inShellLayout = false;
this._edgeGroups = new Map();
this._edgeGroupDisposables = new Map();
this._floatingGroups = [];
this._popoutGroups = [];
this._popoutRestorationPromise = Promise.resolve();
this._popoutRestorationCleanups = new Set();
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._tabGroupColorPalette = buildTabGroupColorPalette(options);
this.popupService = new PopupService(this.element);
this.contextMenuController = new ContextMenuController(this);
this._api = new DockviewApi(this);
// The shell always wraps the dockview element so edge groups can be
// added at any time via addEdgeGroup() without re-parenting the DOM.
this.disableResizing = true;
container.removeChild(this.element);
this._shellManager = new ShellManager(container, this.element, (w, h) => this._layoutFromShell(w, h), (_d = (_c = options.theme) === null || _c === void 0 ? void 0 : _c.gap) !== null && _d !== void 0 ? _d : 0, (_e = options.theme) === null || _e === void 0 ? void 0 : _e.edgeGroupCollapsedSize);
// The shell wraps the dockview element, so move the popup anchor
// into the shell so overflow dropdowns in edge groups position correctly
this.popupService.updateRoot(this._shellManager.element);
this._shellThemeClassnames = new Classnames(this._shellManager.element);
// Anchor the overlay container to the shell element so that edge groups
// (which live outside this.element in the shell layout) are covered when
// dndOverlayMounting is 'absolute'.
this.rootDropTargetContainer = new DropTargetAnchorContainer(this._shellManager.element, { disabled: true });
this.overlayRenderContainer = new OverlayRenderContainer(this._shellManager.element, this);
// Hosted in the shell (not inside `.dv-dockview`) so floating overlays
// share a stacking context with `dv-render-overlay` panels; sized to
// mirror the gridview rect so saved positions remain valid.
this._floatingOverlayHost = document.createElement('div');
this._floatingOverlayHost.className = 'dv-floating-overlay-host';
this._shellManager.element.appendChild(this._floatingOverlayHost);
const rootCanDisplayOverlay = (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;
};
this._rootDropTarget = html5Backend.createDropTarget(this.element, {
className: 'dv-drop-target-edge',
canDisplayOverlay: rootCanDisplayOverlay,
acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'],
overlayModel: (_f = options.rootOverlayModel) !== null && _f !== void 0 ? _f : DEFAULT_ROOT_OVERLAY_MODEL,
getOverrideTarget: () => { var _a; return (_a = this.rootDropTargetContainer) === null || _a === void 0 ? void 0 : _a.model; },
});
this._rootPointerDropTarget = pointerBackend.createDropTarget(this.element, {
className: 'dv-drop-target-edge',
canDisplayOverlay: rootCanDisplayOverlay,
acceptedTargetZones: [
'top',
'bottom',
'left',
'right',
'center',
],
overlayModel: (_g = options.rootOverlayModel) !== null && _g !== void 0 ? _g : 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._onDidMovePanel.event(() => {
/**
* Update overlay positions after DOM layout completes to prevent 0×0 dimensions.
* With defaultRenderer="always" this results in panel content not showing after move operations.
* Debounced to avoid multiple calls when moving groups with multiple panels.
*/
this.debouncedUpdateAllPositions();
}), this._onDidAddGroup, this._onDidRemoveGroup, this._onDidActiveGroupChange, this._onUnhandledDragOverEvent, this._onDidMaximizedGroupChange, this._onDidOptionsChange, this._onDidPopoutGroupSizeChange, this._onDidPopoutGroupPositionChange, this._onDidOpenPopoutWindowFail, this._onDidCreateTabGroup, this._onDidDestroyTabGroup, this._onDidAddPanelToTabGroup, this._onDidRemovePanelFromTabGroup, this._onDidTabGroupChange, this._onDidTabGroupCollapsedChange, 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.onDidRemoveGroup, this.onDidMovePanel, this.onDidActivePanelChange, this.onDidPopoutGroupPositionChange, this.onDidPopoutGroupSizeChange, this.onDidCreateTabGroup, this.onDidDestroyTabGroup, this.onDidAddPanelToTabGroup, this.onDidRemovePanelFromTabGroup, this.onDidTabGroupChange, this.onDidTabGroupCollapsedChange)(() => {
this._bufferOnDidLayoutChange.fire();
}), Disposable.from(() => {
var _a;
// Cancel any pending popout-restoration timers scheduled by
// fromJSON so they don't open new browser windows after
// dispose, and resolve their promises so callers awaiting
// popoutRestorationPromise don't hang. See issue #851.
for (const cleanup of [...this._popoutRestorationCleanups]) {
cleanup();
}
this._popoutRestorationCleanups.clear();
// 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();
}
(_a = this._shellManager) === null || _a === void 0 ? void 0 : _a.dispose();
for (const d of this._edgeGroupDisposables.values()) {
d.dispose();
}
this._edgeGroupDisposables.clear();
}), this._rootDropTarget, this._rootPointerDropTarget, Event.any(this._rootDropTarget.onWillShowOverlay, this._rootPointerDropTarget.onWillShowOverlay)((event) => {
if (this.gridview.length > 0 && event.position === 'center') {
// option only available when no panels in primary grid
return;
}
this._onWillShowOverlay.fire(new DockviewWillShowOverlayLocationEvent(event, {
kind: 'edge',
panel: undefined,
api: this._api,
group: undefined,
getData: getPanelData,
}));
}), Event.any(this._rootDropTarget.onDrop, this._rootPointerDropTarget.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,
}));
}
}));
}
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;
case 'edge':
// Edge group visibility is managed via setEdgeGroupVisible
break;
}
}
/**
* Returns the {@link PopupService} that should host popovers (context
* menus, tab overflow menus) for the given group. Popout groups have their
* own service rooted in their popout window so the popover renders there
* and dismisses on events from that window.
*/
getPopupServiceForGroup(group) {
var _a;
return (_a = this._popoutPopupServices.get(group.id)) !== null && _a !== void 0 ? _a : this.popupService;
}
addPopoutGroup(itemToPopout, options) {
var _a, _b, _c, _d, _e, _f;
if (itemToPopout instanceof DockviewGroupPanel &&
itemToPopout.model.location.type === 'edge') {
// edge groups are permanent structural elements and cannot be popped out
return Promise.resolve(false);
}
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,
nonce: (_f = this.options) === null || _f === void 0 ? void 0 : _f.nonce,
});
const popoutWindowDisposable = new CompositeDisposable(_window, _window.onDidClose(() => {
popoutWindowDisposable.dispose();
}));
return _window
.open()
.then((popoutContainer) => {
var _a;
if (_window.isDisposed) {
return false;
}
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 occurrence
* 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 });
if (popoutContainer) {
this._onDidAddGroup.fire(group);
}
}
if (popoutContainer === null) {
console.error('dockview: failed to create popout. perhaps you need to allow pop-ups for this website');
popoutWindowDisposable.dispose();
this._onDidOpenPopoutWindowFail.fire();
// if the popout window was blocked, we need to move the group back to the reference group
// and set it to visible
this.movingLock(() => moveGroupWithoutDestroying({
from: group,
to: referenceGroup,
}));
if (!referenceGroup.api.isVisible) {
referenceGroup.api.setVisible(true);
}
return false;
}
const gready = document.createElement('div');
gready.className = 'dv-overlay-render-container';
const overlayRenderContainer = new OverlayRenderContainer(gready, this);
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;
// Each popout group needs its own popover service so that
// tab context menus, chip menus, and tab overflow menus
// render in the popout window (not the main window) and
// their pointerdown/keydown listeners fire on the right
// window for outside-click and Escape dismissal.
const popoutPopupService = new PopupService(popoutContainer, _window.window);
this._popoutPopupServices.set(group.id, popoutPopupService);
popoutWindowDisposable.addDisposables(popoutPopupService, Disposable.from(() => {
this._popoutPopupServices.delete(group.id);
}));
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.', err);
return false;
});
}
addFloatingGroup(item, options) {
var _a, _b, _c, _d, _e, _f;
if (item instanceof DockviewGroupPanel &&
item.model.location.type === 'edge') {
// edge groups are permanent structural elements and cannot be floated
return;
}
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: (_b = this._floatingOverlayHost) !== null && _b !== void 0 ? _b : this.gridview.element, content: group.element }, anchoredBox), { minimumInViewportWidth: this.options.floatingGroupBounds === 'boundedWithinViewport'
? undefined
: ((_d = (_c = this.options.floatingGroupBounds) === null || _c === void 0 ? void 0 : _c.minimumWidthWithinViewport) !== null && _d !== void 0 ? _d : DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE), minimumInViewportHeight: this.options.floatingGroupBounds === 'boundedWithinViewport'
? undefined
: ((_f = (_e = this.options.floatingGroupBounds) === null || _e === void 0 ? void 0 : _e.minimumHeightWithinViewport) !== null && _f !== void 0 ? _f : DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE) }));
const el = group.element.querySelector('.dv-void-container');
if (!el) {
throw new Error('dockview: 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();
}
}), (() => {
let lastWidth = -1;
let lastHeight = -1;
return watchElementResize(group.element, (entry) => {
const width = Math.round(entry.contentRect.width);
const height = Math.round(entry.contentRect.height);
if (width === lastWidth && height === lastHeight) {
return;
}
lastWidth = width;
lastHeight = height;
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) {
this.gridview.normalize();
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(`dockview: unsupported position ${position}`);
}
}
updateOptions(options) {
var _a, _b, _c, _d, _e;
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);
const oldDisableDnd = this.options.disableDnd;
const oldDndStrategy = this.options.dndStrategy;
this._options = Object.assign(Object.assign({}, this.options), options);
const newDisableDnd = this.options.disableDnd;
const newDndStrategy = this.options.dndStrategy;
if (oldDisableDnd !== newDisableDnd ||
oldDndStrategy !== newDndStrategy) {
this.updateDragAndDropState();
}
if ('theme' in options) {
this.updateTheme();
}
if ('createRightHeaderActionComponent' in options ||
'createLeftHeaderActionComponent' in options ||
'createPrefixHeaderActionComponent' in options) {
for (const group of this.groups) {
group.model.updateHeaderActions();
}
}
if ('createWatermarkComponent' in options) {
if (this._watermark) {
this._watermark.element.parentElement.remove();
(_d = (_c = this._watermark).dispose) === null || _d === void 0 ? void 0 : _d.call(_c);
this._watermark = null;
}
this.updateWatermark();
for (const group of this.groups) {
group.model.refreshWatermark();
}
}
if ('tabGroupColors' in options || 'tabGroupAccent' in options) {
this._tabGroupColorPalette.setEntries((_e = this._options.tabGroupColors) !== null && _e !== void 0 ? _e : DEFAULT_TAB_GROUP_COLORS);
this._tabGroupColorPalette.enabled =
this._options.tabGroupAccent !== 'off';
for (const group of this.groups) {
group.model.refreshTabGroupAccent();
}
}
this._onDidOptionsChange.fire();
this._layoutFromShell(this.gridview.width, this.gridview.height);
}
layout(width, height, forceResize) {
if (this._shellManager && !this._inShellLayout) {
this._shellManager.layout(width, height);
}
else {
super.layout(width, height, forceResize);
}
this._syncFloatingOverlayHost();
if (this._floatingGroups) {
for (const floating of this._floatingGroups) {
// ensure floting groups stay within visible boundaries
floating.overlay.setBounds();
}
}
}
_syncFloatingOverlayHost() {
if (!this._floatingOverlayHost || !this._shellManager) {
return;
}
const shellRect = this._shellManager.element.getBoundingClientRect();
const gridRect = this.element.getBoundingClientRect();
const host = this._floatingOverlayHost;
host.style.left = `${gridRect.left - shellRect.left}px`;
host.style.top = `${gridRect.top - shellRect.top}px`;
host.style.width = `${gridRect.width}px`;
host.style.height = `${gridRect.height}px`;
}
_layoutFromShell(width, height) {
this._inShellLayout = true;
this.layout(width, height, true);
this._inShellLayout = false;
}
forceRelayout() {
if (this._shellManager) {
this._layoutFromShell(this.width, this.height);
}
else {
super.forceRelayout();
}
}
addEdgeGroup(position, options) {
if (this._edgeGroups.has(position)) {
throw new Error(`dockview: edge group already exists at position '${position}'`);
}
const group = this.createGroup({ id: options.id });
group.model.location = { type: 'edge', position };
group.model.headerPosition = position;
this._edgeGroups.set(position, group);
this._onDidAddGroup.fire(group);
// Collapse when the group becomes empty
const autoCollapseDisposable = group.model.onDidRemovePanel(() => {
if (group.model.isEmpty) {
this.setEdgeGroupCollapsed(group, true);
}
});
this._edgeGroupDisposables.set(position, autoCollapseDisposable);
this._shellManager.addEdgeView(position, options, group);
return group.api;
}
getEdgeGroup(position) {
var _a;
return (_a = this._edgeGroups.get(position)) === null || _a === void 0 ? void 0 : _a.api;
}
setEdgeGroupVisible(position, visible) {
this._shellManager.setEdgeGroupVisible(position, visible);
}
isEdgeGroupVisible(position) {
return this._shellManager.isEdgeGroupVisible(position);
}
removeEdgeGroup(position) {
var _a;
const group = this._edgeGroups.get(position);
if (!group) {
throw new Error(`dockview: no edge group exists at position '${position}'`);
}
// Remove panels inside the group first
for (const panel of [...group.panels]) {
this.removePanel(panel, {
removeEmptyGroup: false,
skipDispose: false,
});
}
// Dispose the auto-collapse listener
(_a = this._edgeGroupDisposables.get(position)) === null || _a === void 0 ? void 0 : _a.dispose();
this._edgeGroupDisposables.delete(position);
// Remove from the shell splitview
this._shellManager.removeEdgeView(position);
// Clean up group state
this._edgeGroups.delete(position);
group.dispose();
this._groups.delete(group.id);
this._onDidRemoveGroup.fire(group);
}
setEdgeGroupCollapsed(group, collapsed) {
for (const [position, edgeGroup] of this._edgeGroups) {
if (edgeGroup === group) {
if (this._shellManager.isEdgeGroupCollapsed(position) ===
collapsed) {
// Skip the splitview resize on a no-op: with non-zero
// theme gap, redundan