funda-ui 4.7.171 → 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.
@@ -1,16 +1,18 @@
1
1
  /**
2
2
  * History Tracker
3
+ * @since 20250515
3
4
  *
4
5
  * @usage:
5
6
 
6
7
  const App = () => {
7
8
  const {
8
- history,
9
- forwardHistory,
10
- currentUrl,
11
- firstUrl,
9
+ getReady,
12
10
  clearHistory,
13
- goBack
11
+ goBack,
12
+ getFirstUrl,
13
+ getCurrentUrl,
14
+ getForwardHistory,
15
+ getHistory,
14
16
  } = useHistoryTracker({
15
17
  onChange: ({
16
18
  isReady,
@@ -41,32 +43,50 @@ const App = () => {
41
43
  }
42
44
  });
43
45
 
46
+ // useEffect(() => {
47
+ // console.log(getReady(), getFirstUrl(), getCurrentUrl(), getForwardHistory(), getHistory());
48
+ // }, [getReady, getFirstUrl, getCurrentUrl]);
49
+
50
+
51
+ // useEffect(() => {
52
+ // setTimeout(async () => {
53
+ // console.log('--> clean history within 2m');
54
+ // await clearHistory();
55
+ // }, 2000);
56
+ // }, []);
57
+
58
+
44
59
  return (
45
60
  <div>
46
61
 
62
+ <div>
63
+ <h3>isReady:</h3>
64
+ <p>{String(getReady())}</p>
65
+ </div>
66
+
47
67
  <div>
48
68
  <h3>First URL:</h3>
49
- <p>{firstUrl}</p>
69
+ <p>{getFirstUrl()}</p>
50
70
  </div>
51
71
 
52
72
  <div>
53
73
  <h3>Current URL:</h3>
54
- <p>{currentUrl}</p>
74
+ <p>{getCurrentUrl()}</p>
55
75
  </div>
56
76
 
57
77
  <div>
58
- <h3>History ({history.length}):</h3>
78
+ <h3>History ({getHistory().length}):</h3>
59
79
  <ul>
60
- {history.map((url, index) => (
80
+ {getHistory().map((url, index) => (
61
81
  <li key={index}>{url}</li>
62
82
  ))}
63
83
  </ul>
64
84
  </div>
65
85
 
66
86
  <div>
67
- <h3>Forward History ({forwardHistory.length}):</h3>
87
+ <h3>Forward History ({getForwardHistory().length}):</h3>
68
88
  <ul>
69
- {forwardHistory.map((url, index) => (
89
+ {getForwardHistory().map((url, index) => (
70
90
  <li key={index}>{url}</li>
71
91
  ))}
72
92
  </ul>
@@ -116,89 +136,91 @@ const App = () => {
116
136
  };
117
137
 
118
138
  */
139
+
119
140
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
120
141
 
121
- export type UseHistoryTrackerChangeFnType = (args: {
142
+ export interface HistoryTrackerChange {
122
143
  isReady: boolean;
123
144
  history: string[];
124
- forwardHistory: string[],
145
+ forwardHistory: string[];
125
146
  currentUrl: string;
126
147
  firstUrl: string;
127
148
  canGoBack: boolean;
128
149
  canGoForward: boolean;
129
- }) => void;
130
-
131
-
132
- export type UseHistoryTrackerProps = {
133
- onChange?: UseHistoryTrackerChangeFnType | null;
134
- };
150
+ }
151
+
152
+ export interface UseHistoryTrackerProps {
153
+ onChange?: (data: HistoryTrackerChange) => void;
154
+ }
155
+
156
+ export interface UseHistoryTrackerReturn {
157
+ getReady: () => boolean;
158
+ getHistory: () => string[];
159
+ getForwardHistory: () => string[];
160
+ getCurrentUrl: () => string;
161
+ getFirstUrl: () => string;
162
+ clearHistory: () => Promise<HistoryTrackerChange>;
163
+ goToHistory: (index: number) => void;
164
+ goBack: () => Promise<HistoryTrackerChange>;
165
+ goForward: () => Promise<HistoryTrackerChange>;
166
+ canGoBack: () => boolean;
167
+ canGoForward: () => boolean;
168
+ addHistoryToFirst: (url: string) => Promise<HistoryTrackerChange>;
169
+ }
135
170
 
136
171
  // Create a secure version of useLayoutEffect that is downgraded to useEffect when SSR
137
172
  const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
138
173
 
