Something went wrong on our end
Select Git revision
buildtree.mjs
-
Volker Schukai authoredVolker Schukai authored
buildtree.mjs 7.41 KiB
/**
* Copyright schukai GmbH and contributors 2023. All Rights Reserved.
* Node module: @schukai/monster
* This file is licensed under the AGPLv3 License.
* License text available at https://www.gnu.org/licenses/agpl-3.0.en.html
*/
import { isArray, isObject } from "../types/is.mjs";
import { Node } from "../types/node.mjs";
import { NodeList } from "../types/nodelist.mjs";
import { assembleParts } from "./buildmap.mjs";
import { extend } from "./extend.mjs";
export { buildTree };
/**
* @private
* @type {symbol}
*/
const parentSymbol = Symbol("parent");
/**
* @private
* @type {symbol}
*/
const rootSymbol = Symbol("root");
/**
* @typedef {Object} buildTreeOptions
* @property {array} options.rootReferences=[null, undefined] defines the values for elements without parents
* @property {Monster.Data~exampleFilterCallback} options.filter filtering of the values
* @memberOf Monster.Data
*/
/**
* Creates a tree structure from a given subject using a selector and specified ID and parent ID keys.
*
* The buildTree function is a powerful tool for creating tree-like data structures from plain JavaScript
* objects. It takes in four required parameters: the subject object that you want to turn into a tree, a
* selector that identifies which parts of the subject to use when building the tree, and two keys
* (idKey and parentIDKey) that specify which properties in the subject represent the unique identifiers
* and parent-child relationships between nodes in the tree.
*
* Optionally, you can also pass in an options object to further configure the behavior of the function,
* such as specifying which values should be treated as roots of the tree, or providing a custom filter
* function to only include certain nodes in the final output.
*
* The buildTree function works by first using the assembleParts helper function to extract the relevant
* parts of the subject based on the selector, and then iterates over the resulting map to create Node
* objects and organize them into parent-child relationships based on the values of the idKey and parentIDKey properties.
*
* The resulting NodeList represents the tree structure, with each Node object containing the original
* object data as well as additional metadata about its position in the tree. You can then use the childNodes
* property of each Node to access its children, or the parent property to access its parent.
*
* Overall, the buildTree function is a flexible and powerful way to transform flat data into hierarchical
* structures, and can be especially useful in scenarios such as displaying folder structures or
* visualizing complex data relationships.
*
* Let's say you have an array of data objects representing a file system directory structure, and you want
* to turn it into a tree-like structure where each node represents a folder or file, and child nodes
* represent the contents of the folder:
*
* ```javascript
* const fileSystem = [
* { id: 'folder1', name: 'Folder 1', type: 'folder', parent: null },
* { id: 'file1', name: 'File 1', type: 'file', parent: 'folder1' },
* { id: 'file2', name: 'File 2', type: 'file', parent: 'folder1' },
* { id: 'subfolder1', name: 'Subfolder 1', type: 'folder', parent: 'folder1' },
* { id: 'file3', name: 'File 3', type: 'file', parent: 'subfolder1' },
* { id: 'file4', name: 'File 4', type: 'file', parent: 'subfolder1' },
* { id: 'subfolder2', name: 'Subfolder 2', type: 'folder', parent: 'folder1' },
* { id: 'file5', name: 'File 5', type: 'file', parent: 'subfolder2' },
* { id: 'file6', name: 'File 6', type: 'file', parent: 'subfolder2' },
* { id: 'folder2', name: 'Folder 2', type: 'folder', parent: null },
* { id: 'file7', name: 'File 7', type: 'file', parent: 'folder2' },
* { id: 'file8', name: 'File 8', type: 'file', parent: 'folder2' },
* { id: 'subfolder3', name: 'Subfolder 3', type: 'folder', parent: 'folder2' },
* { id: 'file9', name: 'File 9', type: 'file', parent: 'subfolder3' },
* { id: 'file10', name: 'File 10', type: 'file', parent: 'subfolder3' },
* ];
*
* const tree = buildTree(fileSystem, 'id', 'id', 'parent', { rootReferences: [null] });
*
* console.log(tree.toString());
* ```
*
* The buildTree function takes in the array of data objects, as well as some configuration options specifying
* the keys to use for identifying nodes and their parent-child relationships. In this example, we use the id
* key to identify nodes, and the parent key to specify the parent of each node.
*
* The resulting tree object is a nested tree structure, where each node is an object representing a file or
* folder, and has child nodes representing its contents. The toString method of the tree object
* can be used to print out the tree in a readable format:
*
* ```markdown
* - Folder 1
* - File 1
* - File 2
* - Subfolder 1
* - File 3
* - File 4
* - Subfolder 2
* - File 5
* - File 6
* - Folder 2
* - File 7
* - File 8
* - Subfolder 3
* - File 9
* - File 10
* ```
*
* @memberof Monster.Data
*
* @param {*} subject - The object or array to build the tree from.
* @param {string|Monster.Data~exampleSelectorCallback} selector - Either a string to specify a property of each object to use as a selector, or a selector function to generate a map of objects.
* @param {string} idKey - The property key to use as the unique ID of each node.
* @param {string} parentIDKey - The property key to use as the parent ID of each node.
* @param {object} [options] - Additional options to modify the function behavior.
* @param {Array<*>} [options.rootReferences=[null, undefined]] - An array of values to treat as root references when creating the tree.
* @param {function} [options.filter] - A filter function to apply to each node.
*
* @return {*} The resulting tree structure as a NodeList.
*
* @throws {TypeError} selector is neither a string nor a function.
* @throws {TypeError} the selector callback must return a map.
* @throws {Error} the object has no value for the specified id.
*
* @license AGPLv3
*
* @since 1.26.0
*/
function buildTree(subject, selector, idKey, parentIDKey, options) {
const nodes = new Map();
if (!isObject(options)) {
options = {};
}
options = extend(
{},
{
rootReferences: [null, undefined],
filter: undefined,
},
options,
);
const filter = options?.filter;
let rootReferences = options.rootReferences;
if (!isArray(rootReferences)) {
rootReferences = [rootReferences];
}
const childMap = assembleParts(subject, selector, filter, function (o, k, m) {
const key = o?.[idKey];
let ref = o?.[parentIDKey];
if (rootReferences.indexOf(ref) !== -1) ref = rootSymbol;
if (key === undefined) {
throw new Error("the object has no value for the specified id");
}
o[parentSymbol] = ref;
const node = new Node(o);
this.has(ref) ? this.get(ref).add(node) : this.set(ref, new NodeList().add(node));
nodes.set(key, node);
});
nodes.forEach((node) => {
let id = node?.["value"]?.[idKey];
if (childMap.has(id)) {
node.childNodes = childMap.get(id);
childMap.delete(id);
}
});
const list = new NodeList();
childMap.forEach((s) => {
if (s instanceof Set) {
s.forEach((n) => {
list.add(n);
});
}
});
return list;
}