/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */ /* eslint-env browser */ "use strict";
/** * A fast, generic, expandable and collapsible tree component. * * This tree component is fast: it can handle trees with *many* items. It only * renders the subset of those items which are visible in the viewport. It's * been battle tested on huge trees in the memory panel. We've optimized tree * traversal and rendering, even in the presence of cross-compartment wrappers. * * This tree component doesn't make any assumptions about the structure of your * tree data. Whether children are computed on demand, or stored in an array in * the parent's `_children` property, it doesn't matter. We only require the * implementation of `getChildren`, `getRoots`, `getParent`, and `isExpanded` * functions. * * This tree component is well tested and reliable. See * devtools/client/shared/components/test/mochitest/test_tree_* and its usage in * the performance and memory panels. * * This tree component doesn't make any assumptions about how to render items in * the tree. You provide a `renderItem` function, and this component will ensure * that only those items whose parents are expanded and which are visible in the * viewport are rendered. The `renderItem` function could render the items as a * "traditional" tree or as rows in a table or anything else. It doesn't * restrict you to only one certain kind of tree. * * The only requirement is that every item in the tree render as the same * height. This is required in order to compute which items are visible in the * viewport in constant time. * * ### Example Usage * * Suppose we have some tree data where each item has this form: * * { * id: Number, * label: String, * parent: Item or null, * children: Array of child items, * expanded: bool, * } * * Here is how we could render that data with this component: * * class MyTree extends Component { * static get propTypes() { * // The root item of the tree, with the form described above. * return { * root: PropTypes.object.isRequired * }; * } * * render() { * return Tree({ * itemHeight: 20, // px * * getRoots: () => [this.props.root], * * getParent: item => item.parent, * getChildren: item => item.children, * getKey: item => item.id, * isExpanded: item => item.expanded, * * renderItem: (item, depth, isFocused, arrow, isExpanded) => { * let className = "my-tree-item"; * if (isFocused) { * className += " focused"; * } * return dom.div( * { * className, * // Apply 10px nesting per expansion depth. * style: { marginLeft: depth * 10 + "px" } * }, * // Here is the expando arrow so users can toggle expansion and * // collapse state. * arrow, * // And here is the label for this item. * dom.span({ className: "my-tree-item-label" }, item.label) * ); * }, * * onExpand: item => dispatchExpandActionToRedux(item), * onCollapse: item => dispatchCollapseActionToRedux(item), * }); * } * }
*/ class Tree extends Component { static get propTypes() { return { // Required props
// A function to get an item's parent, or null if it is a root. // // Type: getParent(item: Item) -> Maybe<Item> // // Example: // // // The parent of this item is stored in its `parent` property. // getParent: item => item.parent
getParent: PropTypes.func.isRequired,
// A function to get an item's children. // // Type: getChildren(item: Item) -> [Item] // // Example: // // // This item's children are stored in its `children` property. // getChildren: item => item.children
getChildren: PropTypes.func.isRequired,
// A function which takes an item and ArrowExpander component instance and // returns a component, or text, or anything else that React considers // renderable. // // Type: renderItem(item: Item, // depth: Number, // isFocused: Boolean, // arrow: ReactComponent, // isExpanded: Boolean) -> ReactRenderable // // Example: // // renderItem: (item, depth, isFocused, arrow, isExpanded) => { // let className = "my-tree-item"; // if (isFocused) { // className += " focused"; // } // return dom.div( // { // className, // style: { marginLeft: depth * 10 + "px" } // }, // arrow, // dom.span({ className: "my-tree-item-label" }, item.label) // ); // },
renderItem: PropTypes.func.isRequired,
// A function which returns the roots of the tree (forest). // // Type: getRoots() -> [Item] // // Example: // // // In this case, we only have one top level, root item. You could // // return multiple items if you have many top level items in your // // tree. // getRoots: () => [this.props.rootOfMyTree]
getRoots: PropTypes.func.isRequired,
// A function to get a unique key for the given item. This helps speed up // React's rendering a *TON*. // // Type: getKey(item: Item) -> String // // Example: // // getKey: item => `my-tree-item-${item.uniqueId}`
getKey: PropTypes.func.isRequired,
// A function to get whether an item is expanded or not. If an item is not // expanded, then it must be collapsed. // // Type: isExpanded(item: Item) -> Boolean // // Example: // // isExpanded: item => item.expanded,
isExpanded: PropTypes.func.isRequired,
// The height of an item in the tree including margin and padding, in // pixels.
itemHeight: PropTypes.number.isRequired,
// Optional props
// The currently focused item, if any such item exists.
focused: PropTypes.any,
// Handle when a new item is focused.
onFocus: PropTypes.func,
// The currently active (keyboard) item, if any such item exists.
active: PropTypes.any,
// Handle when item is activated with a keyboard (using Space or Enter)
onActivate: PropTypes.func,
// The currently shown item, if any such item exists.
shown: PropTypes.any,
// Indicates if pressing ArrowRight key should only expand expandable node // or if the selection should also move to the next node.
preventNavigationOnArrowRight: PropTypes.bool,
// The depth to which we should automatically expand new items.
autoExpandDepth: PropTypes.number,
// Note: the two properties below are mutually exclusive. Only one of the // label properties is necessary. // ID of an element whose textual content serves as an accessible label for // a tree.
labelledby: PropTypes.string, // Accessibility label for a tree widget.
label: PropTypes.string,
// Optional event handlers for when items are expanded or collapsed. Useful // for dispatching redux events and updating application state, maybe lazily // loading subtrees from a worker, etc. // // Type: // onExpand(item: Item) // onCollapse(item: Item) // // Example: // // onExpand: item => dispatchExpandActionToRedux(item)
onExpand: PropTypes.func,
onCollapse: PropTypes.func,
};
}
_autoExpand() { if (!this.props.autoExpandDepth) { return;
}
// Automatically expand the first autoExpandDepth levels for new items. Do // not use the usual DFS infrastructure because we don't want to ignore // collapsed nodes. const autoExpand = (item, currentDepth) => { if (
currentDepth >= this.props.autoExpandDepth || this.state.seen.has(item)
) { return;
}
if (expandAllChildren) { const children = this._dfs(item); const length = children.length; for (let i = 0; i < length; i++) { this.props.onExpand(children[i].item);
}
}
}
}
/** * Collapses current row. * * @param {Object} item
*/
_onCollapse(item) { if (this.props.onCollapse) { this.props.onCollapse(item);
}
}
/** * Scroll item into view. Depending on whether the item is already rendered, * we might have to calculate the position of the item based on its index and * the item height. * * @param {Object} item * The item to be scrolled into view. * @param {Number|undefined} index * The index of the item in a full DFS traversal (ignoring collapsed * nodes) or undefined. * @param {Object} options * Optional information regarding item's requested alignement when * scrolling.
*/
_scrollIntoView(item, index, options = {}) { const treeElement = this.refs.tree; if (!treeElement) { return;
}
const element = document.getElementById(this.props.getKey(item)); if (element) {
scrollIntoView(element, { ...options, container: treeElement }); return;
}
if (index == null) { // If index is not provided, determine item index from traversal. const traversal = this._dfsFromRoots();
index = traversal.findIndex(({ item: i }) => i === item);
}
/** * Sets the passed in item to be the focused item. * * @param {Number} index * The index of the item in a full DFS traversal (ignoring collapsed * nodes). Ignored if `item` is undefined. * * @param {Object|undefined} item * The item to be focused, or undefined to focus no item.
*/
_focus(index, item, options = {}) { if (item !== undefined && !options.preventAutoScroll) { this._scrollIntoView(item, index, options);
}
if (this.props.active != null) { this._activate(null); if (this.refs.tree !== this.activeElement) { this.refs.tree.focus();
}
}
if (this.props.onFocus) { this.props.onFocus(item);
}
}
_activate(item) { if (this.props.onActivate) { this.props.onActivate(item);
}
}
/** * Update state height and tree's scrollTop if necessary.
*/
_onResize() { // When tree size changes without direct user action, scroll top cat get re-set to 0 // (for example, when tree height changes via CSS rule change). We need to ensure that // the tree's scrollTop is in sync with the scroll state. if (this.state.scroll !== this.refs.tree.scrollTop) { this.refs.tree.scrollTo({ left: 0, top: this.state.scroll });
}
this._updateHeight();
}
/** * Fired on a scroll within the tree's container, updates * the stored position of the view port to handle virtual view rendering.
*/
_onScroll() { this.setState({
scroll: Math.max(this.refs.tree.scrollTop, 0),
height: this.refs.tree.clientHeight,
});
}
/** * Handles key down events in the tree's container. * * @param {Event} e
*/ // eslint-disable-next-line complexity
_onKeyDown(e) { if (this.props.focused == null) { return;
}
// Allow parent nodes to use navigation arrows with modifiers. if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) { return;
}
case"Enter": case" ": // On space or enter make focused tree node active. This means keyboard focus // handling is passed on to the tree node itself. if (this.refs.tree === this.activeElement) {
preventDefaultAndStopPropagation(e); if (this.props.active !== this.props.focused) { this._activate(this.props.focused);
}
} break;
case"Escape":
preventDefaultAndStopPropagation(e); if (this.props.active != null) { this._activate(null);
}
if (this.refs.tree !== this.activeElement) { this.refs.tree.focus();
} break;
}
}
get activeElement() { returnthis.refs.tree.ownerDocument.activeElement;
}
/** * Sets the previous node relative to the currently focused item, to focused.
*/
_focusPrevNode() { // Start a depth first search and keep going until we reach the currently // focused node. Focus the previous node in the DFS, if it exists. If it // doesn't exist, we're at the first node already.
let prev;
let prevIndex;
const traversal = this._dfsFromRoots(); const length = traversal.length; for (let i = 0; i < length; i++) { const item = traversal[i].item; if (item === this.props.focused) { break;
}
prev = item;
prevIndex = i;
}
/** * Handles the down arrow key which will focus either the next child * or sibling row.
*/
_focusNextNode() { // Start a depth first search and keep going until we reach the currently // focused node. Focus the next node in the DFS, if it exists. If it // doesn't exist, we're at the last node already.
const traversal = this._dfsFromRoots(); const length = traversal.length;
let i = 0;
while (i < length) { if (traversal[i].item === this.props.focused) { break;
}
i++;
}
if (i + 1 < traversal.length) { this._focus(i + 1, traversal[i + 1].item, { alignTo: "bottom" });
}
}
/** * Handles the left arrow key, going back up to the current rows' * parent row.
*/
_focusParentNode() { const parent = this.props.getParent(this.props.focused); if (!parent) { return;
}
const traversal = this._dfsFromRoots(); const length = traversal.length;
let parentIndex = 0; for (; parentIndex < length; parentIndex++) { if (traversal[parentIndex].item === parent) { break;
}
}
// 'begin' and 'end' are the index of the first (at least partially) visible item // and the index after the last (at least partially) visible item, respectively. // `NUMBER_OF_OFFSCREEN_ITEMS` is removed from `begin` and added to `end` so that // the top and bottom of the page are filled with the `NUMBER_OF_OFFSCREEN_ITEMS` // previous and next items respectively, which helps the user to see fewer empty // gaps when scrolling quickly. const { itemHeight, active, focused } = this.props; const { scroll, height } = this.state; const begin = Math.max(
((scroll / itemHeight) | 0) - NUMBER_OF_OFFSCREEN_ITEMS,
0
); const end =
Math.ceil((scroll + height) / itemHeight) + NUMBER_OF_OFFSCREEN_ITEMS; const toRender = traversal.slice(begin, end); const topSpacerHeight = begin * itemHeight; const bottomSpacerHeight = Math.max(traversal.length - end, 0) * itemHeight;
for (let i = 0; i < toRender.length; i++) { const index = begin + i; const first = index == 0; const last = index == traversal.length - 1; const { item, depth } = toRender[i]; const key = this.props.getKey(item);
nodes.push(
TreeNode({ // We make a key unique depending on whether the tree node is in active or // inactive state to make sure that it is actually replaced and the tabbable // state is reset.
key: `${key}-${active === item ? "active" : "inactive"}`,
index,
first,
last,
item,
depth,
id: key,
renderItem: this.props.renderItem,
focused: focused === item,
active: active === item,
expanded: this.props.isExpanded(item),
hasChildren: !!this.props.getChildren(item).length,
onExpand: this._onExpand,
onCollapse: this._onCollapse, // Since the user just clicked the node, there's no need to check if // it should be scrolled into view.
onClick: () => this._focus(begin + i, item, { preventAutoScroll: true }),
})
);
}
// Only set default focus to the first tree node if focused node is // not yet set and the focus event is not the result of a mouse // interarction. this._focus(begin, toRender[0].item);
},
onBlur: e => { if (active != null) { const { relatedTarget } = e; if (!this.refs.tree.contains(relatedTarget)) { this._activate(null);
}
}
},
onClick: () => { // Focus should always remain on the tree container itself. this.refs.tree.focus();
}, "aria-label": this.props.label, "aria-labelledby": this.props.labelledby, "aria-activedescendant": focused && this.props.getKey(focused),
style: {
padding: 0,
margin: 0,
},
},
nodes
);
}
}
/** * An arrow that displays whether its node is expanded (▼) or collapsed * (▶). When its node has no children, it is hidden.
*/ class ArrowExpanderClass extends Component { static get propTypes() { return {
item: PropTypes.any.isRequired,
visible: PropTypes.bool.isRequired,
expanded: PropTypes.bool.isRequired,
onCollapse: PropTypes.func.isRequired,
onExpand: PropTypes.func.isRequired,
};
}
componentDidMount() { // Make sure that none of the focusable elements inside the tree node container are // tabbable if the tree node is not active. If the tree node is active and focus is // outside its container, focus on the first focusable element inside. const elms = getFocusableElements(this.refs.treenode); if (elms.length === 0) { return;
}
if (!this.props.active) {
elms.forEach(elm => elm.setAttribute("tabindex", "-1")); return;
}
if (!elms.includes(this.refs.treenode.ownerDocument.activeElement)) {
elms[0].focus();
}
}
const focusMoved = !!wrapMoveFocus(
getFocusableElements(this.refs.treenode),
target,
shiftKey
); if (focusMoved) { // Focus was moved to the begining/end of the list, so we need to prevent the // default focus change that would happen here.
e.preventDefault();
}
/** * Create a function that calls the given function `fn` only once per animation * frame. * * @param {Function} fn * @returns {Function}
*/ function oncePerAnimationFrame(fn) {
let animationId = null;
let argsToPass = null; returnfunction (...args) {
argsToPass = args; if (animationId !== null) { return;
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.