dockview
Version:
Zero dependency layout manager supporting tabs, grids and splitviews with ReactJS support
397 lines (396 loc) • 15.8 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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);
}
}