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

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,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.