UNPKG

dockview

Version:

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

397 lines (396 loc) 15.8 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/grid *--------------------------------------------------------------------------------------------*/ import { Orientation, Sizing, } from '../splitview/core/splitview'; import { Position } from '../dnd/droptarget'; import { tail } from '../array'; import { LeafNode } from './leafNode'; import { BranchNode } from './branchNode'; import { Emitter } from '../events'; import { MutableDisposable } from '../lifecycle'; function flipNode(node, size, orthogonalSize) { if (node instanceof BranchNode) { const result = new BranchNode(orthogonal(node.orientation), node.proportionalLayout, node.styles, size, orthogonalSize); let totalSize = 0; for (let i = node.children.length - 1; i >= 0; i--) { const child = node.children[i]; const childSize = child instanceof BranchNode ? child.orthogonalSize : child.size; let newSize = node.size === 0 ? 0 : Math.round((size * childSize) / node.size); totalSize += newSize; // The last view to add should adjust to rounding errors if (i === 0) { newSize += size - totalSize; } result.addChild(flipNode(child, orthogonalSize, newSize), newSize, 0, true); } return result; } else { return new LeafNode(node.view, orthogonal(node.orientation), orthogonalSize); } } export function indexInParent(element) { const parentElement = element.parentElement; if (!parentElement) { throw new Error('Invalid grid element'); } let el = parentElement.firstElementChild; let index = 0; while (el !== element && el !== parentElement.lastElementChild && el) { el = el.nextElementSibling; index++; } return index; } /** * Find the grid location of a specific DOM element by traversing the parent * chain and finding each child index on the way. * * This will break as soon as DOM structures of the Splitview or Gridview change. */ export function getGridLocation(element) { const parentElement = element.parentElement; if (!parentElement) { throw new Error('Invalid grid element'); } if (/\bgrid-view\b/.test(parentElement.className)) { return []; } const index = indexInParent(parentElement); const ancestor = parentElement.parentElement.parentElement.parentElement; return [...getGridLocation(ancestor), index]; } export function getRelativeLocation(rootOrientation, location, direction) { const orientation = getLocationOrientation(rootOrientation, location); const directionOrientation = getDirectionOrientation(direction); if (orientation === directionOrientation) { const [rest, _index] = tail(location); let index = _index; if (direction === Position.Right || direction === Position.Bottom) { index += 1; } return [...rest, index]; } else { const index = direction === Position.Right || direction === Position.Bottom ? 1 : 0; return [...location, index]; } } export function getDirectionOrientation(direction) { return direction === Position.Top || direction === Position.Bottom ? Orientation.VERTICAL : Orientation.HORIZONTAL; } export function getLocationOrientation(rootOrientation, location) { return location.length % 2 === 0 ? orthogonal(rootOrientation) : rootOrientation; } export const orthogonal = (orientation) => orientation === Orientation.HORIZONTAL ? Orientation.VERTICAL : Orientation.HORIZONTAL; export function isGridBranchNode(node) { return !!node.children; } const serializeBranchNode = (node, orientation) => { const size = orientation === Orientation.VERTICAL ? node.box.width : node.box.height; if (!isGridBranchNode(node)) { if (typeof node.cachedVisibleSize === 'number') { return { type: 'leaf', data: node.view.toJSON(), size: node.cachedVisibleSize, visible: false, }; } return { type: 'leaf', data: node.view.toJSON(), size }; } return { type: 'branch', data: node.children.map((c) => serializeBranchNode(c, orthogonal(orientation))), size, }; }; export class Gridview { constructor(proportionalLayout, styles, orientation) { this.proportionalLayout = proportionalLayout; this.styles = styles; this.disposable = new MutableDisposable(); this._onDidChange = new Emitter(); this.onDidChange = this._onDidChange.event; this.element = document.createElement('div'); this.element.className = 'grid-view'; this.root = new BranchNode(orientation, proportionalLayout, styles, 0, 0); } serialize() { const root = serializeBranchNode(this.getView(), this.orientation); return { root, width: this.width, height: this.height, orientation: this.orientation, }; } dispose() { this.disposable.dispose(); this._onDidChange.dispose(); this.root.dispose(); } clear() { const orientation = this.root.orientation; this.root = new BranchNode(orientation, this.proportionalLayout, this.styles, this.root.size, this.root.orthogonalSize); } deserialize(json, deserializer) { const orientation = json.orientation; const height = json.height; this._deserialize(json.root, orientation, deserializer, height); } _deserialize(root, orientation, deserializer, orthogonalSize) { this.root = this._deserializeNode(root, orientation, deserializer, orthogonalSize); } _deserializeNode(node, orientation, deserializer, orthogonalSize) { let result; if (node.type === 'branch') { const serializedChildren = node.data; const children = serializedChildren.map((serializedChild) => { return { node: this._deserializeNode(serializedChild, orthogonal(orientation), deserializer, node.size), visible: serializedChild.visible, }; }); result = new BranchNode(orientation, this.proportionalLayout, this.styles, node.size, orthogonalSize, children); } else { result = new LeafNode(deserializer.fromJSON(node), orientation, orthogonalSize, node.size); } return result; } get orientation() { return this.root.orientation; } set orientation(orientation) { if (this.root.orientation === orientation) { return; } const { size, orthogonalSize } = this.root; this.root = flipNode(this.root, orthogonalSize, size); this.root.layout(size, orthogonalSize); } get root() { return this._root; } set root(root) { const oldRoot = this._root; if (oldRoot) { oldRoot.dispose(); this.element.removeChild(oldRoot.element); } this._root = root; this.element.appendChild(this._root.element); this.disposable.value = this._root.onDidChange((e) => { this._onDidChange.fire(e); }); } next(location) { return this.progmaticSelect(location); } previous(location) { return this.progmaticSelect(location, true); } getView(location) { const node = location ? this.getNode(location)[1] : this.root; return this._getViews(node, this.orientation); } _getViews(node, orientation, cachedVisibleSize) { const box = { height: node.height, width: node.width }; if (node instanceof LeafNode) { return { box, view: node.view, cachedVisibleSize }; } const children = []; for (let i = 0; i < node.children.length; i++) { const child = node.children[i]; const nodeCachedVisibleSize = node.getChildCachedVisibleSize(i); children.push(this._getViews(child, orthogonal(orientation), nodeCachedVisibleSize)); } return { box, children }; } progmaticSelect(location, reverse = false) { const [path, node] = this.getNode(location); if (!(node instanceof LeafNode)) { throw new Error('invalid location'); } const findLeaf = (candiateNode, last) => { if (candiateNode instanceof LeafNode) { return candiateNode; } if (candiateNode instanceof BranchNode) { return findLeaf(candiateNode.children[last ? candiateNode.children.length - 1 : 0], last); } throw new Error('invalid node'); }; for (let i = path.length - 1; i > -1; i--) { const n = path[i]; const l = location[i] || 0; const canProgressInCurrentLevel = reverse ? l - 1 > -1 : l + 1 < n.children.length; if (canProgressInCurrentLevel) { return findLeaf(n.children[reverse ? l - 1 : l + 1], reverse); } } return findLeaf(this.root, reverse); } get width() { return this.root.width; } get height() { return this.root.height; } get minimumWidth() { return this.root.minimumWidth; } get minimumHeight() { return this.root.minimumHeight; } get maximumWidth() { return this.root.maximumHeight; } get maximumHeight() { return this.root.maximumHeight; } isViewVisible(location) { const [rest, index] = tail(location); const [, parent] = this.getNode(rest); if (!(parent instanceof BranchNode)) { throw new Error('Invalid from location'); } return parent.isChildVisible(index); } setViewVisible(location, visible) { const [rest, index] = tail(location); const [, parent] = this.getNode(rest); if (!(parent instanceof BranchNode)) { throw new Error('Invalid from location'); } parent.setChildVisible(index, visible); } moveView(parentLocation, from, to) { const [, parent] = this.getNode(parentLocation); if (!(parent instanceof BranchNode)) { throw new Error('Invalid location'); } parent.moveChild(from, to); } addView(view, size, location) { const [rest, index] = tail(location); const [pathToParent, parent] = this.getNode(rest); if (parent instanceof BranchNode) { const node = new LeafNode(view, orthogonal(parent.orientation), parent.orthogonalSize); parent.addChild(node, size, index); } else { const [grandParent, ..._] = [...pathToParent].reverse(); const [parentIndex, ...__] = [...rest].reverse(); let newSiblingSize = 0; const newSiblingCachedVisibleSize = grandParent.getChildCachedVisibleSize(parentIndex); if (typeof newSiblingCachedVisibleSize === 'number') { newSiblingSize = Sizing.Invisible(newSiblingCachedVisibleSize); } grandParent.removeChild(parentIndex); const newParent = new BranchNode(parent.orientation, this.proportionalLayout, this.styles, parent.size, parent.orthogonalSize); grandParent.addChild(newParent, parent.size, parentIndex); const newSibling = new LeafNode(parent.view, grandParent.orientation, parent.size); newParent.addChild(newSibling, newSiblingSize, 0); if (typeof size !== 'number' && size.type === 'split') { size = { type: 'split', index: 0 }; } const node = new LeafNode(view, grandParent.orientation, parent.size); newParent.addChild(node, size, index); } } remove(view, sizing) { const location = getGridLocation(view.element); return this.removeView(location, sizing); } removeView(location, sizing) { const [rest, index] = tail(location); const [pathToParent, parent] = this.getNode(rest); if (!(parent instanceof BranchNode)) { throw new Error('Invalid location'); } const node = parent.children[index]; if (!(node instanceof LeafNode)) { throw new Error('Invalid location'); } parent.removeChild(index, sizing); if (parent.children.length === 0) { // throw new Error('Invalid grid state'); return node.view; } if (parent.children.length > 1) { return node.view; } if (pathToParent.length === 0) { // parent is root const sibling = parent.children[0]; if (sibling instanceof LeafNode) { return node.view; } // we must promote sibling to be the new root parent.removeChild(0, sizing); this.root = sibling; return node.view; } const [grandParent, ..._] = [...pathToParent].reverse(); const [parentIndex, ...__] = [...rest].reverse(); const sibling = parent.children[0]; const isSiblingVisible = parent.isChildVisible(0); parent.removeChild(0, sizing); const sizes = grandParent.children.map((size, i) => grandParent.getChildSize(i)); grandParent.removeChild(parentIndex, sizing); if (sibling instanceof BranchNode) { sizes.splice(parentIndex, 1, ...sibling.children.map((c) => c.size)); for (let i = 0; i < sibling.children.length; i++) { const child = sibling.children[i]; grandParent.addChild(child, child.size, parentIndex + i); } } else { const newSibling = new LeafNode(sibling.view, orthogonal(sibling.orientation), sibling.size); const siblingSizing = isSiblingVisible ? sibling.orthogonalSize : Sizing.Invisible(sibling.orthogonalSize); grandParent.addChild(newSibling, siblingSizing, parentIndex); } for (let i = 0; i < sizes.length; i++) { grandParent.resizeChild(i, sizes[i]); } return node.view; } layout(width, height) { const [size, orthogonalSize] = this.root.orientation === Orientation.HORIZONTAL ? [height, width] : [width, height]; this.root.layout(size, orthogonalSize); } getNode(location, node = this.root, path = []) { if (location.length === 0) { return [path, node]; } if (!(node instanceof BranchNode)) { throw new Error('Invalid location'); } const [index, ...rest] = location; if (index < 0 || index >= node.children.length) { throw new Error('Invalid location'); } const child = node.children[index]; path.push(node); return this.getNode(rest, child, path); } }