UNPKG

@intuitionrobotics/thunderstorm

Version:
287 lines 12 kB
import * as React from 'react'; import {} from 'react'; import { removeItemFromArray } from "@intuitionrobotics/ts-common"; import {} from "./types.js"; import { KeyboardListener } from "../../tools/KeyboardListener.js"; import { stopPropagation } from '../../utils/tools.js'; import { Adapter } from "../adapter/Adapter.js"; import {} from "../adapter/BaseRenderer.js"; export class Tree extends React.Component { static defaultProps = { indentPx: 20, checkExpanded: (expanded, path) => expanded[path] }; containerRefs = {}; rendererRefs = {}; renderedElements = []; constructor(props) { super(props); this.state = { adapter: this.props.adapter, expanded: props.expanded || { "/": true } }; } static getDerivedStateFromProps(props, state) { if (props.adapter.data === state.adapter.data) return null; state.adapter = props.adapter; // Tree.recalculateExpanded(props, state); return state; } componentDidMount() { this.renderedElementsInit(); } renderedElementsInit = () => { const keys = Object.keys(this.state.expanded); this.renderedElements = keys.reduce((carry, key) => { if (this.state.expanded[key]) return carry; if (this.state.adapter.hideRoot) removeItemFromArray(carry, '/'); keys.forEach(el => { if (el.startsWith(key) && el !== key) removeItemFromArray(carry, el); }); return carry; }, keys); }; render() { return React.createElement(KeyboardListener, { id: this.props.id, onKeyboardEventListener: this.keyEventHandler, onFocus: this.onFocus, onBlur: this.onBlur }, this.renderNode(this.state.adapter.data, "", "", 1)); } renderNode = (_data, key, _path, level) => { const nodePath = `${_path}${key}/`; const adjustedNode = this.state.adapter.adjust(_data); const data = adjustedNode.data; let filteredKeys = []; let expanded = !!this.props.checkExpanded(this.state.expanded, nodePath); if (nodePath.endsWith("_children/")) expanded = true; let renderChildren = expanded; if (typeof data !== "object") renderChildren = false; if (renderChildren) filteredKeys = this.state.adapter.getFilteredChildren(data); const nodeRefResolver = this.nodeResolver(nodePath, renderChildren, filteredKeys); const containerRefResolver = this.resolveContainer(nodePath, renderChildren, filteredKeys); return React.createElement("div", { key: nodePath, ref: nodeRefResolver }, this.renderItem(data, nodePath, key, expanded), this.renderChildren(data, nodePath, _path, level, filteredKeys, renderChildren, adjustedNode, containerRefResolver)); }; nodeResolver(nodePath, renderChildren, filteredKeys) { return (_ref) => { if (this.rendererRefs[nodePath]) return; this.rendererRefs[nodePath] = _ref; if (this.containerRefs[nodePath] && renderChildren && filteredKeys.length > 0) this.forceUpdate(); }; } resolveContainer(nodePath, renderChildren, filteredKeys) { return (_ref) => { if (this.containerRefs[nodePath]) return; this.containerRefs[nodePath] = _ref; if (renderChildren && filteredKeys.length > 0) this.forceUpdate(); }; } renderChildren(data, nodePath, _path, level, filteredKeys, renderChildren, adjustedNode, containerRefResolver) { if (!(filteredKeys.length > 0 && renderChildren)) return; const containerRef = this.containerRefs[nodePath]; return (React.createElement("div", { style: this.getChildrenContainerStyle(level, this.rendererRefs[nodePath], containerRef, this.containerRefs[_path]), ref: containerRefResolver }, containerRef && filteredKeys.map((childKey) => this.renderNode(data[childKey], childKey, nodePath + (adjustedNode.deltaPath ? adjustedNode.deltaPath + "/" : ""), level + 1)))); } renderItem(item, path, key, expanded) { if (this.state.adapter.hideRoot && path.length === 1) return null; const TreeNodeRenderer = this.state.adapter.treeNodeRenderer; // console.log("isParent: ", this.state.adapter.isParent(item)); const node = { adapter: this.state.adapter, propKey: key, path, item, expandToggler: this.state.adapter.isParent(item) ? this.toggleExpandState : this.ignoreToggler, onClick: this.onNodeClicked, onFocus: this.onNodeFocused, expanded: !!expanded, focused: path === this.state.focused, selected: item === this.props.selectedItem }; return React.createElement("div", { onMouseEnter: () => this.setState({ focused: node.path }), onMouseLeave: () => this.setState({ focused: '' }) }, React.createElement(TreeNodeRenderer, { item: item, node: node })); } getChildrenContainerStyle = (level, parentNodeRef, containerRef, parentContainerRef) => { if (!containerRef) return {}; if (this.props.childrenContainerStyle) return this.props.childrenContainerStyle(level, parentNodeRef, containerRef, parentContainerRef); return { marginLeft: this.props.indentPx }; }; setFocusedNode(path) { this.rendererRefs[path].scrollIntoView({ block: "nearest" }); this.setState({ focused: path }); } keyEventHandler = (node, e) => { if (this.props.keyEventHandler) this.props.keyEventHandler(node, e); let keyCode = e.code; if (keyCode === "Escape") { stopPropagation(e); return this.props.unMountFromOutside ? this.props.unMountFromOutside() : node.blur(); } const focused = this.state.focused; const idx = this.renderedElements.findIndex(el => el === focused); if (idx >= this.renderedElements.length) return; if (focused && keyCode === "ArrowRight") { stopPropagation(e); if (!this.props.checkExpanded(this.state.expanded, focused)) return this.expandOrCollapse(focused, true); else keyCode = "ArrowDown"; } if (focused && keyCode === "ArrowLeft") { stopPropagation(e); if (this.props.checkExpanded(this.state.expanded, focused)) return this.expandOrCollapse(focused, false); else { const temp = focused.substr(0, focused.length - 1); if (temp.length === 0) return; const parentFocused = temp.substring(0, temp.lastIndexOf("/") + 1); return this.setFocusedNode(parentFocused); } } if (keyCode === "ArrowDown") { stopPropagation(e); if (idx === -1 || idx + 1 === this.renderedElements.length) return this.setFocusedNode(this.renderedElements[0]); return this.setFocusedNode(this.renderedElements[idx + 1]); } if (keyCode === "ArrowUp") { stopPropagation(e); if (idx === -1) return this.setFocusedNode(this.renderedElements[0]); if (idx === 0) return this.setFocusedNode(this.renderedElements[this.renderedElements.length - 1]); return this.setFocusedNode(this.renderedElements[idx - 1]); } if (focused && keyCode === "Enter") { stopPropagation(e); const item = this.getItemByPath(focused); if (item.action && typeof item.action === "function") return item.action(); if (this.props.onNodeClicked) this.props.onNodeClicked(focused, item); } }; getItemByPath(path) { let item = this.state.adapter.data; const hierarchy = path.split('/'); hierarchy.shift(); for (const el of hierarchy) { if (el) { item = item[el]; if (!item) return; } } const deltaPath = this.state.adapter.adjust(item).deltaPath; if (deltaPath) item = item[deltaPath]; return item; } ignoreToggler = () => { }; toggleExpandState = (e, _expanded) => { const path = e.currentTarget.id; this.expandOrCollapse(path, _expanded); }; expandOrCollapse = (path, forceExpandState) => { if (path === "/" && this.state.adapter.hideRoot && forceExpandState === false) return; const treeExpandedState = this.state.expanded; const currentExpandState = treeExpandedState[path]; let newExpandState = currentExpandState === undefined; if (forceExpandState !== undefined) newExpandState = forceExpandState ? forceExpandState : false; if (newExpandState) treeExpandedState[path] = newExpandState; else delete treeExpandedState[path]; this.setState({ focused: path }); this.forceUpdate(); }; onNodeFocused = (e) => { // This is an assumption that we should document somewhere const path = e.currentTarget.id; const item = this.getItemByPath(path); if (this.props.onNodeFocused) this.props.onNodeFocused(path, item); if (this.state.focused === path) return; this.setFocusedNode(path); }; onNodeClicked = (e) => { this.onNodeFocused(e); // This is an assumption that we should document somewhere const path = e.currentTarget.id; const item = this.getItemByPath(path); if (this.props.onNodeClicked) this.props.onNodeClicked(path, item); }; onBlur = () => { if (this.props.onBlur) this.props.onBlur(); this.setState(state => { if (!state.focused) return state; return { ...state, lastFocused: state.focused, focused: '' }; }); }; onFocus = () => { if (this.props.onFocus) this.props.onFocus(); this.setState(state => { const focused = state.lastFocused || (this.state.adapter.hideRoot ? Object.keys(state.expanded)[1] : Object.keys(state.expanded)[0]); if (state.focused === focused) return state; return { ...state, lastFocused: '', focused }; }); }; static recursivelyExpand(adapter, expandCondition = () => true, state = { '/': expandCondition('/', adapter.data, 0, '/') || undefined }) { return recursivelyExpandImpl(adapter.data, state, expandCondition, adapter); } } const recursivelyExpandImpl = (obj, state, condition, adapter, path = "/", level = 1) => { if (obj === null) return state; const _obj = adapter.adjust(obj); const children = adapter.getFilteredChildren(obj); return children.reduce((_state, _key) => { const key = _key; const value = obj[_key]; const newPath = `${path}${key}/`; if (!_obj.deltaPath) { const b = condition(key, value, level, newPath); if (b) _state[newPath] = b; } // if (condition(key, value, level, newPath) && typeof value === "object") if (typeof value === "object") recursivelyExpandImpl(value, _state, condition, adapter, newPath, level + 1); return _state; }, state); }; //# sourceMappingURL=Tree.js.map