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