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.
- package/CascadingSelect/index.d.ts +1 -0
- package/CascadingSelect/index.js +7 -3
- package/CascadingSelectE2E/index.d.ts +1 -0
- package/CascadingSelectE2E/index.js +7 -3
- package/Date/index.js +25 -2
- package/EventCalendar/index.js +25 -2
- package/EventCalendarTimeline/index.js +25 -2
- package/README.md +9 -10
- package/SplitterPanel/index.css +63 -0
- package/SplitterPanel/index.d.ts +20 -0
- package/SplitterPanel/index.js +736 -0
- package/Stepper/index.js +3 -2
- package/Utils/date.d.ts +15 -5
- package/Utils/date.js +22 -2
- package/Utils/time.d.ts +34 -0
- package/Utils/time.js +162 -0
- package/Utils/useHistoryTracker.d.ts +26 -0
- package/Utils/useHistoryTracker.js +475 -0
- package/all.d.ts +1 -0
- package/all.js +1 -0
- package/lib/cjs/CascadingSelect/index.d.ts +1 -0
- package/lib/cjs/CascadingSelect/index.js +7 -3
- package/lib/cjs/CascadingSelectE2E/index.d.ts +1 -0
- package/lib/cjs/CascadingSelectE2E/index.js +7 -3
- package/lib/cjs/Date/index.js +25 -2
- package/lib/cjs/EventCalendar/index.js +25 -2
- package/lib/cjs/EventCalendarTimeline/index.js +25 -2
- package/lib/cjs/SplitterPanel/index.d.ts +20 -0
- package/lib/cjs/SplitterPanel/index.js +736 -0
- package/lib/cjs/Stepper/index.js +3 -2
- package/lib/cjs/Utils/date.d.ts +15 -5
- package/lib/cjs/Utils/date.js +22 -2
- package/lib/cjs/Utils/time.d.ts +34 -0
- package/lib/cjs/Utils/time.js +162 -0
- package/lib/cjs/Utils/useHistoryTracker.d.ts +26 -0
- package/lib/cjs/Utils/useHistoryTracker.js +475 -0
- package/lib/cjs/index.d.ts +1 -0
- package/lib/cjs/index.js +1 -0
- package/lib/css/SplitterPanel/index.css +63 -0
- package/lib/esm/CascadingSelect/Group.tsx +4 -2
- package/lib/esm/CascadingSelect/index.tsx +3 -0
- package/lib/esm/CascadingSelectE2E/Group.tsx +4 -2
- package/lib/esm/CascadingSelectE2E/index.tsx +3 -0
- package/lib/esm/SplitterPanel/index.scss +82 -0
- package/lib/esm/SplitterPanel/index.tsx +174 -0
- package/lib/esm/Stepper/index.tsx +4 -2
- package/lib/esm/Utils/hooks/useHistoryTracker.tsx +403 -0
- package/lib/esm/Utils/libs/date.ts +28 -8
- package/lib/esm/Utils/libs/time.ts +125 -0
- package/lib/esm/index.js +1 -0
- package/package.json +1 -1
- package/Utils/useGlobalUrlListener.d.ts +0 -2
- package/Utils/useGlobalUrlListener.js +0 -157
- package/lib/cjs/Utils/useGlobalUrlListener.d.ts +0 -2
- package/lib/cjs/Utils/useGlobalUrlListener.js +0 -157
- 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;
|