Skip to content

Commit d834f70

Browse files
committed
support init with a wrapper class that isn't recursed; various minor fixes
1 parent 8811ff9 commit d834f70

File tree

12 files changed

+197
-67
lines changed

12 files changed

+197
-67
lines changed

README.md

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,21 @@ Basic example:
2424

2525
Returns: `node`
2626

27-
Fired when a node is hovered.
27+
Fired when a mouse enters the node.
2828

29+
```handlebars
30+
{{x-tree model=tree hover=(action hover)}}
31+
```
32+
33+
#### hoverOut
34+
35+
Returns: `node`
36+
37+
Fired when a mouse leaves the node.
38+
39+
```handlebars
40+
{{x-tree model=tree hoverOut=(action hoverOut)}}
41+
```
2942

3043
#### select
3144

@@ -47,7 +60,35 @@ Accepts: `boolean`
4760
```
4861

4962
Displays a checkbox for each node.
50-
Use in conjunction with `isChecked`.
63+
Use in conjunction with `model.isChecked`.
64+
65+
#### chosenId
66+
67+
Default: `undefined`
68+
69+
Accepts: `id`
70+
71+
```handlebars
72+
{{x-tree model=tree chosenId=someId}}
73+
```
74+
75+
Applies 'chosen' styling (`font-weight: bold;`) to the specified node.
76+
A tree will also auto-expand to a the chosen node if a valid `chosenId` is provided.
77+
`chosenId` should relate to a node's `model.id`.
78+
79+
#### expandDepth
80+
81+
Default: `0`
82+
83+
Accepts: `number`
84+
85+
```handlebars
86+
{{x-tree model=tree expandDepth=-1}}
87+
```
88+
89+
Expands the tree to a given depth.
90+
`0` will not expand the tree at all, a negative number will fully expand a tree, a positive number will expand a tree to the given depth.
91+
5192

5293
### Blocks
5394

@@ -105,7 +146,7 @@ The model requires specific properties to properly function:
105146
- `isVisible` - `boolean` used to display or hide a node
106147

107148
```js
108-
{
149+
[{
109150
id: 0,
110151
name: 'Root',
111152
isExpanded: true,
@@ -138,5 +179,7 @@ The model requires specific properties to properly function:
138179
]
139180
}
140181
]
141-
}
182+
}]
142183
```
184+
185+
A utility class is provided to convert a flat structure into a tree structure and vice-versa.

addon/components/x-tree-branch.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import Component from '@ember/component';
2+
import layout from '../templates/components/x-tree-branch';
3+
4+
export default Component.extend({
5+
layout,
6+
tagName: 'ul',
7+
classNames: ['tree-branch']
8+
});

addon/components/x-tree-node.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ export default Component.extend({
77
classNameBindings: ['model.isSelected:tree-highlight', 'isChosen:tree-chosen'],
88

99
isChosen: computed('model.id', 'chosenId', function() {
10-
let chosenId = this.get('chosenId');
11-
return chosenId ? this.get('model.id') === chosenId : false;
10+
return this.get('model.id') === this.get('chosenId');
1211
}),
1312

1413
click() {
15-
this.attrs.select(this.get('model'));
14+
let select = this.get('select');
15+
if (select) {
16+
select(this.get('model'));
17+
}
1618
},
1719
mouseEnter() {
1820
this.set('model.isSelected', true);
@@ -23,6 +25,10 @@ export default Component.extend({
2325
},
2426
mouseLeave() {
2527
this.set('model.isSelected', false);
28+
let hoverOut = this.get('hoverOut');
29+
if (hoverOut) {
30+
hoverOut(this.get('model'));
31+
}
2632
},
2733

2834
actions: {

addon/components/x-tree.js

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,32 @@
11
import Component from '@ember/component';
2-
import { computed } from '@ember/object';
3-
import { isEmpty } from '@ember/utils';
42
import layout from '../templates/components/x-tree';
3+
import { getDescendents, getAncestors, getParent } from '../utils/tree';
4+
import { get, set } from '@ember/object';
55

66
export default Component.extend({
77
layout,
8-
tagName: 'ul',
9-
classNames: ['tree-branch']
8+
9+
init() {
10+
this._super(...arguments);
11+
let tree = this.get('model');
12+
13+
// Make sure chosen item is highlighted and expanded-to in the tree
14+
let chosenId = this.get('chosenId');
15+
if (chosenId) {
16+
let chosen = getDescendents(tree).findBy('id', chosenId);
17+
if (chosen) {
18+
getAncestors(tree, chosen).forEach(x => {
19+
if (get(x, 'id') !== chosenId) {
20+
set(x, 'isExpanded', true);
21+
}
22+
});
23+
}
24+
}
25+
26+
// Expand to given depth
27+
let expandDepth = this.get('expandDepth');
28+
if (expandDepth) {
29+
getDescendents(tree, expandDepth).setEach('isExpanded', true);
30+
}
31+
}
1032
});

addon/styles/x-tree.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,7 @@
3030
color: #333;
3131
cursor: pointer;
3232
}
33+
34+
.tree-node .tree-chosen {
35+
font-weight: bold;
36+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{{#each model as |child|}}
2+
{{#if child.isVisible}}
3+
{{#if hasBlock}}
4+
{{yield child}}
5+
{{else}}
6+
{{x-tree-children
7+
model=child
8+
select=select
9+
hover=hover
10+
hoverOut=hoverOut
11+
checkable=checkable
12+
chosenId=chosenId}}
13+
{{/if}}
14+
{{/if}}
15+
{{/each}}
Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
1-
{{x-tree-node model=model select=select hover=hover chosenId=chosenId}}
1+
{{x-tree-node
2+
model=model
3+
select=select
4+
hover=hover
5+
hoverOut=hoverOut
6+
checkable=checkable
7+
chosenId=chosenId}}
28

39
{{#if model.isExpanded}}
4-
{{x-tree model=model.children select=select hover=hover chosenId=chosenId}}
10+
{{x-tree-branch
11+
model=model.children
12+
select=select
13+
hover=hover
14+
hoverOut=hoverOut
15+
checkable=checkable
16+
chosenId=chosenId}}
517
{{/if}}

addon/templates/components/x-tree-node.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{{#if hasBlock}}
22
{{yield}}
33
{{else}}
4-
{{#if model.checkable}}
4+
{{#if checkable}}
55
<input type="checkbox" checked={{model.isChecked}}
66
onclick={{action 'toggleCheck' bubbles=false}}>
77
{{/if}}
Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1-
{{#each model as |child|}}
2-
{{#if child.isVisible}}
3-
{{#if hasBlock}}
4-
{{yield child}}
5-
{{else}}
6-
{{x-tree-children model=child select=select hover=hover chosenId=chosenId}}
7-
{{/if}}
8-
{{/if}}
9-
{{/each}}
1+
{{#if hasBlock}}
2+
{{#x-tree-branch
3+
model=model
4+
select=select
5+
hover=hover
6+
hoverOut=hoverOut
7+
checkable=checkable
8+
chosenId=chosenId}}
9+
{{yield}}
10+
{{/x-tree-branch}}
11+
{{else}}
12+
{{x-tree-branch
13+
model=model
14+
select=select
15+
hover=hover
16+
hoverOut=hoverOut
17+
checkable=checkable
18+
chosenId=chosenId}}
19+
{{/if}}

addon/utils/tree.js

Lines changed: 52 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { A } from '@ember/array';
2-
import { get } from '@ember/object';
3-
import { isPresent, isEmpty } from '@ember/utils';
4-
import ObjectProxy from '@ember/object/proxy';
2+
import { get, set } from '@ember/object';
3+
import { isEmpty } from '@ember/utils';
54

65
/* Build a tree (nested objects) from a plain array
76
* using `id` and `parentId` as references for the
@@ -18,38 +17,24 @@ export function buildTree(model, options) {
1817
return roots;
1918
}
2019

21-
let element = model[0] || get(model, 'firstObject');
22-
if (typeof element !== 'object') {
23-
// Not a model of objects, hence it should be a flat list
24-
return buildFlatList(model);
25-
}
26-
27-
// Add all nodes to tree
20+
// Set defaults and add all nodes to tree
2821
model.forEach(node => {
29-
let child = {
30-
content: node,
31-
children: A(),
32-
isSelected: false,
33-
isVisible: true
34-
};
35-
3622
// Alternative name for `id`
3723
if (options.valueKey) {
38-
child.id = get(node, options.valueKey);
24+
set(node, 'id', get(node, options.valueKey));
3925
}
4026

4127
// Alternative name for `name`
4228
if (options.labelKey) {
43-
child.name = get(node, options.labelKey);
29+
set(node, 'name', get(node, options.labelKey));
4430
}
4531

46-
// Decide if node is expanded
47-
if (isPresent(options.isExpanded)) {
48-
child.isExpanded = options.isExpanded;
49-
}
32+
// Defaults
33+
set(node, 'children', A());
34+
set(node, 'isVisible', get(node, 'isVisible') || true);
35+
set(node, 'isExpanded', get(node, 'isExpanded') || false);
5036

51-
// Proxy options to keep model intact
52-
tree[get(child, 'id')] = ObjectProxy.create(child);
37+
tree[get(node, 'id')] = node;
5338
});
5439

5540
// Connect all children to their parent
@@ -58,35 +43,59 @@ export function buildTree(model, options) {
5843
let parent = get(node, 'parentId');
5944

6045
if (isEmpty(parent)) {
61-
roots.push(child);
46+
roots.pushObject(child);
6247
} else {
63-
tree[parent].children.push(child);
48+
get(tree[parent], 'children').pushObject(child);
6449
}
6550
});
6651

6752
return roots;
6853
}
6954

70-
// Builds a list of proxies from a model of values
71-
export function buildFlatList(model) {
72-
let list = model.map(node => ObjectProxy.create({
73-
content: node,
74-
id: node,
75-
name: node,
76-
isSelected: false,
77-
isVisible: true
78-
}));
79-
80-
return A(list);
55+
// Gets all descendents of a tree (array)
56+
// Returns a flat list of all descenents, including the top level of the tree
57+
export function getDescendents(tree, depth = -1) {
58+
let descendents = A();
59+
60+
if (depth < 0) { // Unlimited depth
61+
tree.forEach(node => {
62+
descendents.pushObject(node);
63+
descendents.pushObjects(getDescendents(node.children));
64+
});
65+
} else if (depth > 0) {
66+
tree.forEach(node => {
67+
descendents.pushObject(node);
68+
descendents.pushObjects(getDescendents(node.children, depth - 1));
69+
});
70+
}
71+
72+
return descendents;
8173
}
8274

83-
export function getDescendents(tree) {
84-
let descendents = A();
75+
// Gets all ancestors of a childNode given a tree (array)
76+
// Returns a flat list of ancestors, including the childNode
77+
export function getAncestors(tree, childNode) {
78+
let ancestors = A();
79+
let childId = childNode.get('id');
8580

8681
tree.forEach(node => {
87-
descendents.pushObject(node);
88-
descendents.pushObjects(getDescendents(node.children));
82+
if (!ancestors.isAny('id', childId)) {
83+
if (node.id === childId) {
84+
ancestors.pushObject(node);
85+
} else if (node.children.length > 0) {
86+
ancestors.pushObjects(getAncestors(node.children, childNode));
87+
if (ancestors.length > 0) {
88+
ancestors.pushObject(node);
89+
}
90+
}
91+
}
8992
});
9093

91-
return descendents;
94+
return ancestors;
9295
}
96+
97+
// Gets the direct parent of a childNode given a flat list
98+
// Returns single object (parent) or undefined
99+
export function getParent(list, childNode) {
100+
return list.find(x => x.children.find(y => y.id === childNode.get('id')));
101+
}

0 commit comments

Comments
 (0)