UNPKG

dockview

Version:

Zero dependency layout manager supporting tabs, grids and splitviews with ReactJS support

690 lines (689 loc) 30 kB
/*--------------------------------------------------------------------------------------------- * Accreditation: This file is largly based upon the MIT licenced VSCode sourcecode found at: * https://github.com/microsoft/vscode/tree/main/src/vs/base/browser/ui/splitview *--------------------------------------------------------------------------------------------*/ import { removeClasses, addClasses, toggleClass, getElementsByTagName, } from '../../dom'; import { clamp } from '../../math'; import { Emitter } from '../../events'; import { pushToStart, pushToEnd, range, firstIndex } from '../../array'; import { ViewItem } from './viewItem'; export var Orientation; (function (Orientation) { Orientation["HORIZONTAL"] = "HORIZONTAL"; Orientation["VERTICAL"] = "VERTICAL"; })(Orientation || (Orientation = {})); export var SashState; (function (SashState) { SashState[SashState["MAXIMUM"] = 0] = "MAXIMUM"; SashState[SashState["MINIMUM"] = 1] = "MINIMUM"; SashState[SashState["DISABLED"] = 2] = "DISABLED"; SashState[SashState["ENABLED"] = 3] = "ENABLED"; })(SashState || (SashState = {})); export var LayoutPriority; (function (LayoutPriority) { LayoutPriority["Low"] = "low"; LayoutPriority["High"] = "high"; LayoutPriority["Normal"] = "normal"; })(LayoutPriority || (LayoutPriority = {})); export var Sizing; (function (Sizing) { Sizing.Distribute = { type: 'distribute' }; function Split(index) { return { type: 'split', index }; } Sizing.Split = Split; function Invisible(cachedVisibleSize) { return { type: 'invisible', cachedVisibleSize }; } Sizing.Invisible = Invisible; })(Sizing || (Sizing = {})); export class Splitview { constructor(container, options) { this.container = container; this.views = []; this.sashes = []; this._size = 0; this._orthogonalSize = 0; this.contentSize = 0; this._proportions = undefined; this._onDidSashEnd = new Emitter(); this.onDidSashEnd = this._onDidSashEnd.event; this._onDidAddView = new Emitter(); this.onDidAddView = this._onDidAddView.event; this._onDidRemoveView = new Emitter(); this.onDidRemoveView = this._onDidAddView.event; this._startSnappingEnabled = true; this._endSnappingEnabled = true; this.resize = (index, delta, sizes = this.views.map((x) => x.size), lowPriorityIndexes, highPriorityIndexes, overloadMinDelta = Number.NEGATIVE_INFINITY, overloadMaxDelta = Number.POSITIVE_INFINITY, snapBefore, snapAfter) => { if (index < 0 || index > this.views.length) { return 0; } const upIndexes = range(index, -1); const downIndexes = range(index + 1, this.views.length); // if (highPriorityIndexes) { for (const i of highPriorityIndexes) { pushToStart(upIndexes, i); pushToStart(downIndexes, i); } } if (lowPriorityIndexes) { for (const i of lowPriorityIndexes) { pushToEnd(upIndexes, i); pushToEnd(downIndexes, i); } } // const upItems = upIndexes.map((i) => this.views[i]); const upSizes = upIndexes.map((i) => sizes[i]); // const downItems = downIndexes.map((i) => this.views[i]); const downSizes = downIndexes.map((i) => sizes[i]); // const minDeltaUp = upIndexes.reduce((_, i) => _ + this.views[i].minimumSize - sizes[i], 0); const maxDeltaUp = upIndexes.reduce((_, i) => _ + this.views[i].maximumSize - sizes[i], 0); // const maxDeltaDown = downIndexes.length === 0 ? Number.POSITIVE_INFINITY : downIndexes.reduce((_, i) => _ + sizes[i] - this.views[i].minimumSize, 0); const minDeltaDown = downIndexes.length === 0 ? Number.NEGATIVE_INFINITY : downIndexes.reduce((_, i) => _ + sizes[i] - this.views[i].maximumSize, 0); // const minDelta = Math.max(minDeltaUp, minDeltaDown); const maxDelta = Math.min(maxDeltaDown, maxDeltaUp); // let snapped = false; if (snapBefore) { const snapView = this.views[snapBefore.index]; const visible = delta >= snapBefore.limitDelta; snapped = visible !== snapView.visible; snapView.setVisible(visible, snapBefore.size); } if (!snapped && snapAfter) { const snapView = this.views[snapAfter.index]; const visible = delta < snapAfter.limitDelta; snapped = visible !== snapView.visible; snapView.setVisible(visible, snapAfter.size); } if (snapped) { return this.resize(index, delta, sizes, lowPriorityIndexes, highPriorityIndexes, overloadMinDelta, overloadMaxDelta); } // const tentativeDelta = clamp(delta, minDelta, maxDelta); let actualDelta = 0; // let deltaUp = tentativeDelta; for (let i = 0; i < upItems.length; i++) { const item = upItems[i]; const size = clamp(upSizes[i] + deltaUp, item.minimumSize, item.maximumSize); const viewDelta = size - upSizes[i]; actualDelta += viewDelta; deltaUp -= viewDelta; item.size = size; } // let deltaDown = actualDelta; for (let i = 0; i < downItems.length; i++) { const item = downItems[i]; const size = clamp(downSizes[i] - deltaDown, item.minimumSize, item.maximumSize); const viewDelta = size - downSizes[i]; deltaDown += viewDelta; item.size = size; } // return delta; }; this._orientation = options.orientation; this.element = this.createContainer(); this.proportionalLayout = options.proportionalLayout === undefined ? true : !!options.proportionalLayout; this.viewContainer = this.createViewContainer(); this.sashContainer = this.createSashContainer(); this.element.appendChild(this.sashContainer); this.element.appendChild(this.viewContainer); this.container.appendChild(this.element); this.style(options.styles); // We have an existing set of view, add them now if (options.descriptor) { this._size = options.descriptor.size; options.descriptor.views.forEach((viewDescriptor, index) => { const sizing = viewDescriptor.visible === undefined || viewDescriptor.visible ? viewDescriptor.size : { type: 'invisible', cachedVisibleSize: viewDescriptor.size, }; const view = viewDescriptor.view; this.addView(view, sizing, index, true // true skip layout ); }); // Initialize content size and proportions for first layout this.contentSize = this.views.reduce((r, i) => r + i.size, 0); this.saveProportions(); } } get size() { return this._size; } set size(value) { this._size = value; } get orthogonalSize() { return this._orthogonalSize; } set orthogonalSize(value) { this._orthogonalSize = value; } get length() { return this.views.length; } get proportions() { return this._proportions ? [...this._proportions] : undefined; } get orientation() { return this._orientation; } set orientation(value) { this._orientation = value; const tmp = this.size; this.size = this.orthogonalSize; this.orthogonalSize = tmp; removeClasses(this.element, 'horizontal', 'vertical'); this.element.classList.add(this.orientation == Orientation.HORIZONTAL ? 'horizontal' : 'vertical'); } get minimumSize() { return this.views.reduce((r, item) => r + item.minimumSize, 0); } get maximumSize() { return this.length === 0 ? Number.POSITIVE_INFINITY : this.views.reduce((r, item) => r + item.maximumSize, 0); } get startSnappingEnabled() { return this._startSnappingEnabled; } set startSnappingEnabled(startSnappingEnabled) { if (this._startSnappingEnabled === startSnappingEnabled) { return; } this._startSnappingEnabled = startSnappingEnabled; this.updateSashEnablement(); } get endSnappingEnabled() { return this._endSnappingEnabled; } set endSnappingEnabled(endSnappingEnabled) { if (this._endSnappingEnabled === endSnappingEnabled) { return; } this._endSnappingEnabled = endSnappingEnabled; this.updateSashEnablement(); } style(styles) { if ((styles === null || styles === void 0 ? void 0 : styles.separatorBorder) === 'transparent') { removeClasses(this.element, 'separator-border'); this.element.style.removeProperty('--dv-separator-border'); } else { addClasses(this.element, 'separator-border'); if (styles === null || styles === void 0 ? void 0 : styles.separatorBorder) { this.element.style.setProperty('--dv-separator-border', styles.separatorBorder); } } } isViewVisible(index) { if (index < 0 || index >= this.views.length) { throw new Error('Index out of bounds'); } const viewItem = this.views[index]; return viewItem.visible; } setViewVisible(index, visible) { if (index < 0 || index >= this.views.length) { throw new Error('Index out of bounds'); } toggleClass(this.container, 'visible', visible); const viewItem = this.views[index]; toggleClass(this.container, 'visible', visible); viewItem.setVisible(visible, viewItem.size); this.distributeEmptySpace(index); this.layoutViews(); this.saveProportions(); } getViewSize(index) { if (index < 0 || index >= this.views.length) { return -1; } return this.views[index].size; } resizeView(index, size) { if (index < 0 || index >= this.views.length) { return; } const indexes = range(this.views.length).filter((i) => i !== index); const lowPriorityIndexes = [ ...indexes.filter((i) => this.views[i].priority === LayoutPriority.Low), index, ]; const highPriorityIndexes = indexes.filter((i) => this.views[i].priority === LayoutPriority.High); const item = this.views[index]; size = Math.round(size); size = clamp(size, item.minimumSize, Math.min(item.maximumSize, this._size)); item.size = size; this.relayout(lowPriorityIndexes, highPriorityIndexes); } getViews() { return this.views.map((x) => x.view); } onDidChange(item, size) { const index = this.views.indexOf(item); if (index < 0 || index >= this.views.length) { return; } size = typeof size === 'number' ? size : item.size; size = clamp(size, item.minimumSize, item.maximumSize); item.size = size; this.relayout([index]); } addView(view, size = { type: 'distribute' }, index = this.views.length, skipLayout) { const container = document.createElement('div'); container.className = 'view'; container.appendChild(view.element); let viewSize; if (typeof size === 'number') { viewSize = size; } else if (size.type === 'split') { viewSize = this.getViewSize(size.index) / 2; } else if (size.type === 'invisible') { viewSize = { cachedVisibleSize: size.cachedVisibleSize }; } else { viewSize = view.minimumSize; } const disposable = view.onDidChange((newSize) => this.onDidChange(viewItem, newSize)); const dispose = () => { disposable === null || disposable === void 0 ? void 0 : disposable.dispose(); this.viewContainer.removeChild(container); }; const viewItem = new ViewItem(container, view, viewSize, { dispose }); if (index === this.views.length) { this.viewContainer.appendChild(container); } else { this.viewContainer.insertBefore(container, this.viewContainer.children.item(index)); } this.views.splice(index, 0, viewItem); if (this.views.length > 1) { //add sash const sash = document.createElement('div'); sash.className = 'sash'; const onStart = (event) => { for (const item of this.views) { item.enabled = false; } const iframes = [ ...getElementsByTagName('iframe'), ...getElementsByTagName('webview'), ]; for (const iframe of iframes) { iframe.style.pointerEvents = 'none'; } const start = this._orientation === Orientation.HORIZONTAL ? event.clientX : event.clientY; const sashIndex = firstIndex(this.sashes, (s) => s.container === sash); // const sizes = this.views.map((x) => x.size); // let snapBefore; let snapAfter; const upIndexes = range(sashIndex, -1); const downIndexes = range(sashIndex + 1, this.views.length); const minDeltaUp = upIndexes.reduce((r, i) => r + (this.views[i].minimumSize - sizes[i]), 0); const maxDeltaUp = upIndexes.reduce((r, i) => r + (this.views[i].viewMaximumSize - sizes[i]), 0); const maxDeltaDown = downIndexes.length === 0 ? Number.POSITIVE_INFINITY : downIndexes.reduce((r, i) => r + (sizes[i] - this.views[i].minimumSize), 0); const minDeltaDown = downIndexes.length === 0 ? Number.NEGATIVE_INFINITY : downIndexes.reduce((r, i) => r + (sizes[i] - this.views[i].viewMaximumSize), 0); const minDelta = Math.max(minDeltaUp, minDeltaDown); const maxDelta = Math.min(maxDeltaDown, maxDeltaUp); const snapBeforeIndex = this.findFirstSnapIndex(upIndexes); const snapAfterIndex = this.findFirstSnapIndex(downIndexes); if (typeof snapBeforeIndex === 'number') { const snappedViewItem = this.views[snapBeforeIndex]; const halfSize = Math.floor(snappedViewItem.viewMinimumSize / 2); snapBefore = { index: snapBeforeIndex, limitDelta: snappedViewItem.visible ? minDelta - halfSize : minDelta + halfSize, size: snappedViewItem.size, }; } if (typeof snapAfterIndex === 'number') { const snappedViewItem = this.views[snapAfterIndex]; const halfSize = Math.floor(snappedViewItem.viewMinimumSize / 2); snapAfter = { index: snapAfterIndex, limitDelta: snappedViewItem.visible ? maxDelta + halfSize : maxDelta - halfSize, size: snappedViewItem.size, }; } // const mousemove = (mousemoveEvent) => { const current = this._orientation === Orientation.HORIZONTAL ? mousemoveEvent.clientX : mousemoveEvent.clientY; const delta = current - start; this.resize(sashIndex, delta, sizes, undefined, undefined, minDelta, maxDelta, snapBefore, snapAfter); this.distributeEmptySpace(); this.layoutViews(); }; const end = () => { for (const item of this.views) { item.enabled = true; } for (const iframe of iframes) { iframe.style.pointerEvents = 'auto'; } this.saveProportions(); document.removeEventListener('mousemove', mousemove); document.removeEventListener('mouseup', end); document.removeEventListener('mouseend', end); this._onDidSashEnd.fire(undefined); }; document.addEventListener('mousemove', mousemove); document.addEventListener('mouseup', end); document.addEventListener('mouseend', end); }; sash.addEventListener('mousedown', onStart); const sashItem = { container: sash, disposable: () => { sash.removeEventListener('mousedown', onStart); this.sashContainer.removeChild(sash); }, }; this.sashContainer.appendChild(sash); this.sashes.push(sashItem); } if (!skipLayout) { this.relayout([index]); } if (!skipLayout && typeof size !== 'number' && size.type === 'distribute') { this.distributeViewSizes(); } this._onDidAddView.fire(view); } distributeViewSizes() { const flexibleViewItems = []; let flexibleSize = 0; for (const item of this.views) { if (item.maximumSize - item.minimumSize > 0) { flexibleViewItems.push(item); flexibleSize += item.size; } } const size = Math.floor(flexibleSize / flexibleViewItems.length); for (const item of flexibleViewItems) { item.size = clamp(size, item.minimumSize, item.maximumSize); } const indexes = range(this.views.length); const lowPriorityIndexes = indexes.filter((i) => this.views[i].priority === LayoutPriority.Low); const highPriorityIndexes = indexes.filter((i) => this.views[i].priority === LayoutPriority.High); this.relayout(lowPriorityIndexes, highPriorityIndexes); } removeView(index, sizing, skipLayout = false) { // Remove view const viewItem = this.views.splice(index, 1)[0]; viewItem.dispose(); // Remove sash if (this.views.length >= 1) { const sashIndex = Math.max(index - 1, 0); const sashItem = this.sashes.splice(sashIndex, 1)[0]; sashItem.disposable(); } if (!skipLayout) { this.relayout(); } if (sizing && sizing.type === 'distribute') { this.distributeViewSizes(); } this._onDidRemoveView.fire(viewItem.view); return viewItem.view; } getViewCachedVisibleSize(index) { if (index < 0 || index >= this.views.length) { throw new Error('Index out of bounds'); } const viewItem = this.views[index]; return viewItem.cachedVisibleSize; } moveView(from, to) { const cachedVisibleSize = this.getViewCachedVisibleSize(from); const sizing = typeof cachedVisibleSize === 'undefined' ? this.getViewSize(from) : Sizing.Invisible(cachedVisibleSize); const view = this.removeView(from, undefined, true); this.addView(view, sizing, to); } layout(size, orthogonalSize) { const previousSize = Math.max(this.size, this.contentSize); this.size = size; this.orthogonalSize = orthogonalSize; if (!this.proportions) { const indexes = range(this.views.length); const lowPriorityIndexes = indexes.filter((i) => this.views[i].priority === LayoutPriority.Low); const highPriorityIndexes = indexes.filter((i) => this.views[i].priority === LayoutPriority.High); this.resize(this.views.length - 1, size - previousSize, undefined, lowPriorityIndexes, highPriorityIndexes); } else { for (let i = 0; i < this.views.length; i++) { const item = this.views[i]; item.size = clamp(Math.round(this.proportions[i] * size), item.minimumSize, item.maximumSize); } } this.distributeEmptySpace(); this.layoutViews(); } relayout(lowPriorityIndexes, highPriorityIndexes) { const contentSize = this.views.reduce((r, i) => r + i.size, 0); this.resize(this.views.length - 1, this._size - contentSize, undefined, lowPriorityIndexes, highPriorityIndexes); this.distributeEmptySpace(); this.layoutViews(); this.saveProportions(); } distributeEmptySpace(lowPriorityIndex) { const contentSize = this.views.reduce((r, i) => r + i.size, 0); let emptyDelta = this.size - contentSize; const indexes = range(this.views.length - 1, -1); const lowPriorityIndexes = indexes.filter((i) => this.views[i].priority === LayoutPriority.Low); const highPriorityIndexes = indexes.filter((i) => this.views[i].priority === LayoutPriority.High); for (const index of highPriorityIndexes) { pushToStart(indexes, index); } for (const index of lowPriorityIndexes) { pushToEnd(indexes, index); } if (typeof lowPriorityIndex === 'number') { pushToEnd(indexes, lowPriorityIndex); } for (let i = 0; emptyDelta !== 0 && i < indexes.length; i++) { const item = this.views[indexes[i]]; const size = clamp(item.size + emptyDelta, item.minimumSize, item.maximumSize); const viewDelta = size - item.size; emptyDelta -= viewDelta; item.size = size; } } saveProportions() { if (this.proportionalLayout && this.contentSize > 0) { this._proportions = this.views.map((i) => i.size / this.contentSize); } } layoutViews() { this.contentSize = this.views.reduce((r, i) => r + i.size, 0); let sum = 0; const x = []; this.updateSashEnablement(); for (let i = 0; i < this.views.length - 1; i++) { sum += this.views[i].size; x.push(sum); const offset = Math.min(Math.max(0, sum - 2), this.size - 4); if (this._orientation === Orientation.HORIZONTAL) { this.sashes[i].container.style.left = `${offset}px`; this.sashes[i].container.style.top = `0px`; } if (this._orientation === Orientation.VERTICAL) { this.sashes[i].container.style.left = `0px`; this.sashes[i].container.style.top = `${offset}px`; } } this.views.forEach((view, i) => { if (this._orientation === Orientation.HORIZONTAL) { view.container.style.width = `${view.size}px`; view.container.style.left = i == 0 ? '0px' : `${x[i - 1]}px`; view.container.style.top = ''; view.container.style.height = ''; } if (this._orientation === Orientation.VERTICAL) { view.container.style.height = `${view.size}px`; view.container.style.top = i == 0 ? '0px' : `${x[i - 1]}px`; view.container.style.width = ''; view.container.style.left = ''; } view.view.layout(view.size, this._orthogonalSize); }); } findFirstSnapIndex(indexes) { // visible views first for (const index of indexes) { const viewItem = this.views[index]; if (!viewItem.visible) { continue; } if (viewItem.snap) { return index; } } // then, hidden views for (const index of indexes) { const viewItem = this.views[index]; if (viewItem.visible && viewItem.maximumSize - viewItem.minimumSize > 0) { return undefined; } if (!viewItem.visible && viewItem.snap) { return index; } } return undefined; } updateSashEnablement() { let previous = false; const collapsesDown = this.views.map((i) => (previous = i.size - i.minimumSize > 0 || previous)); previous = false; const expandsDown = this.views.map((i) => (previous = i.maximumSize - i.size > 0 || previous)); const reverseViews = [...this.views].reverse(); previous = false; const collapsesUp = reverseViews .map((i) => (previous = i.size - i.minimumSize > 0 || previous)) .reverse(); previous = false; const expandsUp = reverseViews .map((i) => (previous = i.maximumSize - i.size > 0 || previous)) .reverse(); let position = 0; for (let index = 0; index < this.sashes.length; index++) { const sash = this.sashes[index]; const viewItem = this.views[index]; position += viewItem.size; const min = !(collapsesDown[index] && expandsUp[index + 1]); const max = !(expandsDown[index] && collapsesUp[index + 1]); if (min && max) { const upIndexes = range(index, -1); const downIndexes = range(index + 1, this.views.length); const snapBeforeIndex = this.findFirstSnapIndex(upIndexes); const snapAfterIndex = this.findFirstSnapIndex(downIndexes); const snappedBefore = typeof snapBeforeIndex === 'number' && !this.views[snapBeforeIndex].visible; const snappedAfter = typeof snapAfterIndex === 'number' && !this.views[snapAfterIndex].visible; if (snappedBefore && collapsesUp[index] && (position > 0 || this.startSnappingEnabled)) { this.updateSash(sash, SashState.MINIMUM); } else if (snappedAfter && collapsesDown[index] && (position < this.contentSize || this.endSnappingEnabled)) { this.updateSash(sash, SashState.MAXIMUM); } else { this.updateSash(sash, SashState.DISABLED); } } else if (min && !max) { this.updateSash(sash, SashState.MINIMUM); } else if (!min && max) { this.updateSash(sash, SashState.MAXIMUM); } else { this.updateSash(sash, SashState.ENABLED); } } } updateSash(sash, state) { toggleClass(sash.container, 'disabled', state === SashState.DISABLED); toggleClass(sash.container, 'enabled', state === SashState.ENABLED); toggleClass(sash.container, 'maximum', state === SashState.MAXIMUM); toggleClass(sash.container, 'minimum', state === SashState.MINIMUM); } createViewContainer() { const element = document.createElement('div'); element.className = 'view-container'; return element; } createSashContainer() { const element = document.createElement('div'); element.className = 'sash-container'; return element; } createContainer() { const element = document.createElement('div'); const orientationClassname = this._orientation === Orientation.HORIZONTAL ? 'horizontal' : 'vertical'; element.className = `split-view-container ${orientationClassname}`; return element; } dispose() { this._onDidSashEnd.dispose(); this._onDidAddView.dispose(); this._onDidRemoveView.dispose(); this.element.remove(); for (let i = 0; i < this.element.children.length; i++) { if (this.element.children.item(i) === this.element) { this.element.removeChild(this.element); break; } } } }