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