funda-ui 4.7.111 → 4.7.125

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.
Files changed (56) hide show
  1. package/CascadingSelect/index.d.ts +1 -0
  2. package/CascadingSelect/index.js +7 -3
  3. package/CascadingSelectE2E/index.d.ts +1 -0
  4. package/CascadingSelectE2E/index.js +7 -3
  5. package/Date/index.js +25 -2
  6. package/EventCalendar/index.js +25 -2
  7. package/EventCalendarTimeline/index.js +25 -2
  8. package/README.md +9 -10
  9. package/SplitterPanel/index.css +63 -0
  10. package/SplitterPanel/index.d.ts +20 -0
  11. package/SplitterPanel/index.js +736 -0
  12. package/Stepper/index.js +3 -2
  13. package/Utils/date.d.ts +15 -5
  14. package/Utils/date.js +22 -2
  15. package/Utils/time.d.ts +34 -0
  16. package/Utils/time.js +162 -0
  17. package/Utils/useHistoryTracker.d.ts +26 -0
  18. package/Utils/useHistoryTracker.js +475 -0
  19. package/all.d.ts +1 -0
  20. package/all.js +1 -0
  21. package/lib/cjs/CascadingSelect/index.d.ts +1 -0
  22. package/lib/cjs/CascadingSelect/index.js +7 -3
  23. package/lib/cjs/CascadingSelectE2E/index.d.ts +1 -0
  24. package/lib/cjs/CascadingSelectE2E/index.js +7 -3
  25. package/lib/cjs/Date/index.js +25 -2
  26. package/lib/cjs/EventCalendar/index.js +25 -2
  27. package/lib/cjs/EventCalendarTimeline/index.js +25 -2
  28. package/lib/cjs/SplitterPanel/index.d.ts +20 -0
  29. package/lib/cjs/SplitterPanel/index.js +736 -0
  30. package/lib/cjs/Stepper/index.js +3 -2
  31. package/lib/cjs/Utils/date.d.ts +15 -5
  32. package/lib/cjs/Utils/date.js +22 -2
  33. package/lib/cjs/Utils/time.d.ts +34 -0
  34. package/lib/cjs/Utils/time.js +162 -0
  35. package/lib/cjs/Utils/useHistoryTracker.d.ts +26 -0
  36. package/lib/cjs/Utils/useHistoryTracker.js +475 -0
  37. package/lib/cjs/index.d.ts +1 -0
  38. package/lib/cjs/index.js +1 -0
  39. package/lib/css/SplitterPanel/index.css +63 -0
  40. package/lib/esm/CascadingSelect/Group.tsx +4 -2
  41. package/lib/esm/CascadingSelect/index.tsx +3 -0
  42. package/lib/esm/CascadingSelectE2E/Group.tsx +4 -2
  43. package/lib/esm/CascadingSelectE2E/index.tsx +3 -0
  44. package/lib/esm/SplitterPanel/index.scss +82 -0
  45. package/lib/esm/SplitterPanel/index.tsx +174 -0
  46. package/lib/esm/Stepper/index.tsx +4 -2
  47. package/lib/esm/Utils/hooks/useHistoryTracker.tsx +403 -0
  48. package/lib/esm/Utils/libs/date.ts +28 -8
  49. package/lib/esm/Utils/libs/time.ts +125 -0
  50. package/lib/esm/index.js +1 -0
  51. package/package.json +1 -1
  52. package/Utils/useGlobalUrlListener.d.ts +0 -2
  53. package/Utils/useGlobalUrlListener.js +0 -157
  54. package/lib/cjs/Utils/useGlobalUrlListener.d.ts +0 -2
  55. package/lib/cjs/Utils/useGlobalUrlListener.js +0 -157
  56. package/lib/esm/Utils/hooks/useGlobalUrlListener.tsx +0 -46
