Skip to content

Commit 8c1aa1c

Browse files
committed
Add the ability to expand or collapse all nodes in the tree
Resolves #79.
1 parent 6867130 commit 8c1aa1c

File tree

10 files changed

+389
-8
lines changed

10 files changed

+389
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### New Features
66

7+
* [#79]: Add the ability to expand or collapse all nodes in the tree
78
* [#102]: Add `icons` property to allow specification of icon components
89
* [#103]: Add `title` node property and `showNodeTitle` tree property
910

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ All node objects **must** have a unique `value`. This value is serialized into t
104104
| `noCascade` | bool | If true, toggling a parent node will **not** cascade its check state to its children. | `false` |
105105
| `onlyLeafCheckboxes` | bool | If true, checkboxes will only be shown for leaf nodes. | `false` |
106106
| `optimisticToggle` | bool | If true, toggling a partially-checked node will select all children. If false, it will deselect. | `true` |
107+
| `showExpandAll` | bool | If true, buttons for expanding and collapsing all parent nodes will appear in the tree. | `false` |
107108
| `showNodeIcon` | bool | If true, each node will show a parent or leaf icon. | `true` |
108109
| `showNodeTitle` | bool | If true, the `label` of each node will become the `title` of the resulting DOM node. Overridden by `node.title`. | `false` |
109110
| `onCheck` | function | onCheck handler: `function(checked) {}` | `() => {}` |

examples/src/index.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ <h2>Hide Checkboxes Example</h2>
7373
</p>
7474
<div id="hidden-checkboxes-example"></div>
7575

76+
<h2>Expand All/Collapse All Example</h2>
77+
<p>
78+
By passing in the <code>showExpandAll</code> property, two additional options will appear at the top of the tree
79+
allow the user to easily expand or collapse all parent nodes.
80+
</p>
81+
<div id="expand-all-example"></div>
82+
7683
<h2>Large Data Example</h2>
7784
<p>The checkbox tree is capable of supporting a large number of nodes at once.</p>
7885
<div id="large-data-example"></div>

examples/src/index.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
import React from 'react';
22
import ReactDOM from 'react-dom';
33

4-
import ClickableLabelsExample from './js/ClickableLabelsExample';
54
import BasicExample from './js/BasicExample';
65
import CustomIconsExample from './js/CustomIconsExample';
6+
import ClickableLabelsExample from './js/ClickableLabelsExample';
77
import DisabledExample from './js/DisabledExample';
8+
import ExpandAllExample from './js/ExpandAllExample';
89
import HiddenCheckboxesExample from './js/HiddenCheckboxesExample';
910
import NoCascadeExample from './js/NoCascadeExample';
10-
import PessimisticToggleExample from './js/PessimisticToggleExample';
1111
import LargeDataExample from './js/LargeDataExample';
12+
import PessimisticToggleExample from './js/PessimisticToggleExample';
1213

1314
ReactDOM.render(<BasicExample />, document.getElementById('basic-example'));
1415
ReactDOM.render(<CustomIconsExample />, document.getElementById('custom-icons-example'));
1516
ReactDOM.render(<DisabledExample />, document.getElementById('disabled-example'));
16-
ReactDOM.render(<HiddenCheckboxesExample />, document.getElementById('hidden-checkboxes-example'));
1717
ReactDOM.render(<NoCascadeExample />, document.getElementById('no-cascade-example'));
1818
ReactDOM.render(<PessimisticToggleExample />, document.getElementById('pessimistic-toggle-example'));
19-
ReactDOM.render(<LargeDataExample />, document.getElementById('large-data-example'));
2019
ReactDOM.render(<ClickableLabelsExample />, document.getElementById('clickable-labels-example'));
20+
ReactDOM.render(<HiddenCheckboxesExample />, document.getElementById('hidden-checkboxes-example'));
21+
ReactDOM.render(<ExpandAllExample />, document.getElementById('expand-all-example'));
22+
ReactDOM.render(<LargeDataExample />, document.getElementById('large-data-example'));
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import React from 'react';
2+
import CheckboxTree from 'react-checkbox-tree';
3+
4+
const nodes = [
5+
{
6+
value: '/app',
7+
label: 'app',
8+
children: [
9+
{
10+
value: '/app/Http',
11+
label: 'Http',
12+
children: [
13+
{
14+
value: '/app/Http/Controllers',
15+
label: 'Controllers',
16+
children: [{
17+
value: '/app/Http/Controllers/WelcomeController.js',
18+
label: 'WelcomeController.js',
19+
}],
20+
},
21+
{
22+
value: '/app/Http/routes.js',
23+
label: 'routes.js',
24+
},
25+
],
26+
},
27+
{
28+
value: '/app/Providers',
29+
label: 'Providers',
30+
children: [{
31+
value: '/app/Http/Providers/EventServiceProvider.js',
32+
label: 'EventServiceProvider.js',
33+
}],
34+
},
35+
],
36+
},
37+
{
38+
value: '/config',
39+
label: 'config',
40+
children: [
41+
{
42+
value: '/config/app.js',
43+
label: 'app.js',
44+
},
45+
{
46+
value: '/config/database.js',
47+
label: 'database.js',
48+
},
49+
],
50+
},
51+
{
52+
value: '/public',
53+
label: 'public',
54+
children: [
55+
{
56+
value: '/public/assets/',
57+
label: 'assets',
58+
children: [{
59+
value: '/public/assets/style.css',
60+
label: 'style.css',
61+
}],
62+
},
63+
{
64+
value: '/public/index.html',
65+
label: 'index.html',
66+
},
67+
],
68+
},
69+
{
70+
value: '/.env',
71+
label: '.env',
72+
},
73+
{
74+
value: '/.gitignore',
75+
label: '.gitignore',
76+
},
77+
{
78+
value: '/README.md',
79+
label: 'README.md',
80+
},
81+
];
82+
83+
class ExpandAllExample extends React.Component {
84+
constructor() {
85+
super();
86+
87+
this.state = {
88+
checked: [
89+
'/app/Http/Controllers/WelcomeController.js',
90+
'/app/Http/routes.js',
91+
'/public/assets/style.css',
92+
'/public/index.html',
93+
'/.gitignore',
94+
],
95+
expanded: [
96+
'/app',
97+
],
98+
};
99+
100+
this.onCheck = this.onCheck.bind(this);
101+
this.onExpand = this.onExpand.bind(this);
102+
}
103+
104+
onCheck(checked) {
105+
this.setState({ checked });
106+
}
107+
108+
onExpand(expanded) {
109+
this.setState({ expanded });
110+
}
111+
112+
render() {
113+
const { checked, expanded } = this.state;
114+
115+
return (
116+
<div className="expand-all-container">
117+
<CheckboxTree
118+
checked={checked}
119+
expanded={expanded}
120+
nodes={nodes}
121+
showExpandAll
122+
onCheck={this.onCheck}
123+
onExpand={this.onExpand}
124+
/>
125+
</div>
126+
);
127+
}
128+
}
129+
130+
export default ExpandAllExample;

examples/src/scss/style.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,7 @@ $rct-border-color: #ccc;
1919
width: 50%;
2020
}
2121
}
22+
23+
.expand-all-container {
24+
max-width: 400px;
25+
}

src/js/CheckboxTree.js

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class CheckboxTree extends React.Component {
2525
noCascade: PropTypes.bool,
2626
onlyLeafCheckboxes: PropTypes.bool,
2727
optimisticToggle: PropTypes.bool,
28+
showExpandAll: PropTypes.bool,
2829
showNodeIcon: PropTypes.bool,
2930
showNodeTitle: PropTypes.bool,
3031
onCheck: PropTypes.func,
@@ -44,6 +45,8 @@ class CheckboxTree extends React.Component {
4445
halfCheck: <span className="rct-icon rct-icon-half-check" />,
4546
expandClose: <span className="rct-icon rct-icon-expand-close" />,
4647
expandOpen: <span className="rct-icon rct-icon-expand-open" />,
48+
expandAll: <span className="rct-icon rct-icon-expand-all" />,
49+
collapseAll: <span className="rct-icon rct-icon-collapse-all" />,
4750
parentClose: <span className="rct-icon rct-icon-parent-close" />,
4851
parentOpen: <span className="rct-icon rct-icon-parent-open" />,
4952
leaf: <span className="rct-icon rct-icon-leaf" />,
@@ -54,6 +57,7 @@ class CheckboxTree extends React.Component {
5457
noCascade: false,
5558
onlyLeafCheckboxes: false,
5659
optimisticToggle: true,
60+
showExpandAll: false,
5761
showNodeIcon: true,
5862
showNodeTitle: false,
5963
onCheck: () => {},
@@ -75,6 +79,8 @@ class CheckboxTree extends React.Component {
7579

7680
this.onCheck = this.onCheck.bind(this);
7781
this.onExpand = this.onExpand.bind(this);
82+
this.onExpandAll = this.onExpandAll.bind(this);
83+
this.onCollapseAll = this.onCollapseAll.bind(this);
7884
}
7985

8086
componentWillReceiveProps({ nodes, checked, expanded }) {
@@ -99,6 +105,14 @@ class CheckboxTree extends React.Component {
99105
onExpand(this.serializeList('expanded'), node);
100106
}
101107

108+
onExpandAll() {
109+
this.expandAllNodes();
110+
}
111+
112+
onCollapseAll() {
113+
this.expandAllNodes(false);
114+
}
115+
102116
getFormattedNodes(nodes) {
103117
return nodes.map((node) => {
104118
const formatted = { ...node };
@@ -107,7 +121,7 @@ class CheckboxTree extends React.Component {
107121
formatted.expanded = this.nodes[node.value].expanded;
108122
formatted.showCheckbox = node.showCheckbox !== undefined ? node.showCheckbox : true;
109123

110-
if (Array.isArray(node.children) && node.children.length > 0) {
124+
if (this.nodes[node.value].isParent) {
111125
formatted.children = this.getFormattedNodes(formatted.children);
112126
} else {
113127
formatted.children = null;
@@ -145,6 +159,22 @@ class CheckboxTree extends React.Component {
145159
return Boolean(node.disabled);
146160
}
147161

162+
nodeHasChildren(node) {
163+
return Array.isArray(node.children) && node.children.length > 0;
164+
}
165+
166+
expandAllNodes(expand = true) {
167+
const { onExpand } = this.props;
168+
169+
Object.keys(this.nodes).forEach((value) => {
170+
if (this.nodes[value].isParent) {
171+
this.nodes[value].expanded = expand;
172+
}
173+
});
174+
175+
onExpand(this.serializeList('expanded', null));
176+
}
177+
148178
toggleChecked(node, isChecked, noCascade) {
149179
if (node.children === null || noCascade) {
150180
// Set the check status of a leaf node or an uncoupled parent
@@ -167,7 +197,12 @@ class CheckboxTree extends React.Component {
167197
}
168198

169199
nodes.forEach((node) => {
170-
this.nodes[node.value] = {};
200+
const isParent = this.nodeHasChildren(node);
201+
202+
this.nodes[node.value] = {
203+
isParent,
204+
isLeaf: !isParent,
205+
};
171206
this.flattenNodes(node.children);
172207
});
173208
}
@@ -288,12 +323,45 @@ class CheckboxTree extends React.Component {
288323
return null;
289324
}
290325

326+
renderExpandAll() {
327+
const { icons: { expandAll, collapseAll }, showExpandAll } = this.props;
328+
329+
if (!showExpandAll) {
330+
return null;
331+
}
332+
333+
return (
334+
<div className="rct-options">
335+
<button
336+
aria-label="Expand all"
337+
className="rct-option rct-option-expand-all"
338+
title="Expand all"
339+
type="button"
340+
onClick={this.onExpandAll}
341+
>
342+
{expandAll}
343+
</button>
344+
<button
345+
aria-label="Expand all"
346+
className="rct-option rct-option-collapse-all"
347+
title="Collapse all"
348+
type="button"
349+
onClick={this.onCollapseAll}
350+
>
351+
{collapseAll}
352+
</button>
353+
</div>
354+
);
355+
}
356+
291357
renderHiddenInput() {
292-
if (this.props.name === undefined) {
358+
const { name, nameAsArray } = this.props;
359+
360+
if (name === undefined) {
293361
return null;
294362
}
295363

296-
if (this.props.nameAsArray) {
364+
if (nameAsArray) {
297365
return this.renderArrayHiddenInput();
298366
}
299367

@@ -325,6 +393,7 @@ class CheckboxTree extends React.Component {
325393

326394
return (
327395
<div className={className}>
396+
{this.renderExpandAll()}
328397
{this.renderHiddenInput()}
329398
{treeNodes}
330399
</div>

0 commit comments

Comments
 (0)