playbook_ui 12.25.0.pre.alpha.railsmultilevelimprovements758 → 12.25.0.pre.alpha.railsmultilevelimprovements780

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,9 +1,24 @@
1
- import React, { useState, useEffect, useMemo } from "react";
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 { findItemById, checkIt, unCheckIt, getParentAndAncestorsIds } from "./helper_functions";
6
- import MultiSelectHelper from "./_multi_select_helper";
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
- const [selectedItems, setSelectedItems] = useState([]);
41
- const [checkedData, setCheckedData] = useState([]);
42
-
43
- const onChange = (currentNode: { [key: string]: any }) => {
44
- console.log("currentNode", currentNode)
45
- const updatedData = formattedData.map((item: any) => {
46
- if (item.id === currentNode._id) {
47
- console.log("GETTING HERE --------- item", item)
48
- if (currentNode.checked) {
49
- console.log("GETTING HERE --------- currentNode.checked", currentNode.checked)
50
- checkIt(item, selectedItems, setSelectedItems, false);
51
- } else {
52
- unCheckIt(item, selectedItems, setSelectedItems, false);
53
- }
54
- } else if (item.children) {
55
- console.log("GETTING HERE --------- item.children", item.children)
56
- const foundItem = findItemById(item.children, currentNode._id);
57
- if (foundItem) {
58
- console.log("GETTING HERE --------- foundItem", foundItem)
59
- if (currentNode.checked) {
60
- console.log("GETTING HERE --------- currentNode.checked other one", currentNode.checked)
61
- checkIt(foundItem, selectedItems, setSelectedItems, false);
62
- if (currentNode._parent) {
63
- const parents = getParentAndAncestorsIds(currentNode._parent, formattedData)
64
- parents.forEach((item:string) => {
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
- const selected = selectedItems.filter(
99
- (item: { [key: string]: any }) => item.checked
100
- );
101
- //filter to remove duplicate items
102
- const uniqueSelected = selected.filter(
103
- (obj, index, self) => index === self.findIndex((t) => t.id === obj.id)
104
- );
105
- setCheckedData(uniqueSelected);
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
- }, [selectedItems]);
180
+ //logic to uncheck clickedItem in formattedData
181
+ unCheckIt(formattedData, clickedItem.id);
182
+ };
108
183
 
109
- useEffect(() => {
110
- let el = document.getElementById(`pb_data_wrapper_${id}`);
111
- if (el) {
112
- el.setAttribute("data-tree", JSON.stringify(checkedData));
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
- updateHiddenInputValue(checkedData);
115
- if (returnAllSelected) {
116
- onSelect(checkedData);
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
- const DropDownSelectComponent = useMemo(() => {
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
- <MultiSelectHelper
123
- treeData={formattedData}
124
- onChange={(
125
- // @ts-ignore
126
- selectedNodes: { [key: string]: any }[],
127
- currentNode: { [key: string]: any }[]
128
- ) => {
129
- setCheckedData(currentNode);
130
- onSelect(currentNode);
131
-
132
- }}
133
- id={id}
134
- {...props}
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
- }, [formattedData])
342
+ };
138
343
 
139
344
  return (
140
345
  <div {...ariaProps} {...dataProps} className={classes} id={id}>
141
-
142
- <input type="hidden" id={id} name={name} value="" />
143
- {returnAllSelected ? (
144
- <MultiSelectHelper
145
- treeData={formattedData}
146
- treeMode={returnAllSelected}
147
- id={id}
148
- onChange={onChange}
149
- {...props}
150
- />
151
- ) : (
152
- <>{DropDownSelectComponent}</>
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
  };
@@ -64,9 +64,9 @@
64
64
  }] %>
65
65
 
66
66
 
67
- <%= pb_rails("multi_level_select", props: {
68
- id: "default-multi-level-select",
69
- tree_data:treeData
70
- }) %>
67
+ <%= pb_rails("multi_level_select", props: {
68
+ id: "default-multi-level-select",
69
+ tree_data:treeData
70
+ }) %>
71
71
 
72
72
 
@@ -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 the MultiLevelSelect.
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.