@@ -0,0 +1,82 @@
1
+
2
+ /* ======================================================
3
+ <!-- Splitter Panel -->
4
+ /* ====================================================== */
5
+ .splitter-panel {
6
+
7
+ --splitter-panel-divider-color: rgba(0, 0, 0, 0.15);
8
+ --splitter-panel-divider-hover-color: rgba(210, 253, 253, .86);
9
+
10
+ display: flex;
11
+ position: relative;
12
+
13
+ .splitter-panel-left {
14
+ width: var(--splitter-panel-left-w, 0);
15
+ }
16
+
17
+ .splitter-panel-divider {
18
+ position: relative;
19
+ width: 1px;
20
+ cursor: col-resize;
21
+ padding: 0 2px;
22
+ transition: all 0.25s;
23
+ z-index: 1;
24
+
25
+ &::before {
26
+ content: '';
27
+ position: absolute;
28
+ top: 0;
29
+ left: 0;
30
+ width: 1px;
31
+ height: 100%;
32
+ background: var(--splitter-panel-divider-color);
33
+ }
34
+
35
+ &::after {
36
+ content: '';
37
+ position: absolute;
38
+ top: 0;
39
+ left: -2px;
40
+ width: 5px;
41
+ height: 100%;
42
+ }
43
+
44
+
45
+ &:hover::after {
46
+ background: var(--splitter-panel-divider-hover-color);
47
+ }
48
+ }
49
+
50
+
51
+ .splitter-panel-right {
52
+ flex: 1;
53
+ }
54
+
55
+
56
+ }
57
+
58
+
59
+
60
+ .splitter-panel-vertical {
61
+ display: flex;
62
+ flex-direction: column;
63
+
64
+ .splitter-panel-top,
65
+ .splitter-panel-bottom {
66
+ min-height: 50px;
67
+ }
68
+
69
+ }
70
+
71
+
72
+ @media (max-width: 767px) {
73
+ .splitter-panel {
74
+ display: none !important;
75
+ }
76
+ }
77
+
78
+ @media (min-width: 768px) {
79
+ .splitter-panel-vertical {
80
+ display: none !important;
81
+ }
82
+ }
@@ -0,0 +1,174 @@
1
+ import React, { useEffect, useState, useRef, useImperativeHandle } from 'react';
2
+
3
+ import { clsWrite, combinedCls } from 'funda-utils/dist/cjs/cls';
4
+ import useIsMobile from 'funda-utils/dist/cjs/useIsMobile';
5
+
6
+
7
+ interface SplitterPanelSubProps {
8
+ children: React.ReactNode;
9
+ }
10
+
11
+ const SplitterPanelLeft: React.FC<SplitterPanelSubProps> = ({ children }) => <>{children}</>;
12
+ const SplitterPanelRight: React.FC<SplitterPanelSubProps> = ({ children }) => <>{children}</>;
13
+
14
+ interface SplitterPanelProps {
15
+ wrapperClassName?: string;
16
+ draggable?: boolean
17
+ initialWidth?: number;
18
+ minWidth?: number;
19
+ maxWidth?: number;
20
+ onDrag?: (type: 'dragStart' | 'dragEnd' | 'drag', leftWidth: number) => void;
21
+ children?: React.ReactNode;
22
+ }
23
+
24
+
25
+ const SplitterPanel: React.FC<SplitterPanelProps> & {
26
+ Left: typeof SplitterPanelLeft;
27
+ Right: typeof SplitterPanelRight;
28
+ } = (props) => {
29
+ const {
30
+ wrapperClassName,
31
+ draggable = true,
32
+ maxWidth,
33
+ minWidth = 100,
34
+ initialWidth = 200,
35
+ onDrag,
36
+ children
37
+ } = props;
38
+
39
+ const containerRef = useRef<HTMLDivElement>(null);
40
+ const dividerRef = useRef<HTMLDivElement>(null);
41
+ const getDefaultWidth = () => {
42
+ if (typeof initialWidth === 'number') return initialWidth;
43
+ if (typeof minWidth === 'number') return minWidth;
44
+ return 200;
45
+ };
46
+ const [leftWidth, setLeftWidth] = useState<number>(getDefaultWidth());
47
+ const isMobile = useIsMobile();
48
+
49
+ // adjust split panel
50
+ const [dragging, setDragging] = useState<boolean>(false);
51
+
52
+ // Update only when initialWidth changes and is valid, and only when it has not been dragged
53
+ const hasDragged = useRef(false);
54
+
55
+ let left: React.ReactNode = null;
56
+ let right: React.ReactNode = null;
57
+
58
+ React.Children.forEach(children, (child) => {
59
+ if (!React.isValidElement(child)) return;
60
+ if (child.type === SplitterPanelLeft) left = child.props.children;
61
+ if (child.type === SplitterPanelRight) right = child.props.children;
62
+ });
63
+
64
+
65
+
66
+ // dragdrop
67
+ function handleSplitPanelsChange(clientX: number) {
68
+ if (!containerRef.current) return;
69
+ const containerLeft = containerRef.current.getBoundingClientRect().left;
70
+ const separatorXPosition = clientX - containerLeft;
71
+ const minWidthValue = minWidth || 100;
72
+ const maxWidthValue = maxWidth || window.innerWidth / 2;
73
+ if (dragging && separatorXPosition > minWidthValue && separatorXPosition < maxWidthValue) {
74
+ setLeftWidth(separatorXPosition);
75
+ onDrag?.('drag', separatorXPosition);
76
+ hasDragged.current = true;
77
+ }
78
+ }
79
+ function handleMouseDownSplitPanels(e: React.MouseEvent) {
80
+ setDragging(true);
81
+ onDrag?.('dragStart', leftWidth ?? initialWidth);
82
+ }
83
+ function handleTouchStartSplitPanels(e: React.TouchEvent) {
84
+ setDragging(true);
85
+ onDrag?.('dragStart', leftWidth ?? initialWidth);
86
+ }
87
+
88
+ function handleMouseMoveSplitPanels(e: MouseEvent) {
89
+ handleSplitPanelsChange(e.clientX);
90
+ }
91
+ function handleTouchMoveSplitPanels(e: TouchEvent) {
92
+ handleSplitPanelsChange(e.touches[0].clientX);
93
+ }
94
+
95
+ function handleMouseUpSplitPanels() {
96
+ setDragging(false);
97
+ onDrag?.('dragEnd', leftWidth ?? initialWidth);
98
+ }
99
+
100
+
101
+ useEffect(() => {
102
+ if (!draggable) return;
103
+
104
+ // 事件监听用原生事件
105
+ document.addEventListener('mousemove', handleMouseMoveSplitPanels, false);
106
+ document.addEventListener('touchmove', handleTouchMoveSplitPanels, false);
107
+ document.addEventListener('mouseup', handleMouseUpSplitPanels, false);
108
+ document.addEventListener('touchend', handleMouseUpSplitPanels, false);
109
+ return () => {
110
+ document.removeEventListener('mousemove', handleMouseMoveSplitPanels, false);
111
+ document.removeEventListener('touchmove', handleTouchMoveSplitPanels, false);
112
+ document.removeEventListener('mouseup', handleMouseUpSplitPanels, false);
113
+ document.removeEventListener('touchend', handleMouseUpSplitPanels, false);
114
+ };
115
+ }, [draggable, dragging]);
116
+
117
+ useEffect(() => {
118
+ if (!hasDragged.current && typeof initialWidth === 'number') {
119
+ setLeftWidth(initialWidth);
120
+ }
121
+ }, [initialWidth]);
122
+
123
+ return (
124
+ <>
125
+
126
+ {isMobile ? <>
127
+ <div className={combinedCls(
128
+ 'splitter-panel-vertical',
129
+ clsWrite(wrapperClassName, '')
130
+ )}>
131
+ <div className="splitter-panel-top">{left}</div>
132
+ <div className="splitter-panel-bottom">{right}</div>
133
+ </div>
134
+
135
+ </> : <>
136
+ <div
137
+ className={combinedCls(
138
+ 'splitter-panel',
139
+ clsWrite(wrapperClassName, '')
140
+ )}
141
+ ref={containerRef}
142
+ >
143
+
144
+ <div
145
+ className="splitter-panel-left"
146
+ style={{ "--splitter-panel-left-w": `${leftWidth}px` } as React.CSSProperties}
147
+ >
148
+ {left}
149
+ </div>
150
+
151
+ {/*<!--DIVIDER-->*/}
152
+ <div
153
+ ref={dividerRef}
154
+ className={`splitter-panel-divider ${dragging ? 'dragging' : ''}`}
155
+ onMouseDown={handleMouseDownSplitPanels}
156
+ onTouchStart={handleTouchStartSplitPanels}
157
+ onTouchEnd={handleMouseUpSplitPanels}
158
+ />
159
+ {/*<!--/DIVIDER-->*/}
160
+
161
+ <div className="splitter-panel-right">
162
+ {right}
163
+ </div>
164
+ </div>
165
+ </>}
166
+
167
+ </>
168
+ )
169
+ };
170
+
171
+ SplitterPanel.Left = SplitterPanelLeft;
172
+ SplitterPanel.Right = SplitterPanelRight;
173
+
174
+ export default SplitterPanel;
@@ -257,6 +257,8 @@ const Stepper = forwardRef<StepperRef, StepperProps>((props, ref) => {
257
257
  '--stepper-progress-height': `${progress}%`
258
258
  } as React.CSSProperties;
259
259
  } else {
260
+
261
+ const defaultProgress = (activeIndex / (panels.length - 1)) * 100;
260
262
  const firstItem = stepItems[0] as HTMLDivElement;
261
263
  const lastItem = stepItems[stepItems.length - 1] as HTMLDivElement;
262
264
  if (!firstItem || !lastItem) return {};
@@ -269,10 +271,10 @@ const Stepper = forwardRef<StepperRef, StepperProps>((props, ref) => {
269
271
  if (!currentItem) return {};
270
272
 
271
273
  const currentCenter = currentItem.offsetLeft + (currentItem.clientWidth / 2);
272
- const progress = ((currentCenter - firstCenter) / totalWidth) * 100;
274
+ const progress = defaultProgress !== 0 && activeIndex > 0 ? defaultProgress : ((currentCenter - firstCenter) / totalWidth) * 100;
273
275
 
274
276
  return {
275
- '--stepper-progress-width': `${progress}%`
277
+ '--stepper-progress-width': `${isNaN(progress) ? 0 : progress}%`
276
278
  } as React.CSSProperties;
277
279
  }
278
280
  };
@@ -0,0 +1,403 @@
1
+ /**
2
+ * History Tracker
3
+ *
4
+ * @usage:
5
+
6
+ const App = () => {
7
+ const {
8
+ history,
9
+ forwardHistory,
10
+ currentUrl,
11
+ firstUrl,
12
+ clearHistory,
13
+ goBack
14
+ } = useHistoryTracker({
15
+ onChange: ({
16
+ isReady,
17
+ history,
18
+ forwardHistory,
19
+ currentUrl,
20
+ firstUrl,
21
+ canGoBack,
22
+ canGoForward
23
+ } : {
24
+ isReady: boolean;
25
+ history: string[];
26
+ forwardHistory: string[];
27
+ currentUrl: string;
28
+ firstUrl: string;
29
+ canGoBack: boolean;
30
+ canGoForward: boolean;
31
+ }) => {
32
+ console.log('--> onChange: ',
33
+ isReady,
34
+ history,
35
+ forwardHistory,
36
+ currentUrl,
37
+ firstUrl,
38
+ canGoBack,
39
+ canGoForward
40
+ );
41
+ }
42
+ });
43
+
44
+ return (
45
+ <div>
46
+
47
+ <div>
48
+ <h3>First URL:</h3>
49
+ <p>{firstUrl}</p>
50
+ </div>
51
+
52
+ <div>
53
+ <h3>Current URL:</h3>
54
+ <p>{currentUrl}</p>
55
+ </div>
56
+
57
+ <div>
58
+ <h3>History ({history.length}):</h3>
59
+ <ul>
60
+ {history.map((url, index) => (
61
+ <li key={index}>{url}</li>
62
+ ))}
63
+ </ul>
64
+ </div>
65
+
66
+ <div>
67
+ <h3>Forward History ({forwardHistory.length}):</h3>
68
+ <ul>
69
+ {forwardHistory.map((url, index) => (
70
+ <li key={index}>{url}</li>
71
+ ))}
72
+ </ul>
73
+ </div>
74
+
75
+
76
+
77
+ <button onClick={clearHistory}>
78
+ Clear History
79
+ </button>
80
+
81
+ <button onClick={async () => {
82
+ try {
83
+ const {
84
+ isReady,
85
+ history,
86
+ forwardHistory,
87
+ canGoBack,
88
+ canGoForward
89
+ } : {
90
+ isReady: boolean;
91
+ history: string[];
92
+ forwardHistory: string[];
93
+ canGoBack: boolean;
94
+ canGoForward: boolean;
95
+ } = await goBack();
96
+
97
+ console.log('--> goBack: ',
98
+ isReady,
99
+ history,
100
+ forwardHistory,
101
+ currentUrl,
102
+ firstUrl,
103
+ canGoBack,
104
+ canGoForward
105
+ );
106
+ } catch (error) {
107
+ console.error('Navigation failed', error);
108
+ }
109
+
110
+ }}>
111
+ Back
112
+ </button>
113
+
114
+ </div>
115
+ );
116
+ };
117
+
118
+ */
119
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
120
+
121
+ export type UseHistoryTrackerChangeFnType = (args: {
122
+ isReady: boolean;
123
+ history: string[];
124
+ forwardHistory: string[],
125
+ currentUrl: string;
126
+ firstUrl: string;
127
+ canGoBack: boolean;
128
+ canGoForward: boolean;
129
+ }) => void;
130
+
131
+
132
+ export type UseHistoryTrackerProps = {
133
+ onChange?: UseHistoryTrackerChangeFnType | null;
134
+ };
135
+
136
+ // Create a secure version of useLayoutEffect that is downgraded to useEffect when SSR
137
+ const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
138
+
139
+ const useHistoryTracker = (props: UseHistoryTrackerProps) => {
140
+ const {
141
+ onChange
142
+ } = props;
143
+
144
+ const [isReady, setIsReady] = useState<boolean>(false);
145
+ const historyRef = useRef<string[]>([]);
146
+ const forwardHistoryRef = useRef<string[]>([]);
147
+ const firstUrlRef = useRef<string>('');
148
+ const [currentUrl, setCurrentUrl] = useState<string>('');
149
+
150
+ const initialize = useCallback(() => {
151
+ if (typeof window === 'undefined') return;
152
+
153
+ const currentLocation = window.location.href as string;
154
+
155
+ // If the history is empty, set the first record
156
+ if (historyRef.current.length === 0) {
157
+ firstUrlRef.current = currentLocation;
158
+ historyRef.current = [currentLocation];
159
+ setCurrentUrl(currentLocation);
160
+
161
+ onChange?.({
162
+ isReady: false,
163
+ history: [currentLocation],
164
+ forwardHistory: [],
165
+ currentUrl: currentLocation,
166
+ firstUrl: currentLocation,
167
+ canGoBack: false,
168
+ canGoForward: false
169
+ });
170
+
171
+ }
172
+
173
+ setIsReady(true);
174
+ }, []);
175
+
176
+ useIsomorphicLayoutEffect(() => {
177
+ initialize();
178
+ }, [initialize]);
179
+
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) => {
202
+ if (typeof window === 'undefined') return;
203
+ if (index < 0 || index >= historyRef.current.length) return;
204
+
205
+ const targetUrl = historyRef.current[index];
206
+ if (targetUrl && targetUrl !== window.location.href) {
207
+ window.location.href = targetUrl;
208
+ }
209
+ }, []);
210
+
211
+ const goBack = useCallback(() => {
212
+ if (typeof window === 'undefined') return Promise.reject('Window is undefined');
213
+ if (historyRef.current.length <= 1) return Promise.reject('Cannot go back');
214
+
215
+ return new Promise((resolve) => {
216
+ // Moves the current URL into the forward history
217
+ const removedUrl = historyRef.current.pop() as string;
218
+ forwardHistoryRef.current.push(removedUrl);
219
+
220
+ const newCurrentUrl = historyRef.current[historyRef.current.length - 1];
221
+ setCurrentUrl(newCurrentUrl);
222
+
223
+ // Create initial data object
224
+ const data = {
225
+ isReady: true,
226
+ history: [...historyRef.current],
227
+ forwardHistory: [...forwardHistoryRef.current],
228
+ currentUrl: newCurrentUrl,
229
+ firstUrl: firstUrlRef.current,
230
+ canGoBack: canGoBack(),
231
+ canGoForward: canGoForward()
232
+ };
233
+
234
+ // Notify about the history change
235
+ onChange?.(data);
236
+
237
+ // Create one-time listener for popstate
238
+ const handlePopState = () => {
239
+ // Remove the listener after it's called
240
+ window.removeEventListener('popstate', handlePopState);
241
+
242
+ // Get the final data after URL has changed
243
+ const finalData = {
244
+ isReady: true,
245
+ history: [...historyRef.current],
246
+ forwardHistory: [...forwardHistoryRef.current],
247
+ currentUrl: window.location.href,
248
+ firstUrl: firstUrlRef.current,
249
+ canGoBack: canGoBack(),
250
+ canGoForward: canGoForward()
251
+ };
252
+
253
+ resolve(finalData);
254
+ };
255
+
256
+ // Add the listener
257
+ window.addEventListener('popstate', handlePopState);
258
+
259
+ // Trigger the navigation
260
+ window.history.go(-1);
261
+ });
262
+ }, [onChange]);
263
+
264
+ const goForward = useCallback(() => {
265
+ if (typeof window === 'undefined') return Promise.reject('Window is undefined');
266
+ if (forwardHistoryRef.current.length === 0) return Promise.reject('Cannot go forward');
267
+
268
+ return new Promise((resolve) => {
269
+ // 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);
273
+
274
+ // Create initial data object
275
+ const data = {
276
+ isReady: true,
277
+ history: [...historyRef.current],
278
+ forwardHistory: [...forwardHistoryRef.current],
279
+ currentUrl: nextUrl,
280
+ firstUrl: firstUrlRef.current,
281
+ canGoBack: canGoBack(),
282
+ canGoForward: canGoForward()
283
+ };
284
+
285
+ onChange?.(data);
286
+
287
+ // Create one-time listener for popstate
288
+ const handlePopState = () => {
289
+ // Remove the listener after it's called
290
+ window.removeEventListener('popstate', handlePopState);
291
+
292
+ // Get the final data after URL has changed
293
+ const finalData = {
294
+ isReady: true,
295
+ history: [...historyRef.current],
296
+ forwardHistory: [...forwardHistoryRef.current],
297
+ currentUrl: window.location.href,
298
+ firstUrl: firstUrlRef.current,
299
+ canGoBack: canGoBack(),
300
+ canGoForward: canGoForward()
301
+ };
302
+
303
+ resolve(finalData);
304
+ };
305
+
306
+ // Add the listener
307
+ window.addEventListener('popstate', handlePopState);
308
+
309
+ // Trigger the navigation
310
+ window.history.go(1);
311
+ });
312
+ }, [onChange]);
313
+
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(() => {
324
+ if (typeof window === 'undefined') return;
325
+
326
+ const newUrl = window.location.href;
327
+
328
+ // If the history is empty, set to the first URL
329
+ if (historyRef.current.length === 0) {
330
+ firstUrlRef.current = newUrl;
331
+ }
332
+
333
+ // Avoid recording the same URL
334
+ if (historyRef.current[historyRef.current.length - 1] !== newUrl) {
335
+ historyRef.current.push(newUrl);
336
+
337
+ // Clear the advance history, as new navigation invalidates the advance history
338
+ forwardHistoryRef.current = [];
339
+ setCurrentUrl(newUrl);
340
+
341
+ onChange?.({
342
+ isReady: true,
343
+ history: [...historyRef.current],
344
+ forwardHistory: [...forwardHistoryRef.current],
345
+ currentUrl: newUrl,
346
+ firstUrl: firstUrlRef.current || newUrl, // Make sure there is always a value
347
+ canGoBack: canGoBack(),
348
+ canGoForward: canGoForward()
349
+ });
350
+ }
351
+ }, [onChange]); // only "onChange"
352
+
353
+
354
+ useEffect(() => {
355
+ if (typeof window === 'undefined') return;
356
+
357
+ // Listen for popstate events (browser forward/back)
358
+ window.addEventListener('popstate', handleUrlChange);
359
+
360
+ // Listen for hashchange events
361
+ window.addEventListener('hashchange', handleUrlChange);
362
+
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();
368
+ }
369
+ });
370
+ });
371
+
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
378
+ });
379
+
380
+ return () => {
381
+ window.removeEventListener('popstate', handleUrlChange);
382
+ window.removeEventListener('hashchange', handleUrlChange);
383
+ observer.disconnect();
384
+ };
385
+ }, [handleUrlChange]);
386
+
387
+
388
+ return {
389
+ isReady,
390
+ history: historyRef.current,
391
+ forwardHistory: forwardHistoryRef.current,
392
+ currentUrl,
393
+ firstUrl: firstUrlRef.current,
394
+ clearHistory,
395
+ goToHistory,
396
+ goBack,
397
+ goForward,
398
+ canGoBack,
399
+ canGoForward
400
+ };
401
+ };
402
+
403
+ export default useHistoryTracker;