@lonli-lokli/react-mosaic-component
Version:
A React Tiling Window Manager
425 lines (424 loc) • 14.3 kB
JavaScript
// libs/react-mosaic-component/src/lib/util/mosaicUpdates.ts
import update from "immutability-helper";
import { dropRight, isEqual, set } from "lodash-es";
import { MosaicDropTargetPosition } from "../internalTypes.mjs";
import {
getAndAssertNodeAtPathExists,
getNodeAtPath,
getParentAndChildIndex,
isSplitNode,
isTabsNode
} from "./mosaicUtilities.mjs";
import { assertNever } from "./assertNever.mjs";
function buildSpecFromUpdate(mosaicUpdate) {
if (mosaicUpdate.path.length > 0) {
return set({}, mosaicUpdate.path, mosaicUpdate.spec);
} else {
return mosaicUpdate.spec;
}
}
function buildSpecFromPath(tree, path, spec) {
return path.reduceRight((acc, pathSegment, index) => {
const parentPath = path.slice(0, index);
const parentNode = getNodeAtPath(tree, parentPath);
let property = "children";
if (parentNode && typeof parentNode === "object" && "type" in parentNode && parentNode.type === "tabs") {
property = "tabs";
}
return {
[property]: {
[pathSegment]: acc
}
};
}, spec);
}
function updateTree(tree, updates) {
if (updates.length === 0) {
return tree;
}
let currentTree = tree;
for (const { path, spec } of updates) {
if (path.length === 0) {
currentTree = update(currentTree, spec);
} else {
const nestedSpec = buildSpecFromPath(currentTree, path, spec);
currentTree = update(currentTree, nestedSpec);
}
}
return currentTree;
}
function createRemoveUpdate(root, path) {
if (path.length === 0) {
throw new Error("Cannot remove root node");
}
const parentInfo = getParentAndChildIndex(root, path);
if (!parentInfo) {
throw new Error("Cannot find parent for removal");
}
const { parent, childIndex } = parentInfo;
if (isSplitNode(parent)) {
if (parent.children.length === 2) {
const siblingIndex = childIndex === 0 ? 1 : 0;
const sibling = parent.children[siblingIndex];
return {
path: dropRight(path),
spec: {
$set: sibling
}
};
} else {
const newChildren = parent.children.filter(
(_, index) => index !== childIndex
);
const oldPercentages = parent.splitPercentages || Array(parent.children.length).fill(100 / parent.children.length);
const removedPercentage = oldPercentages[childIndex];
const redistributeAmount = removedPercentage / newChildren.length;
const newPercentages = oldPercentages.filter((_, index) => index !== childIndex).map((percentage) => percentage + redistributeAmount);
return {
path: dropRight(path),
spec: {
children: { $set: newChildren },
splitPercentages: { $set: newPercentages }
}
};
}
} else if (isTabsNode(parent)) {
const newTabs = parent.tabs.filter((_, index) => index !== childIndex);
if (newTabs.length === 0) {
throw new Error("Cannot remove last tab from tabs node");
}
let newActiveTabIndex = parent.activeTabIndex;
if (childIndex === parent.activeTabIndex) {
newActiveTabIndex = childIndex > 0 ? childIndex - 1 : 0;
} else if (childIndex < parent.activeTabIndex) {
newActiveTabIndex = parent.activeTabIndex - 1;
}
return {
path: dropRight(path),
spec: {
tabs: { $set: newTabs },
activeTabIndex: { $set: newActiveTabIndex }
}
};
}
throw new Error("Invalid parent node type for removal");
}
function createDragToUpdates(root, sourcePath, destinationPath, dropInfo) {
if (isEqual(sourcePath, destinationPath)) {
return [];
}
const destinationNode = getAndAssertNodeAtPathExists(root, destinationPath);
const sourceNode = getAndAssertNodeAtPathExists(root, sourcePath);
switch (dropInfo.type) {
case "tab-container":
case "tab-reorder": {
if (!isTabsNode(destinationNode)) {
throw new Error(
`Expected tab container at destination path ${destinationPath.join(", ")}, but found: ${JSON.stringify(destinationNode)}`
);
}
const updates = [];
updates.push(createRemoveUpdate(root, sourcePath));
let adjustedDestinationPath = adjustPathAfterRemoval(
sourcePath,
destinationPath
);
const rootAfterRemoval = updateTree(root, updates);
let destinationAfterRemoval = getNodeAtPath(
rootAfterRemoval,
adjustedDestinationPath
);
if (!destinationAfterRemoval || !isTabsNode(destinationAfterRemoval)) {
let findTabsNodeInTree2 = function(node, currentPath = []) {
if (node && typeof node === "object") {
if (isTabsNode(node) && isTabsNode(destinationNode) && node.tabs && JSON.stringify(node.tabs) === JSON.stringify(destinationNode.tabs)) {
return { node, path: currentPath };
}
if (isSplitNode(node)) {
for (let i = 0; i < node.children.length; i++) {
const result2 = findTabsNodeInTree2(node.children[i], [
...currentPath,
i
]);
if (result2) {
return result2;
}
}
}
}
return null;
};
var findTabsNodeInTree = findTabsNodeInTree2;
const result = findTabsNodeInTree2(rootAfterRemoval);
if (result) {
adjustedDestinationPath = result.path;
destinationAfterRemoval = result.node;
}
}
if (!destinationAfterRemoval || !isTabsNode(destinationAfterRemoval)) {
throw new Error(
`Could not find tabs container after removal. Original path: ${destinationPath.join(", ")}, Adjusted path: ${adjustedDestinationPath.join(", ")}`
);
}
if (dropInfo.type === "tab-reorder") {
const newTabs = [...destinationAfterRemoval.tabs];
newTabs.splice(dropInfo.insertIndex, 0, sourceNode);
updates.push({
path: adjustedDestinationPath,
spec: {
tabs: { $set: newTabs },
activeTabIndex: { $set: dropInfo.insertIndex }
// Make the inserted tab active
}
});
} else {
updates.push({
path: adjustedDestinationPath,
spec: {
tabs: { $push: [sourceNode] },
activeTabIndex: { $set: destinationAfterRemoval.tabs.length }
}
});
}
return updates;
}
case "split": {
const updates = [];
updates.push(createRemoveUpdate(root, sourcePath));
let adjustedDestinationPath = adjustPathAfterRemoval(
sourcePath,
destinationPath
);
const rootAfterRemoval = updateTree(root, updates);
let destinationAfterRemoval = getNodeAtPath(
rootAfterRemoval,
adjustedDestinationPath
);
if (!destinationAfterRemoval) {
for (let i = adjustedDestinationPath.length - 1; i >= 0; i--) {
const shorterPath = adjustedDestinationPath.slice(0, i);
const nodeAtShorterPath = getNodeAtPath(
rootAfterRemoval,
shorterPath
);
if (nodeAtShorterPath) {
adjustedDestinationPath = shorterPath;
destinationAfterRemoval = nodeAtShorterPath;
break;
}
}
if (!destinationAfterRemoval) {
adjustedDestinationPath = [];
destinationAfterRemoval = rootAfterRemoval;
}
}
if (!destinationAfterRemoval) {
console.error(
"Could not find valid destination after removing source from:",
sourcePath,
"original destination was:",
destinationPath
);
throw new Error(
`Could not find valid destination after removing source from [${sourcePath.join(", ")}]`
);
}
let direction = "column";
if (dropInfo.position === MosaicDropTargetPosition.LEFT || dropInfo.position === MosaicDropTargetPosition.RIGHT) {
direction = "row";
}
if (isSplitNode(destinationAfterRemoval) && destinationAfterRemoval.direction === direction) {
const insertIndex = dropInfo.position === MosaicDropTargetPosition.LEFT || dropInfo.position === MosaicDropTargetPosition.TOP ? 0 : destinationAfterRemoval.children.length;
updates.push(
createAddChildUpdate(
adjustedDestinationPath,
sourceNode,
insertIndex
)
);
} else {
let first;
let second;
if (dropInfo.position === MosaicDropTargetPosition.LEFT || dropInfo.position === MosaicDropTargetPosition.TOP) {
first = sourceNode;
second = destinationAfterRemoval;
} else {
first = destinationAfterRemoval;
second = sourceNode;
}
updates.push({
path: adjustedDestinationPath,
spec: {
$set: {
type: "split",
direction,
children: [first, second],
splitPercentages: [50, 50]
}
}
});
}
return updates;
}
default:
assertNever(dropInfo);
}
}
function adjustPathAfterRemoval(removedPath, targetPath) {
if (removedPath.length === 0 || targetPath.length === 0) {
return targetPath;
}
if (isEqual(removedPath, targetPath)) {
return [];
}
const removedParentPath = removedPath.slice(0, -1);
const targetParentPath = targetPath.slice(0, -1);
if (!isEqual(removedParentPath, targetParentPath)) {
return targetPath;
}
const removedIndex = removedPath[removedPath.length - 1];
const targetIndex = targetPath[targetPath.length - 1];
if (typeof removedIndex === "number" && typeof targetIndex === "number") {
if (targetIndex > removedIndex) {
const adjustedPath = [...targetPath];
adjustedPath[adjustedPath.length - 1] = targetIndex - 1;
return adjustedPath;
}
}
return targetPath;
}
function createHideUpdate(root, path) {
if (path.length === 0) {
throw new Error("Cannot hide root node");
}
const parentInfo = getParentAndChildIndex(root, path);
if (!parentInfo) {
throw new Error("Cannot hide node: parent not found");
}
const { parent, childIndex } = parentInfo;
if (isSplitNode(parent)) {
const currentPercentages = parent.splitPercentages || Array(parent.children.length).fill(100 / parent.children.length);
const hidePercentage = currentPercentages[childIndex];
const otherIndices = currentPercentages.map((_, i) => i).filter((i) => i !== childIndex);
if (otherIndices.length === 0) {
throw new Error("Cannot hide the only child of a split node");
}
const redistributeAmount = hidePercentage / otherIndices.length;
const newPercentages = currentPercentages.map((percentage, index) => {
if (index === childIndex) {
return 0;
} else {
return percentage + redistributeAmount;
}
});
return {
path: dropRight(path),
spec: {
splitPercentages: {
$set: newPercentages
}
}
};
} else if (isTabsNode(parent)) {
if (parent.tabs.length <= 1) {
throw new Error("Cannot hide the only tab in a tab container");
}
let newActiveTabIndex = parent.activeTabIndex;
if (childIndex === parent.activeTabIndex) {
newActiveTabIndex = childIndex > 0 ? childIndex - 1 : childIndex + 1;
}
return {
path: dropRight(path),
spec: {
activeTabIndex: { $set: newActiveTabIndex }
}
};
}
throw new Error("Cannot hide node: parent is not a split or tabs node");
}
function createExpandUpdate(path, percentage) {
let spec = {};
for (let i = path.length - 1; i >= 0; i--) {
const childIndex = typeof path[i] === "number" ? path[i] : Number(path[i]);
spec = {
splitPercentages: {
$apply: (currentPercentages) => {
if (!currentPercentages) {
throw new Error(
"Cannot expand: parent node has no splitPercentages defined"
);
}
const newPercentages = [...currentPercentages];
const targetChildPercentage = percentage;
const otherChildrenCount = newPercentages.length - 1;
if (otherChildrenCount === 0) {
return [100];
}
const remainingPercentage = 100 - targetChildPercentage;
const otherChildPercentage = remainingPercentage / otherChildrenCount;
for (let j = 0; j < newPercentages.length; j++) {
if (j === childIndex) {
newPercentages[j] = targetChildPercentage;
} else {
newPercentages[j] = otherChildPercentage;
}
}
return newPercentages;
}
},
children: {
[childIndex]: spec
}
};
}
return {
spec,
path: []
};
}
function createAddChildUpdate(path, newChild, insertIndex) {
return {
path,
spec: {
children: {
$apply: (currentChildren) => {
const newChildren = [...currentChildren];
const index = insertIndex !== void 0 ? insertIndex : newChildren.length;
newChildren.splice(index, 0, newChild);
return newChildren;
}
},
splitPercentages: {
$apply: (currentPercentages) => {
const currentLength = currentPercentages ? currentPercentages.length : 0;
const newLength = currentLength + 1;
const equalPercentage = 100 / newLength;
if (!currentPercentages) {
return Array(newLength).fill(equalPercentage);
}
const newPercentages = Array(newLength).fill(equalPercentage);
return newPercentages;
}
}
}
};
}
function convertToDropInfo(position, tabReorderIndex) {
if (tabReorderIndex !== void 0) {
return { type: "tab-reorder", insertIndex: tabReorderIndex };
} else if (position === void 0) {
return { type: "tab-container" };
} else {
return { type: "split", position };
}
}
export {
buildSpecFromUpdate,
convertToDropInfo,
createAddChildUpdate,
createDragToUpdates,
createExpandUpdate,
createHideUpdate,
createRemoveUpdate,
updateTree
};