• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import m from 'mithril';
2
3import {classNames} from '../classnames';
4import {globals} from '../globals';
5
6import {Button} from './button';
7import {Spinner} from './spinner';
8import {hasChildren} from './utils';
9
10export enum TreeLayout {
11  // Classic heirachical tree layout with no columnar alignment.
12  // Example:
13  // foo: bar
14  //  ├ baz: qux
15  //  └ quux: corge
16  // grault: garply
17  Tree = 'tree',
18
19  // Heirachical tree layout but right values are horizontally aligned.
20  // Example:
21  // foo     bar
22  //  ├ baz  qux
23  //  └ quux corge
24  // grault  garply
25  Grid = 'grid',
26}
27
28interface TreeAttrs {
29  // The style of layout.
30  // Defaults to grid.
31  layout?: TreeLayout;
32  // Space delimited class list applied to our tree element.
33  className?: string;
34}
35
36export class Tree implements m.ClassComponent<TreeAttrs> {
37  view({attrs, children}: m.Vnode<TreeAttrs>): m.Children {
38    const {
39      layout: style = TreeLayout.Grid,
40      className = '',
41    } = attrs;
42
43    if (style === TreeLayout.Grid) {
44      return m('.pf-ptree-grid', {class: className}, children);
45    } else if (style === TreeLayout.Tree) {
46      return m('.pf-ptree', {class: className}, children);
47    } else {
48      return null;
49    }
50  }
51}
52
53interface TreeNodeAttrs {
54  // Content to display in the left hand column.
55  // If omitted, this side will be blank.
56  left?: m.Children;
57  // Content to display in the right hand column.
58  // If omitted, this side will be left blank.
59  right?: m.Children;
60  // Content to display in the right hand column when the node is collapsed.
61  // If omitted, the value of `right` shall be shown when collapsed instead.
62  // If the node has no children, this value is never shown.
63  summary?: m.Children;
64  // Whether this node is collapsed or not.
65  // If omitted, collapsed state 'uncontrolled' - i.e. controlled internally.
66  collapsed?: boolean;
67  // Called when the collapsed state is changed, mainly used in controlled mode.
68  onCollapseChanged?: (collapsed: boolean, attrs: TreeNodeAttrs) => void;
69}
70
71export class TreeNode implements m.ClassComponent<TreeNodeAttrs> {
72  private collapsed = false;
73  view(vnode: m.CVnode<TreeNodeAttrs>): m.Children {
74    return [
75      m(
76          '.pf-tree-node',
77          this.renderLeft(vnode),
78          this.renderRight(vnode),
79          ),
80      hasChildren(vnode) && this.renderChildren(vnode),
81    ];
82  }
83
84  private renderLeft(vnode: m.CVnode<TreeNodeAttrs>) {
85    const {
86      attrs: {left},
87    } = vnode;
88
89    return m(
90        '.pf-tree-left',
91        left,
92        hasChildren(vnode) && this.renderCollapseButton(vnode),
93    );
94  }
95
96  private renderRight(vnode: m.CVnode<TreeNodeAttrs>) {
97    const {attrs: {right, summary}} = vnode;
98    if (hasChildren(vnode) && this.isCollapsed(vnode)) {
99      return m('.pf-tree-right', summary ?? right);
100    } else {
101      return m('.pf-tree-right', right);
102    }
103  }
104
105  private renderChildren(vnode: m.CVnode<TreeNodeAttrs>) {
106    const {children} = vnode;
107
108    return m(
109        '.pf-tree-children',
110        {
111          class: classNames(this.isCollapsed(vnode) && 'pf-pgrid-hidden'),
112        },
113        children,
114    );
115  }
116
117  private renderCollapseButton(vnode: m.Vnode<TreeNodeAttrs>) {
118    const {attrs, attrs: {onCollapseChanged = () => {}}} = vnode;
119
120    return m(Button, {
121      icon: this.isCollapsed(vnode) ? 'chevron_right' : 'expand_more',
122      minimal: true,
123      compact: true,
124      onclick: () => {
125        this.collapsed = !this.isCollapsed(vnode);
126        onCollapseChanged(this.collapsed, attrs);
127        globals.rafScheduler.scheduleFullRedraw();
128      },
129    });
130  }
131
132  private isCollapsed({attrs}: m.Vnode<TreeNodeAttrs>): boolean {
133    // If collapsed is omitted, use our local collapsed state instead.
134    const {
135      collapsed = this.collapsed,
136    } = attrs;
137
138    return collapsed;
139  }
140}
141
142export function dictToTree(dict: {[key: string]: m.Child}): m.Children {
143  const children: m.Child[] = [];
144  for (const key of Object.keys(dict)) {
145    children.push(m(TreeNode, {
146      left: key,
147      right: dict[key],
148    }));
149  }
150  return m(Tree, children);
151}
152
153interface LazyTreeNodeAttrs {
154  // Same as TreeNode (see above).
155  left?: m.Children;
156  // Same as TreeNode (see above).
157  right?: m.Children;
158  // Same as TreeNode (see above).
159  summary?: m.Children;
160  // A callback to be called when the TreeNode is expanded, in order to fetch
161  // child nodes.
162  // The callback must return a promise to a function which returns m.Children.
163  // The reason the promise must return a function rather than the actual
164  // children is to avoid storing vnodes between render cycles, which is a bug
165  // in Mithril.
166  fetchData: () => Promise<() => m.Children>;
167  // Whether to keep child nodes in memory after the node has been collapsed.
168  // Defaults to true
169  hoardData?: boolean;
170}
171
172// This component is a TreeNode which only loads child nodes when it's expanded.
173// This allows us to represent huge trees without having to load all the data
174// up front, and even allows us to represent infinite or recursive trees.
175export class LazyTreeNode implements m.ClassComponent<LazyTreeNodeAttrs> {
176  private collapsed: boolean = true;
177  private renderChildren = this.renderSpinner;
178
179  private renderSpinner(): m.Children {
180    return m(TreeNode, {left: m(Spinner)});
181  }
182
183  view({attrs}: m.CVnode<LazyTreeNodeAttrs>): m.Children {
184    const {
185      left,
186      right,
187      summary,
188      fetchData,
189      hoardData = true,
190    } = attrs;
191
192    return m(
193        TreeNode,
194        {
195          left,
196          right,
197          summary,
198          collapsed: this.collapsed,
199          onCollapseChanged: (collapsed) => {
200            if (collapsed) {
201              if (!hoardData) {
202                this.renderChildren = this.renderSpinner;
203              }
204            } else {
205              fetchData().then((result) => {
206                if (!this.collapsed) {
207                  this.renderChildren = result;
208                  globals.rafScheduler.scheduleFullRedraw();
209                }
210              });
211            }
212            this.collapsed = collapsed;
213            globals.rafScheduler.scheduleFullRedraw();
214          },
215        },
216        this.renderChildren());
217  }
218}
219