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.
@@ -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.
@@ -3,6 +3,7 @@
3
3
  value: "Power Home Remodeling",
4
4
  id: "powerhome1",
5
5
  expanded: true,
6
+
6
7
  children: [
7
8
  {
8
9
  label: "People",
@@ -16,7 +16,7 @@ const treeData = {
16
16
  {
17
17
  label: 'No one can get me',
18
18
  value: 'anonymous',
19
- id:'default2',
19
+ id:'default3',
20
20
  },
21
21
  ],
22
22
  },