playbook_ui 12.25.0.pre.alpha.railsmultilevelimprovements758 → 12.25.0.pre.alpha.railsmultilevelimprovements776
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/pb_kits/playbook/pb_avatar/docs/_avatar_swift.md +82 -1
- data/app/pb_kits/playbook/pb_docs/kit_example.html.erb +14 -13
- data/app/pb_kits/playbook/pb_form_pill/_form_pill.tsx +3 -2
- data/app/pb_kits/playbook/pb_multi_level_select/_helper_functions.tsx +212 -0
- data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.scss +58 -98
- data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.tsx +347 -98
- data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_default.html.erb +4 -4
- data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_default.md +1 -1
- data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_return_all_selected.html.erb +1 -0
- data/app/pb_kits/playbook/pb_multi_level_select/multi_level_select.test.jsx +1 -1
- data/dist/playbook-rails.js +7 -7
- data/lib/playbook/version.rb +1 -1
- metadata +3 -4
- data/app/pb_kits/playbook/pb_multi_level_select/_multi_select_helper.tsx +0 -31
- data/app/pb_kits/playbook/pb_multi_level_select/helper_functions.ts +0 -87
@@ -1,9 +1,24 @@
|
|
1
|
-
import React, { useState, useEffect,
|
1
|
+
import React, { useState, useEffect, useRef } from "react";
|
2
2
|
import classnames from "classnames";
|
3
3
|
import { buildAriaProps, buildCss, buildDataProps } from "../utilities/props";
|
4
|
-
import { globalProps } from "../utilities/globalProps";
|
5
|
-
import
|
6
|
-
import
|
4
|
+
import { globalProps, GlobalProps } from "../utilities/globalProps";
|
5
|
+
import Icon from "../pb_icon/_icon";
|
6
|
+
import Checkbox from "../pb_checkbox/_checkbox";
|
7
|
+
import FormPill from "../pb_form_pill/_form_pill";
|
8
|
+
import CircleIconButton from "../pb_circle_icon_button/_circle_icon_button";
|
9
|
+
import {
|
10
|
+
unCheckIt,
|
11
|
+
getAncestorsOfUnchecked,
|
12
|
+
unCheckedRecursive,
|
13
|
+
checkedRecursive,
|
14
|
+
filterFormattedDataById,
|
15
|
+
findByFilter,
|
16
|
+
getCheckedItems,
|
17
|
+
updateReturnItems,
|
18
|
+
recursiveReturnOnlyParent,
|
19
|
+
removeChildrenIfParentChecked,
|
20
|
+
getChildIds,
|
21
|
+
} from "./_helper_functions";
|
7
22
|
|
8
23
|
type MultiLevelSelectProps = {
|
9
24
|
aria?: { [key: string]: string };
|
@@ -14,7 +29,7 @@ type MultiLevelSelectProps = {
|
|
14
29
|
returnAllSelected?: boolean;
|
15
30
|
treeData?: { [key: string]: string }[];
|
16
31
|
onSelect?: (prop: { [key: string]: any }) => void;
|
17
|
-
};
|
32
|
+
} & GlobalProps;
|
18
33
|
|
19
34
|
const MultiLevelSelect = (props: MultiLevelSelectProps) => {
|
20
35
|
const {
|
@@ -36,54 +51,42 @@ const MultiLevelSelect = (props: MultiLevelSelectProps) => {
|
|
36
51
|
className
|
37
52
|
);
|
38
53
|
|
54
|
+
const dropdownRef = useRef(null);
|
55
|
+
|
56
|
+
//state for whether dropdown is open or closed
|
57
|
+
const [isClosed, setIsClosed] = useState(true);
|
58
|
+
//state from onchange for textinput, to use for filtering to create typeahead
|
59
|
+
const [filterItem, setFilterItem] = useState("");
|
60
|
+
//this is essentially the return that the user will get when they use the kit
|
61
|
+
const [returnedArray, setReturnedArray] = useState([]);
|
62
|
+
//formattedData with checked and parent_id added
|
39
63
|
const [formattedData, setFormattedData] = useState(treeData);
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
const ancestor = findItemById(formattedData,item)
|
66
|
-
ancestor.expanded = true
|
67
|
-
});
|
68
|
-
}
|
69
|
-
} else {
|
70
|
-
unCheckIt(foundItem, selectedItems, setSelectedItems, false);
|
71
|
-
if (currentNode._parent) {
|
72
|
-
const parents = getParentAndAncestorsIds(currentNode._parent, formattedData)
|
73
|
-
parents.forEach((item:string) => {
|
74
|
-
const ancestor = findItemById(formattedData,item)
|
75
|
-
ancestor.expanded = true
|
76
|
-
});
|
77
|
-
}
|
78
|
-
}
|
79
|
-
}
|
80
|
-
}
|
64
|
+
//toggle chevron in dropdown
|
65
|
+
//@ts-ignore
|
66
|
+
const [isToggled, setIsToggled] = useState<{ [id: number]: boolean }>({});
|
67
|
+
//state for return for default
|
68
|
+
const [defaultReturn, setDefaultReturn] = useState([]);
|
69
|
+
|
70
|
+
useEffect(() => {
|
71
|
+
let el = document.getElementById(`pb_data_wrapper_${id}`);
|
72
|
+
if (el) {
|
73
|
+
el.setAttribute(
|
74
|
+
"data-tree",
|
75
|
+
JSON.stringify(returnAllSelected ? returnedArray : defaultReturn)
|
76
|
+
);
|
77
|
+
}
|
78
|
+
updateHiddenInputValue(returnAllSelected ? returnedArray : defaultReturn);
|
79
|
+
returnAllSelected
|
80
|
+
? onSelect(returnedArray)
|
81
|
+
: onSelect(
|
82
|
+
defaultReturn.filter(
|
83
|
+
(item, index, self) =>
|
84
|
+
index === self.findIndex((obj) => obj.id === item.id)
|
85
|
+
)
|
86
|
+
);
|
87
|
+
}, [returnedArray, defaultReturn]);
|
88
|
+
|
81
89
|
|
82
|
-
return item;
|
83
|
-
});
|
84
|
-
console.log("updatedData", updatedData)
|
85
|
-
setFormattedData(updatedData);
|
86
|
-
};
|
87
90
|
|
88
91
|
const updateHiddenInputValue = (value: any) => {
|
89
92
|
const hiddenInput = document.querySelector('input#'+id) as HTMLInputElement;
|
@@ -92,65 +95,311 @@ const MultiLevelSelect = (props: MultiLevelSelectProps) => {
|
|
92
95
|
}
|
93
96
|
};
|
94
97
|
|
95
|
-
|
96
98
|
useEffect(() => {
|
99
|
+
//Create new formattedData array for use
|
100
|
+
setFormattedData(addCheckedAndParentProperty(treeData));
|
101
|
+
// Function to handle clicks outside the dropdown
|
102
|
+
const handleClickOutside = (event: any) => {
|
103
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
104
|
+
setIsClosed(true);
|
105
|
+
}
|
106
|
+
};
|
107
|
+
//if any items already checked in first render, set return accordingly
|
108
|
+
const initialChecked = getCheckedItems(treeData)
|
109
|
+
initialChecked && returnAllSelected && setReturnedArray(initialChecked)
|
110
|
+
initialChecked && !returnAllSelected && setDefaultReturn(initialChecked)
|
111
|
+
|
112
|
+
// Attach the event listener
|
113
|
+
window.addEventListener("click", handleClickOutside);
|
114
|
+
// Clean up the event listener on unmount
|
115
|
+
return () => {
|
116
|
+
window.removeEventListener("click", handleClickOutside);
|
117
|
+
};
|
118
|
+
}, []);
|
119
|
+
|
120
|
+
|
121
|
+
//function to map over data and add parent_id + depth property to each item
|
122
|
+
const addCheckedAndParentProperty = (
|
123
|
+
treeData: { [key: string]: any }[],
|
124
|
+
parent_id: string = null,
|
125
|
+
depth: number = 0,
|
126
|
+
) => {
|
127
|
+
if (!Array.isArray(treeData)) {
|
128
|
+
return;
|
129
|
+
}
|
130
|
+
return treeData.map((item: { [key: string]: any } | any) => {
|
131
|
+
const newItem = {
|
132
|
+
...item,
|
133
|
+
parent_id,
|
134
|
+
depth,
|
135
|
+
};
|
136
|
+
if (newItem.children && newItem.children.length > 0) {
|
137
|
+
newItem.children = addCheckedAndParentProperty(
|
138
|
+
newItem.children,
|
139
|
+
newItem.id,
|
140
|
+
depth + 1,
|
141
|
+
);
|
142
|
+
}
|
143
|
+
return newItem;
|
144
|
+
});
|
145
|
+
};
|
146
|
+
|
147
|
+
//click event for x on form pill
|
148
|
+
const handlePillClose = (event: any, clickedItem: { [key: string]: any }) => {
|
149
|
+
// prevents the dropdown from closing when clicking on the pill
|
150
|
+
event.stopPropagation();
|
151
|
+
//logic for removing items from returnArray or defaultReturn when pills clicked
|
97
152
|
if (returnAllSelected) {
|
98
|
-
|
99
|
-
(
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
153
|
+
if (returnedArray.includes(clickedItem)) {
|
154
|
+
if (clickedItem.children && clickedItem.children.length > 0) {
|
155
|
+
const childrenOfChecked = getChildIds(clickedItem, returnedArray);
|
156
|
+
const updatedFiltered = returnedArray
|
157
|
+
.filter((item) => item !== clickedItem)
|
158
|
+
.filter((item) => !childrenOfChecked.includes(item.id));
|
159
|
+
setReturnedArray(updatedFiltered);
|
160
|
+
} else {
|
161
|
+
const updatedFiltered = returnedArray.filter(
|
162
|
+
(item) => item !== clickedItem
|
163
|
+
);
|
164
|
+
setReturnedArray(updatedFiltered);
|
165
|
+
}
|
166
|
+
}
|
167
|
+
} else {
|
168
|
+
if (defaultReturn.includes(clickedItem)) {
|
169
|
+
getAncestorsOfUnchecked(formattedData, clickedItem);
|
170
|
+
const newChecked = getCheckedItems(formattedData);
|
171
|
+
const filteredReturn = updateReturnItems(newChecked).filter(
|
172
|
+
(item) => item.id !== clickedItem.id
|
173
|
+
);
|
174
|
+
setDefaultReturn(filteredReturn);
|
175
|
+
}
|
176
|
+
}
|
177
|
+
if (clickedItem.children && clickedItem.children.length > 0) {
|
178
|
+
unCheckedRecursive(clickedItem);
|
106
179
|
}
|
107
|
-
|
180
|
+
//logic to uncheck clickedItem in formattedData
|
181
|
+
unCheckIt(formattedData, clickedItem.id);
|
182
|
+
};
|
108
183
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
184
|
+
//handle click on input wrapper(entire div with pills, typeahead, etc) so it doesn't close when input or form pill is clicked
|
185
|
+
const handleInputWrapperClick = (e: any) => {
|
186
|
+
e.stopPropagation();
|
187
|
+
if (
|
188
|
+
e.target.id === "multiselect_input" ||
|
189
|
+
e.target.classList.contains("pb_form_pill_tag")
|
190
|
+
) {
|
191
|
+
return;
|
113
192
|
}
|
114
|
-
|
115
|
-
|
116
|
-
|
193
|
+
setIsClosed(!isClosed);
|
194
|
+
};
|
195
|
+
|
196
|
+
//Main function to handle any click inside dropdown
|
197
|
+
const handledropdownItemClick = (e: any) => {
|
198
|
+
const clickedItem = e.target.parentNode.id;
|
199
|
+
//setting filterItem to "" will clear textinput and clear typeahead
|
200
|
+
setFilterItem("");
|
201
|
+
|
202
|
+
const filtered = filterFormattedDataById(formattedData, clickedItem);
|
203
|
+
//check and uncheck all children of checked/unchecked parent item
|
204
|
+
if (filtered[0].children && filtered[0].children.length > 0) {
|
205
|
+
if (filtered[0].checked) {
|
206
|
+
filtered[0].children.forEach((item: { [key: string]: any }) => {
|
207
|
+
checkedRecursive(item);
|
208
|
+
});
|
209
|
+
} else if (!filtered[0].checked) {
|
210
|
+
filtered[0].children.forEach((item: { [key: string]: any }) => {
|
211
|
+
unCheckedRecursive(item);
|
212
|
+
});
|
213
|
+
}
|
214
|
+
}
|
215
|
+
|
216
|
+
const checkedItems = getCheckedItems(formattedData);
|
217
|
+
|
218
|
+
//checking and unchecking items for returnAllSelected variant
|
219
|
+
if (returnedArray.includes(filtered[0])) {
|
220
|
+
if (!filtered[0].checked) {
|
221
|
+
if (filtered[0].children && filtered[0].children.length > 0) {
|
222
|
+
const childrenOfChecked = getChildIds(filtered[0], returnedArray);
|
223
|
+
const updatedFiltered = returnedArray
|
224
|
+
.filter((item) => item !== filtered[0])
|
225
|
+
.filter((item) => !childrenOfChecked.includes(item.id));
|
226
|
+
|
227
|
+
setReturnedArray(updatedFiltered);
|
228
|
+
} else {
|
229
|
+
const updatedFiltered = returnedArray.filter(
|
230
|
+
(item) => item !== filtered[0]
|
231
|
+
);
|
232
|
+
setReturnedArray(updatedFiltered);
|
233
|
+
}
|
234
|
+
}
|
235
|
+
} else {
|
236
|
+
setReturnedArray(checkedItems);
|
117
237
|
}
|
118
|
-
}, [checkedData]);
|
119
238
|
|
120
|
-
|
239
|
+
//when item is unchecked for default variant
|
240
|
+
if (!filtered[0].checked && !returnAllSelected) {
|
241
|
+
//uncheck parent and grandparent if any child unchecked
|
242
|
+
getAncestorsOfUnchecked(formattedData, filtered[0]);
|
243
|
+
|
244
|
+
const newChecked = getCheckedItems(formattedData);
|
245
|
+
//get all checked items, and filter to check if all children checked, if yes return only parent
|
246
|
+
const filteredReturn = updateReturnItems(newChecked);
|
247
|
+
setDefaultReturn(filteredReturn);
|
248
|
+
}
|
249
|
+
|
250
|
+
//when item is checked for default variant
|
251
|
+
if (!returnAllSelected && filtered[0].checked) {
|
252
|
+
//if checked item has children
|
253
|
+
if (filtered[0].children && filtered[0].children.length > 0) {
|
254
|
+
removeChildrenIfParentChecked(
|
255
|
+
filtered[0],
|
256
|
+
defaultReturn,
|
257
|
+
setDefaultReturn
|
258
|
+
);
|
259
|
+
}
|
260
|
+
|
261
|
+
//if clicked item has parent_id, find parent and check if all children checked or not
|
262
|
+
if (filtered[0].parent_id !== null) {
|
263
|
+
recursiveReturnOnlyParent(
|
264
|
+
filtered[0],
|
265
|
+
formattedData,
|
266
|
+
defaultReturn,
|
267
|
+
setDefaultReturn
|
268
|
+
);
|
269
|
+
} else {
|
270
|
+
setDefaultReturn([filtered[0]]);
|
271
|
+
}
|
272
|
+
}
|
273
|
+
};
|
274
|
+
|
275
|
+
//handle click on chevron toggles in dropdown
|
276
|
+
const handleToggleClick = (id: string, event: React.MouseEvent) => {
|
277
|
+
event.stopPropagation();
|
278
|
+
setIsToggled((prevState: { [id: string]: boolean }) => ({
|
279
|
+
...prevState,
|
280
|
+
[id]: !prevState[id],
|
281
|
+
}));
|
282
|
+
const clickedItem = filterFormattedDataById(formattedData, id);
|
283
|
+
|
284
|
+
if (clickedItem) {
|
285
|
+
clickedItem[0].expanded = !clickedItem[0].expanded;
|
286
|
+
}
|
287
|
+
};
|
288
|
+
|
289
|
+
//rendering formattedData to UI based on typeahead
|
290
|
+
const renderNestedOptions = (items: { [key: string]: any }[]) => {
|
121
291
|
return (
|
122
|
-
<
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
292
|
+
<ul>
|
293
|
+
{Array.isArray(items) &&
|
294
|
+
items.map((item: { [key: string]: any }) => {
|
295
|
+
return (
|
296
|
+
<>
|
297
|
+
<li
|
298
|
+
key={item.id}
|
299
|
+
className="dropdown_item"
|
300
|
+
data-name={item.id}
|
301
|
+
>
|
302
|
+
<div className="dropdown_item_checkbox_row">
|
303
|
+
<div
|
304
|
+
key={
|
305
|
+
item.expanded ? "chevron-down" : "chevron-right"
|
306
|
+
}
|
307
|
+
>
|
308
|
+
<CircleIconButton
|
309
|
+
icon={
|
310
|
+
item.expanded ? "chevron-down" : "chevron-right"
|
311
|
+
}
|
312
|
+
className={item.children && item.children.length > 0 ? "" : "toggle_icon"}
|
313
|
+
onClick={(event) => handleToggleClick(item.id, event)}
|
314
|
+
variant="link"
|
315
|
+
/>
|
316
|
+
</div>
|
317
|
+
<Checkbox text={item.label} id={item.id}>
|
318
|
+
<input
|
319
|
+
checked={item.checked}
|
320
|
+
type="checkbox"
|
321
|
+
name={item.label}
|
322
|
+
value={item.label}
|
323
|
+
onChange={(e) => {
|
324
|
+
item.checked = !item.checked;
|
325
|
+
handledropdownItemClick(e);
|
326
|
+
}}
|
327
|
+
/>
|
328
|
+
</Checkbox>
|
329
|
+
</div>
|
330
|
+
{item.expanded &&
|
331
|
+
item.children &&
|
332
|
+
item.children.length > 0 &&
|
333
|
+
!filterItem && ( // Show children if expanded is true
|
334
|
+
<div>{renderNestedOptions(item.children)}</div>
|
335
|
+
)}
|
336
|
+
</li>
|
337
|
+
</>
|
338
|
+
);
|
339
|
+
})}
|
340
|
+
</ul>
|
136
341
|
);
|
137
|
-
}
|
342
|
+
};
|
138
343
|
|
139
344
|
return (
|
140
345
|
<div {...ariaProps} {...dataProps} className={classes} id={id}>
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
346
|
+
<div ref={dropdownRef} className="wrapper">
|
347
|
+
<div className="input_wrapper" onClick={handleInputWrapperClick}>
|
348
|
+
<div className="input_inner_container">
|
349
|
+
<input type="hidden" id={id} name={name} value="" />
|
350
|
+
{returnedArray.length !== 0 && returnAllSelected
|
351
|
+
? returnedArray.map((item, index) => (
|
352
|
+
<FormPill
|
353
|
+
key={index}
|
354
|
+
text={item.label}
|
355
|
+
size="small"
|
356
|
+
onClick={(event) => handlePillClose(event, item)}
|
357
|
+
/>
|
358
|
+
))
|
359
|
+
: null}
|
360
|
+
{!returnAllSelected &&
|
361
|
+
defaultReturn.length !== 0 &&
|
362
|
+
defaultReturn
|
363
|
+
.filter(
|
364
|
+
(item, index, self) =>
|
365
|
+
index === self.findIndex((obj) => obj.id === item.id)
|
366
|
+
)
|
367
|
+
.map((item, index) => (
|
368
|
+
<FormPill
|
369
|
+
key={index}
|
370
|
+
text={item.label}
|
371
|
+
size="small"
|
372
|
+
onClick={(event) => handlePillClose(event, item)}
|
373
|
+
/>
|
374
|
+
))}
|
375
|
+
{returnedArray.length !== 0 && returnAllSelected && <br />}
|
376
|
+
{defaultReturn.length !== 0 && !returnAllSelected && <br />}
|
377
|
+
<input
|
378
|
+
id="multiselect_input"
|
379
|
+
onChange={(e) => {
|
380
|
+
setFilterItem(e.target.value);
|
381
|
+
}}
|
382
|
+
placeholder="Start typing..."
|
383
|
+
value={filterItem}
|
384
|
+
onClick={() => setIsClosed(false)}
|
385
|
+
/>
|
386
|
+
</div>
|
387
|
+
{isClosed ? (
|
388
|
+
<div key="chevron-down">
|
389
|
+
<Icon icon="chevron-down" />
|
390
|
+
</div>
|
391
|
+
) : (
|
392
|
+
<div key="chevron-up">
|
393
|
+
<Icon icon="chevron-up" />
|
394
|
+
</div>
|
395
|
+
)}
|
396
|
+
</div>
|
397
|
+
<div className={`dropdown_menu ${isClosed ? "close" : "open"}`}>
|
398
|
+
{renderNestedOptions(
|
399
|
+
filterItem ? findByFilter(formattedData, filterItem) : formattedData
|
400
|
+
)}
|
401
|
+
</div>
|
402
|
+
</div>
|
154
403
|
</div>
|
155
404
|
);
|
156
405
|
};
|
@@ -2,4 +2,4 @@ The MultiLevelSelect kit renders a multi leveled select dropdown based on data f
|
|
2
2
|
|
3
3
|
For the React version of the kit, the `onSelect` prop returns an array of all checked items, irrespective of whether it is a parent, child or grandchild. Open the console on this example and check and uncheck checkboxes to see this is action!
|
4
4
|
|
5
|
-
For the Rails version, the array of checked items is attached to the DOM in a data attribute titled `data-tree` on the wrapping div around
|
5
|
+
For the Rails version, the array of checked items is attached to the DOM in a data attribute titled `data-tree` on the wrapping div around the MultiLevelSelect.
|