@lonli-lokli/react-mosaic-component
Version:
A React Tiling Window Manager
391 lines (390 loc) • 12 kB
JavaScript
// libs/react-mosaic-component/src/lib/util/mosaicUtilities.ts
import update from "immutability-helper";
import { clone } from "lodash-es";
function alternateDirection(node, direction = "row") {
if (isSplitNode(node)) {
const nextDirection = getOtherDirection(direction);
return {
type: "split",
direction,
children: node.children.map(
(child) => alternateDirection(child, nextDirection)
),
splitPercentages: node.splitPercentages
};
} else {
return node;
}
}
var Corner = /* @__PURE__ */ ((Corner2) => {
Corner2[Corner2["TOP_LEFT"] = 1] = "TOP_LEFT";
Corner2[Corner2["TOP_RIGHT"] = 2] = "TOP_RIGHT";
Corner2[Corner2["BOTTOM_LEFT"] = 3] = "BOTTOM_LEFT";
Corner2[Corner2["BOTTOM_RIGHT"] = 4] = "BOTTOM_RIGHT";
return Corner2;
})(Corner || {});
function isSplitNode(node) {
return typeof node === "object" && node !== null && "type" in node && node.type === "split";
}
function isTabsNode(node) {
return typeof node === "object" && node !== null && "tabs" in node && Array.isArray(node.tabs);
}
function getParentNode(root, path) {
return getNodeAtPath(root, getParentPath(path));
}
function getParentPath(path) {
return path.slice(0, -1);
}
function createBalancedTreeFromLeaves(leaves, startDirection = "row") {
if (leaves.length === 0) {
return null;
}
if (leaves.length === 1) {
return leaves[0];
}
let current = clone(leaves);
let next = [];
while (current.length > 1) {
while (current.length > 0) {
if (current.length > 1) {
const first = current.shift();
const second = current.shift();
next.push({
type: "split",
direction: "row",
children: [first, second],
splitPercentages: [50, 50]
});
} else {
next.unshift(current.shift());
}
}
current = next;
next = [];
}
return alternateDirection(current[0], startDirection);
}
function createBalancedNaryTreeFromLeaves(leaves, startDirection = "row", maxChildrenPerSplit = 4) {
if (leaves.length === 0) {
return null;
}
if (leaves.length === 1) {
return leaves[0];
}
if (leaves.length <= maxChildrenPerSplit) {
const equalPercentage = 100 / leaves.length;
return {
type: "split",
direction: startDirection,
children: leaves,
splitPercentages: Array(leaves.length).fill(equalPercentage)
};
}
const mid = Math.ceil(leaves.length / 2);
const left = leaves.slice(0, mid);
const right = leaves.slice(mid);
return {
type: "split",
direction: startDirection,
children: [
createBalancedNaryTreeFromLeaves(
left,
getOtherDirection(startDirection),
maxChildrenPerSplit
),
createBalancedNaryTreeFromLeaves(
right,
getOtherDirection(startDirection),
maxChildrenPerSplit
)
],
splitPercentages: [50, 50]
};
}
function getOtherChildIndices(childIndex, totalChildren) {
const siblings = [];
for (let i = 0; i < totalChildren; i++) {
if (i !== childIndex) {
siblings.push(i);
}
}
return siblings;
}
function getOtherDirection(direction) {
if (direction === "row") {
return "column";
} else {
return "row";
}
}
function getPathToCorner(tree, corner) {
let currentNode = tree;
const currentPath = [];
while (isSplitNode(currentNode)) {
let targetIndex;
if (currentNode.direction === "row") {
if (corner === 1 /* TOP_LEFT */ || corner === 3 /* BOTTOM_LEFT */) {
targetIndex = 0;
} else {
targetIndex = currentNode.children.length - 1;
}
} else {
if (corner === 1 /* TOP_LEFT */ || corner === 2 /* TOP_RIGHT */) {
targetIndex = 0;
} else {
targetIndex = currentNode.children.length - 1;
}
}
currentPath.push(targetIndex);
currentNode = currentNode.children[targetIndex];
}
return currentPath;
}
function getLeaves(tree) {
if (tree == null) {
return [];
} else if (isSplitNode(tree)) {
return tree.children.flatMap((child) => getLeaves(child));
} else if (isTabsNode(tree)) {
return tree.tabs;
} else {
return [tree];
}
}
function convertLegacyToNary(legacyNode) {
if (typeof legacyNode !== "object" || legacyNode === null || !("first" in legacyNode)) {
return legacyNode;
}
const parentNode = legacyNode;
const newSplitNode = {
type: "split",
direction: parentNode.direction,
children: [
convertLegacyToNary(parentNode.first),
convertLegacyToNary(parentNode.second)
],
splitPercentages: parentNode.splitPercentage !== void 0 ? [parentNode.splitPercentage, 100 - parentNode.splitPercentage] : void 0
// Let the renderer decide on default (e.g., 50/50)
};
return newSplitNode;
}
function normalizeMosaicTree(node) {
if (node === null || typeof node !== "object") {
return node;
}
if (node.type === "split") {
const normalizedChildren = node.children.map((child) => normalizeMosaicTree(child)).filter((child) => child !== null);
if (normalizedChildren.length === 0) {
return null;
}
if (normalizedChildren.length === 1) {
return normalizedChildren[0];
}
const flattenedChildren = [];
const flattenedPercentages = [];
let percentagesAreValid = true;
for (let i = 0; i < normalizedChildren.length; i++) {
const child = normalizedChildren[i];
if (isSplitNode(child) && child.direction === node.direction) {
flattenedChildren.push(...child.children);
if (percentagesAreValid && node.splitPercentages && child.splitPercentages) {
const parentPercentage = node.splitPercentages[i];
const scaledChildPercentages = child.splitPercentages.map(
(childPerc) => childPerc * parentPercentage / 100
);
flattenedPercentages.push(...scaledChildPercentages);
} else {
percentagesAreValid = false;
}
} else {
flattenedChildren.push(child);
if (percentagesAreValid && node.splitPercentages) {
flattenedPercentages.push(node.splitPercentages[i]);
} else {
percentagesAreValid = false;
}
}
}
let finalSplitPercentages;
const childrenWereFlattened = flattenedChildren.length !== normalizedChildren.length;
const childrenWereRemoved = node.children.length !== normalizedChildren.length;
if (percentagesAreValid && flattenedPercentages.length === flattenedChildren.length) {
finalSplitPercentages = flattenedPercentages;
} else if (!childrenWereFlattened && !childrenWereRemoved && node.splitPercentages) {
finalSplitPercentages = node.splitPercentages;
} else if (childrenWereRemoved && node.splitPercentages) {
const remainingPercentages = [];
let totalRemainingPercentage = 0;
for (let i = 0; i < normalizedChildren.length; i++) {
const child = normalizedChildren[i];
const originalIndex = node.children.findIndex(
(originalChild) => normalizeMosaicTree(originalChild) === child
);
if (originalIndex >= 0 && node.splitPercentages[originalIndex]) {
remainingPercentages.push(node.splitPercentages[originalIndex]);
totalRemainingPercentage += node.splitPercentages[originalIndex];
} else {
remainingPercentages.push(0);
}
}
if (totalRemainingPercentage > 0) {
finalSplitPercentages = remainingPercentages.map(
(p) => p / totalRemainingPercentage * 100
);
} else {
const equalPercentage = 100 / flattenedChildren.length;
finalSplitPercentages = Array(flattenedChildren.length).fill(
equalPercentage
);
}
} else {
const equalPercentage = 100 / flattenedChildren.length;
finalSplitPercentages = Array(flattenedChildren.length).fill(
equalPercentage
);
}
return {
...node,
children: flattenedChildren,
splitPercentages: finalSplitPercentages
};
}
if (node.type === "tabs") {
const validTabs = node.tabs;
if (validTabs.length === 0) {
return null;
}
if (validTabs.length === 1) {
return validTabs[0];
}
const activeTabIndex = Math.max(
0,
Math.min(node.activeTabIndex, validTabs.length - 1)
);
return { ...node, activeTabIndex };
}
return node;
}
function getNodeAtPath(tree, path) {
if (!tree) {
return null;
}
let current = tree;
for (const index of path) {
if (current === null || typeof current !== "object") {
return null;
}
if ("type" in current) {
switch (current.type) {
case "split":
current = current.children[index] ?? null;
break;
case "tabs":
current = current.tabs[index] ?? null;
break;
default:
return null;
}
} else {
return null;
}
}
return current;
}
function resizeSplit(tree, path, splitterIndex, deltaPercentage, minimumPaneSizePercentage = 10) {
const parentNode = getNodeAtPath(tree, path);
if (!parentNode || typeof parentNode !== "object" || parentNode.type !== "split") {
console.error(
"Path does not point to a valid split node. No update will be performed."
);
return tree;
}
let currentPercentages = parentNode.splitPercentages;
if (!currentPercentages) {
const numChildren = parentNode.children.length;
const equalPart = 100 / numChildren;
currentPercentages = Array(numChildren).fill(equalPart);
}
const firstPaneIndex = splitterIndex;
const secondPaneIndex = splitterIndex + 1;
if (secondPaneIndex >= currentPercentages.length) {
console.error("Invalid splitter index. No update will be performed.");
return tree;
}
const firstPaneSize = currentPercentages[firstPaneIndex];
const secondPaneSize = currentPercentages[secondPaneIndex];
const maxAllowedDelta = secondPaneSize - minimumPaneSizePercentage;
const minAllowedDelta = -(firstPaneSize - minimumPaneSizePercentage);
const clampedDelta = Math.max(
minAllowedDelta,
Math.min(deltaPercentage, maxAllowedDelta)
);
const newPercentages = [...currentPercentages];
newPercentages[firstPaneIndex] += clampedDelta;
newPercentages[secondPaneIndex] -= clampedDelta;
const spec = {
splitPercentages: { $set: newPercentages }
};
let nestedSpec = spec;
for (let i = path.length - 1; i >= 0; i--) {
nestedSpec = { children: { [path[i]]: nestedSpec } };
}
return update(tree, nestedSpec);
}
function getAndAssertNodeAtPathExists(tree, path) {
if (tree == null) {
throw new Error("Root is empty, cannot fetch path");
}
const node = getNodeAtPath(tree, path);
if (node == null) {
throw new Error(`Path [${path.join(", ")}] did not resolve to a node`);
}
return node;
}
function getParentAndChildIndex(tree, path) {
if (path.length === 0 || tree == null) {
return null;
}
const parentPath = path.slice(0, -1);
const lastBranch = path[path.length - 1];
const childIndex = typeof lastBranch === "number" ? lastBranch : Number(lastBranch);
if (!Number.isInteger(childIndex) || childIndex < 0) {
return null;
}
const parent = getNodeAtPath(tree, parentPath);
if (!parent) {
return null;
}
if (isSplitNode(parent)) {
if (childIndex >= parent.children.length) {
return null;
}
return { parent, childIndex };
}
if (isTabsNode(parent)) {
if (childIndex >= parent.tabs.length) {
return null;
}
return { parent, childIndex };
}
return null;
}
export {
Corner,
convertLegacyToNary,
createBalancedNaryTreeFromLeaves,
createBalancedTreeFromLeaves,
getAndAssertNodeAtPathExists,
getLeaves,
getNodeAtPath,
getOtherChildIndices,
getOtherDirection,
getParentAndChildIndex,
getParentNode,
getParentPath,
getPathToCorner,
isSplitNode,
isTabsNode,
normalizeMosaicTree,
resizeSplit
};