react-form-rewind 0.4.0 → 0.5.0
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/README.md +3 -2
- package/dist/index.d.mts +2 -6
- package/dist/index.d.ts +2 -6
- package/dist/index.js +1 -284
- package/dist/index.mjs +1 -282
- package/package.json +1 -1
- package/dist/index.js.map +0 -1
- package/dist/index.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ import { useFormHistory } from "react-form-rewind";
|
|
|
51
51
|
function MyForm() {
|
|
52
52
|
const { state, setState, undo, redo, canUndo, canRedo } = useFormHistory(
|
|
53
53
|
{ name: "", email: "" },
|
|
54
|
-
{ persist: { key: "my-form-draft" } }
|
|
54
|
+
{ keyboard: true, persist: { key: "my-form-draft" } }
|
|
55
55
|
);
|
|
56
56
|
|
|
57
57
|
return (
|
|
@@ -108,6 +108,7 @@ The core hook that manages a history-backed state stack.
|
|
|
108
108
|
| `maxHistory` | `number` | `100` | Maximum past entries to retain |
|
|
109
109
|
| `debounceMs` | `number` | `300` | Debounce window for rapid state changes |
|
|
110
110
|
| `persist` | `boolean \| PersistOptions` | `false` | Enable draft persistence |
|
|
111
|
+
| `keyboard` | `boolean` | `false` | Enable Ctrl+Z / Ctrl+Shift+Z keyboard shortcuts |
|
|
111
112
|
| `onUndo` | `(state: T) => void` | — | Callback after undo |
|
|
112
113
|
| `onRedo` | `(state: T) => void` | — | Callback after redo |
|
|
113
114
|
| `onSnapshot` | `(entry: HistoryEntry<T>) => void` | — | Callback when a snapshot is committed |
|
|
@@ -126,7 +127,7 @@ The core hook that manages a history-backed state stack.
|
|
|
126
127
|
|
|
127
128
|
### Keyboard Shortcuts
|
|
128
129
|
|
|
129
|
-
|
|
130
|
+
Pass `keyboard: true` to enable built-in shortcuts. Press **Ctrl+Z** to undo, **Ctrl+Shift+Z** or **Ctrl+Y** to redo. On macOS, **Ctrl** maps to **Cmd** automatically.
|
|
130
131
|
|
|
131
132
|
### Snapshot Debouncing
|
|
132
133
|
|
package/dist/index.d.mts
CHANGED
|
@@ -3,15 +3,11 @@ interface HistoryEntry<T> {
|
|
|
3
3
|
timestamp: number;
|
|
4
4
|
label?: string;
|
|
5
5
|
}
|
|
6
|
-
interface HistoryStack<T> {
|
|
7
|
-
past: HistoryEntry<T>[];
|
|
8
|
-
present: T;
|
|
9
|
-
future: HistoryEntry<T>[];
|
|
10
|
-
}
|
|
11
6
|
interface UseFormHistoryOptions<T> {
|
|
12
7
|
maxHistory?: number;
|
|
13
8
|
debounceMs?: number;
|
|
14
9
|
persist?: boolean | PersistOptions;
|
|
10
|
+
keyboard?: boolean;
|
|
15
11
|
onUndo?: (state: T) => void;
|
|
16
12
|
onRedo?: (state: T) => void;
|
|
17
13
|
onSnapshot?: (entry: HistoryEntry<T>) => void;
|
|
@@ -37,4 +33,4 @@ interface UseFormHistoryReturn<T> {
|
|
|
37
33
|
|
|
38
34
|
declare function useFormHistory<T>(initialState: T, options?: UseFormHistoryOptions<T>): UseFormHistoryReturn<T>;
|
|
39
35
|
|
|
40
|
-
export { type HistoryEntry, type
|
|
36
|
+
export { type HistoryEntry, type PersistOptions, type UseFormHistoryOptions, type UseFormHistoryReturn, useFormHistory };
|
package/dist/index.d.ts
CHANGED
|
@@ -3,15 +3,11 @@ interface HistoryEntry<T> {
|
|
|
3
3
|
timestamp: number;
|
|
4
4
|
label?: string;
|
|
5
5
|
}
|
|
6
|
-
interface HistoryStack<T> {
|
|
7
|
-
past: HistoryEntry<T>[];
|
|
8
|
-
present: T;
|
|
9
|
-
future: HistoryEntry<T>[];
|
|
10
|
-
}
|
|
11
6
|
interface UseFormHistoryOptions<T> {
|
|
12
7
|
maxHistory?: number;
|
|
13
8
|
debounceMs?: number;
|
|
14
9
|
persist?: boolean | PersistOptions;
|
|
10
|
+
keyboard?: boolean;
|
|
15
11
|
onUndo?: (state: T) => void;
|
|
16
12
|
onRedo?: (state: T) => void;
|
|
17
13
|
onSnapshot?: (entry: HistoryEntry<T>) => void;
|
|
@@ -37,4 +33,4 @@ interface UseFormHistoryReturn<T> {
|
|
|
37
33
|
|
|
38
34
|
declare function useFormHistory<T>(initialState: T, options?: UseFormHistoryOptions<T>): UseFormHistoryReturn<T>;
|
|
39
35
|
|
|
40
|
-
export { type HistoryEntry, type
|
|
36
|
+
export { type HistoryEntry, type PersistOptions, type UseFormHistoryOptions, type UseFormHistoryReturn, useFormHistory };
|
package/dist/index.js
CHANGED
|
@@ -1,284 +1 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var react = require('react');
|
|
4
|
-
|
|
5
|
-
// src/useFormHistory.ts
|
|
6
|
-
var DEFAULT_MAX_HISTORY = 100;
|
|
7
|
-
var DEFAULT_DEBOUNCE_MS = 300;
|
|
8
|
-
var DEFAULT_PERSIST_DEBOUNCE_MS = 500;
|
|
9
|
-
var DEFAULT_PERSIST_VERSION = 1;
|
|
10
|
-
function normalizePersist(persist) {
|
|
11
|
-
if (!persist) return null;
|
|
12
|
-
if (typeof persist === "boolean") return null;
|
|
13
|
-
return {
|
|
14
|
-
key: persist.key,
|
|
15
|
-
debounceMs: persist.debounceMs ?? DEFAULT_PERSIST_DEBOUNCE_MS,
|
|
16
|
-
version: persist.version ?? DEFAULT_PERSIST_VERSION
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
function readDraft(key, version) {
|
|
20
|
-
try {
|
|
21
|
-
if (typeof window === "undefined" || !window.localStorage) return null;
|
|
22
|
-
const raw = window.localStorage.getItem(key);
|
|
23
|
-
if (!raw) return null;
|
|
24
|
-
const parsed = JSON.parse(raw);
|
|
25
|
-
if (parsed.__version !== version) {
|
|
26
|
-
window.localStorage.removeItem(key);
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
return parsed.__state;
|
|
30
|
-
} catch {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
function writeDraft(key, state, version) {
|
|
35
|
-
try {
|
|
36
|
-
if (typeof window === "undefined" || !window.localStorage) return;
|
|
37
|
-
window.localStorage.setItem(key, JSON.stringify({ __state: state, __version: version }));
|
|
38
|
-
} catch {
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
function removeDraft(key) {
|
|
42
|
-
try {
|
|
43
|
-
if (typeof window === "undefined" || !window.localStorage) return;
|
|
44
|
-
window.localStorage.removeItem(key);
|
|
45
|
-
} catch {
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
function useFormHistory(initialState, options = {}) {
|
|
49
|
-
const {
|
|
50
|
-
maxHistory = DEFAULT_MAX_HISTORY,
|
|
51
|
-
debounceMs = DEFAULT_DEBOUNCE_MS,
|
|
52
|
-
persist: persistOption,
|
|
53
|
-
onUndo,
|
|
54
|
-
onRedo,
|
|
55
|
-
onSnapshot
|
|
56
|
-
} = options;
|
|
57
|
-
const persist = normalizePersist(persistOption);
|
|
58
|
-
const initializedRef = react.useRef(false);
|
|
59
|
-
const [state, setStateRaw] = react.useState(() => {
|
|
60
|
-
if (persist) {
|
|
61
|
-
const draft = readDraft(persist.key, persist.version);
|
|
62
|
-
if (draft !== null) {
|
|
63
|
-
initializedRef.current = true;
|
|
64
|
-
return draft;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return initialState;
|
|
68
|
-
});
|
|
69
|
-
const stateRef = react.useRef(state);
|
|
70
|
-
stateRef.current = state;
|
|
71
|
-
const pastRef = react.useRef([]);
|
|
72
|
-
const futureRef = react.useRef([]);
|
|
73
|
-
const [past, setPast] = react.useState([]);
|
|
74
|
-
const [future, setFuture] = react.useState([]);
|
|
75
|
-
const debounceTimerRef = react.useRef(null);
|
|
76
|
-
const pendingStateRef = react.useRef(null);
|
|
77
|
-
const preDebounceStateRef = react.useRef(null);
|
|
78
|
-
const persistTimerRef = react.useRef(null);
|
|
79
|
-
const pushToPast = react.useCallback(
|
|
80
|
-
(entry) => {
|
|
81
|
-
const next = [...pastRef.current, entry];
|
|
82
|
-
if (next.length > maxHistory) {
|
|
83
|
-
next.splice(0, next.length - maxHistory);
|
|
84
|
-
}
|
|
85
|
-
pastRef.current = next;
|
|
86
|
-
setPast(next);
|
|
87
|
-
},
|
|
88
|
-
[maxHistory]
|
|
89
|
-
);
|
|
90
|
-
react.useEffect(() => {
|
|
91
|
-
return () => {
|
|
92
|
-
if (debounceTimerRef.current !== null) {
|
|
93
|
-
clearTimeout(debounceTimerRef.current);
|
|
94
|
-
}
|
|
95
|
-
if (persistTimerRef.current !== null) {
|
|
96
|
-
clearTimeout(persistTimerRef.current);
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
}, []);
|
|
100
|
-
const schedulePersist = react.useCallback(
|
|
101
|
-
(nextState) => {
|
|
102
|
-
if (!persist) return;
|
|
103
|
-
if (persistTimerRef.current !== null) {
|
|
104
|
-
clearTimeout(persistTimerRef.current);
|
|
105
|
-
}
|
|
106
|
-
persistTimerRef.current = setTimeout(() => {
|
|
107
|
-
writeDraft(persist.key, nextState, persist.version);
|
|
108
|
-
persistTimerRef.current = null;
|
|
109
|
-
}, persist.debounceMs);
|
|
110
|
-
},
|
|
111
|
-
[persist]
|
|
112
|
-
);
|
|
113
|
-
const persistNow = react.useCallback(
|
|
114
|
-
(nextState) => {
|
|
115
|
-
if (!persist) return;
|
|
116
|
-
if (persistTimerRef.current !== null) {
|
|
117
|
-
clearTimeout(persistTimerRef.current);
|
|
118
|
-
persistTimerRef.current = null;
|
|
119
|
-
}
|
|
120
|
-
writeDraft(persist.key, nextState, persist.version);
|
|
121
|
-
},
|
|
122
|
-
[persist]
|
|
123
|
-
);
|
|
124
|
-
const setState = react.useCallback(
|
|
125
|
-
(value, label) => {
|
|
126
|
-
const prev = stateRef.current;
|
|
127
|
-
const next = typeof value === "function" ? value(prev) : value;
|
|
128
|
-
if (Object.is(prev, next)) return;
|
|
129
|
-
setStateRaw(next);
|
|
130
|
-
stateRef.current = next;
|
|
131
|
-
schedulePersist(next);
|
|
132
|
-
if (debounceMs <= 0) {
|
|
133
|
-
const entry = { state: prev, timestamp: Date.now(), label };
|
|
134
|
-
pushToPast(entry);
|
|
135
|
-
futureRef.current = [];
|
|
136
|
-
setFuture([]);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
if (pendingStateRef.current === null) {
|
|
140
|
-
preDebounceStateRef.current = prev;
|
|
141
|
-
}
|
|
142
|
-
pendingStateRef.current = next;
|
|
143
|
-
if (debounceTimerRef.current !== null) {
|
|
144
|
-
clearTimeout(debounceTimerRef.current);
|
|
145
|
-
}
|
|
146
|
-
debounceTimerRef.current = setTimeout(() => {
|
|
147
|
-
const entry = {
|
|
148
|
-
state: preDebounceStateRef.current,
|
|
149
|
-
timestamp: Date.now(),
|
|
150
|
-
label
|
|
151
|
-
};
|
|
152
|
-
pushToPast(entry);
|
|
153
|
-
futureRef.current = [];
|
|
154
|
-
setFuture([]);
|
|
155
|
-
debounceTimerRef.current = null;
|
|
156
|
-
pendingStateRef.current = null;
|
|
157
|
-
preDebounceStateRef.current = null;
|
|
158
|
-
}, debounceMs);
|
|
159
|
-
},
|
|
160
|
-
[debounceMs, pushToPast, schedulePersist]
|
|
161
|
-
);
|
|
162
|
-
const undo = react.useCallback(() => {
|
|
163
|
-
if (debounceTimerRef.current !== null) {
|
|
164
|
-
clearTimeout(debounceTimerRef.current);
|
|
165
|
-
debounceTimerRef.current = null;
|
|
166
|
-
}
|
|
167
|
-
if (pendingStateRef.current !== null && preDebounceStateRef.current !== null) {
|
|
168
|
-
const entry = {
|
|
169
|
-
state: preDebounceStateRef.current,
|
|
170
|
-
timestamp: Date.now()
|
|
171
|
-
};
|
|
172
|
-
pushToPast(entry);
|
|
173
|
-
futureRef.current = [];
|
|
174
|
-
setFuture([]);
|
|
175
|
-
pendingStateRef.current = null;
|
|
176
|
-
preDebounceStateRef.current = null;
|
|
177
|
-
}
|
|
178
|
-
if (pastRef.current.length === 0) return;
|
|
179
|
-
const prev = pastRef.current[pastRef.current.length - 1];
|
|
180
|
-
const newPast = pastRef.current.slice(0, -1);
|
|
181
|
-
pastRef.current = newPast;
|
|
182
|
-
setPast(newPast);
|
|
183
|
-
const currentEntry = {
|
|
184
|
-
state: stateRef.current,
|
|
185
|
-
timestamp: Date.now()
|
|
186
|
-
};
|
|
187
|
-
futureRef.current = [...futureRef.current, currentEntry];
|
|
188
|
-
setFuture(futureRef.current);
|
|
189
|
-
setStateRaw(prev.state);
|
|
190
|
-
stateRef.current = prev.state;
|
|
191
|
-
persistNow(prev.state);
|
|
192
|
-
onUndo?.(prev.state);
|
|
193
|
-
}, [pushToPast, onUndo, persistNow]);
|
|
194
|
-
const redo = react.useCallback(() => {
|
|
195
|
-
if (futureRef.current.length === 0) return;
|
|
196
|
-
const next = futureRef.current[futureRef.current.length - 1];
|
|
197
|
-
const newFuture = futureRef.current.slice(0, -1);
|
|
198
|
-
futureRef.current = newFuture;
|
|
199
|
-
setFuture(newFuture);
|
|
200
|
-
const currentEntry = {
|
|
201
|
-
state: stateRef.current,
|
|
202
|
-
timestamp: Date.now()
|
|
203
|
-
};
|
|
204
|
-
pastRef.current = [...pastRef.current, currentEntry];
|
|
205
|
-
setPast(pastRef.current);
|
|
206
|
-
setStateRaw(next.state);
|
|
207
|
-
stateRef.current = next.state;
|
|
208
|
-
persistNow(next.state);
|
|
209
|
-
onRedo?.(next.state);
|
|
210
|
-
}, [onRedo, persistNow]);
|
|
211
|
-
const clearHistory = react.useCallback(() => {
|
|
212
|
-
if (debounceTimerRef.current !== null) {
|
|
213
|
-
clearTimeout(debounceTimerRef.current);
|
|
214
|
-
debounceTimerRef.current = null;
|
|
215
|
-
}
|
|
216
|
-
pendingStateRef.current = null;
|
|
217
|
-
preDebounceStateRef.current = null;
|
|
218
|
-
pastRef.current = [];
|
|
219
|
-
futureRef.current = [];
|
|
220
|
-
setPast([]);
|
|
221
|
-
setFuture([]);
|
|
222
|
-
}, []);
|
|
223
|
-
const snapshot = react.useCallback(
|
|
224
|
-
(label) => {
|
|
225
|
-
let didFlush = false;
|
|
226
|
-
if (debounceTimerRef.current !== null) {
|
|
227
|
-
clearTimeout(debounceTimerRef.current);
|
|
228
|
-
debounceTimerRef.current = null;
|
|
229
|
-
}
|
|
230
|
-
if (pendingStateRef.current !== null && preDebounceStateRef.current !== null) {
|
|
231
|
-
const entry = {
|
|
232
|
-
state: preDebounceStateRef.current,
|
|
233
|
-
timestamp: Date.now()
|
|
234
|
-
};
|
|
235
|
-
pushToPast(entry);
|
|
236
|
-
futureRef.current = [];
|
|
237
|
-
setFuture([]);
|
|
238
|
-
pendingStateRef.current = null;
|
|
239
|
-
preDebounceStateRef.current = null;
|
|
240
|
-
didFlush = true;
|
|
241
|
-
}
|
|
242
|
-
if (!didFlush) {
|
|
243
|
-
const entry = {
|
|
244
|
-
state: stateRef.current,
|
|
245
|
-
timestamp: Date.now(),
|
|
246
|
-
label
|
|
247
|
-
};
|
|
248
|
-
pushToPast(entry);
|
|
249
|
-
futureRef.current = [];
|
|
250
|
-
setFuture([]);
|
|
251
|
-
onSnapshot?.(entry);
|
|
252
|
-
} else if (label && pastRef.current.length > 0) {
|
|
253
|
-
pastRef.current[pastRef.current.length - 1].label = label;
|
|
254
|
-
setPast([...pastRef.current]);
|
|
255
|
-
}
|
|
256
|
-
},
|
|
257
|
-
[pushToPast, onSnapshot]
|
|
258
|
-
);
|
|
259
|
-
const clearDraft = react.useCallback(() => {
|
|
260
|
-
clearHistory();
|
|
261
|
-
if (persist) {
|
|
262
|
-
removeDraft(persist.key);
|
|
263
|
-
}
|
|
264
|
-
setStateRaw(initialState);
|
|
265
|
-
stateRef.current = initialState;
|
|
266
|
-
}, [clearHistory, initialState, persist]);
|
|
267
|
-
return {
|
|
268
|
-
state,
|
|
269
|
-
setState,
|
|
270
|
-
undo,
|
|
271
|
-
redo,
|
|
272
|
-
canUndo: past.length > 0,
|
|
273
|
-
canRedo: future.length > 0,
|
|
274
|
-
clearHistory,
|
|
275
|
-
clearDraft,
|
|
276
|
-
snapshot,
|
|
277
|
-
past,
|
|
278
|
-
future
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
exports.useFormHistory = useFormHistory;
|
|
283
|
-
//# sourceMappingURL=index.js.map
|
|
284
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
'use strict';var react=require('react');var j=100,V=300,X=500,Y=1;function q(e){return !e||typeof e=="boolean"?null:{key:e.key,debounceMs:e.debounceMs??X,version:e.version??Y}}function U(){return typeof window<"u"&&!!window.localStorage}function G(e,g){try{if(!U())return null;let f=window.localStorage.getItem(e);if(!f)return null;let E=JSON.parse(f);return E.__v!==g?(window.localStorage.removeItem(e),null):E.__s}catch{return null}}function K(e,g,f){try{if(!U())return;window.localStorage.setItem(e,JSON.stringify({__s:g,__v:f}));}catch{}}function Q(e){try{if(!U())return;window.localStorage.removeItem(e);}catch{}}function W(e,g={}){let{maxHistory:f=j,debounceMs:E=V,persist:C,keyboard:P=false,onUndo:k,onRedo:I,onSnapshot:M}=g,n=q(C),[R,h]=react.useState(()=>{if(n){let t=G(n.key,n.version);if(t!==null)return t}return e}),a=react.useRef(R);a.current=R;let u=react.useRef([]),c=react.useRef([]),[x,H]=react.useState([]),[L,S]=react.useState([]),o=react.useRef(null),m=react.useRef(null),i=react.useRef(null),y=react.useRef(null),T=react.useCallback(()=>{c.current=[],S([]);},[]),p=react.useCallback(t=>{let r=[...u.current,t];r.length>f&&r.splice(0,r.length-f),u.current=r,H(r);},[f]);react.useEffect(()=>()=>{o.current!==null&&clearTimeout(o.current),y.current!==null&&clearTimeout(y.current);},[]);let N=react.useCallback(t=>{n&&(y.current!==null&&clearTimeout(y.current),y.current=setTimeout(()=>{K(n.key,t,n.version),y.current=null;},n.debounceMs));},[n]),v=react.useCallback(t=>{n&&(y.current!==null&&(clearTimeout(y.current),y.current=null),K(n.key,t,n.version));},[n]),z=react.useCallback((t,r)=>{let s=a.current,d=typeof t=="function"?t(s):t;if(!Object.is(s,d)){if(h(d),a.current=d,N(d),E<=0){let F={state:s,timestamp:Date.now(),label:r};p(F),T();return}m.current===null&&(i.current=s),m.current=d,o.current!==null&&clearTimeout(o.current),o.current=setTimeout(()=>{let F={state:i.current,timestamp:Date.now(),label:r};p(F),T(),o.current=null,m.current=null,i.current=null;},E);}},[E,p,N,T]),D=react.useCallback(()=>{if(o.current!==null&&(clearTimeout(o.current),o.current=null),m.current!==null&&i.current!==null){let d={state:i.current,timestamp:Date.now()};p(d),T(),m.current=null,i.current=null;}if(u.current.length===0)return;let t=u.current[u.current.length-1],r=u.current.slice(0,-1);u.current=r,H(r);let s={state:a.current,timestamp:Date.now()};c.current=[...c.current,s],S(c.current),h(t.state),a.current=t.state,v(t.state),k?.(t.state);},[p,k,v,T]),_=react.useCallback(()=>{if(c.current.length===0)return;let t=c.current[c.current.length-1],r=c.current.slice(0,-1);c.current=r,S(r);let s={state:a.current,timestamp:Date.now()};u.current=[...u.current,s],H(u.current),h(t.state),a.current=t.state,v(t.state),I?.(t.state);},[I,v]);react.useEffect(()=>{if(!P)return;let t=r=>{!(r.metaKey||r.ctrlKey)||r.key!=="z"||(r.preventDefault(),r.shiftKey?_():D());};return window.addEventListener("keydown",t),()=>window.removeEventListener("keydown",t)},[P,D,_]);let b=react.useCallback(()=>{o.current!==null&&(clearTimeout(o.current),o.current=null),m.current=null,i.current=null,u.current=[],c.current=[],H([]),S([]);},[]),B=react.useCallback(t=>{let r=false;if(o.current!==null&&(clearTimeout(o.current),o.current=null),m.current!==null&&i.current!==null){let s={state:i.current,timestamp:Date.now()};p(s),T(),m.current=null,i.current=null,r=true;}if(r)t&&u.current.length>0&&(u.current[u.current.length-1].label=t,H([...u.current]));else {let s={state:a.current,timestamp:Date.now(),label:t};p(s),T(),M?.(s);}},[p,M,T]),J=react.useCallback(()=>{b(),n&&Q(n.key),h(e),a.current=e;},[b,e,n]);return {state:R,setState:z,undo:D,redo:_,canUndo:x.length>0,canRedo:L.length>0,clearHistory:b,clearDraft:J,snapshot:B,past:x,future:L}}exports.useFormHistory=W;
|
package/dist/index.mjs
CHANGED
|
@@ -1,282 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
// src/useFormHistory.ts
|
|
4
|
-
var DEFAULT_MAX_HISTORY = 100;
|
|
5
|
-
var DEFAULT_DEBOUNCE_MS = 300;
|
|
6
|
-
var DEFAULT_PERSIST_DEBOUNCE_MS = 500;
|
|
7
|
-
var DEFAULT_PERSIST_VERSION = 1;
|
|
8
|
-
function normalizePersist(persist) {
|
|
9
|
-
if (!persist) return null;
|
|
10
|
-
if (typeof persist === "boolean") return null;
|
|
11
|
-
return {
|
|
12
|
-
key: persist.key,
|
|
13
|
-
debounceMs: persist.debounceMs ?? DEFAULT_PERSIST_DEBOUNCE_MS,
|
|
14
|
-
version: persist.version ?? DEFAULT_PERSIST_VERSION
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
function readDraft(key, version) {
|
|
18
|
-
try {
|
|
19
|
-
if (typeof window === "undefined" || !window.localStorage) return null;
|
|
20
|
-
const raw = window.localStorage.getItem(key);
|
|
21
|
-
if (!raw) return null;
|
|
22
|
-
const parsed = JSON.parse(raw);
|
|
23
|
-
if (parsed.__version !== version) {
|
|
24
|
-
window.localStorage.removeItem(key);
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
return parsed.__state;
|
|
28
|
-
} catch {
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
function writeDraft(key, state, version) {
|
|
33
|
-
try {
|
|
34
|
-
if (typeof window === "undefined" || !window.localStorage) return;
|
|
35
|
-
window.localStorage.setItem(key, JSON.stringify({ __state: state, __version: version }));
|
|
36
|
-
} catch {
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
function removeDraft(key) {
|
|
40
|
-
try {
|
|
41
|
-
if (typeof window === "undefined" || !window.localStorage) return;
|
|
42
|
-
window.localStorage.removeItem(key);
|
|
43
|
-
} catch {
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
function useFormHistory(initialState, options = {}) {
|
|
47
|
-
const {
|
|
48
|
-
maxHistory = DEFAULT_MAX_HISTORY,
|
|
49
|
-
debounceMs = DEFAULT_DEBOUNCE_MS,
|
|
50
|
-
persist: persistOption,
|
|
51
|
-
onUndo,
|
|
52
|
-
onRedo,
|
|
53
|
-
onSnapshot
|
|
54
|
-
} = options;
|
|
55
|
-
const persist = normalizePersist(persistOption);
|
|
56
|
-
const initializedRef = useRef(false);
|
|
57
|
-
const [state, setStateRaw] = useState(() => {
|
|
58
|
-
if (persist) {
|
|
59
|
-
const draft = readDraft(persist.key, persist.version);
|
|
60
|
-
if (draft !== null) {
|
|
61
|
-
initializedRef.current = true;
|
|
62
|
-
return draft;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return initialState;
|
|
66
|
-
});
|
|
67
|
-
const stateRef = useRef(state);
|
|
68
|
-
stateRef.current = state;
|
|
69
|
-
const pastRef = useRef([]);
|
|
70
|
-
const futureRef = useRef([]);
|
|
71
|
-
const [past, setPast] = useState([]);
|
|
72
|
-
const [future, setFuture] = useState([]);
|
|
73
|
-
const debounceTimerRef = useRef(null);
|
|
74
|
-
const pendingStateRef = useRef(null);
|
|
75
|
-
const preDebounceStateRef = useRef(null);
|
|
76
|
-
const persistTimerRef = useRef(null);
|
|
77
|
-
const pushToPast = useCallback(
|
|
78
|
-
(entry) => {
|
|
79
|
-
const next = [...pastRef.current, entry];
|
|
80
|
-
if (next.length > maxHistory) {
|
|
81
|
-
next.splice(0, next.length - maxHistory);
|
|
82
|
-
}
|
|
83
|
-
pastRef.current = next;
|
|
84
|
-
setPast(next);
|
|
85
|
-
},
|
|
86
|
-
[maxHistory]
|
|
87
|
-
);
|
|
88
|
-
useEffect(() => {
|
|
89
|
-
return () => {
|
|
90
|
-
if (debounceTimerRef.current !== null) {
|
|
91
|
-
clearTimeout(debounceTimerRef.current);
|
|
92
|
-
}
|
|
93
|
-
if (persistTimerRef.current !== null) {
|
|
94
|
-
clearTimeout(persistTimerRef.current);
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
}, []);
|
|
98
|
-
const schedulePersist = useCallback(
|
|
99
|
-
(nextState) => {
|
|
100
|
-
if (!persist) return;
|
|
101
|
-
if (persistTimerRef.current !== null) {
|
|
102
|
-
clearTimeout(persistTimerRef.current);
|
|
103
|
-
}
|
|
104
|
-
persistTimerRef.current = setTimeout(() => {
|
|
105
|
-
writeDraft(persist.key, nextState, persist.version);
|
|
106
|
-
persistTimerRef.current = null;
|
|
107
|
-
}, persist.debounceMs);
|
|
108
|
-
},
|
|
109
|
-
[persist]
|
|
110
|
-
);
|
|
111
|
-
const persistNow = useCallback(
|
|
112
|
-
(nextState) => {
|
|
113
|
-
if (!persist) return;
|
|
114
|
-
if (persistTimerRef.current !== null) {
|
|
115
|
-
clearTimeout(persistTimerRef.current);
|
|
116
|
-
persistTimerRef.current = null;
|
|
117
|
-
}
|
|
118
|
-
writeDraft(persist.key, nextState, persist.version);
|
|
119
|
-
},
|
|
120
|
-
[persist]
|
|
121
|
-
);
|
|
122
|
-
const setState = useCallback(
|
|
123
|
-
(value, label) => {
|
|
124
|
-
const prev = stateRef.current;
|
|
125
|
-
const next = typeof value === "function" ? value(prev) : value;
|
|
126
|
-
if (Object.is(prev, next)) return;
|
|
127
|
-
setStateRaw(next);
|
|
128
|
-
stateRef.current = next;
|
|
129
|
-
schedulePersist(next);
|
|
130
|
-
if (debounceMs <= 0) {
|
|
131
|
-
const entry = { state: prev, timestamp: Date.now(), label };
|
|
132
|
-
pushToPast(entry);
|
|
133
|
-
futureRef.current = [];
|
|
134
|
-
setFuture([]);
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
if (pendingStateRef.current === null) {
|
|
138
|
-
preDebounceStateRef.current = prev;
|
|
139
|
-
}
|
|
140
|
-
pendingStateRef.current = next;
|
|
141
|
-
if (debounceTimerRef.current !== null) {
|
|
142
|
-
clearTimeout(debounceTimerRef.current);
|
|
143
|
-
}
|
|
144
|
-
debounceTimerRef.current = setTimeout(() => {
|
|
145
|
-
const entry = {
|
|
146
|
-
state: preDebounceStateRef.current,
|
|
147
|
-
timestamp: Date.now(),
|
|
148
|
-
label
|
|
149
|
-
};
|
|
150
|
-
pushToPast(entry);
|
|
151
|
-
futureRef.current = [];
|
|
152
|
-
setFuture([]);
|
|
153
|
-
debounceTimerRef.current = null;
|
|
154
|
-
pendingStateRef.current = null;
|
|
155
|
-
preDebounceStateRef.current = null;
|
|
156
|
-
}, debounceMs);
|
|
157
|
-
},
|
|
158
|
-
[debounceMs, pushToPast, schedulePersist]
|
|
159
|
-
);
|
|
160
|
-
const undo = useCallback(() => {
|
|
161
|
-
if (debounceTimerRef.current !== null) {
|
|
162
|
-
clearTimeout(debounceTimerRef.current);
|
|
163
|
-
debounceTimerRef.current = null;
|
|
164
|
-
}
|
|
165
|
-
if (pendingStateRef.current !== null && preDebounceStateRef.current !== null) {
|
|
166
|
-
const entry = {
|
|
167
|
-
state: preDebounceStateRef.current,
|
|
168
|
-
timestamp: Date.now()
|
|
169
|
-
};
|
|
170
|
-
pushToPast(entry);
|
|
171
|
-
futureRef.current = [];
|
|
172
|
-
setFuture([]);
|
|
173
|
-
pendingStateRef.current = null;
|
|
174
|
-
preDebounceStateRef.current = null;
|
|
175
|
-
}
|
|
176
|
-
if (pastRef.current.length === 0) return;
|
|
177
|
-
const prev = pastRef.current[pastRef.current.length - 1];
|
|
178
|
-
const newPast = pastRef.current.slice(0, -1);
|
|
179
|
-
pastRef.current = newPast;
|
|
180
|
-
setPast(newPast);
|
|
181
|
-
const currentEntry = {
|
|
182
|
-
state: stateRef.current,
|
|
183
|
-
timestamp: Date.now()
|
|
184
|
-
};
|
|
185
|
-
futureRef.current = [...futureRef.current, currentEntry];
|
|
186
|
-
setFuture(futureRef.current);
|
|
187
|
-
setStateRaw(prev.state);
|
|
188
|
-
stateRef.current = prev.state;
|
|
189
|
-
persistNow(prev.state);
|
|
190
|
-
onUndo?.(prev.state);
|
|
191
|
-
}, [pushToPast, onUndo, persistNow]);
|
|
192
|
-
const redo = useCallback(() => {
|
|
193
|
-
if (futureRef.current.length === 0) return;
|
|
194
|
-
const next = futureRef.current[futureRef.current.length - 1];
|
|
195
|
-
const newFuture = futureRef.current.slice(0, -1);
|
|
196
|
-
futureRef.current = newFuture;
|
|
197
|
-
setFuture(newFuture);
|
|
198
|
-
const currentEntry = {
|
|
199
|
-
state: stateRef.current,
|
|
200
|
-
timestamp: Date.now()
|
|
201
|
-
};
|
|
202
|
-
pastRef.current = [...pastRef.current, currentEntry];
|
|
203
|
-
setPast(pastRef.current);
|
|
204
|
-
setStateRaw(next.state);
|
|
205
|
-
stateRef.current = next.state;
|
|
206
|
-
persistNow(next.state);
|
|
207
|
-
onRedo?.(next.state);
|
|
208
|
-
}, [onRedo, persistNow]);
|
|
209
|
-
const clearHistory = useCallback(() => {
|
|
210
|
-
if (debounceTimerRef.current !== null) {
|
|
211
|
-
clearTimeout(debounceTimerRef.current);
|
|
212
|
-
debounceTimerRef.current = null;
|
|
213
|
-
}
|
|
214
|
-
pendingStateRef.current = null;
|
|
215
|
-
preDebounceStateRef.current = null;
|
|
216
|
-
pastRef.current = [];
|
|
217
|
-
futureRef.current = [];
|
|
218
|
-
setPast([]);
|
|
219
|
-
setFuture([]);
|
|
220
|
-
}, []);
|
|
221
|
-
const snapshot = useCallback(
|
|
222
|
-
(label) => {
|
|
223
|
-
let didFlush = false;
|
|
224
|
-
if (debounceTimerRef.current !== null) {
|
|
225
|
-
clearTimeout(debounceTimerRef.current);
|
|
226
|
-
debounceTimerRef.current = null;
|
|
227
|
-
}
|
|
228
|
-
if (pendingStateRef.current !== null && preDebounceStateRef.current !== null) {
|
|
229
|
-
const entry = {
|
|
230
|
-
state: preDebounceStateRef.current,
|
|
231
|
-
timestamp: Date.now()
|
|
232
|
-
};
|
|
233
|
-
pushToPast(entry);
|
|
234
|
-
futureRef.current = [];
|
|
235
|
-
setFuture([]);
|
|
236
|
-
pendingStateRef.current = null;
|
|
237
|
-
preDebounceStateRef.current = null;
|
|
238
|
-
didFlush = true;
|
|
239
|
-
}
|
|
240
|
-
if (!didFlush) {
|
|
241
|
-
const entry = {
|
|
242
|
-
state: stateRef.current,
|
|
243
|
-
timestamp: Date.now(),
|
|
244
|
-
label
|
|
245
|
-
};
|
|
246
|
-
pushToPast(entry);
|
|
247
|
-
futureRef.current = [];
|
|
248
|
-
setFuture([]);
|
|
249
|
-
onSnapshot?.(entry);
|
|
250
|
-
} else if (label && pastRef.current.length > 0) {
|
|
251
|
-
pastRef.current[pastRef.current.length - 1].label = label;
|
|
252
|
-
setPast([...pastRef.current]);
|
|
253
|
-
}
|
|
254
|
-
},
|
|
255
|
-
[pushToPast, onSnapshot]
|
|
256
|
-
);
|
|
257
|
-
const clearDraft = useCallback(() => {
|
|
258
|
-
clearHistory();
|
|
259
|
-
if (persist) {
|
|
260
|
-
removeDraft(persist.key);
|
|
261
|
-
}
|
|
262
|
-
setStateRaw(initialState);
|
|
263
|
-
stateRef.current = initialState;
|
|
264
|
-
}, [clearHistory, initialState, persist]);
|
|
265
|
-
return {
|
|
266
|
-
state,
|
|
267
|
-
setState,
|
|
268
|
-
undo,
|
|
269
|
-
redo,
|
|
270
|
-
canUndo: past.length > 0,
|
|
271
|
-
canRedo: future.length > 0,
|
|
272
|
-
clearHistory,
|
|
273
|
-
clearDraft,
|
|
274
|
-
snapshot,
|
|
275
|
-
past,
|
|
276
|
-
future
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
export { useFormHistory };
|
|
281
|
-
//# sourceMappingURL=index.mjs.map
|
|
282
|
-
//# sourceMappingURL=index.mjs.map
|
|
1
|
+
import {useState,useRef,useCallback,useEffect}from'react';var j=100,V=300,X=500,Y=1;function q(e){return !e||typeof e=="boolean"?null:{key:e.key,debounceMs:e.debounceMs??X,version:e.version??Y}}function U(){return typeof window<"u"&&!!window.localStorage}function G(e,g){try{if(!U())return null;let f=window.localStorage.getItem(e);if(!f)return null;let E=JSON.parse(f);return E.__v!==g?(window.localStorage.removeItem(e),null):E.__s}catch{return null}}function K(e,g,f){try{if(!U())return;window.localStorage.setItem(e,JSON.stringify({__s:g,__v:f}));}catch{}}function Q(e){try{if(!U())return;window.localStorage.removeItem(e);}catch{}}function W(e,g={}){let{maxHistory:f=j,debounceMs:E=V,persist:C,keyboard:P=false,onUndo:k,onRedo:I,onSnapshot:M}=g,n=q(C),[R,h]=useState(()=>{if(n){let t=G(n.key,n.version);if(t!==null)return t}return e}),a=useRef(R);a.current=R;let u=useRef([]),c=useRef([]),[x,H]=useState([]),[L,S]=useState([]),o=useRef(null),m=useRef(null),i=useRef(null),y=useRef(null),T=useCallback(()=>{c.current=[],S([]);},[]),p=useCallback(t=>{let r=[...u.current,t];r.length>f&&r.splice(0,r.length-f),u.current=r,H(r);},[f]);useEffect(()=>()=>{o.current!==null&&clearTimeout(o.current),y.current!==null&&clearTimeout(y.current);},[]);let N=useCallback(t=>{n&&(y.current!==null&&clearTimeout(y.current),y.current=setTimeout(()=>{K(n.key,t,n.version),y.current=null;},n.debounceMs));},[n]),v=useCallback(t=>{n&&(y.current!==null&&(clearTimeout(y.current),y.current=null),K(n.key,t,n.version));},[n]),z=useCallback((t,r)=>{let s=a.current,d=typeof t=="function"?t(s):t;if(!Object.is(s,d)){if(h(d),a.current=d,N(d),E<=0){let F={state:s,timestamp:Date.now(),label:r};p(F),T();return}m.current===null&&(i.current=s),m.current=d,o.current!==null&&clearTimeout(o.current),o.current=setTimeout(()=>{let F={state:i.current,timestamp:Date.now(),label:r};p(F),T(),o.current=null,m.current=null,i.current=null;},E);}},[E,p,N,T]),D=useCallback(()=>{if(o.current!==null&&(clearTimeout(o.current),o.current=null),m.current!==null&&i.current!==null){let d={state:i.current,timestamp:Date.now()};p(d),T(),m.current=null,i.current=null;}if(u.current.length===0)return;let t=u.current[u.current.length-1],r=u.current.slice(0,-1);u.current=r,H(r);let s={state:a.current,timestamp:Date.now()};c.current=[...c.current,s],S(c.current),h(t.state),a.current=t.state,v(t.state),k?.(t.state);},[p,k,v,T]),_=useCallback(()=>{if(c.current.length===0)return;let t=c.current[c.current.length-1],r=c.current.slice(0,-1);c.current=r,S(r);let s={state:a.current,timestamp:Date.now()};u.current=[...u.current,s],H(u.current),h(t.state),a.current=t.state,v(t.state),I?.(t.state);},[I,v]);useEffect(()=>{if(!P)return;let t=r=>{!(r.metaKey||r.ctrlKey)||r.key!=="z"||(r.preventDefault(),r.shiftKey?_():D());};return window.addEventListener("keydown",t),()=>window.removeEventListener("keydown",t)},[P,D,_]);let b=useCallback(()=>{o.current!==null&&(clearTimeout(o.current),o.current=null),m.current=null,i.current=null,u.current=[],c.current=[],H([]),S([]);},[]),B=useCallback(t=>{let r=false;if(o.current!==null&&(clearTimeout(o.current),o.current=null),m.current!==null&&i.current!==null){let s={state:i.current,timestamp:Date.now()};p(s),T(),m.current=null,i.current=null,r=true;}if(r)t&&u.current.length>0&&(u.current[u.current.length-1].label=t,H([...u.current]));else {let s={state:a.current,timestamp:Date.now(),label:t};p(s),T(),M?.(s);}},[p,M,T]),J=useCallback(()=>{b(),n&&Q(n.key),h(e),a.current=e;},[b,e,n]);return {state:R,setState:z,undo:D,redo:_,canUndo:x.length>0,canRedo:L.length>0,clearHistory:b,clearDraft:J,snapshot:B,past:x,future:L}}export{W as useFormHistory};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-form-rewind",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Zero-dependency React state engine with auto-saved history stacks, time-traveling undo/redo, keyboard shortcuts, and draft persistence for forms.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/useFormHistory.ts"],"names":["useRef","useState","useCallback","useEffect"],"mappings":"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","file":"index.js","sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { HistoryEntry, PersistOptions, UseFormHistoryOptions, UseFormHistoryReturn } from \"./types\";\n\nconst DEFAULT_MAX_HISTORY = 100;\nconst DEFAULT_DEBOUNCE_MS = 300;\nconst DEFAULT_PERSIST_DEBOUNCE_MS = 500;\nconst DEFAULT_PERSIST_VERSION = 1;\n\ninterface PersistStorage {\n key: string;\n debounceMs: number;\n version: number;\n}\n\nfunction normalizePersist(persist: boolean | PersistOptions | undefined): PersistStorage | null {\n if (!persist) return null;\n if (typeof persist === \"boolean\") return null;\n return {\n key: persist.key,\n debounceMs: persist.debounceMs ?? DEFAULT_PERSIST_DEBOUNCE_MS,\n version: persist.version ?? DEFAULT_PERSIST_VERSION,\n };\n}\n\nfunction readDraft<T>(key: string, version: number): T | null {\n try {\n if (typeof window === \"undefined\" || !window.localStorage) return null;\n const raw = window.localStorage.getItem(key);\n if (!raw) return null;\n const parsed = JSON.parse(raw);\n if (parsed.__version !== version) {\n window.localStorage.removeItem(key);\n return null;\n }\n return parsed.__state as T;\n } catch {\n return null;\n }\n}\n\nfunction writeDraft<T>(key: string, state: T, version: number): void {\n try {\n if (typeof window === \"undefined\" || !window.localStorage) return;\n window.localStorage.setItem(key, JSON.stringify({ __state: state, __version: version }));\n } catch {\n // localStorage full or disabled — silently ignore\n }\n}\n\nfunction removeDraft(key: string): void {\n try {\n if (typeof window === \"undefined\" || !window.localStorage) return;\n window.localStorage.removeItem(key);\n } catch {\n // silently ignore\n }\n}\n\nexport function useFormHistory<T>(\n initialState: T,\n options: UseFormHistoryOptions<T> = {},\n): UseFormHistoryReturn<T> {\n const {\n maxHistory = DEFAULT_MAX_HISTORY,\n debounceMs = DEFAULT_DEBOUNCE_MS,\n persist: persistOption,\n onUndo,\n onRedo,\n onSnapshot,\n } = options;\n\n const persist = normalizePersist(persistOption);\n const initializedRef = useRef(false);\n\n // Hydrate from localStorage on first render\n const [state, setStateRaw] = useState<T>(() => {\n if (persist) {\n const draft = readDraft<T>(persist.key, persist.version);\n if (draft !== null) {\n initializedRef.current = true;\n return draft;\n }\n }\n return initialState;\n });\n\n const stateRef = useRef<T>(state);\n stateRef.current = state;\n\n const pastRef = useRef<HistoryEntry<T>[]>([]);\n const futureRef = useRef<HistoryEntry<T>[]>([]);\n\n const [past, setPast] = useState<HistoryEntry<T>[]>([]);\n const [future, setFuture] = useState<HistoryEntry<T>[]>([]);\n\n const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const pendingStateRef = useRef<T | null>(null);\n const preDebounceStateRef = useRef<T | null>(null);\n\n // Persist debounce timer\n const persistTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const pushToPast = useCallback(\n (entry: HistoryEntry<T>) => {\n const next = [...pastRef.current, entry];\n if (next.length > maxHistory) {\n next.splice(0, next.length - maxHistory);\n }\n pastRef.current = next;\n setPast(next);\n },\n [maxHistory],\n );\n\n // Cleanup timers on unmount\n useEffect(() => {\n return () => {\n if (debounceTimerRef.current !== null) {\n clearTimeout(debounceTimerRef.current);\n }\n if (persistTimerRef.current !== null) {\n clearTimeout(persistTimerRef.current);\n }\n };\n }, []);\n\n // Debounced persist helper\n const schedulePersist = useCallback(\n (nextState: T) => {\n if (!persist) return;\n if (persistTimerRef.current !== null) {\n clearTimeout(persistTimerRef.current);\n }\n persistTimerRef.current = setTimeout(() => {\n writeDraft(persist.key, nextState, persist.version);\n persistTimerRef.current = null;\n }, persist.debounceMs);\n },\n [persist],\n );\n\n // Persist immediately (used by undo/redo/snapshot/clearDraft which bypass debounce)\n const persistNow = useCallback(\n (nextState: T) => {\n if (!persist) return;\n if (persistTimerRef.current !== null) {\n clearTimeout(persistTimerRef.current);\n persistTimerRef.current = null;\n }\n writeDraft(persist.key, nextState, persist.version);\n },\n [persist],\n );\n\n const setState = useCallback(\n (value: T | ((prev: T) => T), label?: string) => {\n const prev = stateRef.current;\n const next = typeof value === \"function\" ? (value as (prev: T) => T)(prev) : value;\n\n if (Object.is(prev, next)) return;\n\n setStateRaw(next);\n stateRef.current = next;\n schedulePersist(next);\n\n if (debounceMs <= 0) {\n const entry: HistoryEntry<T> = { state: prev, timestamp: Date.now(), label };\n pushToPast(entry);\n futureRef.current = [];\n setFuture([]);\n return;\n }\n\n if (pendingStateRef.current === null) {\n preDebounceStateRef.current = prev;\n }\n\n pendingStateRef.current = next;\n\n if (debounceTimerRef.current !== null) {\n clearTimeout(debounceTimerRef.current);\n }\n\n debounceTimerRef.current = setTimeout(() => {\n const entry: HistoryEntry<T> = {\n state: preDebounceStateRef.current!,\n timestamp: Date.now(),\n label,\n };\n pushToPast(entry);\n futureRef.current = [];\n setFuture([]);\n debounceTimerRef.current = null;\n pendingStateRef.current = null;\n preDebounceStateRef.current = null;\n }, debounceMs);\n },\n [debounceMs, pushToPast, schedulePersist],\n );\n\n const undo = useCallback(() => {\n if (debounceTimerRef.current !== null) {\n clearTimeout(debounceTimerRef.current);\n debounceTimerRef.current = null;\n }\n\n if (pendingStateRef.current !== null && preDebounceStateRef.current !== null) {\n const entry: HistoryEntry<T> = {\n state: preDebounceStateRef.current,\n timestamp: Date.now(),\n };\n pushToPast(entry);\n futureRef.current = [];\n setFuture([]);\n pendingStateRef.current = null;\n preDebounceStateRef.current = null;\n }\n\n if (pastRef.current.length === 0) return;\n\n const prev = pastRef.current[pastRef.current.length - 1];\n const newPast = pastRef.current.slice(0, -1);\n pastRef.current = newPast;\n setPast(newPast);\n\n const currentEntry: HistoryEntry<T> = {\n state: stateRef.current,\n timestamp: Date.now(),\n };\n futureRef.current = [...futureRef.current, currentEntry];\n setFuture(futureRef.current);\n\n setStateRaw(prev.state);\n stateRef.current = prev.state;\n persistNow(prev.state);\n onUndo?.(prev.state);\n }, [pushToPast, onUndo, persistNow]);\n\n const redo = useCallback(() => {\n if (futureRef.current.length === 0) return;\n\n const next = futureRef.current[futureRef.current.length - 1];\n const newFuture = futureRef.current.slice(0, -1);\n futureRef.current = newFuture;\n setFuture(newFuture);\n\n const currentEntry: HistoryEntry<T> = {\n state: stateRef.current,\n timestamp: Date.now(),\n };\n pastRef.current = [...pastRef.current, currentEntry];\n setPast(pastRef.current);\n\n setStateRaw(next.state);\n stateRef.current = next.state;\n persistNow(next.state);\n onRedo?.(next.state);\n }, [onRedo, persistNow]);\n\n const clearHistory = useCallback(() => {\n if (debounceTimerRef.current !== null) {\n clearTimeout(debounceTimerRef.current);\n debounceTimerRef.current = null;\n }\n pendingStateRef.current = null;\n preDebounceStateRef.current = null;\n pastRef.current = [];\n futureRef.current = [];\n setPast([]);\n setFuture([]);\n }, []);\n\n const snapshot = useCallback(\n (label?: string) => {\n let didFlush = false;\n\n if (debounceTimerRef.current !== null) {\n clearTimeout(debounceTimerRef.current);\n debounceTimerRef.current = null;\n }\n\n if (pendingStateRef.current !== null && preDebounceStateRef.current !== null) {\n const entry: HistoryEntry<T> = {\n state: preDebounceStateRef.current,\n timestamp: Date.now(),\n };\n pushToPast(entry);\n futureRef.current = [];\n setFuture([]);\n pendingStateRef.current = null;\n preDebounceStateRef.current = null;\n didFlush = true;\n }\n\n if (!didFlush) {\n const entry: HistoryEntry<T> = {\n state: stateRef.current,\n timestamp: Date.now(),\n label,\n };\n pushToPast(entry);\n futureRef.current = [];\n setFuture([]);\n onSnapshot?.(entry);\n } else if (label && pastRef.current.length > 0) {\n pastRef.current[pastRef.current.length - 1].label = label;\n setPast([...pastRef.current]);\n }\n },\n [pushToPast, onSnapshot],\n );\n\n const clearDraft = useCallback(() => {\n clearHistory();\n if (persist) {\n removeDraft(persist.key);\n }\n setStateRaw(initialState);\n stateRef.current = initialState;\n }, [clearHistory, initialState, persist]);\n\n return {\n state,\n setState,\n undo,\n redo,\n canUndo: past.length > 0,\n canRedo: future.length > 0,\n clearHistory,\n clearDraft,\n snapshot,\n past,\n future,\n };\n}\n"]}
|
package/dist/index.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/useFormHistory.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { HistoryEntry, PersistOptions, UseFormHistoryOptions, UseFormHistoryReturn } from \"./types\";\n\nconst DEFAULT_MAX_HISTORY = 100;\nconst DEFAULT_DEBOUNCE_MS = 300;\nconst DEFAULT_PERSIST_DEBOUNCE_MS = 500;\nconst DEFAULT_PERSIST_VERSION = 1;\n\ninterface PersistStorage {\n key: string;\n debounceMs: number;\n version: number;\n}\n\nfunction normalizePersist(persist: boolean | PersistOptions | undefined): PersistStorage | null {\n if (!persist) return null;\n if (typeof persist === \"boolean\") return null;\n return {\n key: persist.key,\n debounceMs: persist.debounceMs ?? DEFAULT_PERSIST_DEBOUNCE_MS,\n version: persist.version ?? DEFAULT_PERSIST_VERSION,\n };\n}\n\nfunction readDraft<T>(key: string, version: number): T | null {\n try {\n if (typeof window === \"undefined\" || !window.localStorage) return null;\n const raw = window.localStorage.getItem(key);\n if (!raw) return null;\n const parsed = JSON.parse(raw);\n if (parsed.__version !== version) {\n window.localStorage.removeItem(key);\n return null;\n }\n return parsed.__state as T;\n } catch {\n return null;\n }\n}\n\nfunction writeDraft<T>(key: string, state: T, version: number): void {\n try {\n if (typeof window === \"undefined\" || !window.localStorage) return;\n window.localStorage.setItem(key, JSON.stringify({ __state: state, __version: version }));\n } catch {\n // localStorage full or disabled — silently ignore\n }\n}\n\nfunction removeDraft(key: string): void {\n try {\n if (typeof window === \"undefined\" || !window.localStorage) return;\n window.localStorage.removeItem(key);\n } catch {\n // silently ignore\n }\n}\n\nexport function useFormHistory<T>(\n initialState: T,\n options: UseFormHistoryOptions<T> = {},\n): UseFormHistoryReturn<T> {\n const {\n maxHistory = DEFAULT_MAX_HISTORY,\n debounceMs = DEFAULT_DEBOUNCE_MS,\n persist: persistOption,\n onUndo,\n onRedo,\n onSnapshot,\n } = options;\n\n const persist = normalizePersist(persistOption);\n const initializedRef = useRef(false);\n\n // Hydrate from localStorage on first render\n const [state, setStateRaw] = useState<T>(() => {\n if (persist) {\n const draft = readDraft<T>(persist.key, persist.version);\n if (draft !== null) {\n initializedRef.current = true;\n return draft;\n }\n }\n return initialState;\n });\n\n const stateRef = useRef<T>(state);\n stateRef.current = state;\n\n const pastRef = useRef<HistoryEntry<T>[]>([]);\n const futureRef = useRef<HistoryEntry<T>[]>([]);\n\n const [past, setPast] = useState<HistoryEntry<T>[]>([]);\n const [future, setFuture] = useState<HistoryEntry<T>[]>([]);\n\n const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const pendingStateRef = useRef<T | null>(null);\n const preDebounceStateRef = useRef<T | null>(null);\n\n // Persist debounce timer\n const persistTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const pushToPast = useCallback(\n (entry: HistoryEntry<T>) => {\n const next = [...pastRef.current, entry];\n if (next.length > maxHistory) {\n next.splice(0, next.length - maxHistory);\n }\n pastRef.current = next;\n setPast(next);\n },\n [maxHistory],\n );\n\n // Cleanup timers on unmount\n useEffect(() => {\n return () => {\n if (debounceTimerRef.current !== null) {\n clearTimeout(debounceTimerRef.current);\n }\n if (persistTimerRef.current !== null) {\n clearTimeout(persistTimerRef.current);\n }\n };\n }, []);\n\n // Debounced persist helper\n const schedulePersist = useCallback(\n (nextState: T) => {\n if (!persist) return;\n if (persistTimerRef.current !== null) {\n clearTimeout(persistTimerRef.current);\n }\n persistTimerRef.current = setTimeout(() => {\n writeDraft(persist.key, nextState, persist.version);\n persistTimerRef.current = null;\n }, persist.debounceMs);\n },\n [persist],\n );\n\n // Persist immediately (used by undo/redo/snapshot/clearDraft which bypass debounce)\n const persistNow = useCallback(\n (nextState: T) => {\n if (!persist) return;\n if (persistTimerRef.current !== null) {\n clearTimeout(persistTimerRef.current);\n persistTimerRef.current = null;\n }\n writeDraft(persist.key, nextState, persist.version);\n },\n [persist],\n );\n\n const setState = useCallback(\n (value: T | ((prev: T) => T), label?: string) => {\n const prev = stateRef.current;\n const next = typeof value === \"function\" ? (value as (prev: T) => T)(prev) : value;\n\n if (Object.is(prev, next)) return;\n\n setStateRaw(next);\n stateRef.current = next;\n schedulePersist(next);\n\n if (debounceMs <= 0) {\n const entry: HistoryEntry<T> = { state: prev, timestamp: Date.now(), label };\n pushToPast(entry);\n futureRef.current = [];\n setFuture([]);\n return;\n }\n\n if (pendingStateRef.current === null) {\n preDebounceStateRef.current = prev;\n }\n\n pendingStateRef.current = next;\n\n if (debounceTimerRef.current !== null) {\n clearTimeout(debounceTimerRef.current);\n }\n\n debounceTimerRef.current = setTimeout(() => {\n const entry: HistoryEntry<T> = {\n state: preDebounceStateRef.current!,\n timestamp: Date.now(),\n label,\n };\n pushToPast(entry);\n futureRef.current = [];\n setFuture([]);\n debounceTimerRef.current = null;\n pendingStateRef.current = null;\n preDebounceStateRef.current = null;\n }, debounceMs);\n },\n [debounceMs, pushToPast, schedulePersist],\n );\n\n const undo = useCallback(() => {\n if (debounceTimerRef.current !== null) {\n clearTimeout(debounceTimerRef.current);\n debounceTimerRef.current = null;\n }\n\n if (pendingStateRef.current !== null && preDebounceStateRef.current !== null) {\n const entry: HistoryEntry<T> = {\n state: preDebounceStateRef.current,\n timestamp: Date.now(),\n };\n pushToPast(entry);\n futureRef.current = [];\n setFuture([]);\n pendingStateRef.current = null;\n preDebounceStateRef.current = null;\n }\n\n if (pastRef.current.length === 0) return;\n\n const prev = pastRef.current[pastRef.current.length - 1];\n const newPast = pastRef.current.slice(0, -1);\n pastRef.current = newPast;\n setPast(newPast);\n\n const currentEntry: HistoryEntry<T> = {\n state: stateRef.current,\n timestamp: Date.now(),\n };\n futureRef.current = [...futureRef.current, currentEntry];\n setFuture(futureRef.current);\n\n setStateRaw(prev.state);\n stateRef.current = prev.state;\n persistNow(prev.state);\n onUndo?.(prev.state);\n }, [pushToPast, onUndo, persistNow]);\n\n const redo = useCallback(() => {\n if (futureRef.current.length === 0) return;\n\n const next = futureRef.current[futureRef.current.length - 1];\n const newFuture = futureRef.current.slice(0, -1);\n futureRef.current = newFuture;\n setFuture(newFuture);\n\n const currentEntry: HistoryEntry<T> = {\n state: stateRef.current,\n timestamp: Date.now(),\n };\n pastRef.current = [...pastRef.current, currentEntry];\n setPast(pastRef.current);\n\n setStateRaw(next.state);\n stateRef.current = next.state;\n persistNow(next.state);\n onRedo?.(next.state);\n }, [onRedo, persistNow]);\n\n const clearHistory = useCallback(() => {\n if (debounceTimerRef.current !== null) {\n clearTimeout(debounceTimerRef.current);\n debounceTimerRef.current = null;\n }\n pendingStateRef.current = null;\n preDebounceStateRef.current = null;\n pastRef.current = [];\n futureRef.current = [];\n setPast([]);\n setFuture([]);\n }, []);\n\n const snapshot = useCallback(\n (label?: string) => {\n let didFlush = false;\n\n if (debounceTimerRef.current !== null) {\n clearTimeout(debounceTimerRef.current);\n debounceTimerRef.current = null;\n }\n\n if (pendingStateRef.current !== null && preDebounceStateRef.current !== null) {\n const entry: HistoryEntry<T> = {\n state: preDebounceStateRef.current,\n timestamp: Date.now(),\n };\n pushToPast(entry);\n futureRef.current = [];\n setFuture([]);\n pendingStateRef.current = null;\n preDebounceStateRef.current = null;\n didFlush = true;\n }\n\n if (!didFlush) {\n const entry: HistoryEntry<T> = {\n state: stateRef.current,\n timestamp: Date.now(),\n label,\n };\n pushToPast(entry);\n futureRef.current = [];\n setFuture([]);\n onSnapshot?.(entry);\n } else if (label && pastRef.current.length > 0) {\n pastRef.current[pastRef.current.length - 1].label = label;\n setPast([...pastRef.current]);\n }\n },\n [pushToPast, onSnapshot],\n );\n\n const clearDraft = useCallback(() => {\n clearHistory();\n if (persist) {\n removeDraft(persist.key);\n }\n setStateRaw(initialState);\n stateRef.current = initialState;\n }, [clearHistory, initialState, persist]);\n\n return {\n state,\n setState,\n undo,\n redo,\n canUndo: past.length > 0,\n canRedo: future.length > 0,\n clearHistory,\n clearDraft,\n snapshot,\n past,\n future,\n };\n}\n"]}
|