dockview-core
Version:
Zero dependency layout manager supporting tabs, groups, grids and splitviews for vanilla TypeScript
993 lines • 155 kB
JavaScript
import { getRelativeLocation, getGridLocation, orthogonal, Gridview, } from '../gridview/gridview';
import { directionToPosition, } from '../dnd/droptarget';
import { tail, sequenceEquals } 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, Sizing } 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 { Classnames, getDockviewTheme, onDidWindowResizeEnd, onDidWindowMoveEnd, toggleClass, } from '../dom';
import { FloatingTitleBar } from './components/titlebar/floatingTitleBar';
import { assertModule, getRegisteredModules, isDockviewPackageLoaded, ModuleRegistry, } from './modules';
import { AllModules } from './allModules';
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 { DropTargetAnchorContainer } from '../dnd/dropTargetAnchorContainer';
import { themeAbyss } from './theme';
import { ShellManager, } from './dockviewShell';
import { DEFAULT_TAB_GROUP_COLORS, TabGroupColorPalette, } from './tabGroupAccent';
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,
});
});
}
let _hasWarnedUsingCoreDirectly = false;
/**
* `dockview-core` is an internal package. The public `dockview` package calls
* `markDockviewPackageLoaded()` on import; if that marker is absent the consumer
* is using `dockview-core` directly, so emit a one-time console warning
* steering them to `dockview`.
*
* Suppressed in production builds: it is a development-time nudge and most
* bundlers inline `process.env.NODE_ENV` so the branch is dropped entirely. The
* `typeof process` guard keeps this safe in plain browser/UMD contexts where
* `process` is undefined.
*/
function warnIfUsingCoreDirectly() {
if (typeof process !== 'undefined' &&
process.env &&
process.env.NODE_ENV === 'production') {
return;
}
if (_hasWarnedUsingCoreDirectly || isDockviewPackageLoaded()) {
return;
}
_hasWarnedUsingCoreDirectly = true;
console.warn('dockview: do not use "dockview-core" directly — it is an internal ' +
'package. Use the "dockview" package, the JavaScript version of ' +
'dockview, instead. This notice is shown once.');
}
export class DockviewComponent extends BaseGrid {
fireDidCreateTabGroup(event) {
this._onDidCreateTabGroup.fire(event);
}
fireDidDestroyTabGroup(event) {
this._onDidDestroyTabGroup.fire(event);
}
fireDidAddPanelToTabGroup(event) {
this._onDidAddPanelToTabGroup.fire(event);
}
fireDidRemovePanelFromTabGroup(event) {
this._onDidRemovePanelFromTabGroup.fire(event);
}
fireDidTabGroupChange(event) {
this._onDidTabGroupChange.fire(event);
}
fireDidTabGroupCollapsedChange(event) {
this._onDidTabGroupCollapsedChange.fire(event);
}
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() {
var _a, _b, _c;
return ((_c = (_b = (_a = this._moduleRegistry) === null || _a === void 0 ? void 0 : _a.services.floatingGroupService) === null || _b === void 0 ? void 0 : _b.floatingGroups) !== null && _c !== void 0 ? _c : []);
}
/**
* Boxes of the floating groups other than `exclude`, in coordinates
* relative to the floating overlay container. Supplied to a
* `transformFloatingGroupDrag` callback as `context.others` so it can
* align the dragged float against its siblings.
*/
_gatherFloatingGroupBoxes(exclude) {
var _a;
const container = (_a = this._floatingOverlayHost) !== null && _a !== void 0 ? _a : this.gridview.element;
const containerRect = container.getBoundingClientRect();
return this.floatingGroups
.filter((floating) => floating.group !== exclude)
.map((floating) => {
const rect = floating.overlay.element.getBoundingClientRect();
return {
left: rect.left - containerRect.left,
top: rect.top - containerRect.top,
width: rect.width,
height: rect.height,
};
});
}
get _floatingGroupService() {
return this._moduleRegistry.services.floatingGroupService;
}
get _popoutWindowService() {
return this._moduleRegistry.services.popoutWindowService;
}
get _watermarkService() {
// Tier 1 module — optional. Callers must `?.`-guard so the module
// can be removed from AllModules without crashing the component.
return this._moduleRegistry.services.watermarkService;
}
get _edgeGroupService() {
return this._moduleRegistry.services.edgeGroupService;
}
get _rootDropTargetService() {
// Optional like every other module service — RootDropTargetModule can be
// removed from the registered set without crashing the component.
return this._moduleRegistry.services.rootDropTargetService;
}
get _advancedDnDService() {
// Optional — callers `?.`-guard so the module can be removed from
// AllModules. Absent ⇒ the onWill* hooks simply don't fire (≡ no
// subscriber), which is invisible to apps not customising DnD.
return this._moduleRegistry.services.advancedDnDService;
}
get headerActionsService() {
return this._moduleRegistry.services.headerActionsService;
}
isGridEmpty() {
return this.gridview.length === 0;
}
rootDropTargetOverrideTarget() {
var _a;
return (_a = this.rootDropTargetContainer) === null || _a === void 0 ? void 0 : _a.model;
}
dispatchUnhandledDragOver(nativeEvent, position) {
const event = new DockviewUnhandledDragOverEvent(nativeEvent, 'edge', position, getPanelData);
this._onUnhandledDragOver.fire(event);
return event.isAccepted;
}
// IAdvancedDnDHost — the emitters stay here so the public onWill* event
// shape is unchanged; AdvancedDnDService routes the per-group fires
// through these. Engine guards (e.g. disableDnd) run on the component
// ahead of the dispatch.
fireWillDragPanel(event) {
this._onWillDragPanel.fire(event);
}
fireWillDragGroup(event) {
this._onWillDragGroup.fire(event);
}
fireWillDrop(event) {
this._onWillDrop.fire(event);
}
fireWillShowOverlay(event) {
this._onWillShowOverlay.fire(event);
}
/**
* Resolve the custom group drag ghost (via the AdvancedDnD module), or
* `undefined` to fall back to the default chip. Returns `undefined` when
* the module is absent — the default ghost then renders.
*/
buildGroupDragGhost(group) {
var _a;
return (_a = this._advancedDnDService) === null || _a === void 0 ? void 0 : _a.buildGroupDragGhost(group);
}
/**
* Resolve the app-supplied drop overlay model (via the AdvancedDnD module)
* for a group drop target, or `undefined` to keep the target's default.
*/
resolveDropOverlayModel(location, group) {
var _a;
return (_a = this._advancedDnDService) === null || _a === void 0 ? void 0 : _a.resolveOverlayModel(location, group);
}
// IAccessibilityHost — keyboard docking reaches the AdvancedDnD preview +
// LiveRegion announcer through these so the service stays decoupled.
/** Outermost element — the shell (incl. edge groups) once built, else the gridview. */
get rootElement() {
var _a, _b;
return (_b = (_a = this._shellManager) === null || _a === void 0 ? void 0 : _a.element) !== null && _b !== void 0 ? _b : this.element;
}
/**
* The next / previous group in gridview (spatial) order, wrapping round.
* The keyboard accessibility module's focus navigation is built on this
* primitive — the only piece that needs the grid internals; the rest of
* the navigation logic lives in the AccessibilityService.
*/
adjacentGroup(group, reverse) {
var _a;
// gridview traversal only covers grid groups; a floating/popout group
// isn't in the grid, so there's no adjacent grid group to step to.
if (group.api.location.type !== 'grid') {
return undefined;
}
const location = getGridLocation(group.element);
return ((_a = (reverse
? this.gridview.previous(location)
: this.gridview.next(location))) === null || _a === void 0 ? void 0 : _a.view);
}
/**
* The nearest grid group in a spatial direction from `group`, by
* comparing group centre points. Floating and popout groups sit outside
* the grid's geometry and are ignored. Returns `undefined` when there is
* no group in that direction.
*/
adjacentGroupInDirection(group, direction) {
if (group.api.location.type !== 'grid') {
return undefined;
}
const from = group.element.getBoundingClientRect();
const fromX = from.left + from.width / 2;
const fromY = from.top + from.height / 2;
let best;
let bestDistance = Number.POSITIVE_INFINITY;
for (const candidate of this.groups) {
if (candidate === group || candidate.api.location.type !== 'grid') {
continue;
}
const rect = candidate.element.getBoundingClientRect();
const dx = rect.left + rect.width / 2 - fromX;
const dy = rect.top + rect.height / 2 - fromY;
// Require the candidate to sit predominantly in the asked-for
// direction (dominant axis), so 'left' ignores a group that's
// mostly above/below.
const inDirection = direction === 'left'
? dx < 0 && Math.abs(dx) >= Math.abs(dy)
: direction === 'right'
? dx > 0 && Math.abs(dx) >= Math.abs(dy)
: direction === 'up'
? dy < 0 && Math.abs(dy) >= Math.abs(dx)
: dy > 0 && Math.abs(dy) >= Math.abs(dx);
if (!inDirection) {
continue;
}
const distance = dx * dx + dy * dy;
if (distance < bestDistance) {
bestDistance = distance;
best = candidate;
}
}
return best;
}
showDropPreview(group, position) {
var _a, _b;
return ((_b = (_a = this._advancedDnDService) === null || _a === void 0 ? void 0 : _a.showPreviewOverlay(group, position)) !== null && _b !== void 0 ? _b : Disposable.NONE);
}
announce(message) {
var _a;
(_a = this._moduleRegistry.services.liveRegionService) === null || _a === void 0 ? void 0 : _a.announce(message);
}
dockPanel(panel, group, position) {
this.moveGroupOrPanel({
from: { groupId: panel.group.id, panelId: panel.id },
to: { group, position },
});
}
get contextMenuService() {
// Owned by ContextMenuModule — undefined when the module is not
// registered, so callers must `?.`-guard.
return this._moduleRegistry.services.contextMenuService;
}
get mountElement() {
return this.gridview.element;
}
hasVisibleGridGroup() {
return this.groups.some((group) => group.api.location.type === 'grid' && group.api.isVisible);
}
fireLayoutChange() {
this._bufferOnDidLayoutChange.fire();
}
/**
* 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() {
var _a, _b;
return ((_b = (_a = this._popoutWindowService) === null || _a === void 0 ? void 0 : _a.restorationPromise) !== null && _b !== void 0 ? _b : Promise.resolve());
}
constructor(container, options) {
var _a, _b, _c, _d, _e;
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._moduleRegistry = new ModuleRegistry();
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;
// Transaction boundary bracketing each top-level structural mutation.
// Compound operations (e.g. a drag that relocates a panel) nest via the
// depth counter and bracket as a single transaction. See `mutation()`.
this._mutationDepth = 0;
// Current operation origin. Defaults to `'user'`; the DockviewApi boundary
// flips it to `'api'` for the duration of a programmatic call via
// `withOrigin`. Nested operations inherit the outermost origin (tracked by
// `_originDepth`, independent of mutation bracketing so it also covers
// active-panel changes that are not structural mutations).
this._origin = 'user';
this._originDepth = 0;
this._onWillMutateLayout = new Emitter();
this.onWillMutateLayout = this._onWillMutateLayout.event;
this._onDidMutateLayout = new Emitter();
this.onDidMutateLayout = this._onDidMutateLayout.event;
this._onWillShowOverlay = new Emitter();
this.onWillShowOverlay = this._onWillShowOverlay.event;
this._onUnhandledDragOver = new Emitter();
this.onUnhandledDragOver = this._onUnhandledDragOver.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._onDidAddPopoutGroup = new Emitter();
this.onDidAddPopoutGroup = this._onDidAddPopoutGroup.event;
this._onDidRemovePopoutGroup = new Emitter();
this.onDidRemovePopoutGroup = this._onDidRemovePopoutGroup.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._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);
// Internal seam: defaults to the full set, but tests can supply a
// subset to assert every module is independently removable (and the
// opt-in module API will build on this later). Not part of the public
// options surface.
const explicitModules = options
.modules;
const modules = explicitModules !== null && explicitModules !== void 0 ? explicitModules : [
...AllModules,
...getRegisteredModules(),
];
for (const module of modules) {
this._moduleRegistry.register(module);
}
this._moduleRegistry.initialize(this);
// Surface popout removal symmetrically with `onDidAddPopoutGroup`. The
// service is the single point every removal path funnels through — a
// genuine window close and an explicit removal alike — and it suppresses
// the event during component teardown.
const popoutWindowService = this._popoutWindowService;
if (popoutWindowService) {
this.addDisposables(popoutWindowService.onDidRemove((entry) => {
this._onDidRemovePopoutGroup.fire({
id: entry.popoutGroup.id,
group: entry.popoutGroup,
window: entry.getWindow(),
});
}));
}
// Purely a developer warning (no functional effect): nudge anyone using
// the internal `dockview-core` package directly towards `dockview`.
warnIfUsingCoreDirectly();
this.popupService = new PopupService(this.element);
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);
toggleClass(this.gridview.element, 'dv-dockview', true);
toggleClass(this.element, 'dv-debug', !!options.debug);
this.updateTheme();
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._onWillMutateLayout, this._onDidMutateLayout, 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._onUnhandledDragOver, this._onDidMaximizedGroupChange, this._onDidPopoutGroupSizeChange, this._onDidPopoutGroupPositionChange, this._onDidAddPopoutGroup, this._onDidRemovePopoutGroup, this._onDidOpenPopoutWindowFail, this._onDidCreateTabGroup, this._onDidDestroyTabGroup, this._onDidAddPanelToTabGroup, this._onDidRemovePanelFromTabGroup, this._onDidTabGroupChange, this._onDidTabGroupCollapsedChange, Event.any(this.onDidPopoutGroupSizeChange, this.onDidPopoutGroupPositionChange, this.onDidCreateTabGroup, this.onDidDestroyTabGroup, this.onDidAddPanelToTabGroup, this.onDidRemovePanelFromTabGroup, this.onDidTabGroupChange, this.onDidTabGroupCollapsedChange)(() => {
// Popout size/position and tab-group mutations persist as layout changes.
this.fireLayoutChange();
}), this._onDidOptionsChange, 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.onDidAddPanel, this.onDidRemovePanel, this.onDidAddGroup, this.onDidRemove, this.onDidRemoveGroup, this.onDidMovePanel, this.onDidActivePanelChange)(() => {
this._bufferOnDidLayoutChange.fire();
}), Disposable.from(() => {
var _a;
// Registry disposes init() subscriptions then every module
// service that implements IDisposable. The order matters so
// init handlers stop firing into services about to be torn
// down. Includes popout-restoration timer cancellation that
// resolves promises so awaiters of popoutRestorationPromise
// don't hang. See issue #851.
this._moduleRegistry.dispose();
(_a = this._shellManager) === null || _a === void 0 ? void 0 : _a.dispose();
}));
// Root edge-drop wiring lives with its (optional) module — guard it so
// the module is independently removable.
const rootDropTarget = this._rootDropTargetService;
if (rootDropTarget) {
this.addDisposables(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 DockviewWillShowOverlayLocationEvent(event, {
kind: 'edge',
panel: undefined,
api: this._api,
group: undefined,
getData: getPanelData,
}));
}), 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,
}));
}
}));
}
// Final module wiring: runs after the host is fully constructed.
// Modules subscribe to host events here so the component doesn't
// need to manually invoke them at scattered call sites.
this._moduleRegistry.postConstruct(this);
}
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, _b;
return ((_b = (_a = this._popoutWindowService) === null || _a === void 0 ? void 0 : _a.getPopupService(group.id)) !== null && _b !== void 0 ? _b : this.popupService);
}
addPopoutGroup(itemToPopout, options) {
// The transaction brackets the synchronous structural change; the
// popout window opens asynchronously after it resolves.
return this.mutation('popout', () => this._doAddPopoutGroup(itemToPopout, options));
}
/** Enumerate the popout groups currently open in their own windows. */
getPopouts() {
var _a, _b;
return ((_b = (_a = this._popoutWindowService) === null || _a === void 0 ? void 0 : _a.entries.map((entry) => ({
id: entry.popoutGroup.id,
group: entry.popoutGroup,
window: entry.getWindow(),
}))) !== null && _b !== void 0 ? _b : []);
}
_doAddPopoutGroup(itemToPopout, options) {
var _a, _b, _c, _d, _e;
const service = assertModule(this._popoutWindowService, 'PopoutWindow', 'api.addPopoutGroup');
if (!service) {
return Promise.resolve(false);
}
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();
// Resolve the configured popout URL once (per-call override → global
// option). Recorded on the entry / group locations so it survives
// serialization; the '/popout.html' default is applied only when
// actually opening the window, not baked into saved layouts.
const resolvedPopoutUrl = (_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;
const _window = new PopoutWindow(`${this.id}-${groupId}`, // unique id
theme !== null && theme !== void 0 ? theme : '', {
url: resolvedPopoutUrl !== null && resolvedPopoutUrl !== void 0 ? resolvedPopoutUrl : '/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: (_e = this.options) === null || _e === void 0 ? void 0 : _e.nonce,
});
const popoutWindowDisposable = new CompositeDisposable(_window, _window.onDidClose(() => {
popoutWindowDisposable.dispose();
}));
return _window
.open()
.then((popoutContainer) => {
var _a, _b, _c;
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 (options === null || options === void 0 ? void 0 : options.overridePopoutGridview) {
// Restoring a multi-group window: the anchor group is
// already built inside the supplied gridview.
group = (_a = options.overridePopoutGroup) !== null && _a !== void 0 ? _a : referenceGroup;
}
else 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) {
this.handleBlockedPopout({
group,
referenceGroup,
options,
popoutWindowDisposable,
});
return false;
}
const gready = document.createElement('div');
gready.className = 'dv-overlay-render-container';
const overlayRenderContainer = new OverlayRenderContainer(gready, this);
group.model.renderContainer = overlayRenderContainer;
// The popout window hosts its own gridview so it can grow into
// a nested splitview layout. The window starts with the single
// anchor group; further groups arrive via drag-and-drop. On
// restore a pre-populated gridview is supplied instead.
const popoutGridview = (_b = options === null || options === void 0 ? void 0 : options.overridePopoutGridview) !== null && _b !== void 0 ? _b : this.createNestedGridview();
if (!(options === null || options === void 0 ? void 0 : options.overridePopoutGridview)) {
popoutGridview.addView(group, Sizing.Distribute, [0]);
}
// Fill the popout window. Unlike the main grid (explicit px) and
// floating windows (CSS inside .dv-resize-container), the popout
// gridview has no sizing context, so without this it collapses
// to 0 height and nothing renders.
popoutGridview.element.style.width = '100%';
popoutGridview.element.style.height = '100%';
popoutGridview.layout(_window.window.innerWidth, _window.window.innerHeight);
// Guarded so the teardown's re-entrant paths (window close
// re-enters via the anchor's doRemoveGroup) never double-dispose.
let popoutGridviewDisposed = false;
const disposePopoutGridview = () => {
if (!popoutGridviewDisposed) {
popoutGridviewDisposed = true;
popoutGridview.dispose();
}
};
let floatingBox;
if (!(options === null || options === void 0 ? void 0 : options.overridePopoutGroup) &&
!(options === null || options === void 0 ? void 0 : options.overridePopoutGridview) &&
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 = (_c = this.floatingGroups
.find((value) => value.group.api.id ===
itemToPopout.api.id)) === null || _c === void 0 ? void 0 : _c.overlay.toJSON();
this.removeGroup(referenceGroup);
break;
}
}
}
popoutContainer.classList.add('dv-dockview');
popoutContainer.style.overflow = 'hidden';
popoutContainer.appendChild(gready);
popoutContainer.appendChild(popoutGridview.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);
service.setPopupService(group.id, popoutPopupService);
popoutWindowDisposable.addDisposables(popoutPopupService, Disposable.from(() => {
service.deletePopupService(group.id);
}));
group.model.location = {
type: 'popout',
getWindow: () => _window.window,
popoutUrl: resolvedPopoutUrl,
};
if (options === null || options === void 0 ? void 0 : options.overridePopoutGridview) {
// Restored multi-group window. Wire every member (including
// the anchor) to this window's containers and popout
// location now that the gridview is attached and laid out —
// re-setting renderContainer forces a re-render at the right
// time so 'always'-rendered content positions in this
// window rather than where it was first created.
const members = this.groups.filter((candidate) => popoutGridview.element.contains(candidate.element));
for (const member of members) {
member.model.renderContainer = overlayRenderContainer;
member.model.dropTargetContainer = dropTargetContainer;
member.model.location = {
type: 'popout',
getWindow: () => _window.window,
popoutUrl: resolvedPopoutUrl,
};
}
}
if (isGroupAddedToDom &&
itemToPopout.api.location.type === 'grid') {
itemToPopout.api.setVisible(false);
}
this.doSetGroupAndPanelActive(group);
const resizeObserverDisposable = service.observeGridviewSize(_window, popoutGridview, overlayRenderContainer);
if (resizeObserverDisposable) {
popoutWindowDisposable.addDisposables(resizeObserverDisposable);
}
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();
}));
// Holder so the close teardown (extracted below) can publish
// the group that was returned to the main grid back to the
// entry's `dispose()` contract.
const closeResult = {};
const isValidReferenceGroup = isGroupAddedToDom &&
referenceGroup &&
this.getPanel(referenceGroup.id);
const value = {
window: _window,
popoutGroup: group,
gridview: popoutGridview,
overlayRenderContainer,
dropTargetContainer,
getWindow: () => _window.window,
popoutUrl: resolvedPopoutUrl,
referenceGroup: isValidReferenceGroup
? referenceGroup.id
: undefined,
disposable: {
dispose: () => {
popoutWindowDisposable.dispose();
return closeResult.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,
});
}), addDisposableListener(_window.window, 'resize', () => {
popoutGridview.layout(_window.window.innerWidth, _window.window.innerHeight);
}), overlayRenderContainer, Disposable.from(() => this.disposePopoutWindow({
group,
referenceGroup,
popoutGridview,
isGroupAddedToDom,
floatingBox,
disposePopoutGridview,
closeResult,
})));
service.add(value);
this._onDidAddPopoutGroup.fire({
id: value.popoutGroup.id,
group: value.popoutGroup,
window: value.getWindow(),
});
return true;
})
.catch((err) => {
console.error('dockview: failed to create popout.', err);
return false;
});
}
/**
* The popout window was blocked (e.g. by the browser's popup blocker —
* common when restoring popouts on load). Fall back gracefully so the
* group(s) end up valid and visible in the main grid rather than as
* orphans that later crash clear()/remove().
*/
handleBlockedPopout(params) {
const { group, referenceGroup, options, popoutWindowDisposable } = params;
console.error('dockview: failed to create popout. perhaps you need to allow pop-ups for this website');
popoutWindowDisposable.dispose();
this._onDidOpenPopoutWindowFail.fire();
if (options === null || options === void 0 ? void 0 : options.overridePopoutGridview) {
// Restoring a multi-group popout window: its nested gridview was
// built up-front but never attached to a window. Dock every member
// into the main grid so no group is lost, then discard the
// detached gridview.
const blockedGridview = options.overridePopoutGridview;
const members = this.groups.filter((candidate) => blockedGridview.element.contains(candidate.element));
for (const member of members) {
this.movingLock(() => {
blockedGridview.remove(member);
this.redockGroupToMainGrid(member);
});
}
blockedGridview.dispose();
if (referenceGroup && !referenceGroup.api.isVisible) {
referenceGroup.api.setVisible(true);
}
return;
}
if (group === referenceGroup) {
// No separate grid group to return to (e.g. restoring a popout
// straight from JSON) — dock this group into the main grid.
if (!this.gridview.element.contains(group.element)) {
this.movingLock(() => this.doAddGroup(group, [0]));
group.model.location = { type: 'grid' };
}
}
else {
// A fresh group was created for the popout — return its panels to
// the reference group and discard the now-empty popout group so it
// doesn't linger as an orphan.
this.movingLock(() => moveGroupWithoutDestroying({
from: group,
to: referenceGroup,
}));
if (group.model.size === 0 && this._groups.has(group.id)) {
group.dispose();
this._groups.delete(group.id);
this._onDidRemoveGroup.fire(group);
}
}
if (!referenceGroup.api.isVisible) {
referenceGroup.api.setVisible(true);
}
}
/**
* Wire a group that has been displaced from a floating / popout window back
* to the main grid's render & drop-target containers and dock it at the
* root. The caller is responsible for first detaching it from its old
* gridview — the detach strategy differs between the window-teardown path
* (`doRemoveGroup`) and the blocked-window path (`gridview.remove`).
*/
redockGroupToMainGrid(group) {
group.model.renderContainer = this.overlayRenderContainer;
group.model.dropTargetContainer = this.rootDropTargetContainer;
group.model.location = { type: 'grid' };
this.doAddGroup(group, [0]);
}
/**
* Teardown for a popout window's `popoutWindowDisposable`. Runs when the
* window closes (by user, by `removeGroup`, or by component disposal):
* relocates every member group back to the main grid (or to a floating
* window when the anchor came from one), then disposes the nested gridview.
* `closeResult.returnedGroup` is read by the entry's `dispose()` contract.
*/
disposePopoutWindow(params) {
var _a;
const { group, referenceGroup, popoutGridview, isGroupAddedToDom, floatingBox, disposePopoutGridview, closeResult, } = params;
if (this.isDisposed) {
// cleanup may run after instance is disposed; just tear down the
// nested gridview.
disposePopoutGridview();
return;
}
// Distinguish a genuine window close from an explicit `removeGroup`:
// the explicit-removal paths remove the service entry *before* this
// disposable runs. Key off the stable `popoutGridview` rather than the
// captured `group`, which may no longer be the window's anchor (it can
// have been dr