playbook_ui 12.25.0.pre.alpha.PLAY818multilevelrebuild779 → 12.25.0.pre.alpha.play822bolddefaultfortitle3764

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,24 +1,9 @@
1
- import React, { useState, useEffect, useRef } from "react";
1
+ import React, { useState, useEffect, useMemo } from "react";
2
2
  import classnames from "classnames";
3
3
  import { buildAriaProps, buildCss, buildDataProps } from "../utilities/props";
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";
4
+ import { globalProps } from "../utilities/globalProps";
5
+ import { findItemById, checkIt, unCheckIt, getParentAndAncestorsIds } from "./helper_functions";
6
+ import MultiSelectHelper from "./_multi_select_helper";
22
7
 
23
8
  type MultiLevelSelectProps = {
24
9
  aria?: { [key: string]: string };
@@ -28,7 +13,7 @@ type MultiLevelSelectProps = {
28
13
  returnAllSelected?: boolean;
29
14
  treeData?: { [key: string]: string }[];
30
15
  onSelect?: (prop: { [key: string]: any }) => void;
31
- } & GlobalProps;
16
+ };
32
17
 
33
18
  const MultiLevelSelect = (props: MultiLevelSelectProps) => {
34
19
  const {
@@ -49,343 +34,104 @@ const MultiLevelSelect = (props: MultiLevelSelectProps) => {
49
34
  className
50
35
  );
51
36
 
52
- const dropdownRef = useRef(null);
53
-
54
- //state for whether dropdown is open or closed
55
- const [isClosed, setIsClosed] = useState(true);
56
- //state from onchange for textinput, to use for filtering to create typeahead
57
- const [filterItem, setFilterItem] = useState("");
58
- //this is essentially the return that the user will get when they use the kit
59
- const [returnedArray, setReturnedArray] = useState([]);
60
- //formattedData with checked and parent_id added
61
37
  const [formattedData, setFormattedData] = useState(treeData);
62
- //toggle chevron in dropdown
63
- //@ts-ignore
64
- const [isToggled, setIsToggled] = useState<{ [id: number]: boolean }>({});
65
- //state for return for default
66
- const [defaultReturn, setDefaultReturn] = useState([]);
67
-
68
- useEffect(() => {
69
- let el = document.getElementById(`pb_data_wrapper_${id}`);
70
- if (el) {
71
- el.setAttribute(
72
- "data-tree",
73
- JSON.stringify(returnAllSelected ? returnedArray : defaultReturn)
74
- );
75
- }
76
- returnAllSelected
77
- ? onSelect(returnedArray)
78
- : onSelect(
79
- defaultReturn.filter(
80
- (item, index, self) =>
81
- index === self.findIndex((obj) => obj.id === item.id)
82
- )
83
- );
84
- }, [returnedArray, defaultReturn]);
85
-
86
- useEffect(() => {
87
- //Create new formattedData array for use
88
- setFormattedData(addCheckedAndParentProperty(treeData));
89
- // Function to handle clicks outside the dropdown
90
- const handleClickOutside = (event: any) => {
91
- if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
92
- setIsClosed(true);
93
- }
94
- };
95
- //if any items already checked in first render, set return accordingly
96
- const initialChecked = getCheckedItems(treeData)
97
- initialChecked && returnAllSelected && setReturnedArray(initialChecked)
98
- initialChecked && !returnAllSelected && setDefaultReturn(initialChecked)
99
-
100
- // Attach the event listener
101
- window.addEventListener("click", handleClickOutside);
102
- // Clean up the event listener on unmount
103
- return () => {
104
- window.removeEventListener("click", handleClickOutside);
105
- };
106
- }, []);
107
-
108
- //function to map over data and add parent_id + depth property to each item
109
- const addCheckedAndParentProperty = (
110
- treeData: { [key: string]: any }[],
111
- parent_id: string = null,
112
- depth: number = 0,
113
- ) => {
114
- if (!Array.isArray(treeData)) {
115
- return;
116
- }
117
- return treeData.map((item: { [key: string]: any } | any) => {
118
- const newItem = {
119
- ...item,
120
- parent_id,
121
- depth,
122
- };
123
- if (newItem.children && newItem.children.length > 0) {
124
- newItem.children = addCheckedAndParentProperty(
125
- newItem.children,
126
- newItem.id,
127
- depth + 1,
128
- );
129
- }
130
- return newItem;
131
- });
132
- };
133
-
134
- //click event for x on form pill
135
- const handlePillClose = (event: any, clickedItem: { [key: string]: any }) => {
136
- // prevents the dropdown from closing when clicking on the pill
137
- event.stopPropagation();
138
- //logic for removing items from returnArray or defaultReturn when pills clicked
139
- if (returnAllSelected) {
140
- if (returnedArray.includes(clickedItem)) {
141
- if (clickedItem.children && clickedItem.children.length > 0) {
142
- const childrenOfChecked = getChildIds(clickedItem, returnedArray);
143
- const updatedFiltered = returnedArray
144
- .filter((item) => item !== clickedItem)
145
- .filter((item) => !childrenOfChecked.includes(item.id));
146
- setReturnedArray(updatedFiltered);
38
+ const [selectedItems, setSelectedItems] = useState([]);
39
+ const [checkedData, setCheckedData] = useState([]);
40
+
41
+ const onChange = (currentNode: { [key: string]: any }) => {
42
+ const updatedData = formattedData.map((item: any) => {
43
+ if (item.id === currentNode._id) {
44
+ if (currentNode.checked) {
45
+ checkIt(item, selectedItems, setSelectedItems, false);
147
46
  } else {
148
- const updatedFiltered = returnedArray.filter(
149
- (item) => item !== clickedItem
150
- );
151
- setReturnedArray(updatedFiltered);
47
+ unCheckIt(item, selectedItems, setSelectedItems, false);
152
48
  }
153
- }
154
- } else {
155
- if (defaultReturn.includes(clickedItem)) {
156
- getAncestorsOfUnchecked(formattedData, clickedItem);
157
- const newChecked = getCheckedItems(formattedData);
158
- const filteredReturn = updateReturnItems(newChecked).filter(
159
- (item) => item.id !== clickedItem.id
160
- );
161
- setDefaultReturn(filteredReturn);
162
- }
163
- }
164
- if (clickedItem.children && clickedItem.children.length > 0) {
165
- unCheckedRecursive(clickedItem);
166
- }
167
- //logic to uncheck clickedItem in formattedData
168
- unCheckIt(formattedData, clickedItem.id);
169
- };
170
-
171
- //handle click on input wrapper(entire div with pills, typeahead, etc) so it doesn't close when input or form pill is clicked
172
- const handleInputWrapperClick = (e: any) => {
173
- e.stopPropagation();
174
- if (
175
- e.target.id === "multiselect_input" ||
176
- e.target.classList.contains("pb_form_pill_tag")
177
- ) {
178
- return;
179
- }
180
- setIsClosed(!isClosed);
181
- };
182
-
183
- //Main function to handle any click inside dropdown
184
- const handledropdownItemClick = (e: any) => {
185
- const clickedItem = e.target.parentNode.id;
186
- //setting filterItem to "" will clear textinput and clear typeahead
187
- setFilterItem("");
188
-
189
- const filtered = filterFormattedDataById(formattedData, clickedItem);
190
- //check and uncheck all children of checked/unchecked parent item
191
- if (filtered[0].children && filtered[0].children.length > 0) {
192
- if (filtered[0].checked) {
193
- filtered[0].children.forEach((item: { [key: string]: any }) => {
194
- checkedRecursive(item);
195
- });
196
- } else if (!filtered[0].checked) {
197
- filtered[0].children.forEach((item: { [key: string]: any }) => {
198
- unCheckedRecursive(item);
199
- });
200
- }
201
- }
202
-
203
- const checkedItems = getCheckedItems(formattedData);
204
-
205
- //checking and unchecking items for returnAllSelected variant
206
- if (returnedArray.includes(filtered[0])) {
207
- if (!filtered[0].checked) {
208
- if (filtered[0].children && filtered[0].children.length > 0) {
209
- const childrenOfChecked = getChildIds(filtered[0], returnedArray);
210
- const updatedFiltered = returnedArray
211
- .filter((item) => item !== filtered[0])
212
- .filter((item) => !childrenOfChecked.includes(item.id));
213
-
214
- setReturnedArray(updatedFiltered);
215
- } else {
216
- const updatedFiltered = returnedArray.filter(
217
- (item) => item !== filtered[0]
218
- );
219
- setReturnedArray(updatedFiltered);
49
+ } else if (item.children) {
50
+ const foundItem = findItemById(item.children, currentNode._id);
51
+ if (foundItem) {
52
+ if (currentNode.checked) {
53
+ checkIt(foundItem, selectedItems, setSelectedItems, false);
54
+ if (currentNode._parent) {
55
+ const parents = getParentAndAncestorsIds(currentNode._parent, formattedData)
56
+ parents.forEach((item:string) => {
57
+ const ancestor = findItemById(formattedData,item)
58
+ ancestor.expanded = true
59
+ });
60
+ }
61
+ } else {
62
+ unCheckIt(foundItem, selectedItems, setSelectedItems, false);
63
+ if (currentNode._parent) {
64
+ const parents = getParentAndAncestorsIds(currentNode._parent, formattedData)
65
+ parents.forEach((item:string) => {
66
+ const ancestor = findItemById(formattedData,item)
67
+ ancestor.expanded = true
68
+ });
69
+ }
70
+ }
220
71
  }
221
72
  }
222
- } else {
223
- setReturnedArray(checkedItems);
224
- }
225
-
226
- //when item is unchecked for default variant
227
- if (!filtered[0].checked && !returnAllSelected) {
228
- //uncheck parent and grandparent if any child unchecked
229
- getAncestorsOfUnchecked(formattedData, filtered[0]);
230
-
231
- const newChecked = getCheckedItems(formattedData);
232
- //get all checked items, and filter to check if all children checked, if yes return only parent
233
- const filteredReturn = updateReturnItems(newChecked);
234
- setDefaultReturn(filteredReturn);
235
- }
236
73
 
237
- //when item is checked for default variant
238
- if (!returnAllSelected && filtered[0].checked) {
239
- //if checked item has children
240
- if (filtered[0].children && filtered[0].children.length > 0) {
241
- removeChildrenIfParentChecked(
242
- filtered[0],
243
- defaultReturn,
244
- setDefaultReturn
245
- );
246
- }
74
+ return item;
75
+ });
247
76
 
248
- //if clicked item has parent_id, find parent and check if all children checked or not
249
- if (filtered[0].parent_id !== null) {
250
- recursiveReturnOnlyParent(
251
- filtered[0],
252
- formattedData,
253
- defaultReturn,
254
- setDefaultReturn
255
- );
256
- } else {
257
- setDefaultReturn([filtered[0]]);
258
- }
259
- }
77
+ setFormattedData(updatedData);
260
78
  };
261
79
 
262
- //handle click on chevron toggles in dropdown
263
- const handleToggleClick = (id: string, event: React.MouseEvent) => {
264
- event.stopPropagation();
265
- setIsToggled((prevState: { [id: string]: boolean }) => ({
266
- ...prevState,
267
- [id]: !prevState[id],
268
- }));
269
- const clickedItem = filterFormattedDataById(formattedData, id);
80
+ useEffect(() => {
81
+ if (returnAllSelected) {
82
+ const selected = selectedItems.filter(
83
+ (item: { [key: string]: any }) => item.checked
84
+ );
85
+ //filter to remove duplicate items
86
+ const uniqueSelected = selected.filter(
87
+ (obj, index, self) => index === self.findIndex((t) => t.id === obj.id)
88
+ );
89
+ setCheckedData(uniqueSelected);
90
+ }
91
+ }, [selectedItems]);
270
92
 
271
- if (clickedItem) {
272
- clickedItem[0].expanded = !clickedItem[0].expanded;
93
+ useEffect(() => {
94
+ let el = document.getElementById(`pb_data_wrapper_${id}`);
95
+ if (el) {
96
+ el.setAttribute("data-tree", JSON.stringify(checkedData));
273
97
  }
274
- };
98
+ if (returnAllSelected) {
99
+ onSelect(checkedData);
100
+ }
101
+ }, [checkedData]);
275
102
 
276
- //rendering formattedData to UI based on typeahead
277
- const renderNestedOptions = (items: { [key: string]: any }[]) => {
103
+ const DropDownSelectComponent = useMemo(() => {
278
104
  return (
279
- <ul>
280
- {Array.isArray(items) &&
281
- items.map((item: { [key: string]: any }) => {
282
- return (
283
- <>
284
- <li
285
- key={item.id}
286
- className="dropdown_item"
287
- data-name={item.id}
288
- >
289
- <div className="dropdown_item_checkbox_row">
290
- <div
291
- key={
292
- item.expanded ? "chevron-down" : "chevron-right"
293
- }
294
- >
295
- <CircleIconButton
296
- icon={
297
- item.expanded ? "chevron-down" : "chevron-right"
298
- }
299
- className={item.children && item.children.length > 0 ? "" : "toggle_icon"}
300
- onClick={(event) => handleToggleClick(item.id, event)}
301
- variant="link"
302
- />
303
- </div>
304
- <Checkbox text={item.label} id={item.id}>
305
- <input
306
- checked={item.checked}
307
- type="checkbox"
308
- name={item.label}
309
- value={item.label}
310
- onChange={(e) => {
311
- item.checked = !item.checked;
312
- handledropdownItemClick(e);
313
- }}
314
- />
315
- </Checkbox>
316
- </div>
317
- {item.expanded &&
318
- item.children &&
319
- item.children.length > 0 &&
320
- !filterItem && ( // Show children if expanded is true
321
- <div>{renderNestedOptions(item.children)}</div>
322
- )}
323
- </li>
324
- </>
325
- );
326
- })}
327
- </ul>
105
+ <MultiSelectHelper
106
+ treeData={formattedData}
107
+ onChange={(
108
+ // @ts-ignore
109
+ selectedNodes: { [key: string]: any }[],
110
+ currentNode: { [key: string]: any }[]
111
+ ) => {
112
+ setCheckedData(currentNode);
113
+ onSelect(currentNode);
114
+
115
+ }}
116
+ id={id}
117
+ {...props}
118
+ />
328
119
  );
329
- };
120
+ }, [formattedData])
330
121
 
331
122
  return (
332
123
  <div {...ariaProps} {...dataProps} className={classes} id={id}>
333
- <div ref={dropdownRef} className="wrapper">
334
- <div className="input_wrapper" onClick={handleInputWrapperClick}>
335
- <div className="input_inner_container">
336
- {returnedArray.length !== 0 && returnAllSelected
337
- ? returnedArray.map((item, index) => (
338
- <FormPill
339
- key={index}
340
- text={item.label}
341
- size="small"
342
- onClick={(event) => handlePillClose(event, item)}
343
- />
344
- ))
345
- : null}
346
- {!returnAllSelected &&
347
- defaultReturn.length !== 0 &&
348
- defaultReturn
349
- .filter(
350
- (item, index, self) =>
351
- index === self.findIndex((obj) => obj.id === item.id)
352
- )
353
- .map((item, index) => (
354
- <FormPill
355
- key={index}
356
- text={item.label}
357
- size="small"
358
- onClick={(event) => handlePillClose(event, item)}
359
- />
360
- ))}
361
- {returnedArray.length !== 0 && returnAllSelected && <br />}
362
- {defaultReturn.length !== 0 && !returnAllSelected && <br />}
363
- <input
364
- id="multiselect_input"
365
- onChange={(e) => {
366
- setFilterItem(e.target.value);
367
- }}
368
- placeholder="Start typing..."
369
- value={filterItem}
370
- onClick={() => setIsClosed(false)}
371
- />
372
- </div>
373
- {isClosed ? (
374
- <div key="chevron-down">
375
- <Icon icon="chevron-down" />
376
- </div>
377
- ) : (
378
- <div key="chevron-up">
379
- <Icon icon="chevron-up" />
380
- </div>
381
- )}
382
- </div>
383
- <div className={`dropdown_menu ${isClosed ? "close" : "open"}`}>
384
- {renderNestedOptions(
385
- filterItem ? findByFilter(formattedData, filterItem) : formattedData
386
- )}
387
- </div>
388
- </div>
124
+ {returnAllSelected ? (
125
+ <MultiSelectHelper
126
+ treeData={formattedData}
127
+ treeMode={returnAllSelected}
128
+ id={id}
129
+ onChange={onChange}
130
+ {...props}
131
+ />
132
+ ) : (
133
+ <>{DropDownSelectComponent}</>
134
+ )}
389
135
  </div>
390
136
  );
391
137
  };
@@ -0,0 +1,31 @@
1
+ import React from "react"
2
+ import DropdownTreeSelect from "react-dropdown-tree-select"
3
+ import "react-dropdown-tree-select/dist/styles.css"
4
+
5
+ type HelperProps = {
6
+ id?: string
7
+ treeData?: { [key: string]: string }[]
8
+ treeMode?: boolean
9
+ onChange?: any
10
+
11
+ }
12
+
13
+ const MultiSelectHelper = (props: HelperProps) => {
14
+ const { id, treeData, onChange, treeMode } = props
15
+
16
+
17
+ return (
18
+ <DropdownTreeSelect
19
+ data={treeData}
20
+ id={id}
21
+ keepOpenOnSelect
22
+ keepTreeOnSearch
23
+ keepChildrenOnSearch
24
+ onChange={onChange}
25
+ texts={{ placeholder: "Select..." }}
26
+ mode={treeMode ? 'hierarchical' : 'multiSelect'}
27
+ />
28
+ )
29
+ }
30
+
31
+ export default MultiSelectHelper
@@ -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,7 +3,6 @@
3
3
  value: "Power Home Remodeling",
4
4
  id: "powerhome1",
5
5
  expanded: true,
6
-
7
6
  children: [
8
7
  {
9
8
  label: "People",
@@ -0,0 +1,87 @@
1
+ export const findItemById = (
2
+ items: { [key: string]: any }[],
3
+ id: string
4
+ ): any => {
5
+ for (const item of items) {
6
+ if (item.id === id) {
7
+ return item;
8
+ }
9
+ if (item.children) {
10
+ const found = findItemById(item.children, id);
11
+ if (found) {
12
+ return found;
13
+ }
14
+ }
15
+ }
16
+ return null;
17
+ };
18
+
19
+ export const checkIt = (
20
+ foundItem: { [key: string]: any },
21
+ selectedItems: any[],
22
+ setSelectedItems: Function,
23
+ expand: boolean
24
+ ) => {
25
+ if (!foundItem) {
26
+ return;
27
+ }
28
+
29
+ foundItem.checked = true;
30
+ foundItem.expanded = expand;
31
+ selectedItems.push(foundItem);
32
+
33
+ if (foundItem.children) {
34
+ foundItem.children.map((x: any) => {
35
+ checkIt(x, selectedItems, setSelectedItems, expand);
36
+ });
37
+ }
38
+
39
+ setSelectedItems([...selectedItems]);
40
+ };
41
+
42
+ export const unCheckIt = (
43
+ foundItem: { [key: string]: any },
44
+ selectedItems: any,
45
+ setSelectedItems: any,
46
+ expand: boolean
47
+ ) => {
48
+ if (!foundItem) {
49
+ return;
50
+ }
51
+
52
+ foundItem.checked = false;
53
+ foundItem.expanded = false;
54
+ const newSelectedItems = selectedItems.filter(
55
+ (item: any) => item.id !== foundItem.id
56
+ );
57
+ if (foundItem.children) {
58
+ foundItem.children.map((x: any) => {
59
+ unCheckIt(x, selectedItems, setSelectedItems, expand);
60
+ });
61
+ }
62
+ setSelectedItems([...newSelectedItems]);
63
+ };
64
+
65
+
66
+ export const getParentAndAncestorsIds = (itemId:string, items:{ [key: string]: string; }[], ancestors:string[] = []):any => {
67
+ for (let i = 0; i < items.length; i++) {
68
+ const item:any = items[i];
69
+ if (item.id === itemId) {
70
+ // item found in current level of items array
71
+ return [...ancestors, item.id];
72
+ }
73
+ if (item.children && item.children.length > 0) {
74
+ // recursively search through children
75
+ const foundAncestors = getParentAndAncestorsIds(
76
+ itemId,
77
+ item.children,
78
+ [...ancestors, item.id]
79
+ );
80
+ if (foundAncestors) {
81
+ return foundAncestors;
82
+ }
83
+ }
84
+ }
85
+ // item not found in this level of items array or its children
86
+ return null;
87
+ }
@@ -16,7 +16,7 @@ const treeData = {
16
16
  {
17
17
  label: 'No one can get me',
18
18
  value: 'anonymous',
19
- id:'default3',
19
+ id:'default2',
20
20
  },
21
21
  ],
22
22
  },
@@ -30,7 +30,7 @@
30
30
  @include pb_title_dark;
31
31
  }
32
32
 
33
- &[class*=thin] {
33
+ &[class*=_thin] {
34
34
  @include pb_title_thin;
35
35
  }
36
36
  }
@@ -27,7 +27,7 @@ const Title = (props: TitleProps): React.ReactElement => {
27
27
  data = {},
28
28
  id,
29
29
  size = 3,
30
- bold = size === 3 ? false : true,
30
+ bold = true,
31
31
  tag = 'h3',
32
32
  text,
33
33
  variant = null,
@@ -35,10 +35,9 @@ const Title = (props: TitleProps): React.ReactElement => {
35
35
 
36
36
  const ariaProps: {[key: string]: string | number} = buildAriaProps(aria)
37
37
  const dataProps: {[key: string]: string | number} = buildDataProps(data)
38
-
39
38
  const getBold = bold ? '' : 'thin'
40
39
  const classes = classnames(
41
- buildCss("pb_title_kit", `size_${size}`, variant, color) + ` ${getBold}`,
40
+ buildCss('pb_title_kit', `size_${size}`, variant, color, getBold),
42
41
  globalProps(props),
43
42
  className
44
43
  )
@@ -1,2 +1,3 @@
1
1
  <%= pb_rails("title", props: { text: "Title 1", tag: "h1", size: 1, bold: false }) %>
2
2
  <%= pb_rails("title", props: { text: "Title 2", tag: "h2", size: 2, bold: false }) %>
3
+ <%= pb_rails("title", props: { text: "Title 3", tag: "h3", size: 3, bold: false }) %>
@@ -17,6 +17,12 @@ const TitleLightWeight = (props) => {
17
17
  text='Title 2'
18
18
  {...props}
19
19
  />
20
+ <Title bold={false}
21
+ size={3}
22
+ tag='h2'
23
+ text='Title 3'
24
+ {...props}
25
+ />
20
26
  </div>
21
27
  )
22
28
  }
@@ -1,4 +1,3 @@
1
1
  ##### Prop
2
- Title `size 1` & `size 2` will use `font-weight: 700` by default, if you want a lighter font weight, use the `bold` prop set to `false`.
3
- Title `size 3` uses a light font weight by default and will not accept a bold font weight.
2
+ Title `size 1`, `size 2`, & `size 3` will use `font-weight: 700` by default, if you want a lighter font weight, use the `bold` prop set to `false`.
4
3
  Title `size 4` uses a heavy font weight by default and will not accept a lighter font weight.