react-form-rewind 0.3.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 +29 -9
- package/dist/index.d.mts +2 -6
- package/dist/index.d.ts +2 -6
- package/dist/index.js +1 -198
- package/dist/index.mjs +1 -196
- package/package.json +4 -4
- package/dist/index.js.map +0 -1
- package/dist/index.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -2,16 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/react-form-rewind)
|
|
4
4
|
[](https://www.npmjs.com/package/react-form-rewind)
|
|
5
|
-
[](https://github.com/BazilSuhail/npm-react-form-rewind/blob/main/LICENSE)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[]()
|
|
8
|
+
[]()
|
|
9
|
+
[](https://react.dev/)
|
|
10
|
+
[](https://bundlejs.com/?q=react-form-rewind)
|
|
11
|
+
[](https://github.com/BazilSuhail/npm-react-form-rewind)
|
|
9
12
|
|
|
10
13
|
Zero-dependency, tree-shakable React state engine with auto-saved history stacks, time-traveling undo/redo, keyboard shortcuts, and draft persistence.
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
- **Undo/Redo** — full history stack with `Ctrl+Z` / `Ctrl+Shift+Z` keyboard shortcuts
|
|
16
|
+
- **Snapshot debouncing** — rapid keystrokes coalesced into logical history entries
|
|
17
|
+
- **Draft persistence** — auto-save to `localStorage` with schema versioning
|
|
18
|
+
- **Functional updates** — `setState(prev => prev + 1)` supported
|
|
19
|
+
- **History inspection** — access `past` and `future` arrays for custom UIs
|
|
20
|
+
- **Callbacks** — `onUndo`, `onRedo`, `onSnapshot` hooks
|
|
21
|
+
- Zero-config — no providers, no context, just a hook
|
|
22
|
+
- Tree-shakable — ESM + CJS with `sideEffects: false`
|
|
23
|
+
- TypeScript — full generics, all types exported
|
|
24
|
+
|
|
25
|
+
## Install
|
|
13
26
|
|
|
14
|
-
|
|
27
|
+
```bash
|
|
28
|
+
npm install react-form-rewind
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
### Why?
|
|
15
34
|
|
|
16
35
|
| Problem | Solution |
|
|
17
36
|
|---------|----------|
|
|
@@ -20,7 +39,7 @@ Zero-dependency, tree-shakable React state engine with auto-saved history stacks
|
|
|
20
39
|
| Manual debouncers for history snapshots | Automated keystroke coalescing into logical snapshots |
|
|
21
40
|
| Heavy form libraries add validation bloat | Focused solely on history and state persistence |
|
|
22
41
|
|
|
23
|
-
##
|
|
42
|
+
## Usage
|
|
24
43
|
|
|
25
44
|
```bash
|
|
26
45
|
npm install react-form-rewind
|
|
@@ -32,7 +51,7 @@ import { useFormHistory } from "react-form-rewind";
|
|
|
32
51
|
function MyForm() {
|
|
33
52
|
const { state, setState, undo, redo, canUndo, canRedo } = useFormHistory(
|
|
34
53
|
{ name: "", email: "" },
|
|
35
|
-
{ persist: { key: "my-form-draft" } }
|
|
54
|
+
{ keyboard: true, persist: { key: "my-form-draft" } }
|
|
36
55
|
);
|
|
37
56
|
|
|
38
57
|
return (
|
|
@@ -89,6 +108,7 @@ The core hook that manages a history-backed state stack.
|
|
|
89
108
|
| `maxHistory` | `number` | `100` | Maximum past entries to retain |
|
|
90
109
|
| `debounceMs` | `number` | `300` | Debounce window for rapid state changes |
|
|
91
110
|
| `persist` | `boolean \| PersistOptions` | `false` | Enable draft persistence |
|
|
111
|
+
| `keyboard` | `boolean` | `false` | Enable Ctrl+Z / Ctrl+Shift+Z keyboard shortcuts |
|
|
92
112
|
| `onUndo` | `(state: T) => void` | — | Callback after undo |
|
|
93
113
|
| `onRedo` | `(state: T) => void` | — | Callback after redo |
|
|
94
114
|
| `onSnapshot` | `(entry: HistoryEntry<T>) => void` | — | Callback when a snapshot is committed |
|
|
@@ -107,7 +127,7 @@ The core hook that manages a history-backed state stack.
|
|
|
107
127
|
|
|
108
128
|
### Keyboard Shortcuts
|
|
109
129
|
|
|
110
|
-
|
|
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.
|
|
111
131
|
|
|
112
132
|
### Snapshot Debouncing
|
|
113
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,198 +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
|
-
function useFormHistory(initialState, options = {}) {
|
|
9
|
-
const {
|
|
10
|
-
maxHistory = DEFAULT_MAX_HISTORY,
|
|
11
|
-
debounceMs = DEFAULT_DEBOUNCE_MS,
|
|
12
|
-
onUndo,
|
|
13
|
-
onRedo,
|
|
14
|
-
onSnapshot
|
|
15
|
-
} = options;
|
|
16
|
-
const pastRef = react.useRef([]);
|
|
17
|
-
const futureRef = react.useRef([]);
|
|
18
|
-
const [state, setStateRaw] = react.useState(initialState);
|
|
19
|
-
const stateRef = react.useRef(initialState);
|
|
20
|
-
stateRef.current = state;
|
|
21
|
-
const [past, setPast] = react.useState([]);
|
|
22
|
-
const [future, setFuture] = react.useState([]);
|
|
23
|
-
const debounceTimerRef = react.useRef(null);
|
|
24
|
-
const pendingStateRef = react.useRef(null);
|
|
25
|
-
const preDebounceStateRef = react.useRef(null);
|
|
26
|
-
const pushToPast = react.useCallback(
|
|
27
|
-
(entry) => {
|
|
28
|
-
const next = [...pastRef.current, entry];
|
|
29
|
-
if (next.length > maxHistory) {
|
|
30
|
-
next.splice(0, next.length - maxHistory);
|
|
31
|
-
}
|
|
32
|
-
pastRef.current = next;
|
|
33
|
-
setPast(next);
|
|
34
|
-
},
|
|
35
|
-
[maxHistory]
|
|
36
|
-
);
|
|
37
|
-
react.useEffect(() => {
|
|
38
|
-
return () => {
|
|
39
|
-
if (debounceTimerRef.current !== null) {
|
|
40
|
-
clearTimeout(debounceTimerRef.current);
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
}, []);
|
|
44
|
-
const setState = react.useCallback(
|
|
45
|
-
(value, label) => {
|
|
46
|
-
const prev = stateRef.current;
|
|
47
|
-
const next = typeof value === "function" ? value(prev) : value;
|
|
48
|
-
if (Object.is(prev, next)) return;
|
|
49
|
-
setStateRaw(next);
|
|
50
|
-
stateRef.current = next;
|
|
51
|
-
if (debounceMs <= 0) {
|
|
52
|
-
const entry = { state: prev, timestamp: Date.now(), label };
|
|
53
|
-
pushToPast(entry);
|
|
54
|
-
futureRef.current = [];
|
|
55
|
-
setFuture([]);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
if (pendingStateRef.current === null) {
|
|
59
|
-
preDebounceStateRef.current = prev;
|
|
60
|
-
}
|
|
61
|
-
pendingStateRef.current = next;
|
|
62
|
-
if (debounceTimerRef.current !== null) {
|
|
63
|
-
clearTimeout(debounceTimerRef.current);
|
|
64
|
-
}
|
|
65
|
-
debounceTimerRef.current = setTimeout(() => {
|
|
66
|
-
const entry = {
|
|
67
|
-
state: preDebounceStateRef.current,
|
|
68
|
-
timestamp: Date.now(),
|
|
69
|
-
label
|
|
70
|
-
};
|
|
71
|
-
pushToPast(entry);
|
|
72
|
-
futureRef.current = [];
|
|
73
|
-
setFuture([]);
|
|
74
|
-
debounceTimerRef.current = null;
|
|
75
|
-
pendingStateRef.current = null;
|
|
76
|
-
preDebounceStateRef.current = null;
|
|
77
|
-
}, debounceMs);
|
|
78
|
-
},
|
|
79
|
-
[debounceMs, pushToPast]
|
|
80
|
-
);
|
|
81
|
-
const undo = react.useCallback(() => {
|
|
82
|
-
if (debounceTimerRef.current !== null) {
|
|
83
|
-
clearTimeout(debounceTimerRef.current);
|
|
84
|
-
debounceTimerRef.current = null;
|
|
85
|
-
}
|
|
86
|
-
if (pendingStateRef.current !== null && preDebounceStateRef.current !== null) {
|
|
87
|
-
const entry = {
|
|
88
|
-
state: preDebounceStateRef.current,
|
|
89
|
-
timestamp: Date.now()
|
|
90
|
-
};
|
|
91
|
-
pushToPast(entry);
|
|
92
|
-
futureRef.current = [];
|
|
93
|
-
setFuture([]);
|
|
94
|
-
pendingStateRef.current = null;
|
|
95
|
-
preDebounceStateRef.current = null;
|
|
96
|
-
}
|
|
97
|
-
if (pastRef.current.length === 0) return;
|
|
98
|
-
const prev = pastRef.current[pastRef.current.length - 1];
|
|
99
|
-
const newPast = pastRef.current.slice(0, -1);
|
|
100
|
-
pastRef.current = newPast;
|
|
101
|
-
setPast(newPast);
|
|
102
|
-
const currentEntry = {
|
|
103
|
-
state: stateRef.current,
|
|
104
|
-
timestamp: Date.now()
|
|
105
|
-
};
|
|
106
|
-
futureRef.current = [...futureRef.current, currentEntry];
|
|
107
|
-
setFuture(futureRef.current);
|
|
108
|
-
setStateRaw(prev.state);
|
|
109
|
-
stateRef.current = prev.state;
|
|
110
|
-
onUndo?.(prev.state);
|
|
111
|
-
}, [pushToPast, onUndo]);
|
|
112
|
-
const redo = react.useCallback(() => {
|
|
113
|
-
if (futureRef.current.length === 0) return;
|
|
114
|
-
const next = futureRef.current[futureRef.current.length - 1];
|
|
115
|
-
const newFuture = futureRef.current.slice(0, -1);
|
|
116
|
-
futureRef.current = newFuture;
|
|
117
|
-
setFuture(newFuture);
|
|
118
|
-
const currentEntry = {
|
|
119
|
-
state: stateRef.current,
|
|
120
|
-
timestamp: Date.now()
|
|
121
|
-
};
|
|
122
|
-
pastRef.current = [...pastRef.current, currentEntry];
|
|
123
|
-
setPast(pastRef.current);
|
|
124
|
-
setStateRaw(next.state);
|
|
125
|
-
stateRef.current = next.state;
|
|
126
|
-
onRedo?.(next.state);
|
|
127
|
-
}, [onRedo]);
|
|
128
|
-
const clearHistory = react.useCallback(() => {
|
|
129
|
-
if (debounceTimerRef.current !== null) {
|
|
130
|
-
clearTimeout(debounceTimerRef.current);
|
|
131
|
-
debounceTimerRef.current = null;
|
|
132
|
-
}
|
|
133
|
-
pendingStateRef.current = null;
|
|
134
|
-
preDebounceStateRef.current = null;
|
|
135
|
-
pastRef.current = [];
|
|
136
|
-
futureRef.current = [];
|
|
137
|
-
setPast([]);
|
|
138
|
-
setFuture([]);
|
|
139
|
-
}, []);
|
|
140
|
-
const snapshot = react.useCallback(
|
|
141
|
-
(label) => {
|
|
142
|
-
let didFlush = false;
|
|
143
|
-
if (debounceTimerRef.current !== null) {
|
|
144
|
-
clearTimeout(debounceTimerRef.current);
|
|
145
|
-
debounceTimerRef.current = null;
|
|
146
|
-
}
|
|
147
|
-
if (pendingStateRef.current !== null && preDebounceStateRef.current !== null) {
|
|
148
|
-
const entry = {
|
|
149
|
-
state: preDebounceStateRef.current,
|
|
150
|
-
timestamp: Date.now()
|
|
151
|
-
};
|
|
152
|
-
pushToPast(entry);
|
|
153
|
-
futureRef.current = [];
|
|
154
|
-
setFuture([]);
|
|
155
|
-
pendingStateRef.current = null;
|
|
156
|
-
preDebounceStateRef.current = null;
|
|
157
|
-
didFlush = true;
|
|
158
|
-
}
|
|
159
|
-
if (!didFlush) {
|
|
160
|
-
const entry = {
|
|
161
|
-
state: stateRef.current,
|
|
162
|
-
timestamp: Date.now(),
|
|
163
|
-
label
|
|
164
|
-
};
|
|
165
|
-
pushToPast(entry);
|
|
166
|
-
futureRef.current = [];
|
|
167
|
-
setFuture([]);
|
|
168
|
-
onSnapshot?.(entry);
|
|
169
|
-
} else if (label && pastRef.current.length > 0) {
|
|
170
|
-
pastRef.current[pastRef.current.length - 1].label = label;
|
|
171
|
-
setPast([...pastRef.current]);
|
|
172
|
-
}
|
|
173
|
-
},
|
|
174
|
-
[pushToPast, onSnapshot]
|
|
175
|
-
);
|
|
176
|
-
const clearDraft = react.useCallback(() => {
|
|
177
|
-
clearHistory();
|
|
178
|
-
setStateRaw(initialState);
|
|
179
|
-
stateRef.current = initialState;
|
|
180
|
-
}, [clearHistory, initialState]);
|
|
181
|
-
return {
|
|
182
|
-
state,
|
|
183
|
-
setState,
|
|
184
|
-
undo,
|
|
185
|
-
redo,
|
|
186
|
-
canUndo: past.length > 0,
|
|
187
|
-
canRedo: future.length > 0,
|
|
188
|
-
clearHistory,
|
|
189
|
-
clearDraft,
|
|
190
|
-
snapshot,
|
|
191
|
-
past,
|
|
192
|
-
future
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
exports.useFormHistory = useFormHistory;
|
|
197
|
-
//# sourceMappingURL=index.js.map
|
|
198
|
-
//# 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,196 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
// src/useFormHistory.ts
|
|
4
|
-
var DEFAULT_MAX_HISTORY = 100;
|
|
5
|
-
var DEFAULT_DEBOUNCE_MS = 300;
|
|
6
|
-
function useFormHistory(initialState, options = {}) {
|
|
7
|
-
const {
|
|
8
|
-
maxHistory = DEFAULT_MAX_HISTORY,
|
|
9
|
-
debounceMs = DEFAULT_DEBOUNCE_MS,
|
|
10
|
-
onUndo,
|
|
11
|
-
onRedo,
|
|
12
|
-
onSnapshot
|
|
13
|
-
} = options;
|
|
14
|
-
const pastRef = useRef([]);
|
|
15
|
-
const futureRef = useRef([]);
|
|
16
|
-
const [state, setStateRaw] = useState(initialState);
|
|
17
|
-
const stateRef = useRef(initialState);
|
|
18
|
-
stateRef.current = state;
|
|
19
|
-
const [past, setPast] = useState([]);
|
|
20
|
-
const [future, setFuture] = useState([]);
|
|
21
|
-
const debounceTimerRef = useRef(null);
|
|
22
|
-
const pendingStateRef = useRef(null);
|
|
23
|
-
const preDebounceStateRef = useRef(null);
|
|
24
|
-
const pushToPast = useCallback(
|
|
25
|
-
(entry) => {
|
|
26
|
-
const next = [...pastRef.current, entry];
|
|
27
|
-
if (next.length > maxHistory) {
|
|
28
|
-
next.splice(0, next.length - maxHistory);
|
|
29
|
-
}
|
|
30
|
-
pastRef.current = next;
|
|
31
|
-
setPast(next);
|
|
32
|
-
},
|
|
33
|
-
[maxHistory]
|
|
34
|
-
);
|
|
35
|
-
useEffect(() => {
|
|
36
|
-
return () => {
|
|
37
|
-
if (debounceTimerRef.current !== null) {
|
|
38
|
-
clearTimeout(debounceTimerRef.current);
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
}, []);
|
|
42
|
-
const setState = useCallback(
|
|
43
|
-
(value, label) => {
|
|
44
|
-
const prev = stateRef.current;
|
|
45
|
-
const next = typeof value === "function" ? value(prev) : value;
|
|
46
|
-
if (Object.is(prev, next)) return;
|
|
47
|
-
setStateRaw(next);
|
|
48
|
-
stateRef.current = next;
|
|
49
|
-
if (debounceMs <= 0) {
|
|
50
|
-
const entry = { state: prev, timestamp: Date.now(), label };
|
|
51
|
-
pushToPast(entry);
|
|
52
|
-
futureRef.current = [];
|
|
53
|
-
setFuture([]);
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
if (pendingStateRef.current === null) {
|
|
57
|
-
preDebounceStateRef.current = prev;
|
|
58
|
-
}
|
|
59
|
-
pendingStateRef.current = next;
|
|
60
|
-
if (debounceTimerRef.current !== null) {
|
|
61
|
-
clearTimeout(debounceTimerRef.current);
|
|
62
|
-
}
|
|
63
|
-
debounceTimerRef.current = setTimeout(() => {
|
|
64
|
-
const entry = {
|
|
65
|
-
state: preDebounceStateRef.current,
|
|
66
|
-
timestamp: Date.now(),
|
|
67
|
-
label
|
|
68
|
-
};
|
|
69
|
-
pushToPast(entry);
|
|
70
|
-
futureRef.current = [];
|
|
71
|
-
setFuture([]);
|
|
72
|
-
debounceTimerRef.current = null;
|
|
73
|
-
pendingStateRef.current = null;
|
|
74
|
-
preDebounceStateRef.current = null;
|
|
75
|
-
}, debounceMs);
|
|
76
|
-
},
|
|
77
|
-
[debounceMs, pushToPast]
|
|
78
|
-
);
|
|
79
|
-
const undo = useCallback(() => {
|
|
80
|
-
if (debounceTimerRef.current !== null) {
|
|
81
|
-
clearTimeout(debounceTimerRef.current);
|
|
82
|
-
debounceTimerRef.current = null;
|
|
83
|
-
}
|
|
84
|
-
if (pendingStateRef.current !== null && preDebounceStateRef.current !== null) {
|
|
85
|
-
const entry = {
|
|
86
|
-
state: preDebounceStateRef.current,
|
|
87
|
-
timestamp: Date.now()
|
|
88
|
-
};
|
|
89
|
-
pushToPast(entry);
|
|
90
|
-
futureRef.current = [];
|
|
91
|
-
setFuture([]);
|
|
92
|
-
pendingStateRef.current = null;
|
|
93
|
-
preDebounceStateRef.current = null;
|
|
94
|
-
}
|
|
95
|
-
if (pastRef.current.length === 0) return;
|
|
96
|
-
const prev = pastRef.current[pastRef.current.length - 1];
|
|
97
|
-
const newPast = pastRef.current.slice(0, -1);
|
|
98
|
-
pastRef.current = newPast;
|
|
99
|
-
setPast(newPast);
|
|
100
|
-
const currentEntry = {
|
|
101
|
-
state: stateRef.current,
|
|
102
|
-
timestamp: Date.now()
|
|
103
|
-
};
|
|
104
|
-
futureRef.current = [...futureRef.current, currentEntry];
|
|
105
|
-
setFuture(futureRef.current);
|
|
106
|
-
setStateRaw(prev.state);
|
|
107
|
-
stateRef.current = prev.state;
|
|
108
|
-
onUndo?.(prev.state);
|
|
109
|
-
}, [pushToPast, onUndo]);
|
|
110
|
-
const redo = useCallback(() => {
|
|
111
|
-
if (futureRef.current.length === 0) return;
|
|
112
|
-
const next = futureRef.current[futureRef.current.length - 1];
|
|
113
|
-
const newFuture = futureRef.current.slice(0, -1);
|
|
114
|
-
futureRef.current = newFuture;
|
|
115
|
-
setFuture(newFuture);
|
|
116
|
-
const currentEntry = {
|
|
117
|
-
state: stateRef.current,
|
|
118
|
-
timestamp: Date.now()
|
|
119
|
-
};
|
|
120
|
-
pastRef.current = [...pastRef.current, currentEntry];
|
|
121
|
-
setPast(pastRef.current);
|
|
122
|
-
setStateRaw(next.state);
|
|
123
|
-
stateRef.current = next.state;
|
|
124
|
-
onRedo?.(next.state);
|
|
125
|
-
}, [onRedo]);
|
|
126
|
-
const clearHistory = useCallback(() => {
|
|
127
|
-
if (debounceTimerRef.current !== null) {
|
|
128
|
-
clearTimeout(debounceTimerRef.current);
|
|
129
|
-
debounceTimerRef.current = null;
|
|
130
|
-
}
|
|
131
|
-
pendingStateRef.current = null;
|
|
132
|
-
preDebounceStateRef.current = null;
|
|
133
|
-
pastRef.current = [];
|
|
134
|
-
futureRef.current = [];
|
|
135
|
-
setPast([]);
|
|
136
|
-
setFuture([]);
|
|
137
|
-
}, []);
|
|
138
|
-
const snapshot = useCallback(
|
|
139
|
-
(label) => {
|
|
140
|
-
let didFlush = false;
|
|
141
|
-
if (debounceTimerRef.current !== null) {
|
|
142
|
-
clearTimeout(debounceTimerRef.current);
|
|
143
|
-
debounceTimerRef.current = null;
|
|
144
|
-
}
|
|
145
|
-
if (pendingStateRef.current !== null && preDebounceStateRef.current !== null) {
|
|
146
|
-
const entry = {
|
|
147
|
-
state: preDebounceStateRef.current,
|
|
148
|
-
timestamp: Date.now()
|
|
149
|
-
};
|
|
150
|
-
pushToPast(entry);
|
|
151
|
-
futureRef.current = [];
|
|
152
|
-
setFuture([]);
|
|
153
|
-
pendingStateRef.current = null;
|
|
154
|
-
preDebounceStateRef.current = null;
|
|
155
|
-
didFlush = true;
|
|
156
|
-
}
|
|
157
|
-
if (!didFlush) {
|
|
158
|
-
const entry = {
|
|
159
|
-
state: stateRef.current,
|
|
160
|
-
timestamp: Date.now(),
|
|
161
|
-
label
|
|
162
|
-
};
|
|
163
|
-
pushToPast(entry);
|
|
164
|
-
futureRef.current = [];
|
|
165
|
-
setFuture([]);
|
|
166
|
-
onSnapshot?.(entry);
|
|
167
|
-
} else if (label && pastRef.current.length > 0) {
|
|
168
|
-
pastRef.current[pastRef.current.length - 1].label = label;
|
|
169
|
-
setPast([...pastRef.current]);
|
|
170
|
-
}
|
|
171
|
-
},
|
|
172
|
-
[pushToPast, onSnapshot]
|
|
173
|
-
);
|
|
174
|
-
const clearDraft = useCallback(() => {
|
|
175
|
-
clearHistory();
|
|
176
|
-
setStateRaw(initialState);
|
|
177
|
-
stateRef.current = initialState;
|
|
178
|
-
}, [clearHistory, initialState]);
|
|
179
|
-
return {
|
|
180
|
-
state,
|
|
181
|
-
setState,
|
|
182
|
-
undo,
|
|
183
|
-
redo,
|
|
184
|
-
canUndo: past.length > 0,
|
|
185
|
-
canRedo: future.length > 0,
|
|
186
|
-
clearHistory,
|
|
187
|
-
clearDraft,
|
|
188
|
-
snapshot,
|
|
189
|
-
past,
|
|
190
|
-
future
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export { useFormHistory };
|
|
195
|
-
//# sourceMappingURL=index.mjs.map
|
|
196
|
-
//# 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",
|
|
@@ -60,11 +60,11 @@
|
|
|
60
60
|
],
|
|
61
61
|
"repository": {
|
|
62
62
|
"type": "git",
|
|
63
|
-
"url": "https://github.com/
|
|
63
|
+
"url": "https://github.com/BazilSuhail/npm-react-form-rewind.git"
|
|
64
64
|
},
|
|
65
65
|
"bugs": {
|
|
66
|
-
"url": "https://github.com/
|
|
66
|
+
"url": "https://github.com/BazilSuhail/npm-react-form-rewind/issues"
|
|
67
67
|
},
|
|
68
|
-
"homepage": "https://github.com/
|
|
68
|
+
"homepage": "https://github.com/BazilSuhail/npm-react-form-rewind#readme",
|
|
69
69
|
"license": "MIT"
|
|
70
70
|
}
|
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/useFormHistory.ts"],"names":["useRef","useState","useCallback","useEffect"],"mappings":";;;;;AAGA,IAAM,mBAAA,GAAsB,GAAA;AAC5B,IAAM,mBAAA,GAAsB,GAAA;AAErB,SAAS,cAAA,CACd,YAAA,EACA,OAAA,GAAoC,EAAC,EACZ;AACzB,EAAA,MAAM;AAAA,IACJ,UAAA,GAAa,mBAAA;AAAA,IACb,UAAA,GAAa,mBAAA;AAAA,IACb,MAAA;AAAA,IACA,MAAA;AAAA,IACA;AAAA,GACF,GAAI,OAAA;AAEJ,EAAA,MAAM,OAAA,GAAUA,YAAA,CAA0B,EAAE,CAAA;AAC5C,EAAA,MAAM,SAAA,GAAYA,YAAA,CAA0B,EAAE,CAAA;AAE9C,EAAA,MAAM,CAAC,KAAA,EAAO,WAAW,CAAA,GAAIC,eAAY,YAAY,CAAA;AACrD,EAAA,MAAM,QAAA,GAAWD,aAAU,YAAY,CAAA;AACvC,EAAA,QAAA,CAAS,OAAA,GAAU,KAAA;AAEnB,EAAA,MAAM,CAAC,IAAA,EAAM,OAAO,CAAA,GAAIC,cAAA,CAA4B,EAAE,CAAA;AACtD,EAAA,MAAM,CAAC,MAAA,EAAQ,SAAS,CAAA,GAAIA,cAAA,CAA4B,EAAE,CAAA;AAE1D,EAAA,MAAM,gBAAA,GAAmBD,aAA6C,IAAI,CAAA;AAC1E,EAAA,MAAM,eAAA,GAAkBA,aAAiB,IAAI,CAAA;AAC7C,EAAA,MAAM,mBAAA,GAAsBA,aAAiB,IAAI,CAAA;AAEjD,EAAA,MAAM,UAAA,GAAaE,iBAAA;AAAA,IACjB,CAAC,KAAA,KAA2B;AAC1B,MAAA,MAAM,IAAA,GAAO,CAAC,GAAG,OAAA,CAAQ,SAAS,KAAK,CAAA;AACvC,MAAA,IAAI,IAAA,CAAK,SAAS,UAAA,EAAY;AAC5B,QAAA,IAAA,CAAK,MAAA,CAAO,CAAA,EAAG,IAAA,CAAK,MAAA,GAAS,UAAU,CAAA;AAAA,MACzC;AACA,MAAA,OAAA,CAAQ,OAAA,GAAU,IAAA;AAClB,MAAA,OAAA,CAAQ,IAAI,CAAA;AAAA,IACd,CAAA;AAAA,IACA,CAAC,UAAU;AAAA,GACb;AAEA,EAAAC,eAAA,CAAU,MAAM;AACd,IAAA,OAAO,MAAM;AACX,MAAA,IAAI,gBAAA,CAAiB,YAAY,IAAA,EAAM;AACrC,QAAA,YAAA,CAAa,iBAAiB,OAAO,CAAA;AAAA,MACvC;AAAA,IACF,CAAA;AAAA,EACF,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,QAAA,GAAWD,iBAAA;AAAA,IACf,CAAC,OAA6B,KAAA,KAAmB;AAC/C,MAAA,MAAM,OAAO,QAAA,CAAS,OAAA;AACtB,MAAA,MAAM,OAAO,OAAO,KAAA,KAAU,UAAA,GAAc,KAAA,CAAyB,IAAI,CAAA,GAAI,KAAA;AAE7E,MAAA,IAAI,MAAA,CAAO,EAAA,CAAG,IAAA,EAAM,IAAI,CAAA,EAAG;AAE3B,MAAA,WAAA,CAAY,IAAI,CAAA;AAChB,MAAA,QAAA,CAAS,OAAA,GAAU,IAAA;AAEnB,MAAA,IAAI,cAAc,CAAA,EAAG;AACnB,QAAA,MAAM,KAAA,GAAyB,EAAE,KAAA,EAAO,IAAA,EAAM,WAAW,IAAA,CAAK,GAAA,IAAO,KAAA,EAAM;AAC3E,QAAA,UAAA,CAAW,KAAK,CAAA;AAChB,QAAA,SAAA,CAAU,UAAU,EAAC;AACrB,QAAA,SAAA,CAAU,EAAE,CAAA;AACZ,QAAA;AAAA,MACF;AAEA,MAAA,IAAI,eAAA,CAAgB,YAAY,IAAA,EAAM;AACpC,QAAA,mBAAA,CAAoB,OAAA,GAAU,IAAA;AAAA,MAChC;AAEA,MAAA,eAAA,CAAgB,OAAA,GAAU,IAAA;AAE1B,MAAA,IAAI,gBAAA,CAAiB,YAAY,IAAA,EAAM;AACrC,QAAA,YAAA,CAAa,iBAAiB,OAAO,CAAA;AAAA,MACvC;AAEA,MAAA,gBAAA,CAAiB,OAAA,GAAU,WAAW,MAAM;AAC1C,QAAA,MAAM,KAAA,GAAyB;AAAA,UAC7B,OAAO,mBAAA,CAAoB,OAAA;AAAA,UAC3B,SAAA,EAAW,KAAK,GAAA,EAAI;AAAA,UACpB;AAAA,SACF;AACA,QAAA,UAAA,CAAW,KAAK,CAAA;AAChB,QAAA,SAAA,CAAU,UAAU,EAAC;AACrB,QAAA,SAAA,CAAU,EAAE,CAAA;AACZ,QAAA,gBAAA,CAAiB,OAAA,GAAU,IAAA;AAC3B,QAAA,eAAA,CAAgB,OAAA,GAAU,IAAA;AAC1B,QAAA,mBAAA,CAAoB,OAAA,GAAU,IAAA;AAAA,MAChC,GAAG,UAAU,CAAA;AAAA,IACf,CAAA;AAAA,IACA,CAAC,YAAY,UAAU;AAAA,GACzB;AAEA,EAAA,MAAM,IAAA,GAAOA,kBAAY,MAAM;AAC7B,IAAA,IAAI,gBAAA,CAAiB,YAAY,IAAA,EAAM;AACrC,MAAA,YAAA,CAAa,iBAAiB,OAAO,CAAA;AACrC,MAAA,gBAAA,CAAiB,OAAA,GAAU,IAAA;AAAA,IAC7B;AAEA,IAAA,IAAI,eAAA,CAAgB,OAAA,KAAY,IAAA,IAAQ,mBAAA,CAAoB,YAAY,IAAA,EAAM;AAC5E,MAAA,MAAM,KAAA,GAAyB;AAAA,QAC7B,OAAO,mBAAA,CAAoB,OAAA;AAAA,QAC3B,SAAA,EAAW,KAAK,GAAA;AAAI,OACtB;AACA,MAAA,UAAA,CAAW,KAAK,CAAA;AAChB,MAAA,SAAA,CAAU,UAAU,EAAC;AACrB,MAAA,SAAA,CAAU,EAAE,CAAA;AACZ,MAAA,eAAA,CAAgB,OAAA,GAAU,IAAA;AAC1B,MAAA,mBAAA,CAAoB,OAAA,GAAU,IAAA;AAAA,IAChC;AAEA,IAAA,IAAI,OAAA,CAAQ,OAAA,CAAQ,MAAA,KAAW,CAAA,EAAG;AAElC,IAAA,MAAM,OAAO,OAAA,CAAQ,OAAA,CAAQ,OAAA,CAAQ,OAAA,CAAQ,SAAS,CAAC,CAAA;AACvD,IAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,OAAA,CAAQ,KAAA,CAAM,GAAG,EAAE,CAAA;AAC3C,IAAA,OAAA,CAAQ,OAAA,GAAU,OAAA;AAClB,IAAA,OAAA,CAAQ,OAAO,CAAA;AAEf,IAAA,MAAM,YAAA,GAAgC;AAAA,MACpC,OAAO,QAAA,CAAS,OAAA;AAAA,MAChB,SAAA,EAAW,KAAK,GAAA;AAAI,KACtB;AACA,IAAA,SAAA,CAAU,OAAA,GAAU,CAAC,GAAG,SAAA,CAAU,SAAS,YAAY,CAAA;AACvD,IAAA,SAAA,CAAU,UAAU,OAAO,CAAA;AAE3B,IAAA,WAAA,CAAY,KAAK,KAAK,CAAA;AACtB,IAAA,QAAA,CAAS,UAAU,IAAA,CAAK,KAAA;AACxB,IAAA,MAAA,GAAS,KAAK,KAAK,CAAA;AAAA,EACrB,CAAA,EAAG,CAAC,UAAA,EAAY,MAAM,CAAC,CAAA;AAEvB,EAAA,MAAM,IAAA,GAAOA,kBAAY,MAAM;AAC7B,IAAA,IAAI,SAAA,CAAU,OAAA,CAAQ,MAAA,KAAW,CAAA,EAAG;AAEpC,IAAA,MAAM,OAAO,SAAA,CAAU,OAAA,CAAQ,SAAA,CAAU,OAAA,CAAQ,SAAS,CAAC,CAAA;AAC3D,IAAA,MAAM,SAAA,GAAY,SAAA,CAAU,OAAA,CAAQ,KAAA,CAAM,GAAG,EAAE,CAAA;AAC/C,IAAA,SAAA,CAAU,OAAA,GAAU,SAAA;AACpB,IAAA,SAAA,CAAU,SAAS,CAAA;AAEnB,IAAA,MAAM,YAAA,GAAgC;AAAA,MACpC,OAAO,QAAA,CAAS,OAAA;AAAA,MAChB,SAAA,EAAW,KAAK,GAAA;AAAI,KACtB;AACA,IAAA,OAAA,CAAQ,OAAA,GAAU,CAAC,GAAG,OAAA,CAAQ,SAAS,YAAY,CAAA;AACnD,IAAA,OAAA,CAAQ,QAAQ,OAAO,CAAA;AAEvB,IAAA,WAAA,CAAY,KAAK,KAAK,CAAA;AACtB,IAAA,QAAA,CAAS,UAAU,IAAA,CAAK,KAAA;AACxB,IAAA,MAAA,GAAS,KAAK,KAAK,CAAA;AAAA,EACrB,CAAA,EAAG,CAAC,MAAM,CAAC,CAAA;AAEX,EAAA,MAAM,YAAA,GAAeA,kBAAY,MAAM;AACrC,IAAA,IAAI,gBAAA,CAAiB,YAAY,IAAA,EAAM;AACrC,MAAA,YAAA,CAAa,iBAAiB,OAAO,CAAA;AACrC,MAAA,gBAAA,CAAiB,OAAA,GAAU,IAAA;AAAA,IAC7B;AACA,IAAA,eAAA,CAAgB,OAAA,GAAU,IAAA;AAC1B,IAAA,mBAAA,CAAoB,OAAA,GAAU,IAAA;AAC9B,IAAA,OAAA,CAAQ,UAAU,EAAC;AACnB,IAAA,SAAA,CAAU,UAAU,EAAC;AACrB,IAAA,OAAA,CAAQ,EAAE,CAAA;AACV,IAAA,SAAA,CAAU,EAAE,CAAA;AAAA,EACd,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,QAAA,GAAWA,iBAAA;AAAA,IACf,CAAC,KAAA,KAAmB;AAClB,MAAA,IAAI,QAAA,GAAW,KAAA;AAEf,MAAA,IAAI,gBAAA,CAAiB,YAAY,IAAA,EAAM;AACrC,QAAA,YAAA,CAAa,iBAAiB,OAAO,CAAA;AACrC,QAAA,gBAAA,CAAiB,OAAA,GAAU,IAAA;AAAA,MAC7B;AAEA,MAAA,IAAI,eAAA,CAAgB,OAAA,KAAY,IAAA,IAAQ,mBAAA,CAAoB,YAAY,IAAA,EAAM;AAC5E,QAAA,MAAM,KAAA,GAAyB;AAAA,UAC7B,OAAO,mBAAA,CAAoB,OAAA;AAAA,UAC3B,SAAA,EAAW,KAAK,GAAA;AAAI,SACtB;AACA,QAAA,UAAA,CAAW,KAAK,CAAA;AAChB,QAAA,SAAA,CAAU,UAAU,EAAC;AACrB,QAAA,SAAA,CAAU,EAAE,CAAA;AACZ,QAAA,eAAA,CAAgB,OAAA,GAAU,IAAA;AAC1B,QAAA,mBAAA,CAAoB,OAAA,GAAU,IAAA;AAC9B,QAAA,QAAA,GAAW,IAAA;AAAA,MACb;AAEA,MAAA,IAAI,CAAC,QAAA,EAAU;AACb,QAAA,MAAM,KAAA,GAAyB;AAAA,UAC7B,OAAO,QAAA,CAAS,OAAA;AAAA,UAChB,SAAA,EAAW,KAAK,GAAA,EAAI;AAAA,UACpB;AAAA,SACF;AACA,QAAA,UAAA,CAAW,KAAK,CAAA;AAChB,QAAA,SAAA,CAAU,UAAU,EAAC;AACrB,QAAA,SAAA,CAAU,EAAE,CAAA;AACZ,QAAA,UAAA,GAAa,KAAK,CAAA;AAAA,MACpB,CAAA,MAAA,IAAW,KAAA,IAAS,OAAA,CAAQ,OAAA,CAAQ,SAAS,CAAA,EAAG;AAC9C,QAAA,OAAA,CAAQ,QAAQ,OAAA,CAAQ,OAAA,CAAQ,MAAA,GAAS,CAAC,EAAE,KAAA,GAAQ,KAAA;AACpD,QAAA,OAAA,CAAQ,CAAC,GAAG,OAAA,CAAQ,OAAO,CAAC,CAAA;AAAA,MAC9B;AAAA,IACF,CAAA;AAAA,IACA,CAAC,YAAY,UAAU;AAAA,GACzB;AAEA,EAAA,MAAM,UAAA,GAAaA,kBAAY,MAAM;AACnC,IAAA,YAAA,EAAa;AACb,IAAA,WAAA,CAAY,YAAY,CAAA;AACxB,IAAA,QAAA,CAAS,OAAA,GAAU,YAAA;AAAA,EACrB,CAAA,EAAG,CAAC,YAAA,EAAc,YAAY,CAAC,CAAA;AAE/B,EAAA,OAAO;AAAA,IACL,KAAA;AAAA,IACA,QAAA;AAAA,IACA,IAAA;AAAA,IACA,IAAA;AAAA,IACA,OAAA,EAAS,KAAK,MAAA,GAAS,CAAA;AAAA,IACvB,OAAA,EAAS,OAAO,MAAA,GAAS,CAAA;AAAA,IACzB,YAAA;AAAA,IACA,UAAA;AAAA,IACA,QAAA;AAAA,IACA,IAAA;AAAA,IACA;AAAA,GACF;AACF","file":"index.js","sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { HistoryEntry, UseFormHistoryOptions, UseFormHistoryReturn } from \"./types\";\n\nconst DEFAULT_MAX_HISTORY = 100;\nconst DEFAULT_DEBOUNCE_MS = 300;\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 onUndo,\n onRedo,\n onSnapshot,\n } = options;\n\n const pastRef = useRef<HistoryEntry<T>[]>([]);\n const futureRef = useRef<HistoryEntry<T>[]>([]);\n\n const [state, setStateRaw] = useState<T>(initialState);\n const stateRef = useRef<T>(initialState);\n stateRef.current = state;\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 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 useEffect(() => {\n return () => {\n if (debounceTimerRef.current !== null) {\n clearTimeout(debounceTimerRef.current);\n }\n };\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\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],\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 onUndo?.(prev.state);\n }, [pushToPast, onUndo]);\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 onRedo?.(next.state);\n }, [onRedo]);\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 setStateRaw(initialState);\n stateRef.current = initialState;\n }, [clearHistory, initialState]);\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, UseFormHistoryOptions, UseFormHistoryReturn } from \"./types\";\n\nconst DEFAULT_MAX_HISTORY = 100;\nconst DEFAULT_DEBOUNCE_MS = 300;\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 onUndo,\n onRedo,\n onSnapshot,\n } = options;\n\n const pastRef = useRef<HistoryEntry<T>[]>([]);\n const futureRef = useRef<HistoryEntry<T>[]>([]);\n\n const [state, setStateRaw] = useState<T>(initialState);\n const stateRef = useRef<T>(initialState);\n stateRef.current = state;\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 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 useEffect(() => {\n return () => {\n if (debounceTimerRef.current !== null) {\n clearTimeout(debounceTimerRef.current);\n }\n };\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\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],\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 onUndo?.(prev.state);\n }, [pushToPast, onUndo]);\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 onRedo?.(next.state);\n }, [onRedo]);\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 setStateRaw(initialState);\n stateRef.current = initialState;\n }, [clearHistory, initialState]);\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"]}
|