139
- const useHistoryTracker = (props: UseHistoryTrackerProps) => {
174
+ const useHistoryTracker = (props?: UseHistoryTrackerProps): UseHistoryTrackerReturn => {
140
175
  const {
141
176
  onChange
142
- } = props;
177
+ } = props || {};
143
178
 
144
179
  const [isReady, setIsReady] = useState<boolean>(false);
145
180
  const historyRef = useRef<string[]>([]);
146
181
  const forwardHistoryRef = useRef<string[]>([]);
147
182
  const firstUrlRef = useRef<string>('');
148
183
  const [currentUrl, setCurrentUrl] = useState<string>('');
149
-
150
- const initialize = useCallback(() => {
184
+
185
+ const canGoBack = useCallback((): boolean => {
186
+ return historyRef.current.length > 1;
187
+ }, []);
188
+
189
+ const canGoForward = useCallback((): boolean => {
190
+ return forwardHistoryRef.current.length > 0;
191
+ }, []);
192
+
193
+ const initialize = useCallback((ready: boolean = false): void => {
151
194
  if (typeof window === 'undefined') return;
152
195
 
153
- const currentLocation = window.location.href as string;
154
-
196
+ const currentLocation = window.location.href;
197
+
155
198
  // If the history is empty, set the first record
156
199
  if (historyRef.current.length === 0) {
157
200
  firstUrlRef.current = currentLocation;
158
201
  historyRef.current = [currentLocation];
159
202
  setCurrentUrl(currentLocation);
160
203
 
161
- onChange?.({
162
- isReady: false,
204
+ onChange && onChange({
205
+ isReady: ready,
163
206
  history: [currentLocation],
164
207
  forwardHistory: [],
165
208
  currentUrl: currentLocation,
166
209
  firstUrl: currentLocation,
167
210
  canGoBack: false,
168
- canGoForward: false
211
+ canGoForward: false,
169
212
  });
170
213
 
171
214
  }
172
215
 
173
216
  setIsReady(true);
174
- }, []);
217
+ }, [onChange]);
175
218
 
176
219
  useIsomorphicLayoutEffect(() => {
177
220
  initialize();
178
221
  }, [initialize]);
179
222
 
180
-
181
- const clearHistory = useCallback(() => {
182
- if (typeof window === 'undefined') return;
183
-
184
- historyRef.current = [];
185
- forwardHistoryRef.current = [];
186
- firstUrlRef.current = '';
187
- setCurrentUrl('');
188
-
189
- onChange?.({
190
- isReady: true,
191
- history: [],
192
- forwardHistory: [],
193
- currentUrl: '',
194
- firstUrl: '',
195
- canGoBack: false,
196
- canGoForward: false
197
-
198
- });
199
- }, [onChange]); // only "onChange"
200
-
201
- const goToHistory = useCallback((index: number) => {
223
+ const goToHistory = useCallback((index: number): void => {
202
224
  if (typeof window === 'undefined') return;
203
225
  if (index < 0 || index >= historyRef.current.length) return;
204
226
 
@@ -208,20 +230,21 @@ const useHistoryTracker = (props: UseHistoryTrackerProps) => {
208
230
  }
209
231
  }, []);
210
232
 
211
- const goBack = useCallback(() => {
233
+ const goBack = useCallback((): Promise<HistoryTrackerChange> => {
212
234
  if (typeof window === 'undefined') return Promise.reject('Window is undefined');
213
- if (historyRef.current.length <= 1) return Promise.reject('Cannot go back');
235
+ if (historyRef.current.length <= 1) return Promise.reject('History does not meet the criteria (total records are less than 2), cannot go back');
214
236
 
215
237
  return new Promise((resolve) => {
216
238
  // Moves the current URL into the forward history
217
- const removedUrl = historyRef.current.pop() as string;
218
- forwardHistoryRef.current.push(removedUrl);
219
-
239
+ const removedUrl = historyRef.current.pop();
240
+ if (removedUrl) {
241
+ forwardHistoryRef.current.push(removedUrl);
242
+ }
220
243
  const newCurrentUrl = historyRef.current[historyRef.current.length - 1];
221
244
  setCurrentUrl(newCurrentUrl);
222
245
 
223
246
  // Create initial data object
224
- const data = {
247
+ const data: HistoryTrackerChange = {
225
248
  isReady: true,
226
249
  history: [...historyRef.current],
227
250
  forwardHistory: [...forwardHistoryRef.current],
@@ -232,7 +255,7 @@ const useHistoryTracker = (props: UseHistoryTrackerProps) => {
232
255
  };
233
256
 
234
257
  // Notify about the history change
235
- onChange?.(data);
258
+ onChange && onChange(data);
236
259
 
237
260
  // Create one-time listener for popstate
238
261
  const handlePopState = () => {
@@ -240,7 +263,7 @@ const useHistoryTracker = (props: UseHistoryTrackerProps) => {
240
263
  window.removeEventListener('popstate', handlePopState);
241
264
 
242
265
  // Get the final data after URL has changed
243
- const finalData = {
266
+ const finalData: HistoryTrackerChange = {
244
267
  isReady: true,
245
268
  history: [...historyRef.current],
246
269
  forwardHistory: [...forwardHistoryRef.current],
@@ -259,30 +282,32 @@ const useHistoryTracker = (props: UseHistoryTrackerProps) => {
259
282
  // Trigger the navigation
260
283
  window.history.go(-1);
261
284
  });
262
- }, [onChange]);
285
+ }, [onChange, canGoBack, canGoForward]);
263
286
 
264
- const goForward = useCallback(() => {
287
+ const goForward = useCallback((): Promise<HistoryTrackerChange> => {
265
288
  if (typeof window === 'undefined') return Promise.reject('Window is undefined');
266
- if (forwardHistoryRef.current.length === 0) return Promise.reject('Cannot go forward');
289
+ if (forwardHistoryRef.current.length === 0) return Promise.reject('Forward history does not meet the criteria (total 0 records), cannot go forward');
267
290
 
268
291
  return new Promise((resolve) => {
269
292
  // Take the URL from the forward history and add it to the main history
270
- const nextUrl = forwardHistoryRef.current.pop() as string;
271
- historyRef.current.push(nextUrl);
272
- setCurrentUrl(nextUrl);
293
+ const nextUrl = forwardHistoryRef.current.pop();
294
+ if (nextUrl) {
295
+ historyRef.current.push(nextUrl);
296
+ setCurrentUrl(nextUrl);
297
+ }
273
298
 
274
299
  // Create initial data object
275
- const data = {
300
+ const data: HistoryTrackerChange = {
276
301
  isReady: true,
277
302
  history: [...historyRef.current],
278
303
  forwardHistory: [...forwardHistoryRef.current],
279
- currentUrl: nextUrl,
304
+ currentUrl: nextUrl || '',
280
305
  firstUrl: firstUrlRef.current,
281
306
  canGoBack: canGoBack(),
282
307
  canGoForward: canGoForward()
283
308
  };
284
309
 
285
- onChange?.(data);
310
+ onChange && onChange(data);
286
311
 
287
312
  // Create one-time listener for popstate
288
313
  const handlePopState = () => {
@@ -290,7 +315,7 @@ const useHistoryTracker = (props: UseHistoryTrackerProps) => {
290
315
  window.removeEventListener('popstate', handlePopState);
291
316
 
292
317
  // Get the final data after URL has changed
293
- const finalData = {
318
+ const finalData: HistoryTrackerChange = {
294
319
  isReady: true,
295
320
  history: [...historyRef.current],
296
321
  forwardHistory: [...forwardHistoryRef.current],
@@ -309,47 +334,144 @@ const useHistoryTracker = (props: UseHistoryTrackerProps) => {
309
334
  // Trigger the navigation
310
335
  window.history.go(1);
311
336
  });
312
- }, [onChange]);
337
+ }, [onChange, canGoBack, canGoForward]);
313
338
 
314
- const canGoBack = useCallback(() => {
315
- return historyRef.current.length > 1;
316
- }, []);
317
-
318
- const canGoForward = useCallback(() => {
319
- return forwardHistoryRef.current.length > 0;
320
- }, []);
321
-
322
-
323
- const handleUrlChange = useCallback(() => {
339
+ const handleUrlChange = useCallback((): void => {
324
340
  if (typeof window === 'undefined') return;
325
341
 
326
342
  const newUrl = window.location.href;
327
-
328
- // If the history is empty, set to the first URL
343
+
344
+ // If the history is empty, set the first record
329
345
  if (historyRef.current.length === 0) {
330
346
  firstUrlRef.current = newUrl;
331
347
  }
332
348
 
333
- // Avoid recording the same URL
334
- if (historyRef.current[historyRef.current.length - 1] !== newUrl) {
349
+ // Check whether it is back
350
+ const isBackOperation = historyRef.current.length > 1 &&
351
+ historyRef.current[historyRef.current.length - 2] === newUrl;
352
+
353
+ // Check whether it is forward
354
+ const isForwardOperation = forwardHistoryRef.current.length > 0 &&
355
+ forwardHistoryRef.current[forwardHistoryRef.current.length - 1] === newUrl;
356
+
357
+ if (isBackOperation) {
358
+ // If it is a rollback, remove the last URL and add it to forwardHistory
359
+ const removedUrl = historyRef.current.pop();
360
+ if (removedUrl) {
361
+ forwardHistoryRef.current.push(removedUrl);
362
+ }
363
+ } else if (isForwardOperation) {
364
+ // If it is a forward operation, take out the URL from forwardHistory and add it to history
365
+ const nextUrl = forwardHistoryRef.current.pop();
366
+ if (nextUrl) {
367
+ historyRef.current.push(nextUrl);
368
+ }
369
+ } else if (historyRef.current[historyRef.current.length - 1] !== newUrl) {
370
+ // If it's a new navigation, add to history and clear forwardHistory
335
371
  historyRef.current.push(newUrl);
336
-
337
- // Clear the advance history, as new navigation invalidates the advance history
338
372
  forwardHistoryRef.current = [];
339
- setCurrentUrl(newUrl);
340
-
341
- onChange?.({
373
+ }
374
+
375
+ setCurrentUrl(newUrl);
376
+
377
+ onChange && onChange({
378
+ isReady: true,
379
+ history: [...historyRef.current],
380
+ forwardHistory: [...forwardHistoryRef.current],
381
+ currentUrl: newUrl,
382
+ firstUrl: firstUrlRef.current || newUrl,
383
+ canGoBack: canGoBack(),
384
+ canGoForward: canGoForward()
385
+ });
386
+ }, [onChange, canGoBack, canGoForward]);
387
+
388
+ // Create a wrapper for history methods
389
+ const createHistoryWrapper = useCallback(<T extends (...args: any[]) => any>(originalMethod: T): T => {
390
+ return function(this: History, ...args: Parameters<T>): ReturnType<T> {
391
+ const result = originalMethod.apply(this, args);
392
+ handleUrlChange();
393
+ return result;
394
+ } as T;
395
+ }, [handleUrlChange]);
396
+
397
+ const getFirstUrl = useCallback((): string => {
398
+ return firstUrlRef.current;
399
+ }, []);
400
+
401
+ const getCurrentUrl = useCallback((): string => {
402
+ return currentUrl;
403
+ }, [currentUrl]);
404
+
405
+ const getForwardHistory = useCallback((): string[] => {
406
+ return forwardHistoryRef.current;
407
+ }, []);
408
+
409
+ const getHistory = useCallback((): string[] => {
410
+ return historyRef.current;
411
+ }, []);
412
+
413
+ const getReady = useCallback((): boolean => {
414
+ return isReady;
415
+ }, [isReady]);
416
+
417
+ const addHistoryToFirst = useCallback(async (url: string): Promise<HistoryTrackerChange> => {
418
+ if (typeof window === 'undefined') return Promise.reject('Window is undefined');
419
+ if (!url) return Promise.reject('URL does not exist');
420
+
421
+ return new Promise((resolve) => {
422
+ if (historyRef.current.length === 0) {
423
+ firstUrlRef.current = url;
424
+ historyRef.current = [url];
425
+ setCurrentUrl(url);
426
+ } else {
427
+ // Insert at the front
428
+ historyRef.current = [url, ...historyRef.current];
429
+ firstUrlRef.current = url;
430
+ }
431
+
432
+ const result: HistoryTrackerChange = {
342
433
  isReady: true,
343
434
  history: [...historyRef.current],
344
435
  forwardHistory: [...forwardHistoryRef.current],
345
- currentUrl: newUrl,
346
- firstUrl: firstUrlRef.current || newUrl, // Make sure there is always a value
436
+ currentUrl: currentUrl || url,
437
+ firstUrl: firstUrlRef.current,
347
438
  canGoBack: canGoBack(),
348
439
  canGoForward: canGoForward()
349
- });
350
- }
351
- }, [onChange]); // only "onChange"
440
+ };
441
+
442
+ onChange && onChange(result);
443
+ resolve(result);
444
+ });
445
+ }, [onChange, currentUrl, canGoBack, canGoForward]);
446
+
447
+ const clearHistory = useCallback(async (): Promise<HistoryTrackerChange> => {
448
+ if (typeof window === 'undefined') return Promise.reject('Window is undefined');
449
+
450
+ return new Promise((resolve) => {
451
+ historyRef.current = [];
452
+ forwardHistoryRef.current = [];
453
+ firstUrlRef.current = '';
454
+ setCurrentUrl('');
352
455
 
456
+ const result: HistoryTrackerChange = {
457
+ isReady: true,
458
+ history: [],
459
+ forwardHistory: [],
460
+ currentUrl: '',
461
+ firstUrl: '',
462
+ canGoBack: false,
463
+ canGoForward: false
464
+ };
465
+ onChange && onChange(result);
466
+
467
+ // After clearHistory(), immediately take the current url as the first history
468
+ // !!!Fixed: "There is still only 1 record of goBack(), and there is no cumulative forward record"
469
+ setTimeout(() => {
470
+ initialize(true);
471
+ resolve(result);
472
+ }, 0);
473
+ });
474
+ }, [onChange, initialize]);
353
475
 
354
476
  useEffect(() => {
355
477
  if (typeof window === 'undefined') return;
@@ -360,43 +482,72 @@ const useHistoryTracker = (props: UseHistoryTrackerProps) => {
360
482
  // Listen for hashchange events
361
483
  window.addEventListener('hashchange', handleUrlChange);
362
484
 
363
- // Listen for DOM and property changes
364
- const observer = new MutationObserver((mutations) => {
365
- mutations.forEach((mutation) => {
366
- if (mutation.type === 'childList' || mutation.type === 'attributes') {
367
- handleUrlChange();
485
+ // !!!Fixed: "Reinitialize the history, but this will not cause the URL to change either"
486
+
487
+ // Store original methods (Just save the reference, don't modify the global)
488
+ const originalPushState = window.history.pushState.bind(window.history);
489
+ const originalReplaceState = window.history.replaceState.bind(window.history);
490
+ const originalGo = window.history.go.bind(window.history);
491
+
492
+ // Create wrapped versions of history methods
493
+ const wrappedPushState = createHistoryWrapper(originalPushState);
494
+ const wrappedReplaceState = createHistoryWrapper(originalReplaceState);
495
+ const wrappedGo = createHistoryWrapper(originalGo);
496
+
497
+ // Create a proxy for history object
498
+ const historyProxy = new Proxy(window.history, {
499
+ get: function(target: History, prop: string | symbol) {
500
+ if (prop === 'pushState') {
501
+ return wrappedPushState;
368
502
  }
369
- });
503
+ if (prop === 'replaceState') {
504
+ return wrappedReplaceState;
505
+ }
506
+ if (prop === 'go') {
507
+ return wrappedGo;
508
+ }
509
+ return target[prop as keyof History];
510
+ }
370
511
  });
371
512
 
372
-
373
- observer.observe(document.body, {
374
- childList: true, // monitor the addition and deletion of child nodes
375
- subtree: true, // monitor all descendant nodes
376
- attributes: true, // monitor attribute changes
377
- attributeFilter: ['href'] // only monitor changes in the href attribute
513
+ // Replace window.history with our proxy
514
+ Object.defineProperty(window, 'history', {
515
+ value: historyProxy,
516
+ writable: true,
517
+ configurable: true
378
518
  });
379
519
 
380
520
  return () => {
381
521
  window.removeEventListener('popstate', handleUrlChange);
382
522
  window.removeEventListener('hashchange', handleUrlChange);
383
- observer.disconnect();
523
+
524
+ // Restore original history methods
525
+ Object.defineProperty(window, 'history', {
526
+ value: {
527
+ ...window.history,
528
+ pushState: originalPushState,
529
+ replaceState: originalReplaceState,
530
+ go: originalGo
531
+ },
532
+ writable: true,
533
+ configurable: true
534
+ });
384
535
  };
385
- }, [handleUrlChange]);
386
-
536
+ }, [handleUrlChange, createHistoryWrapper]);
387
537
 
388
538
  return {
389
- isReady,
390
- history: historyRef.current,
391
- forwardHistory: forwardHistoryRef.current,
392
- currentUrl,
393
- firstUrl: firstUrlRef.current,
539
+ getReady,
540
+ getHistory,
541
+ getForwardHistory,
542
+ getCurrentUrl,
543
+ getFirstUrl,
394
544
  clearHistory,
395
545
  goToHistory,
396
546
  goBack,
397
547
  goForward,
398
548
  canGoBack,
399
- canGoForward
549
+ canGoForward,
550
+ addHistoryToFirst
400
551
  };
401
552
  };
402
553
 
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.171",
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",