funda-ui 4.7.175 → 4.7.181

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.
@@ -6,18 +6,30 @@ import {
6
6
  deepClone,
7
7
  flatData
8
8
  } from 'funda-utils/dist/cjs/object';
9
+ import {
10
+ getNextSiblings
11
+ } from 'funda-utils/dist/cjs/dom';
9
12
 
10
13
 
11
14
  import TreeList from './TreeList';
12
15
  import { initUlHeight, initAsyncItems } from './init-height';
13
-
16
+ import { activeClass } from './utils/func';
14
17
 
15
18
 
16
19
  export interface ListSearchDataConfig {
17
20
  title: string | number;
18
21
  }
19
22
 
23
+ export interface ItemConfig {
24
+ key: string;
25
+ slug: string;
26
+ link: string;
27
+ optiondata: string;
28
+ }
20
29
 
30
+ export interface UpdateDataFunction {
31
+ (key: React.Key | null, fetch: FetchConfig | null): Promise<void>;
32
+ }
21
33
 
22
34
  export interface DataNode {
23
35
  key: React.Key;
@@ -25,6 +37,7 @@ export interface DataNode {
25
37
  link?: string;
26
38
  active?: boolean;
27
39
  checked?: boolean;
40
+ disabled?: boolean;
28
41
  heading?: string;
29
42
  icon?: string;
30
43
  slug?: string;
@@ -56,6 +69,8 @@ export type TreeProps = {
56
69
  lineStyle?: string;
57
70
  /** Mutually exclusive alternate expansion between the first levels */
58
71
  alternateCollapse?: boolean;
72
+ /** A function to render content of the option, replaces the default content of the option. */
73
+ renderOption?: (optionData: DataNode, key: React.Key) => React.ReactNode;
59
74
  /** set an arrow */
60
75
  arrow?: React.ReactNode;
61
76
  /** Set collapse/expand icon */
@@ -74,9 +89,22 @@ export type TreeProps = {
74
89
  retrieveData?: ListSearchDataConfig[];
75
90
  /** -- */
76
91
  id?: string;
77
- onSelect?: (e: any, val: any, func: Function) => void;
78
- onDoubleSelect?: (e: any, val: any, func: Function) => void;
79
- onCollapse?: (e: any, val: any, func: Function) => void;
92
+ onSelect?: (
93
+ e: React.MouseEvent<HTMLElement>,
94
+ val: ItemConfig,
95
+ updateData: UpdateDataFunction
96
+ ) => void;
97
+ onDoubleSelect?: (
98
+ e: React.MouseEvent<HTMLElement>,
99
+ val: ItemConfig,
100
+ updateData: UpdateDataFunction
101
+ ) => void;
102
+ onCollapse?: (
103
+ e: React.MouseEvent<HTMLElement>,
104
+ val: ItemConfig,
105
+ updateData: UpdateDataFunction,
106
+ isExpanded: boolean
107
+ ) => void;
80
108
  onCheck?: (val: any) => void;
81
109
  };
82
110
 
@@ -87,6 +115,7 @@ const Tree = (props: TreeProps) => {
87
115
  showLine,
88
116
  lineStyle,
89
117
  alternateCollapse,
118
+ renderOption,
90
119
  disableArrow,
91
120
  disableCollapse,
92
121
  arrow,
@@ -112,11 +141,56 @@ const Tree = (props: TreeProps) => {
112
141
  const [checkedData, setCheckedData] = useState<any[]>([]);
113
142
  const expandClassName = `${showLine ? 'show-line' : ''} ${disableArrow ? 'hide-arrow' : ''} ${disableCollapse ? 'collapse-disabled' : ''} ${lineStyle ? `line--${lineStyle}` : ''} ${checkable ? 'has-checkbox' : ''}`;
114
143
 
144
+ const [expandedMap, setExpandedMap] = useState<Record<string, boolean>>({}); // { [key]: true/false }
145
+
115
146
 
147
+ // Handle DOM operations
148
+ const handleDOMOperations = () => {
149
+ // loading status
150
+ [].slice.call(rootRef.current.querySelectorAll('.arrow.async-ready, .nav-link.async-ready')).forEach((node: any) => {
151
+ node.classList.remove('loading');
152
+ if (node.parentElement.querySelector('ul') !== null) {
153
+ node.classList.remove('async-ready');
154
+ node.click();
155
+ }
156
+ });
157
+
158
+ // init <ul> height
159
+ // Initialize async items
160
+ const ul: any = [].slice.call(rootRef.current.querySelectorAll('ul'));
161
+ initAsyncItems(ul as never).then(() => {
162
+ initUlHeight(ul);
163
+ });
164
+ };
165
+
166
+ const observeDOMChanges = () => {
167
+ if (!rootRef.current) return;
168
+
169
+ const observer = new MutationObserver((mutations) => {
170
+ // Check whether any new ul elements have been added
171
+ const hasNewUL = mutations.some(mutation =>
172
+ Array.from(mutation.addedNodes).some(node =>
173
+ node.nodeName === 'UL'
174
+ )
175
+ );
176
+
177
+ if (hasNewUL) {
178
+ observer.disconnect();
179
+ handleDOMOperations();
180
+ }
181
+ });
116
182
 
117
- const updateTreeData = (list: DataNode[] | null, key: React.Key, children: DataNode[]): DataNode[] => {
183
+ observer.observe(rootRef.current, {
184
+ childList: true,
185
+ subtree: true
186
+ });
187
+ };
188
+
189
+
190
+ const updateTreeData = async (list: DataNode[] | null, key: React.Key, children: DataNode[]): Promise<DataNode[]> => {
191
+ if (!list) return [];
118
192
 
119
- return list ? list.map((node) => {
193
+ const updatedList = await Promise.all(list.map(async (node) => {
120
194
  if (node.key === key) {
121
195
  return {
122
196
  ...node,
@@ -126,13 +200,120 @@ const Tree = (props: TreeProps) => {
126
200
  if (node.children) {
127
201
  return {
128
202
  ...node,
129
- children: updateTreeData(node.children, key, children)
203
+ children: await updateTreeData(node.children, key, children)
130
204
  };
131
205
  }
132
206
  return node;
133
- }) : [];
207
+ }));
208
+
209
+ return updatedList;
134
210
  }
211
+
212
+ const closeChild = (hyperlink: HTMLElement, ul: HTMLAllCollection) => {
213
+ if ( ul.length === 0 ) return;
214
+
215
+ activeClass(hyperlink, 'remove');
216
+ hyperlink.setAttribute('aria-expanded', 'false');
217
+ activeClass(hyperlink.parentNode, 'remove');
218
+
219
+ //to close
220
+ [].slice.call(ul).forEach(function(element: any){
221
+ element.style.maxHeight = 0;
222
+ });
223
+ };
224
+
225
+ const openChild = (hyperlink: HTMLElement, ul: HTMLAllCollection) => {
226
+ if ( ul.length === 0 ) return;
227
+
228
+ activeClass(hyperlink, 'add');
229
+ hyperlink.setAttribute('aria-expanded', 'true');
230
+ activeClass(hyperlink.parentNode, 'add');
231
+
232
+ // init <ul> height
233
+ initUlHeight(ul);
234
+
235
+ };
236
+
237
+ function handleCollapse(e: React.MouseEvent<HTMLElement>) {
238
+
239
+ if ( disableCollapse ) return;
240
+
241
+ e.preventDefault();
242
+ e.stopPropagation();
135
243
 
244
+ const hyperlink = e.currentTarget;
245
+ const url = hyperlink.dataset.href;
246
+ const subElement = getNextSiblings(hyperlink, 'ul');
247
+ const asyncReqReady = hyperlink.classList.contains('async-ready');
248
+
249
+ // loading
250
+ //=====================
251
+ if ( asyncReqReady ) {
252
+ activeClass(hyperlink, 'add', 'loading');
253
+ }
254
+
255
+
256
+ // calback
257
+ //=====================
258
+ const fetchFunc: UpdateDataFunction = asyncReqReady ? (typeof initDefaultValue !== 'function' ? async () => void(0) : initDefaultValue) : async () => void(0);
259
+
260
+ const optiondata = typeof hyperlink.dataset.optiondata !== 'undefined' ? hyperlink.dataset.optiondata : '{}';
261
+ onCollapse?.(e, {
262
+ key: hyperlink.dataset.key as string,
263
+ slug: hyperlink.dataset.slug as string,
264
+ link: hyperlink.dataset.link as string,
265
+ optiondata: optiondata as string
266
+ }, fetchFunc, JSON.parse(optiondata as string).isExpanded);
267
+
268
+
269
+ // update expanded status
270
+ //=====================
271
+ const isExpanded = hyperlink.getAttribute('aria-expanded') === 'true';
272
+ setExpandedMap(prev => ({
273
+ ...prev,
274
+ [hyperlink.dataset.key as string]: !isExpanded
275
+ }));
276
+
277
+
278
+ // hide child if expandedLink doesn't exist, on the contrary
279
+ //=====================
280
+ if ( hyperlink.classList.contains('loading') ) return;
281
+
282
+ if ( hyperlink.getAttribute('aria-expanded') === 'false' || hyperlink.getAttribute('aria-expanded') === null ) {
283
+
284
+
285
+ //Hide all other siblings of the selected <ul>
286
+ if (alternateCollapse) {
287
+ [].slice.call(rootRef.current.firstChild.children).forEach(function (li: any) {
288
+
289
+ activeClass(li, 'remove');
290
+
291
+ const _li = li.firstChild;
292
+ activeClass(_li, 'remove');
293
+ _li.setAttribute('aria-expanded', false);
294
+
295
+ [].slice.call(getNextSiblings(_li, 'ul')).forEach(function (element: any) {
296
+ element.style.maxHeight = 0;
297
+ });
298
+ });
299
+ }
300
+
301
+
302
+ //open current
303
+ openChild(hyperlink, subElement as never);
304
+
305
+
306
+
307
+ } else {
308
+
309
+ //close current
310
+ closeChild(hyperlink, subElement as never);
311
+
312
+ }
313
+
314
+
315
+ }
316
+
136
317
 
137
318
  async function fetchData(fetch: FetchConfig, params: any) {
138
319
 
@@ -236,8 +417,8 @@ const Tree = (props: TreeProps) => {
236
417
  }
237
418
 
238
419
 
239
- function initDefaultValue(key: React.Key | null, fetch: FetchConfig | null = null, firstRender: boolean = false, retrieveData: ListSearchDataConfig[] = []) {
240
-
420
+ async function initDefaultValue(key: React.Key | null, fetch: FetchConfig | null = null, firstRender: boolean = false, retrieveData: ListSearchDataConfig[] = []) {
421
+
241
422
  if ( firstRender ) {
242
423
  addKey(data, '', 0);
243
424
 
@@ -265,85 +446,85 @@ const Tree = (props: TreeProps) => {
265
446
  const _clone: any = deepClone(data);
266
447
  setFlatList(flatData(_clone));
267
448
 
268
- return;
449
+ return data;
269
450
  }
270
451
 
271
452
  if (fetch && typeof fetch.fetchFuncAsync === 'object') {
272
- //
273
- const _params: any[] = fetch.fetchFuncMethodParams || [];
274
- fetchData(fetch, (_params).join(',')).then( (response: any) => {
275
-
276
- const _childrenData: DataNode[] = response;
277
453
 
278
- if ( _childrenData.length > 0 ) {
279
- // add children to node
280
- const _newData: DataNode[] = updateTreeData(list, key ? key : '', _childrenData);
281
-
282
- // update data
283
- addKey(_newData, '', 0);
284
-
285
- // filter showing items
286
- if (Array.isArray(retrieveData)) {
287
- updateShowProp(_newData, retrieveData);
288
- } else {
289
- updateShowProp(_newData, retrieveData, true);
290
- }
454
+ let _newData: DataNode[] | null = list;
291
455
 
456
+ //
457
+ const _params: any[] = fetch.fetchFuncMethodParams || [];
458
+ const response: any = await fetchData(fetch, (_params).join(','));
459
+ const _childrenData: DataNode[] = response;
292
460
 
461
+
462
+ if ( _childrenData.length > 0 ) {
463
+ // add children to node
464
+ _newData = await updateTreeData(list, key ? key : '', _childrenData);
465
+
466
+ // set its childrenAsync property to false and active to true since we've successfully loaded its children
467
+ if (key) {
468
+ const findAndUpdateNode = (nodes: DataNode[]) => {
469
+ for (let node of nodes) {
470
+ if (node.key === key) {
471
+ node.childrenAsync = false;
472
+ node.active = true; // Add this line to set active to true
473
+ break;
474
+ }
475
+ if (node.children) {
476
+ findAndUpdateNode(node.children);
477
+ }
478
+ }
479
+ };
480
+ findAndUpdateNode(_newData);
481
+ }
293
482
 
294
- // Initialize default value of checkboxes
295
- if ( checkable ) {
296
- _childrenData.forEach((newitem: any) => {
297
- setCheckedData((prevState) => [{
298
- key: newitem.key,
299
- checked: newitem.checked === true,
300
- show: true,
301
- indeterminate: false
302
- }, ...prevState]);
303
- });
304
- }
305
-
306
- // update list
307
- setList(_newData);
483
+ // update data
484
+ addKey(_newData, '', 0);
485
+
486
+ // filter showing items
487
+ if (Array.isArray(retrieveData)) {
488
+ updateShowProp(_newData, retrieveData);
489
+ } else {
490
+ updateShowProp(_newData, retrieveData, true);
491
+ }
308
492
 
309
- // update retrive list
310
- const _clone: any = deepClone(_newData);
311
- setFlatList(flatData(_clone));
312
-
313
493
 
314
- }
315
494
 
316
- // loading status
317
- setTimeout(() => {
318
- [].slice.call(rootRef.current.querySelectorAll('.arrow.async-ready, .nav-link.async-ready')).forEach( (node: any) => {
319
- node.classList.remove('loading');
320
- if ( node.parentElement.querySelector('ul') !== null ) {
321
- node.classList.remove('async-ready');
322
- node.click();
323
- }
495
+ // Initialize default value of checkboxes
496
+ if ( checkable ) {
497
+ _childrenData.forEach((newitem: any) => {
498
+ setCheckedData((prevState) => [{
499
+ key: newitem.key,
500
+ checked: newitem.checked === true,
501
+ show: true,
502
+ indeterminate: false
503
+ }, ...prevState]);
324
504
  });
505
+ }
506
+
507
+ // update list
508
+ setList(_newData);
325
509
 
510
+ // update retrive list
511
+ const _clone: any = deepClone(_newData);
512
+ setFlatList(flatData(_clone));
326
513
 
327
- // init <ul> height
328
- // Initialize async items
329
- const ul: any = [].slice.call(rootRef.current.querySelectorAll('ul'));
330
- initAsyncItems(ul as never).then(() => {
331
- initUlHeight(ul);
332
- });
333
514
 
334
- }, 500);
515
+ }
335
516
 
336
-
517
+ // dom init
518
+ observeDOMChanges();
519
+
337
520
 
338
- });
521
+ return _newData;
339
522
  }
340
523
 
341
524
  }
342
525
 
343
526
 
344
527
 
345
-
346
-
347
528
  function filterRetriveData(flatData: DataNode[], retrieveData: ListSearchDataConfig[]) {
348
529
  return flatData.filter((item: any) => {
349
530
  return retrieveData.map((v: any) => v.title?.toLowerCase()).includes(item.title?.toLowerCase())
@@ -367,6 +548,7 @@ const Tree = (props: TreeProps) => {
367
548
  rootNode={rootRef}
368
549
  checkboxNamePrefix={idRes}
369
550
  alternateCollapse={alternateCollapse}
551
+ renderOption={renderOption}
370
552
  first={true}
371
553
  disableArrow={disableArrow}
372
554
  disableCollapse={disableCollapse}
@@ -377,13 +559,16 @@ const Tree = (props: TreeProps) => {
377
559
  childClassName={childClassName || 'tree-diagram-default-nav'}
378
560
  onSelect={onSelect}
379
561
  onDoubleSelect={onDoubleSelect}
380
- onCollapse={onCollapse}
381
562
  onCheck={onCheck}
382
563
  evInitValue={initDefaultValue}
383
564
  updateCheckedPrint={setCheckedPrint}
384
565
  getCheckedPrint={checkedPrint}
385
566
  updategetCheckedData={setCheckedData}
386
567
  getCheckedData={checkedData}
568
+
569
+ // Collapse
570
+ expandedMap={expandedMap}
571
+ onCollapse={handleCollapse}
387
572
 
388
573
  />
389
574
 
@@ -1,5 +1,19 @@
1
1
  const initUlHeight = (inputUl: HTMLAllCollection) => {
2
2
  [].slice.call(inputUl).forEach(function(el: HTMLUListElement){
3
+ // Check whether the parent node has "active"
4
+ /*
5
+ Fixed:
6
+
7
+ 1. After asynchronously loading child nodes (after "initDefaultValue()"), manually collapse the node
8
+ 2. When clicking other nodes, the child node ul height of the previously collapsed node is not correctly reset to 0
9
+ 3. It causes the visual display to be expanded, but the arrow is actually collapsed
10
+ */
11
+ const parentLi = el.parentElement;
12
+ if (!parentLi || !parentLi.classList.contains('active')) {
13
+ // If the parent node does not have an active class, keep maxHeight to 0
14
+ return;
15
+ }
16
+
3
17
  if (typeof el.dataset.maxheight === 'undefined') {
4
18
  const _li = [].slice.call(el.querySelectorAll('li'));
5
19
  let _allHeight: number = 0;
@@ -11,7 +25,6 @@ const initUlHeight = (inputUl: HTMLAllCollection) => {
11
25
  } else {
12
26
  el.style.maxHeight = el.dataset.maxheight;
13
27
  }
14
-
15
28
  });
16
29
  };
17
30
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "author": "UIUX Lab",
3
3
  "email": "uiuxlab@gmail.com",
4
4
  "name": "funda-ui",
5
- "version": "4.7.175",
5
+ "version": "4.7.181",
6
6
  "description": "React components using pure Bootstrap 5+ which does not contain any external style and script libraries.",
7
7
  "repository": {
8
8
  "type": "git",