| // Copyright (C) 2023 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import m from 'mithril'; |
| import {classNames} from '../base/classnames'; |
| import {hasChildren} from '../base/mithril_utils'; |
| import {scheduleFullRedraw} from './raf'; |
| |
| // Heirachical tree layout with left and right values. |
| // Right and left values of the same indentation level are horizontally aligned. |
| // Example: |
| // foo bar |
| // ├ baz qux |
| // └ quux corge |
| // ├ looong_left aaa |
| // └ a bbb |
| // grault garply |
| |
| interface TreeAttrs { |
| // Space delimited class list applied to our tree element. |
| className?: string; |
| } |
| |
| export class Tree implements m.ClassComponent<TreeAttrs> { |
| view({attrs, children}: m.Vnode<TreeAttrs>): m.Children { |
| const {className = ''} = attrs; |
| |
| const classes = classNames(className); |
| |
| return m('.pf-tree', {class: classes}, children); |
| } |
| } |
| |
| interface TreeNodeAttrs { |
| // Content to display in the left hand column. |
| // If omitted, this side will be blank. |
| left?: m.Children; |
| // Content to display in the right hand column. |
| // If omitted, this side will be left blank. |
| right?: m.Children; |
| // Content to display in the right hand column when the node is collapsed. |
| // If omitted, the value of `right` shall be shown when collapsed instead. |
| // If the node has no children, this value is never shown. |
| summary?: m.Children; |
| // Whether this node is collapsed or not. |
| // If omitted, collapsed state 'uncontrolled' - i.e. controlled internally. |
| collapsed?: boolean; |
| // Whether the node should start collapsed or not, default: false. |
| startsCollapsed?: boolean; |
| loading?: boolean; |
| showCaret?: boolean; |
| // Optional icon to show to the left of the text. |
| // If this node contains children, this icon is ignored. |
| icon?: string; |
| // Called when the collapsed state is changed, mainly used in controlled mode. |
| onCollapseChanged?: (collapsed: boolean, attrs: TreeNodeAttrs) => void; |
| } |
| |
| export class TreeNode implements m.ClassComponent<TreeNodeAttrs> { |
| private collapsed; |
| |
| constructor({attrs}: m.CVnode<TreeNodeAttrs>) { |
| this.collapsed = attrs.startsCollapsed ?? false; |
| } |
| |
| view(vnode: m.CVnode<TreeNodeAttrs>): m.Children { |
| const { |
| children, |
| attrs, |
| attrs: {left, onCollapseChanged = () => {}}, |
| } = vnode; |
| return [ |
| m( |
| '.pf-tree-node', |
| { |
| class: classNames(this.getClassNameForNode(vnode)), |
| }, |
| m( |
| '.pf-tree-left', |
| m('span.pf-tree-gutter', { |
| onclick: () => { |
| this.collapsed = !this.isCollapsed(vnode); |
| onCollapseChanged(this.collapsed, attrs); |
| scheduleFullRedraw(); |
| }, |
| }), |
| left, |
| ), |
| this.renderRight(vnode), |
| ), |
| hasChildren(vnode) && m('.pf-tree-children', children), |
| ]; |
| } |
| |
| private getClassNameForNode(vnode: m.CVnode<TreeNodeAttrs>) { |
| const {loading = false, showCaret = false} = vnode.attrs; |
| if (loading) { |
| return 'pf-loading'; |
| } else if (hasChildren(vnode) || showCaret) { |
| if (this.isCollapsed(vnode)) { |
| return 'pf-collapsed'; |
| } else { |
| return 'pf-expanded'; |
| } |
| } else { |
| return undefined; |
| } |
| } |
| |
| private renderRight(vnode: m.CVnode<TreeNodeAttrs>) { |
| const { |
| attrs: {right, summary}, |
| } = vnode; |
| if (hasChildren(vnode) && this.isCollapsed(vnode)) { |
| return m('.pf-tree-right', summary ?? right); |
| } else { |
| return m('.pf-tree-right', right); |
| } |
| } |
| |
| private isCollapsed({attrs}: m.Vnode<TreeNodeAttrs>): boolean { |
| // If collapsed is omitted, use our local collapsed state instead. |
| const {collapsed = this.collapsed} = attrs; |
| |
| return collapsed; |
| } |
| } |
| |
| export function dictToTreeNodes(dict: {[key: string]: m.Child}): m.Child[] { |
| const children: m.Child[] = []; |
| for (const key of Object.keys(dict)) { |
| if (dict[key] == undefined) { |
| continue; |
| } |
| children.push( |
| m(TreeNode, { |
| left: key, |
| right: dict[key], |
| }), |
| ); |
| } |
| return children; |
| } |
| |
| // Create a flat tree from a POJO |
| export function dictToTree(dict: {[key: string]: m.Child}): m.Children { |
| return m(Tree, dictToTreeNodes(dict)); |
| } |
| interface LazyTreeNodeAttrs { |
| // Same as TreeNode (see above). |
| left?: m.Children; |
| // Same as TreeNode (see above). |
| right?: m.Children; |
| // Same as TreeNode (see above). |
| icon?: string; |
| // Same as TreeNode (see above). |
| summary?: m.Children; |
| // A callback to be called when the TreeNode is expanded, in order to fetch |
| // child nodes. |
| // The callback must return a promise to a function which returns m.Children. |
| // The reason the promise must return a function rather than the actual |
| // children is to avoid storing vnodes between render cycles, which is a bug |
| // in Mithril. |
| fetchData: () => Promise<() => m.Children>; |
| // Whether to unload children on collapse. |
| // Defaults to false, data will be kept in memory until the node is destroyed. |
| unloadOnCollapse?: boolean; |
| } |
| |
| // This component is a TreeNode which only loads child nodes when it's expanded. |
| // This allows us to represent huge trees without having to load all the data |
| // up front, and even allows us to represent infinite or recursive trees. |
| export class LazyTreeNode implements m.ClassComponent<LazyTreeNodeAttrs> { |
| private collapsed: boolean = true; |
| private loading: boolean = false; |
| private renderChildren?: () => m.Children; |
| |
| view({attrs}: m.CVnode<LazyTreeNodeAttrs>): m.Children { |
| const { |
| left, |
| right, |
| icon, |
| summary, |
| fetchData, |
| unloadOnCollapse = false, |
| } = attrs; |
| |
| return m( |
| TreeNode, |
| { |
| left, |
| right, |
| icon, |
| summary, |
| showCaret: true, |
| loading: this.loading, |
| collapsed: this.collapsed, |
| onCollapseChanged: (collapsed) => { |
| if (collapsed) { |
| if (unloadOnCollapse) { |
| this.renderChildren = undefined; |
| } |
| } else { |
| // Expanding |
| if (this.renderChildren) { |
| this.collapsed = false; |
| scheduleFullRedraw(); |
| } else { |
| this.loading = true; |
| fetchData().then((result) => { |
| this.loading = false; |
| this.collapsed = false; |
| this.renderChildren = result; |
| scheduleFullRedraw(); |
| }); |
| } |
| } |
| this.collapsed = collapsed; |
| scheduleFullRedraw(); |
| }, |
| }, |
| this.renderChildren && this.renderChildren(), |
| ); |
| } |
| } |