UNPKG

dockview-core

Version:

Zero dependency layout manager supporting tabs, groups, grids and splitviews for vanilla TypeScript

687 lines (686 loc) 30.3 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ShellManager = exports.EdgeGroupView = void 0; var events_1 = require("../events"); var lifecycle_1 = require("../lifecycle"); var splitview_1 = require("../splitview/splitview"); var dom_1 = require("../dom"); var EdgeGroupView = /** @class */ (function () { function EdgeGroupView(options, group, orientation) { var _a, _b, _c; this._onDidChange = new events_1.Emitter(); this.onDidChange = this._onDidChange.event; this.snap = false; this.priority = splitview_1.LayoutPriority.Low; this._isCollapsed = false; this._group = group; this._orientation = orientation; group.element.classList.add('dv-edge-group'); group.element.dataset.testid = "dv-edge-group-".concat(options.id); this._collapsedSize = (_a = options.collapsedSize) !== null && _a !== void 0 ? _a : 35; this._expandedMaximumSize = (_b = options.maximumSize) !== null && _b !== void 0 ? _b : Number.POSITIVE_INFINITY; // If the caller explicitly provides a minimumSize, respect it. // Otherwise fall back to collapsedSize + 50 so the expanded state is // visually distinguishable from the collapsed state. this._expandedMinimumSize = options.minimumSize !== undefined ? options.minimumSize : this._collapsedSize + 50; this._lastExpandedSize = (_c = options.initialSize) !== null && _c !== void 0 ? _c : 200; if (options.collapsed) { this._isCollapsed = true; group.element.classList.add('dv-edge-collapsed'); } } Object.defineProperty(EdgeGroupView.prototype, "minimumSize", { get: function () { // When collapsed, lock size to collapsedSize so sash can't drag it open return this._isCollapsed ? this._collapsedSize : this._expandedMinimumSize; }, enumerable: false, configurable: true }); Object.defineProperty(EdgeGroupView.prototype, "maximumSize", { get: function () { // When collapsed, lock size to collapsedSize so sash can't drag it open return this._isCollapsed ? this._collapsedSize : this._expandedMaximumSize; }, enumerable: false, configurable: true }); Object.defineProperty(EdgeGroupView.prototype, "element", { get: function () { return this._group.element; }, enumerable: false, configurable: true }); Object.defineProperty(EdgeGroupView.prototype, "isCollapsed", { get: function () { return this._isCollapsed; }, enumerable: false, configurable: true }); Object.defineProperty(EdgeGroupView.prototype, "lastExpandedSize", { get: function () { return this._lastExpandedSize; }, enumerable: false, configurable: true }); Object.defineProperty(EdgeGroupView.prototype, "collapsedSize", { get: function () { return this._collapsedSize; }, enumerable: false, configurable: true }); EdgeGroupView.prototype.layout = function (size, orthogonalSize) { // Track the last expanded size so we can restore it after collapsing if (!this._isCollapsed) { this._lastExpandedSize = size; } // horizontal (left/right): size=width, orthogonalSize=height → layout(width, height) // vertical (top/bottom): size=height, orthogonalSize=width → layout(width, height) if (this._orientation === 'horizontal') { this._group.layout(size, orthogonalSize); } else { this._group.layout(orthogonalSize, size); } }; EdgeGroupView.prototype.setCollapsed = function (collapsed) { if (this._isCollapsed === collapsed) { return; } this._isCollapsed = collapsed; this._group.element.classList.toggle('dv-edge-collapsed', collapsed); // ShellManager calls resizeView directly after this; no _onDidChange needed }; EdgeGroupView.prototype.setVisible = function (_visible) { // visibility is managed by the parent splitview }; /** * Restore the last-expanded size from serialized state without triggering * a layout. Must be called before setCollapsed(true) during fromJSON so * that expanding after deserialization restores the correct size. */ EdgeGroupView.prototype.restoreExpandedSize = function (size) { this._lastExpandedSize = size; }; /** * Apply new effective collapsed and expanded-minimum sizes after a theme * or gap change. The caller (ShellManager) is responsible for computing * the correct values from the original config and the new gap. */ EdgeGroupView.prototype.updateCollapsedSize = function (newCollapsedSize, newExpandedMinimumSize) { this._collapsedSize = newCollapsedSize; this._expandedMinimumSize = newExpandedMinimumSize; }; EdgeGroupView.prototype.dispose = function () { this._onDidChange.dispose(); }; return EdgeGroupView; }()); exports.EdgeGroupView = EdgeGroupView; var CenterView = /** @class */ (function () { function CenterView(_dockviewElement, _layoutDockview) { this._dockviewElement = _dockviewElement; this._layoutDockview = _layoutDockview; this.priority = splitview_1.LayoutPriority.High; this.minimumSize = 100; this.maximumSize = Number.POSITIVE_INFINITY; this._onDidChange = new events_1.Emitter(); this.onDidChange = this._onDidChange.event; } Object.defineProperty(CenterView.prototype, "element", { get: function () { return this._dockviewElement; }, enumerable: false, configurable: true }); CenterView.prototype.layout = function (size, orthogonalSize) { // Lives in a VERTICAL middle-column splitview: // size = height alloc, orthogonalSize = width this._layoutDockview(orthogonalSize, size); }; CenterView.prototype.setVisible = function (_visible) { // center is always visible }; CenterView.prototype.dispose = function () { this._onDidChange.dispose(); }; return CenterView; }()); /** * The vertical centre column: top (optional) | center | bottom (optional). * This view sits between the left and right edge panels in the outer * horizontal splitview, so its primary axis is width (horizontal). */ var MiddleColumnView = /** @class */ (function () { function MiddleColumnView(centerView, gap) { if (gap === void 0) { gap = 0; } this._onDidChange = new events_1.Emitter(); this.onDidChange = this._onDidChange.event; this.minimumSize = 100; this.maximumSize = Number.POSITIVE_INFINITY; this.priority = splitview_1.LayoutPriority.High; this._element = document.createElement('div'); this._element.className = 'dv-shell-middle-column'; this._element.style.height = '100%'; this._element.style.width = '100%'; this._splitview = new splitview_1.Splitview(this._element, { orientation: splitview_1.Orientation.VERTICAL, proportionalLayout: false, margin: gap, }); this._centerIndex = 0; this._splitview.addView(centerView, { type: 'distribute' }, 0); } Object.defineProperty(MiddleColumnView.prototype, "element", { get: function () { return this._element; }, enumerable: false, configurable: true }); MiddleColumnView.prototype.addTopView = function (view, initialSize) { // Insert before center this._splitview.addView(view, initialSize, 0); this._topIndex = 0; this._centerIndex += 1; if (this._bottomIndex !== undefined) { this._bottomIndex += 1; } }; MiddleColumnView.prototype.addBottomView = function (view, initialSize) { // Append after center (and any existing bottom — shouldn't happen but safe) var newIndex = this._splitview.length; this._splitview.addView(view, initialSize, newIndex); this._bottomIndex = newIndex; }; MiddleColumnView.prototype.removeView = function (position) { var index = position === 'top' ? this._topIndex : this._bottomIndex; if (index === undefined) { return; } this._splitview.removeView(index); if (position === 'top') { this._topIndex = undefined; // center (and bottom if present) shift down by one this._centerIndex -= 1; if (this._bottomIndex !== undefined) { this._bottomIndex -= 1; } } else { this._bottomIndex = undefined; // center and top are unaffected } }; MiddleColumnView.prototype.layout = function (size, orthogonalSize) { // Outer horizontal splitview: size = width, orthogonalSize = height // Inner vertical splitview: layout(height, width) this._splitview.layout(orthogonalSize, size); }; MiddleColumnView.prototype.setVisible = function (_visible) { // middle column is always visible }; MiddleColumnView.prototype.setViewVisible = function (position, visible) { var index = position === 'top' ? this._topIndex : this._bottomIndex; if (index !== undefined) { this._splitview.setViewVisible(index, visible); } }; MiddleColumnView.prototype.isViewVisible = function (position) { var index = position === 'top' ? this._topIndex : this._bottomIndex; if (index !== undefined) { return this._splitview.isViewVisible(index); } return false; }; MiddleColumnView.prototype.getViewSize = function (position) { var index = position === 'top' ? this._topIndex : this._bottomIndex; if (index !== undefined) { return this._splitview.getViewSize(index); } return 0; }; MiddleColumnView.prototype.resizeView = function (position, size) { var index = position === 'top' ? this._topIndex : this._bottomIndex; if (index !== undefined) { this._splitview.resizeView(index, size); } }; MiddleColumnView.prototype.updateMargin = function (gap) { this._splitview.margin = gap; }; MiddleColumnView.prototype.dispose = function () { this._onDidChange.dispose(); this._splitview.dispose(); }; return MiddleColumnView; }()); function adjustedOpts(base, defaultCollapsed, gapAdd) { var _a; var effectiveCollapsed = ((_a = base.collapsedSize) !== null && _a !== void 0 ? _a : defaultCollapsed) + gapAdd; var result = __assign(__assign({}, base), { collapsedSize: effectiveCollapsed }); if (base.minimumSize !== undefined) { result.minimumSize = base.minimumSize + gapAdd; } return result; } var ShellManager = /** @class */ (function () { function ShellManager(container, dockviewElement, layoutGrid, gap, defaultCollapsedSize) { if (gap === void 0) { gap = 0; } if (defaultCollapsedSize === void 0) { defaultCollapsedSize = 35; } var _this = this; this._disposables = new lifecycle_1.CompositeDisposable(); // Retained for updateTheme() recalculations. this._viewConfigs = new Map(); this._currentWidth = 0; this._currentHeight = 0; this._gap = gap; this._defaultCollapsedSize = defaultCollapsedSize; this._shellElement = document.createElement('div'); this._shellElement.className = 'dv-shell'; this._shellElement.style.height = '100%'; this._shellElement.style.width = '100%'; this._shellElement.style.position = 'relative'; container.appendChild(this._shellElement); var centerView = new CenterView(dockviewElement, layoutGrid); this._middleColumn = new MiddleColumnView(centerView, gap); this._outerSplitview = new splitview_1.Splitview(this._shellElement, { orientation: splitview_1.Orientation.HORIZONTAL, proportionalLayout: false, margin: gap, }); this._middleIndex = 0; this._outerSplitview.addView(this._middleColumn, { type: 'distribute' }, 0); this._disposables.addDisposables((0, dom_1.watchElementResize)(this._shellElement, function (entry) { var width = Math.round(entry.contentRect.width); var height = Math.round(entry.contentRect.height); if (width === _this._currentWidth && height === _this._currentHeight) { return; } _this._currentWidth = width; _this._currentHeight = height; _this.layout(width, height); }), this._outerSplitview, this._middleColumn, centerView); } Object.defineProperty(ShellManager.prototype, "element", { get: function () { return this._shellElement; }, enumerable: false, configurable: true }); /** * Add an edge group view at the given position. The view wraps the * provided group element inside the shell's splitview layout. * Throws if a group at this position is already registered. */ ShellManager.prototype.addEdgeView = function (position, options, group) { if (this.hasEdgeGroup(position)) { throw new Error("dockview: edge group already registered at position '".concat(position, "'")); } this._viewConfigs.set(position, options); // Recompute gap adjustments now that _viewConfigs has grown. var outerN = 1 + (this._viewConfigs.has('left') ? 1 : 0) + (this._viewConfigs.has('right') ? 1 : 0); var innerN = 1 + (this._viewConfigs.has('top') ? 1 : 0) + (this._viewConfigs.has('bottom') ? 1 : 0); var outerGapAdd = outerN > 1 ? (this._gap * (outerN - 1)) / outerN : 0; var innerGapAdd = innerN > 1 ? (this._gap * (innerN - 1)) / innerN : 0; var isHorizontal = position === 'left' || position === 'right'; var gapAdd = isHorizontal ? outerGapAdd : innerGapAdd; var orientation = isHorizontal ? 'horizontal' : 'vertical'; var view = new EdgeGroupView(adjustedOpts(__assign({ collapsedSize: this._defaultCollapsedSize }, options), this._defaultCollapsedSize, gapAdd), group, orientation); var initialSize = view.isCollapsed ? view.collapsedSize : view.lastExpandedSize; switch (position) { case 'left': // Insert before the middle column this._outerSplitview.addView(view, initialSize, 0); this._leftIndex = 0; this._middleIndex += 1; if (this._rightIndex !== undefined) { this._rightIndex += 1; } this._leftView = view; break; case 'right': // Append after the middle column { var idx = this._outerSplitview.length; this._outerSplitview.addView(view, initialSize, idx); this._rightIndex = idx; this._rightView = view; } break; case 'top': this._middleColumn.addTopView(view, initialSize); this._topView = view; break; case 'bottom': this._middleColumn.addBottomView(view, initialSize); this._bottomView = view; break; } this._disposables.addDisposables(view); // Recalculate gap adjustments for all views now that n has changed. // updateTheme already guards the layout() call by _currentWidth/_currentHeight. this.updateTheme(this._gap, this._defaultCollapsedSize); return view; }; ShellManager.prototype.layout = function (width, height) { // Outer splitview is HORIZONTAL: layout(size=width, orthogonalSize=height) this._outerSplitview.layout(width, height); }; /** * Called when the active theme changes. Updates splitview margins and * edge-group collapsed sizes so the layout matches the new theme's gap * and tab-strip dimensions. */ ShellManager.prototype.updateTheme = function (gap, defaultCollapsedSize) { var _a, _b, _c, _d; this._gap = gap; this._defaultCollapsedSize = defaultCollapsedSize; var outerN = 1 + (this._viewConfigs.has('left') ? 1 : 0) + (this._viewConfigs.has('right') ? 1 : 0); var innerN = 1 + (this._viewConfigs.has('top') ? 1 : 0) + (this._viewConfigs.has('bottom') ? 1 : 0); var outerGapAdd = outerN > 1 ? (gap * (outerN - 1)) / outerN : 0; var innerGapAdd = innerN > 1 ? (gap * (innerN - 1)) / innerN : 0; // Update splitview margins. this._outerSplitview.margin = gap; this._middleColumn.updateMargin(gap); // Recompute effective collapsed sizes from the original config values. var updateView = function (view, baseCfg, gapAdd) { var _a; var baseCS = (_a = baseCfg.collapsedSize) !== null && _a !== void 0 ? _a : defaultCollapsedSize; var newCS = baseCS + gapAdd; var baseMS = baseCfg.minimumSize; var newMS = baseMS !== undefined ? baseMS + gapAdd : newCS + 50; view.updateCollapsedSize(newCS, newMS); }; var topCfg = this._viewConfigs.get('top'); if (this._topView && topCfg) { updateView(this._topView, topCfg, innerGapAdd); } var bottomCfg = this._viewConfigs.get('bottom'); if (this._bottomView && bottomCfg) { updateView(this._bottomView, bottomCfg, innerGapAdd); } var leftCfg = this._viewConfigs.get('left'); if (this._leftView && leftCfg) { updateView(this._leftView, leftCfg, outerGapAdd); } var rightCfg = this._viewConfigs.get('right'); if (this._rightView && rightCfg) { updateView(this._rightView, rightCfg, outerGapAdd); } // Resize currently-collapsed groups to their new collapsed size so // they immediately match the new theme's tab-strip dimensions. if (((_a = this._leftView) === null || _a === void 0 ? void 0 : _a.isCollapsed) && this._leftIndex !== undefined) { this._outerSplitview.resizeView(this._leftIndex, this._leftView.collapsedSize); } if (((_b = this._rightView) === null || _b === void 0 ? void 0 : _b.isCollapsed) && this._rightIndex !== undefined) { this._outerSplitview.resizeView(this._rightIndex, this._rightView.collapsedSize); } if ((_c = this._topView) === null || _c === void 0 ? void 0 : _c.isCollapsed) { this._middleColumn.resizeView('top', this._topView.collapsedSize); } if ((_d = this._bottomView) === null || _d === void 0 ? void 0 : _d.isCollapsed) { this._middleColumn.resizeView('bottom', this._bottomView.collapsedSize); } // Re-run layout with the current shell dimensions. if (this._currentWidth > 0 && this._currentHeight > 0) { this.layout(this._currentWidth, this._currentHeight); } }; ShellManager.prototype.removeEdgeView = function (position) { var view = this._getView(position); if (!view) { return; } switch (position) { case 'left': this._outerSplitview.removeView(this._leftIndex); this._leftIndex = undefined; this._leftView = undefined; // middle and right shift left by one this._middleIndex -= 1; if (this._rightIndex !== undefined) { this._rightIndex -= 1; } break; case 'right': this._outerSplitview.removeView(this._rightIndex); this._rightIndex = undefined; this._rightView = undefined; break; case 'top': this._middleColumn.removeView('top'); this._topView = undefined; break; case 'bottom': this._middleColumn.removeView('bottom'); this._bottomView = undefined; break; } // Deregister before disposing to avoid double-dispose when ShellManager // itself is eventually disposed. this._disposables.removeDisposable(view); view.dispose(); this._viewConfigs.delete(position); // Recalculate gap adjustments for remaining views. this.updateTheme(this._gap, this._defaultCollapsedSize); }; ShellManager.prototype.hasEdgeGroup = function (position) { switch (position) { case 'top': return this._topView !== undefined; case 'bottom': return this._bottomView !== undefined; case 'left': return this._leftView !== undefined; case 'right': return this._rightView !== undefined; } }; ShellManager.prototype.setEdgeGroupVisible = function (position, visible) { switch (position) { case 'left': if (this._leftIndex !== undefined) { this._outerSplitview.setViewVisible(this._leftIndex, visible); } break; case 'right': if (this._rightIndex !== undefined) { this._outerSplitview.setViewVisible(this._rightIndex, visible); } break; case 'top': case 'bottom': this._middleColumn.setViewVisible(position, visible); break; } }; ShellManager.prototype.isEdgeGroupVisible = function (position) { switch (position) { case 'left': if (this._leftIndex !== undefined) { return this._outerSplitview.isViewVisible(this._leftIndex); } return false; case 'right': if (this._rightIndex !== undefined) { return this._outerSplitview.isViewVisible(this._rightIndex); } return false; case 'top': case 'bottom': return this._middleColumn.isViewVisible(position); } }; ShellManager.prototype.setEdgeGroupCollapsed = function (position, collapsed) { var view = this._getView(position); if (!view) { return; } view.setCollapsed(collapsed); var targetSize = collapsed ? view.collapsedSize : view.lastExpandedSize; switch (position) { case 'left': if (this._leftIndex !== undefined) { this._outerSplitview.resizeView(this._leftIndex, targetSize); } break; case 'right': if (this._rightIndex !== undefined) { this._outerSplitview.resizeView(this._rightIndex, targetSize); } break; case 'top': case 'bottom': this._middleColumn.resizeView(position, targetSize); break; } }; ShellManager.prototype.isEdgeGroupCollapsed = function (position) { var _a, _b; return (_b = (_a = this._getView(position)) === null || _a === void 0 ? void 0 : _a.isCollapsed) !== null && _b !== void 0 ? _b : false; }; ShellManager.prototype._getView = function (position) { switch (position) { case 'top': return this._topView; case 'bottom': return this._bottomView; case 'left': return this._leftView; case 'right': return this._rightView; } }; ShellManager.prototype.toJSON = function () { var edgeGroups = {}; if (this._leftView && this._leftIndex !== undefined) { edgeGroups.left = { size: this._leftView.isCollapsed ? this._leftView.lastExpandedSize : this._outerSplitview.getViewSize(this._leftIndex), visible: this._outerSplitview.isViewVisible(this._leftIndex), collapsed: this._leftView.isCollapsed || undefined, }; } if (this._rightView && this._rightIndex !== undefined) { edgeGroups.right = { size: this._rightView.isCollapsed ? this._rightView.lastExpandedSize : this._outerSplitview.getViewSize(this._rightIndex), visible: this._outerSplitview.isViewVisible(this._rightIndex), collapsed: this._rightView.isCollapsed || undefined, }; } if (this._topView) { edgeGroups.top = { size: this._topView.isCollapsed ? this._topView.lastExpandedSize : this._middleColumn.getViewSize('top'), visible: this._middleColumn.isViewVisible('top'), collapsed: this._topView.isCollapsed || undefined, }; } if (this._bottomView) { edgeGroups.bottom = { size: this._bottomView.isCollapsed ? this._bottomView.lastExpandedSize : this._middleColumn.getViewSize('bottom'), visible: this._middleColumn.isViewVisible('bottom'), collapsed: this._bottomView.isCollapsed || undefined, }; } return edgeGroups; }; ShellManager.prototype.fromJSON = function (data) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v; if (data.left && this._leftIndex !== undefined) { // Always restore the expanded size first. toJSON always records the // expanded size (even when collapsed), so restoredExpandedSize must // be applied before setCollapsed locks min/max to collapsedSize. (_a = this._leftView) === null || _a === void 0 ? void 0 : _a.restoreExpandedSize(data.left.size); (_b = this._leftView) === null || _b === void 0 ? void 0 : _b.setCollapsed((_c = data.left.collapsed) !== null && _c !== void 0 ? _c : false); this._outerSplitview.resizeView(this._leftIndex, data.left.collapsed ? ((_e = (_d = this._leftView) === null || _d === void 0 ? void 0 : _d.collapsedSize) !== null && _e !== void 0 ? _e : data.left.size) : data.left.size); if (!data.left.visible) { this._outerSplitview.setViewVisible(this._leftIndex, false); } } if (data.right && this._rightIndex !== undefined) { (_f = this._rightView) === null || _f === void 0 ? void 0 : _f.restoreExpandedSize(data.right.size); (_g = this._rightView) === null || _g === void 0 ? void 0 : _g.setCollapsed((_h = data.right.collapsed) !== null && _h !== void 0 ? _h : false); this._outerSplitview.resizeView(this._rightIndex, data.right.collapsed ? ((_k = (_j = this._rightView) === null || _j === void 0 ? void 0 : _j.collapsedSize) !== null && _k !== void 0 ? _k : data.right.size) : data.right.size); if (!data.right.visible) { this._outerSplitview.setViewVisible(this._rightIndex, false); } } if (data.top) { (_l = this._topView) === null || _l === void 0 ? void 0 : _l.restoreExpandedSize(data.top.size); (_m = this._topView) === null || _m === void 0 ? void 0 : _m.setCollapsed((_o = data.top.collapsed) !== null && _o !== void 0 ? _o : false); this._middleColumn.resizeView('top', data.top.collapsed ? ((_q = (_p = this._topView) === null || _p === void 0 ? void 0 : _p.collapsedSize) !== null && _q !== void 0 ? _q : data.top.size) : data.top.size); if (!data.top.visible) { this._middleColumn.setViewVisible('top', false); } } if (data.bottom) { (_r = this._bottomView) === null || _r === void 0 ? void 0 : _r.restoreExpandedSize(data.bottom.size); (_s = this._bottomView) === null || _s === void 0 ? void 0 : _s.setCollapsed((_t = data.bottom.collapsed) !== null && _t !== void 0 ? _t : false); this._middleColumn.resizeView('bottom', data.bottom.collapsed ? ((_v = (_u = this._bottomView) === null || _u === void 0 ? void 0 : _u.collapsedSize) !== null && _v !== void 0 ? _v : data.bottom.size) : data.bottom.size); if (!data.bottom.visible) { this._middleColumn.setViewVisible('bottom', false); } } }; ShellManager.prototype.dispose = function () { var _a; this._disposables.dispose(); (_a = this._shellElement.parentElement) === null || _a === void 0 ? void 0 : _a.removeChild(this._shellElement); }; return ShellManager; }()); exports.ShellManager = ShellManager;