predix-ui
Version:
px-* web components as React styled components
491 lines (447 loc) • 16 kB
JavaScript
/* eslint-disable */
import SymbolTree from 'symbol-tree';
export class AssetGraph {
constructor(options) {
/* Save options */
this._options = {};
/* Add default keys */
this._defaultKeys = {
id: 'id',
children: 'children'
};
/* Initialize SymbolTree and prepare its root node */
this._tree = new SymbolTree();
this._rootNode = {
ROOT: true
};
this._symbol = Symbol('AssetGraph data');
}
_node(object) {
const node = object[this._symbol];
if (node) {
return node;
}
return object[this._symbol] = {
isExhausted: null,
isTerminal: null,
isSelectable: null
};
}
_getKey(key, val) {
return val && typeof val === 'string' && val.length ? val : this._defaultKeys[key];
}
/**
* Checks if the node is in the graph.
*
* @param {Object} node
* @return {boolean}
*/
hasNode(node) {
if (this._tree.index(node) > -1) {
return true;
}
return false;
}
getInfo(node, routeKey) {
if (this._tree.index(node) > -1) {
const _routeKey = this._getKey('id', routeKey);
const path = this.getPath(node);
const route = path ? AssetGraph.pathToRoute(path, _routeKey) : null;
const parent = this.getParent(node);
const siblings = this.getSiblings(node);
const children = this.getChildren(node);
const hasChildren = typeof children === 'object' && Array.isArray(children) && children.length > 0;
const isTerminal = this.isTerminal(node);
const isExhausted = this.isExhausted(node);
const isSelectable = this.isSelectable(node);
return {
item: node,
path,
route,
parent,
siblings,
children,
hasChildren,
isTerminal,
isExhausted,
isSelectable
};
}
return null;
}
/**
* Returns a reference to the node's parent. If the node has no parent or is
* not in the graph, returns null.
*
* @param {Object} node
* @return {Object|null}
*/
getParent(node) {
if (node && this._tree.index(node) > -1) {
const parent = this._tree.parent(node);
return parent !== this._rootNode ? parent : null;
}
return null;
}
/**
* This method returns whether or not the passed in item as any siblings.
* @param {Object} node
*/
hasSiblings(node) {
const siblings = this.getSiblings(node);
return siblings && siblings.length > 1;
}
/**
* Returns a reference to the node's siblings (the children of its parent).
* The returned array includes the node.
*
* @param {Object} node
* @return {Array<Object>|null}
*/
getSiblings(node) {
if (node && this._tree.index(node) > -1) {
return this._tree.childrenToArray(this._tree.parent(node));
}
return null;
}
/**
* Returns an array of ancestor nodes from the root of the graph to the requested
* node. The returned array includes the node.
*
* @param {Object} node
* @return {Array<Object>|null}
*/
getPath(node) {
if (node && this._tree.index(node) > -1) {
// reverse so its root->node, slice to remove the virtual root node
return this._tree.ancestorsToArray(node).reverse().slice(1);
}
return null;
}
/**
* Returns an array of unique IDs for each ancestor of the requested node
* starting at the root of the graph and ending with the requested node.
*
* @param {Object} node
* @return {Array<string>|null}
*/
getRoute(node, routeKey) {
if (node && this._tree.index(node) > -1) {
const _routeKey = typeof routeKey === 'string' && routeKey.length ? routeKey : this._defaultKeys.id;
const ancestors = this.getPath(node);
// if (!ancestors) return null;
// const path = [];
// for (let i=0; i<ancestors.length; i++) {
// path.push(typeof ancestors[i][_routeKey] === 'string' && ancestors[i][_routeKey].length ? ancestors[i][_routeKey] : null);
// }
// return path;
return ancestors ? AssetGraph.pathToRoute(ancestors, _routeKey) : null;
}
return null;
}
getNodeAtRoute(route, routeKey) {
if (typeof route !== 'object' || !Array.isArray(route) || !route.length) {
throw new Error('An array of route strings is required.');
}
const _routeKey = typeof routeKey === 'string' && routeKey.length ? routeKey : this._defaultKeys.id;
const searchRoute = route.slice(0);
let items = this._tree.childrenToArray(this._rootNode).slice(0);
let foundItem = null;
while (!foundItem && items.length > 0 && searchRoute.length > 0) {
const item = items.shift();
if (item[_routeKey] === searchRoute[0] && this._tree.childrenCount(item) > 0 && searchRoute.length !== 1) {
searchRoute.shift();
items = this._tree.childrenToArray(item).slice(0);
continue;
}
if (item[_routeKey] === searchRoute[0] && searchRoute.length === 1) {
foundItem = item;
break;
}
}
return foundItem;
}
/**
* Returns a reference to the requested node's children. The returned array
* will be empty if no children are defined.
*
* @param {Object} node
* @return {Array<Object>|null}
*/
getChildren(node) {
const _node = node === null ? this._rootNode : node;
if (_node && (_node.ROOT || this._tree.index(_node) > -1)) {
return this._tree.childrenToArray(_node);
}
return null;
}
/**
* Returns a reference to the root node's children. The returned array will
* be empty if no children are defined.
*
* @return {Array<Object>|null}
*/
getRootChildren() {
const _node = this._rootNode;
if (_node) {
return this._tree.childrenToArray(_node);
}
return null;
}
/**
* Checks if the node has any children.
*
* @param {Object} node
* @return {boolean|null}
*/
hasChildren(node) {
const _node = node === null ? this._rootNode : node;
if (_node && (_node.ROOT || this._tree.index(_node) > -1)) {
return this._tree.childrenCount(_node) > 0;
}
return null;
}
/**
* Adds a child or children to the requested node. Can pass a single object
* to add one child, or an array of objects to add multiple children.
* If `node` is null, the child object(s) will be added to the root of the graph.
*
* @param {Object|null} node
* @param {Object|Array<Object>} children
* @return {Array<Object>|undefined} the updated child array of the node
*/
addChildren(node, children, options) {
if (typeof children !== 'object' || Array.isArray(children) && !children.length) {
throw new Error('A child object or array of child objects is required.');
}
if (node !== null && typeof node === 'object' && !this.hasNode(node)) {
throw new Error('The parent node must be a node in the graph or null.');
}
const parent = node !== null ? node : this._rootNode;
const childArray = Array.isArray(children) ? children : [children];
const childKey = typeof options === 'object' && typeof options.childrenKey === 'string' && options.childrenKey.length ? options.childrenKey : this._defaultKeys.children;
const isRecursive = typeof options === 'object' && typeof options.recursive === 'boolean' ? options.recursive : false;
for (let i = 0; i < childArray.length; i++) {
const info = this._node(childArray[i]);
info.isTerminal = childArray[i].hasOwnProperty('isTerminal') ? childArray[i].isTerminal : null;
info.isExhausted = childArray[i].hasOwnProperty('isExhausted') ? childArray[i].isExhausted : null;
info.isSelectable = childArray[i].hasOwnProperty('isSelectable') ? childArray[i].isSelectable : null;
this._tree.appendChild(parent, childArray[i]);
if (isRecursive && typeof childArray[i][childKey] === 'object' && Array.isArray(childArray[i][childKey]) && childArray[i][childKey].length) {
this.addChildren(childArray[i], childArray[i][childKey], {
recursive: true,
childrenKey: childKey
});
}
}
if (typeof options === 'object' && typeof options.isExhausted === 'boolean') {
const isExhausted = options.isExhausted;
const info = this._node(parent);
info.isExhausted = isExhausted;
}
if (typeof options === 'object' && typeof options.isSelectable === 'boolean') {
const isSelectable = options.isSelectable;
const info = this._node(parent);
info.isSelectable = isSelectable;
}
return this.getChildren(parent);
}
/**
* Removes a child or children from the requested node. Can pass a single object
* by reference to remove one child, or an array of objects by reference to
* remove multiple children. If `node` is null, the child object(s) will be
* removed from the root of the graph.
*
* @param {Object|null} node
* @param {Object|Array<Object>} children
* @return {Array<Object>|undefined} the updated child array of the node
*/
removeChildren(node, children, options) {
if (typeof children !== 'object' || Array.isArray(children) && !children.length) {
throw new Error('A child object or array of child objects is required.');
}
if (node !== null && typeof node === 'object' && !this.hasNode(node)) {
throw new Error('The parent node must be a node in the graph or null.');
}
const parent = node !== null ? node : this._rootNode;
let childArray;
if (children === null) {
childArray = this.getChildren(parent);
} else if (Array.isArray(children)) {
childArray = children;
} else {
childArray = [children];
}
if (!childArray) {
/* Can't figure out how to get the children to remove, give up */
return;
}
for (let i = 0; i < childArray.length; i++) {
if (!this.hasNode(childArray[i])) {
throw new Error('Child node(s) cannot be removed from the graph if it they were never added');
}
if (node !== null && this.getParent(childArray[i]) !== parent || node == null && this.getParent(childArray[i]) !== null) {
throw new Error('Child node(s) passed to "removeChildren" method must be children of the given parent');
}
this._tree.remove(childArray[i]);
}
}
isExhausted(node) {
const _node = node === null ? this._rootNode : node;
if (_node && (_node.ROOT || this._tree.index(_node) > -1)) {
const info = this._node(_node);
return !!(info && info.isExhausted === true);
}
return null;
}
setExhausted(node, isExhausted) {
const _node = node === null ? this._rootNode : node;
if (_node && (_node.ROOT || this._tree.index(_node) > -1)) {
const info = this._node(_node);
info.isExhausted = isExhausted;
return isExhausted;
}
return null;
}
isTerminal(node) {
if (node === null) {
// The root node can never be terminal, it must have children
return false;
}
if (this._tree.index(node) > -1) {
const info = this._node(node);
return !!(info && info.isTerminal === true);
}
return null;
}
setTerminal(node, isTerminal) {
if (node === null) {
// The root node can never be terminal, it must have children
throw new Error('The root node can never be terminal, it must have children.');
}
if (this._tree.index(node) > -1) {
const info = this._node(node);
info.isTerminal = isTerminal;
return isTerminal;
}
return null;
}
isSelectable(node) {
const _node = node === null ? this._rootNode : node;
if (_node && (_node.ROOT || this._tree.index(_node) > -1)) {
const info = this._node(_node);
// isSelectable defaults to `true`, if a node was not explicitly
// marked `isSelectable:false` then it is selectable
if (info && info.isSelectable === false) {
return false;
}
return true;
}
return null;
}
setSelectable(node, isSelectable) {
const _node = node === null ? this._rootNode : node;
if (_node && (_node.ROOT || this._tree.index(_node) > -1)) {
const info = this._node(_node);
info.isSelectable = isSelectable;
return isSelectable;
}
return null;
}
static pathToRoute(path, routeKey) {
return path.map(p => (typeof p[routeKey] === 'string' && p[routeKey].length ? p[routeKey] : null));
}
}
export class AssetTree {
constructor() {
this.keys = {
id: 'id',
label: 'label',
children: 'children',
icon: 'icon'
};
this.items = [];
this.__rootItems = [];
this._assetGraph = {};
this._assetGraph = new AssetGraph();
}
fire(event, data) {
console.log('fire', event, data, ...arguments);
}
/**
* Adds a child or children to the requested node. Pass a single object
* to add one child, or an array of objects to add multiple children.
*
* The `node` should be a direct reference to one of the objects already
* in the asset graph (e.g. one of the `items` objects or another node
* added through the `addChildren` API). To remove children from the root
* of the graph, call with `node` as null.
*
* @param {Object|null} node
* @param {Object|Array<Object>} children
*/
addChildren(node, children, options) {
if (this._assetGraph !== null) {
this._assetGraph.addChildren(node, children, Object.assign({}, {
recursive: true,
childrenKey: this.keys.children
}, options || {}));
if (node === null) {
const childrenArray = Array.isArray(children) ? children : [children];
this.__rootItems = this.__rootItems.concat(childrenArray);
}
this.fire('px-app-asset-children-updated', node === null ? {
item: null,
added: children,
children: this.__rootItems
} : Object.assign({}, this._assetGraph.getInfo(node), {
added: children
}));
}
}
/**
* Removes a child or children from the requested node. Pass `children` a
* single object to remove one child, an array of objects to remove multiple
* children, or null to remove all children.
*
* The `node` should be a direct reference to one of the objects already
* in the asset graph (e.g. one of the `items` objects or another node
* added through the `addChildren` API). To add children to the root
* of the graph, call with `node` as null.
*
* @param {Object|null} node
* @param {Object|Array<Object>} children
*/
removeChildren(node, children, options) {
if (this._assetGraph !== null) {
const childrenArray = Array.isArray(children) ? children : [children];
if (typeof this.activate === 'function' || typeof this.select === 'function') {
// Deactivate or deselect if the active/selected items are in the path
// of one of the removed items
let deactivated;
let deselected;
for (let i = 0; i < childrenArray.length; i++) {
if (!deactivated && typeof this.activate === 'function' && this.activeMeta && (this.activeMeta.item === childrenArray[i] || this.activeMeta.path && this.activeMeta.path.indexOf(childrenArray[i]) > -1)) {
this.activate(null);
deactivated = true;
}
if (!deselected && typeof this.select === 'function' && this.selectedMeta && (this.selectedMeta.item === childrenArray[i] || this.selectedMeta.path && this.selectedMeta.path.indexOf(childrenArray[i]) > -1)) {
this.select(null);
deactivated = true;
}
}
}
this._assetGraph.removeChildren(node, children, options);
if (node === null) {
this.__rootItems = this.__rootItems.filter(item => childrenArray.indexOf(item) === -1);
}
this.fire('px-app-asset-children-updated', node === null ? {
item: null,
removed: children,
children: this.__rootItems
} : Object.assign({}, this._assetGraph.getInfo(node), {
removed: children
}));
}
}
}