UNPKG

@lonli-lokli/react-mosaic-component

Version:
391 lines (390 loc) 12 kB
// 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 };