1*6dbdd20aSAndroid Build Coastguard Worker// Copyright (C) 2023 The Android Open Source Project 2*6dbdd20aSAndroid Build Coastguard Worker// 3*6dbdd20aSAndroid Build Coastguard Worker// Licensed under the Apache License, Version 2.0 (the "License"); 4*6dbdd20aSAndroid Build Coastguard Worker// you may not use this file except in compliance with the License. 5*6dbdd20aSAndroid Build Coastguard Worker// You may obtain a copy of the License at 6*6dbdd20aSAndroid Build Coastguard Worker// 7*6dbdd20aSAndroid Build Coastguard Worker// http://www.apache.org/licenses/LICENSE-2.0 8*6dbdd20aSAndroid Build Coastguard Worker// 9*6dbdd20aSAndroid Build Coastguard Worker// Unless required by applicable law or agreed to in writing, software 10*6dbdd20aSAndroid Build Coastguard Worker// distributed under the License is distributed on an "AS IS" BASIS, 11*6dbdd20aSAndroid Build Coastguard Worker// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12*6dbdd20aSAndroid Build Coastguard Worker// See the License for the specific language governing permissions and 13*6dbdd20aSAndroid Build Coastguard Worker// limitations under the License. 14*6dbdd20aSAndroid Build Coastguard Worker 15*6dbdd20aSAndroid Build Coastguard Workerimport m from 'mithril'; 16*6dbdd20aSAndroid Build Coastguard Workerimport {classNames} from '../base/classnames'; 17*6dbdd20aSAndroid Build Coastguard Workerimport {hasChildren} from '../base/mithril_utils'; 18*6dbdd20aSAndroid Build Coastguard Workerimport {scheduleFullRedraw} from './raf'; 19*6dbdd20aSAndroid Build Coastguard Worker 20*6dbdd20aSAndroid Build Coastguard Worker// Heirachical tree layout with left and right values. 21*6dbdd20aSAndroid Build Coastguard Worker// Right and left values of the same indentation level are horizontally aligned. 22*6dbdd20aSAndroid Build Coastguard Worker// Example: 23*6dbdd20aSAndroid Build Coastguard Worker// foo bar 24*6dbdd20aSAndroid Build Coastguard Worker// ├ baz qux 25*6dbdd20aSAndroid Build Coastguard Worker// └ quux corge 26*6dbdd20aSAndroid Build Coastguard Worker// ├ looong_left aaa 27*6dbdd20aSAndroid Build Coastguard Worker// └ a bbb 28*6dbdd20aSAndroid Build Coastguard Worker// grault garply 29*6dbdd20aSAndroid Build Coastguard Worker 30*6dbdd20aSAndroid Build Coastguard Workerinterface TreeAttrs { 31*6dbdd20aSAndroid Build Coastguard Worker // Space delimited class list applied to our tree element. 32*6dbdd20aSAndroid Build Coastguard Worker className?: string; 33*6dbdd20aSAndroid Build Coastguard Worker} 34*6dbdd20aSAndroid Build Coastguard Worker 35*6dbdd20aSAndroid Build Coastguard Workerexport class Tree implements m.ClassComponent<TreeAttrs> { 36*6dbdd20aSAndroid Build Coastguard Worker view({attrs, children}: m.Vnode<TreeAttrs>): m.Children { 37*6dbdd20aSAndroid Build Coastguard Worker const {className = ''} = attrs; 38*6dbdd20aSAndroid Build Coastguard Worker 39*6dbdd20aSAndroid Build Coastguard Worker const classes = classNames(className); 40*6dbdd20aSAndroid Build Coastguard Worker 41*6dbdd20aSAndroid Build Coastguard Worker return m('.pf-tree', {class: classes}, children); 42*6dbdd20aSAndroid Build Coastguard Worker } 43*6dbdd20aSAndroid Build Coastguard Worker} 44*6dbdd20aSAndroid Build Coastguard Worker 45*6dbdd20aSAndroid Build Coastguard Workerinterface TreeNodeAttrs { 46*6dbdd20aSAndroid Build Coastguard Worker // Content to display in the left hand column. 47*6dbdd20aSAndroid Build Coastguard Worker // If omitted, this side will be blank. 48*6dbdd20aSAndroid Build Coastguard Worker left?: m.Children; 49*6dbdd20aSAndroid Build Coastguard Worker // Content to display in the right hand column. 50*6dbdd20aSAndroid Build Coastguard Worker // If omitted, this side will be left blank. 51*6dbdd20aSAndroid Build Coastguard Worker right?: m.Children; 52*6dbdd20aSAndroid Build Coastguard Worker // Content to display in the right hand column when the node is collapsed. 53*6dbdd20aSAndroid Build Coastguard Worker // If omitted, the value of `right` shall be shown when collapsed instead. 54*6dbdd20aSAndroid Build Coastguard Worker // If the node has no children, this value is never shown. 55*6dbdd20aSAndroid Build Coastguard Worker summary?: m.Children; 56*6dbdd20aSAndroid Build Coastguard Worker // Whether this node is collapsed or not. 57*6dbdd20aSAndroid Build Coastguard Worker // If omitted, collapsed state 'uncontrolled' - i.e. controlled internally. 58*6dbdd20aSAndroid Build Coastguard Worker collapsed?: boolean; 59*6dbdd20aSAndroid Build Coastguard Worker // Whether the node should start collapsed or not, default: false. 60*6dbdd20aSAndroid Build Coastguard Worker startsCollapsed?: boolean; 61*6dbdd20aSAndroid Build Coastguard Worker loading?: boolean; 62*6dbdd20aSAndroid Build Coastguard Worker showCaret?: boolean; 63*6dbdd20aSAndroid Build Coastguard Worker // Optional icon to show to the left of the text. 64*6dbdd20aSAndroid Build Coastguard Worker // If this node contains children, this icon is ignored. 65*6dbdd20aSAndroid Build Coastguard Worker icon?: string; 66*6dbdd20aSAndroid Build Coastguard Worker // Called when the collapsed state is changed, mainly used in controlled mode. 67*6dbdd20aSAndroid Build Coastguard Worker onCollapseChanged?: (collapsed: boolean, attrs: TreeNodeAttrs) => void; 68*6dbdd20aSAndroid Build Coastguard Worker} 69*6dbdd20aSAndroid Build Coastguard Worker 70*6dbdd20aSAndroid Build Coastguard Workerexport class TreeNode implements m.ClassComponent<TreeNodeAttrs> { 71*6dbdd20aSAndroid Build Coastguard Worker private collapsed; 72*6dbdd20aSAndroid Build Coastguard Worker 73*6dbdd20aSAndroid Build Coastguard Worker constructor({attrs}: m.CVnode<TreeNodeAttrs>) { 74*6dbdd20aSAndroid Build Coastguard Worker this.collapsed = attrs.startsCollapsed ?? false; 75*6dbdd20aSAndroid Build Coastguard Worker } 76*6dbdd20aSAndroid Build Coastguard Worker 77*6dbdd20aSAndroid Build Coastguard Worker view(vnode: m.CVnode<TreeNodeAttrs>): m.Children { 78*6dbdd20aSAndroid Build Coastguard Worker const { 79*6dbdd20aSAndroid Build Coastguard Worker children, 80*6dbdd20aSAndroid Build Coastguard Worker attrs, 81*6dbdd20aSAndroid Build Coastguard Worker attrs: {left, onCollapseChanged = () => {}}, 82*6dbdd20aSAndroid Build Coastguard Worker } = vnode; 83*6dbdd20aSAndroid Build Coastguard Worker return [ 84*6dbdd20aSAndroid Build Coastguard Worker m( 85*6dbdd20aSAndroid Build Coastguard Worker '.pf-tree-node', 86*6dbdd20aSAndroid Build Coastguard Worker { 87*6dbdd20aSAndroid Build Coastguard Worker class: classNames(this.getClassNameForNode(vnode)), 88*6dbdd20aSAndroid Build Coastguard Worker }, 89*6dbdd20aSAndroid Build Coastguard Worker m( 90*6dbdd20aSAndroid Build Coastguard Worker '.pf-tree-left', 91*6dbdd20aSAndroid Build Coastguard Worker m('span.pf-tree-gutter', { 92*6dbdd20aSAndroid Build Coastguard Worker onclick: () => { 93*6dbdd20aSAndroid Build Coastguard Worker this.collapsed = !this.isCollapsed(vnode); 94*6dbdd20aSAndroid Build Coastguard Worker onCollapseChanged(this.collapsed, attrs); 95*6dbdd20aSAndroid Build Coastguard Worker scheduleFullRedraw(); 96*6dbdd20aSAndroid Build Coastguard Worker }, 97*6dbdd20aSAndroid Build Coastguard Worker }), 98*6dbdd20aSAndroid Build Coastguard Worker left, 99*6dbdd20aSAndroid Build Coastguard Worker ), 100*6dbdd20aSAndroid Build Coastguard Worker this.renderRight(vnode), 101*6dbdd20aSAndroid Build Coastguard Worker ), 102*6dbdd20aSAndroid Build Coastguard Worker hasChildren(vnode) && m('.pf-tree-children', children), 103*6dbdd20aSAndroid Build Coastguard Worker ]; 104*6dbdd20aSAndroid Build Coastguard Worker } 105*6dbdd20aSAndroid Build Coastguard Worker 106*6dbdd20aSAndroid Build Coastguard Worker private getClassNameForNode(vnode: m.CVnode<TreeNodeAttrs>) { 107*6dbdd20aSAndroid Build Coastguard Worker const {loading = false, showCaret = false} = vnode.attrs; 108*6dbdd20aSAndroid Build Coastguard Worker if (loading) { 109*6dbdd20aSAndroid Build Coastguard Worker return 'pf-loading'; 110*6dbdd20aSAndroid Build Coastguard Worker } else if (hasChildren(vnode) || showCaret) { 111*6dbdd20aSAndroid Build Coastguard Worker if (this.isCollapsed(vnode)) { 112*6dbdd20aSAndroid Build Coastguard Worker return 'pf-collapsed'; 113*6dbdd20aSAndroid Build Coastguard Worker } else { 114*6dbdd20aSAndroid Build Coastguard Worker return 'pf-expanded'; 115*6dbdd20aSAndroid Build Coastguard Worker } 116*6dbdd20aSAndroid Build Coastguard Worker } else { 117*6dbdd20aSAndroid Build Coastguard Worker return undefined; 118*6dbdd20aSAndroid Build Coastguard Worker } 119*6dbdd20aSAndroid Build Coastguard Worker } 120*6dbdd20aSAndroid Build Coastguard Worker 121*6dbdd20aSAndroid Build Coastguard Worker private renderRight(vnode: m.CVnode<TreeNodeAttrs>) { 122*6dbdd20aSAndroid Build Coastguard Worker const { 123*6dbdd20aSAndroid Build Coastguard Worker attrs: {right, summary}, 124*6dbdd20aSAndroid Build Coastguard Worker } = vnode; 125*6dbdd20aSAndroid Build Coastguard Worker if (hasChildren(vnode) && this.isCollapsed(vnode)) { 126*6dbdd20aSAndroid Build Coastguard Worker return m('.pf-tree-right', summary ?? right); 127*6dbdd20aSAndroid Build Coastguard Worker } else { 128*6dbdd20aSAndroid Build Coastguard Worker return m('.pf-tree-right', right); 129*6dbdd20aSAndroid Build Coastguard Worker } 130*6dbdd20aSAndroid Build Coastguard Worker } 131*6dbdd20aSAndroid Build Coastguard Worker 132*6dbdd20aSAndroid Build Coastguard Worker private isCollapsed({attrs}: m.Vnode<TreeNodeAttrs>): boolean { 133*6dbdd20aSAndroid Build Coastguard Worker // If collapsed is omitted, use our local collapsed state instead. 134*6dbdd20aSAndroid Build Coastguard Worker const {collapsed = this.collapsed} = attrs; 135*6dbdd20aSAndroid Build Coastguard Worker 136*6dbdd20aSAndroid Build Coastguard Worker return collapsed; 137*6dbdd20aSAndroid Build Coastguard Worker } 138*6dbdd20aSAndroid Build Coastguard Worker} 139*6dbdd20aSAndroid Build Coastguard Worker 140*6dbdd20aSAndroid Build Coastguard Workerexport function dictToTreeNodes(dict: {[key: string]: m.Child}): m.Child[] { 141*6dbdd20aSAndroid Build Coastguard Worker const children: m.Child[] = []; 142*6dbdd20aSAndroid Build Coastguard Worker for (const key of Object.keys(dict)) { 143*6dbdd20aSAndroid Build Coastguard Worker if (dict[key] == undefined) { 144*6dbdd20aSAndroid Build Coastguard Worker continue; 145*6dbdd20aSAndroid Build Coastguard Worker } 146*6dbdd20aSAndroid Build Coastguard Worker children.push( 147*6dbdd20aSAndroid Build Coastguard Worker m(TreeNode, { 148*6dbdd20aSAndroid Build Coastguard Worker left: key, 149*6dbdd20aSAndroid Build Coastguard Worker right: dict[key], 150*6dbdd20aSAndroid Build Coastguard Worker }), 151*6dbdd20aSAndroid Build Coastguard Worker ); 152*6dbdd20aSAndroid Build Coastguard Worker } 153*6dbdd20aSAndroid Build Coastguard Worker return children; 154*6dbdd20aSAndroid Build Coastguard Worker} 155*6dbdd20aSAndroid Build Coastguard Worker 156*6dbdd20aSAndroid Build Coastguard Worker// Create a flat tree from a POJO 157*6dbdd20aSAndroid Build Coastguard Workerexport function dictToTree(dict: {[key: string]: m.Child}): m.Children { 158*6dbdd20aSAndroid Build Coastguard Worker return m(Tree, dictToTreeNodes(dict)); 159*6dbdd20aSAndroid Build Coastguard Worker} 160*6dbdd20aSAndroid Build Coastguard Workerinterface LazyTreeNodeAttrs { 161*6dbdd20aSAndroid Build Coastguard Worker // Same as TreeNode (see above). 162*6dbdd20aSAndroid Build Coastguard Worker left?: m.Children; 163*6dbdd20aSAndroid Build Coastguard Worker // Same as TreeNode (see above). 164*6dbdd20aSAndroid Build Coastguard Worker right?: m.Children; 165*6dbdd20aSAndroid Build Coastguard Worker // Same as TreeNode (see above). 166*6dbdd20aSAndroid Build Coastguard Worker icon?: string; 167*6dbdd20aSAndroid Build Coastguard Worker // Same as TreeNode (see above). 168*6dbdd20aSAndroid Build Coastguard Worker summary?: m.Children; 169*6dbdd20aSAndroid Build Coastguard Worker // A callback to be called when the TreeNode is expanded, in order to fetch 170*6dbdd20aSAndroid Build Coastguard Worker // child nodes. 171*6dbdd20aSAndroid Build Coastguard Worker // The callback must return a promise to a function which returns m.Children. 172*6dbdd20aSAndroid Build Coastguard Worker // The reason the promise must return a function rather than the actual 173*6dbdd20aSAndroid Build Coastguard Worker // children is to avoid storing vnodes between render cycles, which is a bug 174*6dbdd20aSAndroid Build Coastguard Worker // in Mithril. 175*6dbdd20aSAndroid Build Coastguard Worker fetchData: () => Promise<() => m.Children>; 176*6dbdd20aSAndroid Build Coastguard Worker // Whether to unload children on collapse. 177*6dbdd20aSAndroid Build Coastguard Worker // Defaults to false, data will be kept in memory until the node is destroyed. 178*6dbdd20aSAndroid Build Coastguard Worker unloadOnCollapse?: boolean; 179*6dbdd20aSAndroid Build Coastguard Worker} 180*6dbdd20aSAndroid Build Coastguard Worker 181*6dbdd20aSAndroid Build Coastguard Worker// This component is a TreeNode which only loads child nodes when it's expanded. 182*6dbdd20aSAndroid Build Coastguard Worker// This allows us to represent huge trees without having to load all the data 183*6dbdd20aSAndroid Build Coastguard Worker// up front, and even allows us to represent infinite or recursive trees. 184*6dbdd20aSAndroid Build Coastguard Workerexport class LazyTreeNode implements m.ClassComponent<LazyTreeNodeAttrs> { 185*6dbdd20aSAndroid Build Coastguard Worker private collapsed: boolean = true; 186*6dbdd20aSAndroid Build Coastguard Worker private loading: boolean = false; 187*6dbdd20aSAndroid Build Coastguard Worker private renderChildren?: () => m.Children; 188*6dbdd20aSAndroid Build Coastguard Worker 189*6dbdd20aSAndroid Build Coastguard Worker view({attrs}: m.CVnode<LazyTreeNodeAttrs>): m.Children { 190*6dbdd20aSAndroid Build Coastguard Worker const { 191*6dbdd20aSAndroid Build Coastguard Worker left, 192*6dbdd20aSAndroid Build Coastguard Worker right, 193*6dbdd20aSAndroid Build Coastguard Worker icon, 194*6dbdd20aSAndroid Build Coastguard Worker summary, 195*6dbdd20aSAndroid Build Coastguard Worker fetchData, 196*6dbdd20aSAndroid Build Coastguard Worker unloadOnCollapse = false, 197*6dbdd20aSAndroid Build Coastguard Worker } = attrs; 198*6dbdd20aSAndroid Build Coastguard Worker 199*6dbdd20aSAndroid Build Coastguard Worker return m( 200*6dbdd20aSAndroid Build Coastguard Worker TreeNode, 201*6dbdd20aSAndroid Build Coastguard Worker { 202*6dbdd20aSAndroid Build Coastguard Worker left, 203*6dbdd20aSAndroid Build Coastguard Worker right, 204*6dbdd20aSAndroid Build Coastguard Worker icon, 205*6dbdd20aSAndroid Build Coastguard Worker summary, 206*6dbdd20aSAndroid Build Coastguard Worker showCaret: true, 207*6dbdd20aSAndroid Build Coastguard Worker loading: this.loading, 208*6dbdd20aSAndroid Build Coastguard Worker collapsed: this.collapsed, 209*6dbdd20aSAndroid Build Coastguard Worker onCollapseChanged: (collapsed) => { 210*6dbdd20aSAndroid Build Coastguard Worker if (collapsed) { 211*6dbdd20aSAndroid Build Coastguard Worker if (unloadOnCollapse) { 212*6dbdd20aSAndroid Build Coastguard Worker this.renderChildren = undefined; 213*6dbdd20aSAndroid Build Coastguard Worker } 214*6dbdd20aSAndroid Build Coastguard Worker } else { 215*6dbdd20aSAndroid Build Coastguard Worker // Expanding 216*6dbdd20aSAndroid Build Coastguard Worker if (this.renderChildren) { 217*6dbdd20aSAndroid Build Coastguard Worker this.collapsed = false; 218*6dbdd20aSAndroid Build Coastguard Worker scheduleFullRedraw(); 219*6dbdd20aSAndroid Build Coastguard Worker } else { 220*6dbdd20aSAndroid Build Coastguard Worker this.loading = true; 221*6dbdd20aSAndroid Build Coastguard Worker fetchData().then((result) => { 222*6dbdd20aSAndroid Build Coastguard Worker this.loading = false; 223*6dbdd20aSAndroid Build Coastguard Worker this.collapsed = false; 224*6dbdd20aSAndroid Build Coastguard Worker this.renderChildren = result; 225*6dbdd20aSAndroid Build Coastguard Worker scheduleFullRedraw(); 226*6dbdd20aSAndroid Build Coastguard Worker }); 227*6dbdd20aSAndroid Build Coastguard Worker } 228*6dbdd20aSAndroid Build Coastguard Worker } 229*6dbdd20aSAndroid Build Coastguard Worker this.collapsed = collapsed; 230*6dbdd20aSAndroid Build Coastguard Worker scheduleFullRedraw(); 231*6dbdd20aSAndroid Build Coastguard Worker }, 232*6dbdd20aSAndroid Build Coastguard Worker }, 233*6dbdd20aSAndroid Build Coastguard Worker this.renderChildren && this.renderChildren(), 234*6dbdd20aSAndroid Build Coastguard Worker ); 235*6dbdd20aSAndroid Build Coastguard Worker } 236*6dbdd20aSAndroid Build Coastguard Worker} 237