react-form-rewind 0.4.0 → 1.0.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 +94 -6
- package/dist/index.d.ts +94 -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
|
@@ -1,17 +1,47 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface FieldRules {
|
|
5
|
+
required?: boolean | string;
|
|
6
|
+
pattern?: RegExp | {
|
|
7
|
+
value: RegExp;
|
|
8
|
+
message: string;
|
|
9
|
+
};
|
|
10
|
+
minLength?: number | {
|
|
11
|
+
value: number;
|
|
12
|
+
message: string;
|
|
13
|
+
};
|
|
14
|
+
maxLength?: number | {
|
|
15
|
+
value: number;
|
|
16
|
+
message: string;
|
|
17
|
+
};
|
|
18
|
+
min?: number | {
|
|
19
|
+
value: number;
|
|
20
|
+
message: string;
|
|
21
|
+
};
|
|
22
|
+
max?: number | {
|
|
23
|
+
value: number;
|
|
24
|
+
message: string;
|
|
25
|
+
};
|
|
26
|
+
validate?: (value: unknown) => string | null;
|
|
27
|
+
}
|
|
28
|
+
interface FieldError {
|
|
29
|
+
message: string;
|
|
30
|
+
type: string;
|
|
31
|
+
}
|
|
32
|
+
declare function validateField(value: unknown, rules: FieldRules): FieldError | null;
|
|
33
|
+
declare function validateAll(state: Record<string, unknown>, fieldRules: Record<string, FieldRules>): Record<string, FieldError>;
|
|
34
|
+
|
|
1
35
|
interface HistoryEntry<T> {
|
|
2
36
|
state: T;
|
|
3
37
|
timestamp: number;
|
|
4
38
|
label?: string;
|
|
5
39
|
}
|
|
6
|
-
interface HistoryStack<T> {
|
|
7
|
-
past: HistoryEntry<T>[];
|
|
8
|
-
present: T;
|
|
9
|
-
future: HistoryEntry<T>[];
|
|
10
|
-
}
|
|
11
40
|
interface UseFormHistoryOptions<T> {
|
|
12
41
|
maxHistory?: number;
|
|
13
42
|
debounceMs?: number;
|
|
14
43
|
persist?: boolean | PersistOptions;
|
|
44
|
+
keyboard?: boolean;
|
|
15
45
|
onUndo?: (state: T) => void;
|
|
16
46
|
onRedo?: (state: T) => void;
|
|
17
47
|
onSnapshot?: (entry: HistoryEntry<T>) => void;
|
|
@@ -34,7 +64,65 @@ interface UseFormHistoryReturn<T> {
|
|
|
34
64
|
past: HistoryEntry<T>[];
|
|
35
65
|
future: HistoryEntry<T>[];
|
|
36
66
|
}
|
|
67
|
+
interface FieldMeta {
|
|
68
|
+
name: string;
|
|
69
|
+
rules?: FieldRules;
|
|
70
|
+
touched: boolean;
|
|
71
|
+
}
|
|
72
|
+
interface FormRewindContextValue {
|
|
73
|
+
state: Record<string, unknown>;
|
|
74
|
+
setState: (name: string, value: unknown) => void;
|
|
75
|
+
errors: Record<string, FieldError>;
|
|
76
|
+
setError: (name: string, error: FieldError) => void;
|
|
77
|
+
clearError: (name: string) => void;
|
|
78
|
+
touched: Record<string, boolean>;
|
|
79
|
+
touch: (name: string) => void;
|
|
80
|
+
fields: Record<string, FieldMeta>;
|
|
81
|
+
registerField: (name: string, rules?: FieldRules) => void;
|
|
82
|
+
unregisterField: (name: string) => void;
|
|
83
|
+
}
|
|
37
84
|
|
|
38
85
|
declare function useFormHistory<T>(initialState: T, options?: UseFormHistoryOptions<T>): UseFormHistoryReturn<T>;
|
|
39
86
|
|
|
40
|
-
|
|
87
|
+
declare function useFormRewindContext(): FormRewindContextValue;
|
|
88
|
+
interface FormRewindProps<T extends Record<string, unknown>> {
|
|
89
|
+
initialState: T;
|
|
90
|
+
children: ReactNode;
|
|
91
|
+
onSubmit?: (state: T) => void;
|
|
92
|
+
maxHistory?: UseFormHistoryOptions<T>["maxHistory"];
|
|
93
|
+
debounceMs?: UseFormHistoryOptions<T>["debounceMs"];
|
|
94
|
+
persist?: UseFormHistoryOptions<T>["persist"];
|
|
95
|
+
keyboard?: UseFormHistoryOptions<T>["keyboard"];
|
|
96
|
+
}
|
|
97
|
+
declare function FormRewind<T extends Record<string, unknown>>({ initialState, children, onSubmit, maxHistory, debounceMs, persist, keyboard, }: FormRewindProps<T>): react.JSX.Element;
|
|
98
|
+
|
|
99
|
+
interface BaseFieldProps {
|
|
100
|
+
name: string;
|
|
101
|
+
label?: string;
|
|
102
|
+
rules?: FieldRules;
|
|
103
|
+
className?: string;
|
|
104
|
+
style?: React.CSSProperties;
|
|
105
|
+
}
|
|
106
|
+
interface TextFieldProps extends BaseFieldProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, "name" | "value" | "onChange"> {
|
|
107
|
+
}
|
|
108
|
+
declare function TextField({ name, label, rules, className, style, ...inputProps }: TextFieldProps): react.JSX.Element;
|
|
109
|
+
interface NumberFieldProps extends BaseFieldProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, "name" | "value" | "onChange" | "type"> {
|
|
110
|
+
}
|
|
111
|
+
declare function NumberField({ name, label, rules, className, style, ...inputProps }: NumberFieldProps): react.JSX.Element;
|
|
112
|
+
interface CheckboxFieldProps extends BaseFieldProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, "name" | "checked" | "onChange"> {
|
|
113
|
+
}
|
|
114
|
+
declare function CheckboxField({ name, label, rules, className, style, ...inputProps }: CheckboxFieldProps): react.JSX.Element;
|
|
115
|
+
interface SelectOption {
|
|
116
|
+
value: string;
|
|
117
|
+
label: string;
|
|
118
|
+
}
|
|
119
|
+
interface SelectFieldProps extends BaseFieldProps {
|
|
120
|
+
options: SelectOption[];
|
|
121
|
+
placeholder?: string;
|
|
122
|
+
}
|
|
123
|
+
declare function SelectField({ name, label, rules, options, placeholder, className, style }: SelectFieldProps): react.JSX.Element;
|
|
124
|
+
interface TextareaFieldProps extends BaseFieldProps, Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "name" | "value" | "onChange"> {
|
|
125
|
+
}
|
|
126
|
+
declare function TextareaField({ name, label, rules, className, style, ...textareaProps }: TextareaFieldProps): react.JSX.Element;
|
|
127
|
+
|
|
128
|
+
export { CheckboxField, type FieldError, type FieldMeta, type FieldRules, FormRewind, type FormRewindContextValue, type FormRewindProps, type HistoryEntry, NumberField, type PersistOptions, SelectField, TextField, TextareaField, type UseFormHistoryOptions, type UseFormHistoryReturn, useFormHistory, useFormRewindContext, validateAll, validateField };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,17 +1,47 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface FieldRules {
|
|
5
|
+
required?: boolean | string;
|
|
6
|
+
pattern?: RegExp | {
|
|
7
|
+
value: RegExp;
|
|
8
|
+
message: string;
|
|
9
|
+
};
|
|
10
|
+
minLength?: number | {
|
|
11
|
+
value: number;
|
|
12
|
+
message: string;
|
|
13
|
+
};
|
|
14
|
+
maxLength?: number | {
|
|
15
|
+
value: number;
|
|
16
|
+
message: string;
|
|
17
|
+
};
|
|
18
|
+
min?: number | {
|
|
19
|
+
value: number;
|
|
20
|
+
message: string;
|
|
21
|
+
};
|
|
22
|
+
max?: number | {
|
|
23
|
+
value: number;
|
|
24
|
+
message: string;
|
|
25
|
+
};
|
|
26
|
+
validate?: (value: unknown) => string | null;
|
|
27
|
+
}
|
|
28
|
+
interface FieldError {
|
|
29
|
+
message: string;
|
|
30
|
+
type: string;
|
|
31
|
+
}
|
|
32
|
+
declare function validateField(value: unknown, rules: FieldRules): FieldError | null;
|
|
33
|
+
declare function validateAll(state: Record<string, unknown>, fieldRules: Record<string, FieldRules>): Record<string, FieldError>;
|
|
34
|
+
|
|
1
35
|
interface HistoryEntry<T> {
|
|
2
36
|
state: T;
|
|
3
37
|
timestamp: number;
|
|
4
38
|
label?: string;
|
|
5
39
|
}
|
|
6
|
-
interface HistoryStack<T> {
|
|
7
|
-
past: HistoryEntry<T>[];
|
|
8
|
-
present: T;
|
|
9
|
-
future: HistoryEntry<T>[];
|
|
10
|
-
}
|
|
11
40
|
interface UseFormHistoryOptions<T> {
|
|
12
41
|
maxHistory?: number;
|
|
13
42
|
debounceMs?: number;
|
|
14
43
|
persist?: boolean | PersistOptions;
|
|
44
|
+
keyboard?: boolean;
|
|
15
45
|
onUndo?: (state: T) => void;
|
|
16
46
|
onRedo?: (state: T) => void;
|
|
17
47
|
onSnapshot?: (entry: HistoryEntry<T>) => void;
|
|
@@ -34,7 +64,65 @@ interface UseFormHistoryReturn<T> {
|
|
|
34
64
|
past: HistoryEntry<T>[];
|
|
35
65
|
future: HistoryEntry<T>[];
|
|
36
66
|
}
|
|
67
|
+
interface FieldMeta {
|
|
68
|
+
name: string;
|
|
69
|
+
rules?: FieldRules;
|
|
70
|
+
touched: boolean;
|
|
71
|
+
}
|
|
72
|
+
interface FormRewindContextValue {
|
|
73
|
+
state: Record<string, unknown>;
|
|
74
|
+
setState: (name: string, value: unknown) => void;
|
|
75
|
+
errors: Record<string, FieldError>;
|
|
76
|
+
setError: (name: string, error: FieldError) => void;
|
|
77
|
+
clearError: (name: string) => void;
|
|
78
|
+
touched: Record<string, boolean>;
|
|
79
|
+
touch: (name: string) => void;
|
|
80
|
+
fields: Record<string, FieldMeta>;
|
|
81
|
+
registerField: (name: string, rules?: FieldRules) => void;
|
|
82
|
+
unregisterField: (name: string) => void;
|
|
83
|
+
}
|
|
37
84
|
|
|
38
85
|
declare function useFormHistory<T>(initialState: T, options?: UseFormHistoryOptions<T>): UseFormHistoryReturn<T>;
|
|
39
86
|
|
|
40
|
-
|
|
87
|
+
declare function useFormRewindContext(): FormRewindContextValue;
|
|
88
|
+
interface FormRewindProps<T extends Record<string, unknown>> {
|
|
89
|
+
initialState: T;
|
|
90
|
+
children: ReactNode;
|
|
91
|
+
onSubmit?: (state: T) => void;
|
|
92
|
+
maxHistory?: UseFormHistoryOptions<T>["maxHistory"];
|
|
93
|
+
debounceMs?: UseFormHistoryOptions<T>["debounceMs"];
|
|
94
|
+
persist?: UseFormHistoryOptions<T>["persist"];
|
|
95
|
+
keyboard?: UseFormHistoryOptions<T>["keyboard"];
|
|
96
|
+
}
|
|
97
|
+
declare function FormRewind<T extends Record<string, unknown>>({ initialState, children, onSubmit, maxHistory, debounceMs, persist, keyboard, }: FormRewindProps<T>): react.JSX.Element;
|
|
98
|
+
|
|
99
|
+
interface BaseFieldProps {
|
|
100
|
+
name: string;
|
|
101
|
+
label?: string;
|
|
102
|
+
rules?: FieldRules;
|
|
103
|
+
className?: string;
|
|
104
|
+
style?: React.CSSProperties;
|
|
105
|
+
}
|
|
106
|
+
interface TextFieldProps extends BaseFieldProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, "name" | "value" | "onChange"> {
|
|
107
|
+
}
|
|
108
|
+
declare function TextField({ name, label, rules, className, style, ...inputProps }: TextFieldProps): react.JSX.Element;
|
|
109
|
+
interface NumberFieldProps extends BaseFieldProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, "name" | "value" | "onChange" | "type"> {
|
|
110
|
+
}
|
|
111
|
+
declare function NumberField({ name, label, rules, className, style, ...inputProps }: NumberFieldProps): react.JSX.Element;
|
|
112
|
+
interface CheckboxFieldProps extends BaseFieldProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, "name" | "checked" | "onChange"> {
|
|
113
|
+
}
|
|
114
|
+
declare function CheckboxField({ name, label, rules, className, style, ...inputProps }: CheckboxFieldProps): react.JSX.Element;
|
|
115
|
+
interface SelectOption {
|
|
116
|
+
value: string;
|
|
117
|
+
label: string;
|
|
118
|
+
}
|
|
119
|
+
interface SelectFieldProps extends BaseFieldProps {
|
|
120
|
+
options: SelectOption[];
|
|
121
|
+
placeholder?: string;
|
|
122
|
+
}
|
|
123
|
+
declare function SelectField({ name, label, rules, options, placeholder, className, style }: SelectFieldProps): react.JSX.Element;
|
|
124
|
+
interface TextareaFieldProps extends BaseFieldProps, Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "name" | "value" | "onChange"> {
|
|
125
|
+
}
|
|
126
|
+
declare function TextareaField({ name, label, rules, className, style, ...textareaProps }: TextareaFieldProps): react.JSX.Element;
|
|
127
|
+
|
|
128
|
+
export { CheckboxField, type FieldError, type FieldMeta, type FieldRules, FormRewind, type FormRewindContextValue, type FormRewindProps, type HistoryEntry, NumberField, type PersistOptions, SelectField, TextField, TextareaField, type UseFormHistoryOptions, type UseFormHistoryReturn, useFormHistory, useFormRewindContext, validateAll, validateField };
|
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'),jsxRuntime=require('react/jsx-runtime');var ue=100,ce=300,ae=500,de=1;function pe(e){return !e||typeof e=="boolean"?null:{key:e.key,debounceMs:e.debounceMs??ae,version:e.version??de}}function K(){return typeof window<"u"&&!!window.localStorage}function me(e,t){try{if(!K())return null;let s=window.localStorage.getItem(e);if(!s)return null;let u=JSON.parse(s);return u.__v!==t?(window.localStorage.removeItem(e),null):u.__s}catch{return null}}function Y(e,t,s){try{if(!K())return;window.localStorage.setItem(e,JSON.stringify({__s:t,__v:s}));}catch{}}function ge(e){try{if(!K())return;window.localStorage.removeItem(e);}catch{}}function z(e,t={}){let{maxHistory:s=ue,debounceMs:u=ce,persist:i,keyboard:a=false,onUndo:r,onRedo:c,onSnapshot:T}=t,n=pe(i),[m,x]=react.useState(()=>{if(n){let l=me(n.key,n.version);if(l!==null)return l}return e}),d=react.useRef(m);d.current=m;let y=react.useRef([]),b=react.useRef([]),[U,S]=react.useState([]),[I,L]=react.useState([]),F=react.useRef(null),E=react.useRef(null),o=react.useRef(null),p=react.useRef(null),f=react.useCallback(()=>{b.current=[],L([]);},[]),R=react.useCallback(l=>{let g=[...y.current,l];g.length>s&&g.splice(0,g.length-s),y.current=g,S(g);},[s]);react.useEffect(()=>()=>{F.current!==null&&clearTimeout(F.current),p.current!==null&&clearTimeout(p.current);},[]);let M=react.useCallback(l=>{n&&(p.current!==null&&clearTimeout(p.current),p.current=setTimeout(()=>{Y(n.key,l,n.version),p.current=null;},n.debounceMs));},[n]),_=react.useCallback(l=>{n&&(p.current!==null&&(clearTimeout(p.current),p.current=null),Y(n.key,l,n.version));},[n]),se=react.useCallback((l,g)=>{let v=d.current,C=typeof l=="function"?l(v):l;if(!Object.is(v,C)){if(x(C),d.current=C,M(C),u<=0){let q={state:v,timestamp:Date.now(),label:g};R(q),f();return}E.current===null&&(o.current=v),E.current=C,F.current!==null&&clearTimeout(F.current),F.current=setTimeout(()=>{let q={state:o.current,timestamp:Date.now(),label:g};R(q),f(),F.current=null,E.current=null,o.current=null;},u);}},[u,R,M,f]),A=react.useCallback(()=>{if(F.current!==null&&(clearTimeout(F.current),F.current=null),E.current!==null&&o.current!==null){let C={state:o.current,timestamp:Date.now()};R(C),f(),E.current=null,o.current=null;}if(y.current.length===0)return;let l=y.current[y.current.length-1],g=y.current.slice(0,-1);y.current=g,S(g);let v={state:d.current,timestamp:Date.now()};b.current=[...b.current,v],L(b.current),x(l.state),d.current=l.state,_(l.state),r?.(l.state);},[R,r,_,f]),$=react.useCallback(()=>{if(b.current.length===0)return;let l=b.current[b.current.length-1],g=b.current.slice(0,-1);b.current=g,L(g);let v={state:d.current,timestamp:Date.now()};y.current=[...y.current,v],S(y.current),x(l.state),d.current=l.state,_(l.state),c?.(l.state);},[c,_]);react.useEffect(()=>{if(!a)return;let l=g=>{!(g.metaKey||g.ctrlKey)||g.key!=="z"||(g.preventDefault(),g.shiftKey?$():A());};return window.addEventListener("keydown",l),()=>window.removeEventListener("keydown",l)},[a,A,$]);let V=react.useCallback(()=>{F.current!==null&&(clearTimeout(F.current),F.current=null),E.current=null,o.current=null,y.current=[],b.current=[],S([]),L([]);},[]),ie=react.useCallback(l=>{let g=false;if(F.current!==null&&(clearTimeout(F.current),F.current=null),E.current!==null&&o.current!==null){let v={state:o.current,timestamp:Date.now()};R(v),f(),E.current=null,o.current=null,g=true;}if(g)l&&y.current.length>0&&(y.current[y.current.length-1].label=l,S([...y.current]));else {let v={state:d.current,timestamp:Date.now(),label:l};R(v),f(),T?.(v);}},[R,T,f]),le=react.useCallback(()=>{V(),n&&ge(n.key),x(e),d.current=e;},[V,e,n]);return {state:m,setState:se,undo:A,redo:$,canUndo:U.length>0,canRedo:I.length>0,clearHistory:V,clearDraft:le,snapshot:ie,past:U,future:I}}function N(e,t){return e&&typeof e=="object"&&"value"in e?e:{value:e,message:t}}function B(e,t){let u=typeof e=="number"?e:Number(e);if(t.required&&(typeof e=="string"?e.trim()==="":e==null||e===false))return {message:typeof t.required=="string"?t.required:"Required",type:"required"};if(t.pattern!=null){let{value:i,message:a}=N(t.pattern,"Invalid format");if(typeof e=="string"&&!i.test(e))return {message:a,type:"pattern"}}if(t.minLength!=null&&typeof e=="string"){let{value:i,message:a}=N(t.minLength,`Min ${t.minLength} characters`);if(e.length<i)return {message:a,type:"minLength"}}if(t.maxLength!=null&&typeof e=="string"){let{value:i,message:a}=N(t.maxLength,`Max ${t.maxLength} characters`);if(e.length>i)return {message:a,type:"maxLength"}}if(t.min!=null&&!isNaN(u)){let{value:i,message:a}=N(t.min,`Min ${t.min}`);if(u<i)return {message:a,type:"min"}}if(t.max!=null&&!isNaN(u)){let{value:i,message:a}=N(t.max,`Max ${t.max}`);if(u>i)return {message:a,type:"max"}}if(t.validate){let i=t.validate(e);if(i)return {message:i,type:"validate"}}return null}function J(e,t){let s={};for(let u in t){let i=B(e[u],t[u]);i&&(s[u]=i);}return s}var W=react.createContext(null);function H(){let e=react.useContext(W);if(!e)throw new Error("useFormRewindContext must be used within <FormRewind>");return e}function Z({initialState:e,children:t,onSubmit:s,maxHistory:u,debounceMs:i,persist:a,keyboard:r}){let c=z(e,{maxHistory:u,debounceMs:i,persist:a,keyboard:r}),[T,n]=react.useState({}),[m,x]=react.useState({}),d=react.useRef({}),y=react.useCallback((o,p)=>{d.current[o]=p??{};},[]),b=react.useCallback(o=>{delete d.current[o];},[]),U=react.useCallback((o,p)=>{n(f=>({...f,[o]:p}));},[]),S=react.useCallback(o=>{n(p=>{let f={...p};return delete f[o],f});},[]),I=react.useCallback(o=>{x(p=>({...p,[o]:true}));},[]),L=react.useCallback((o,p)=>{if(c.setState(f=>({...f,[o]:p}),o),m[o]&&d.current[o]){let f=B(p,d.current[o]);n(f?R=>({...R,[o]:f}):R=>{let M={...R};return delete M[o],M});}},[c.setState,m]),F=react.useCallback(o=>{o&&o.preventDefault();let p={};for(let R in d.current)p[R]=true;x(p);let f=J(c.state,d.current);return n(f),Object.keys(f).length===0&&s&&s(c.state),Object.keys(f).length===0},[c.state,s]),E={state:c.state,setState:L,errors:T,setError:U,clearError:S,touched:m,touch:I,fields:Object.fromEntries(Object.entries(d.current).map(([o,p])=>[o,{name:o,rules:p,touched:!!m[o]}])),registerField:y,unregisterField:b};return jsxRuntime.jsx(W.Provider,{value:E,children:jsxRuntime.jsx("form",{onSubmit:F,noValidate:true,children:t})})}function ee({name:e,label:t,rules:s,className:u,style:i,...a}){let r=H();react.useEffect(()=>(r.registerField(e,s),()=>r.unregisterField(e)),[e,s,r.registerField,r.unregisterField]);let c=x=>{r.setState(e,x.target.value);},T=()=>{r.touch(e);},n=r.errors[e],m=String(r.state[e]??"");return jsxRuntime.jsxs("div",{className:u,style:i,children:[t&&jsxRuntime.jsx("label",{htmlFor:e,children:t}),jsxRuntime.jsx("input",{id:e,type:"text",value:m,onChange:c,onBlur:T,"aria-invalid":!!n,"aria-describedby":n?`${e}-error`:void 0,...a}),n&&jsxRuntime.jsx("span",{id:`${e}-error`,role:"alert",children:n.message})]})}function te({name:e,label:t,rules:s,className:u,style:i,...a}){let r=H();react.useEffect(()=>(r.registerField(e,s),()=>r.unregisterField(e)),[e,s,r.registerField,r.unregisterField]);let c=x=>{let d=x.target.value===""?"":Number(x.target.value);r.setState(e,d);},T=()=>{r.touch(e);},n=r.errors[e],m=r.state[e];return jsxRuntime.jsxs("div",{className:u,style:i,children:[t&&jsxRuntime.jsx("label",{htmlFor:e,children:t}),jsxRuntime.jsx("input",{id:e,type:"number",value:m===""||m==null?"":Number(m),onChange:c,onBlur:T,"aria-invalid":!!n,"aria-describedby":n?`${e}-error`:void 0,...a}),n&&jsxRuntime.jsx("span",{id:`${e}-error`,role:"alert",children:n.message})]})}function re({name:e,label:t,rules:s,className:u,style:i,...a}){let r=H();react.useEffect(()=>(r.registerField(e,s),()=>r.unregisterField(e)),[e,s,r.registerField,r.unregisterField]);let c=m=>{r.setState(e,m.target.checked);},T=r.errors[e],n=!!r.state[e];return jsxRuntime.jsxs("div",{className:u,style:i,children:[jsxRuntime.jsxs("label",{children:[jsxRuntime.jsx("input",{type:"checkbox",checked:n,onChange:c,"aria-invalid":!!T,...a}),t]}),T&&jsxRuntime.jsx("span",{role:"alert",children:T.message})]})}function ne({name:e,label:t,rules:s,options:u,placeholder:i,className:a,style:r}){let c=H();react.useEffect(()=>(c.registerField(e,s),()=>c.unregisterField(e)),[e,s,c.registerField,c.unregisterField]);let T=d=>{c.setState(e,d.target.value);},n=()=>{c.touch(e);},m=c.errors[e],x=String(c.state[e]??"");return jsxRuntime.jsxs("div",{className:a,style:r,children:[t&&jsxRuntime.jsx("label",{htmlFor:e,children:t}),jsxRuntime.jsxs("select",{id:e,value:x,onChange:T,onBlur:n,"aria-invalid":!!m,"aria-describedby":m?`${e}-error`:void 0,children:[i&&jsxRuntime.jsx("option",{value:"",disabled:true,children:i}),u.map(d=>jsxRuntime.jsx("option",{value:d.value,children:d.label},d.value))]}),m&&jsxRuntime.jsx("span",{id:`${e}-error`,role:"alert",children:m.message})]})}function oe({name:e,label:t,rules:s,className:u,style:i,...a}){let r=H();react.useEffect(()=>(r.registerField(e,s),()=>r.unregisterField(e)),[e,s,r.registerField,r.unregisterField]);let c=x=>{r.setState(e,x.target.value);},T=()=>{r.touch(e);},n=r.errors[e],m=String(r.state[e]??"");return jsxRuntime.jsxs("div",{className:u,style:i,children:[t&&jsxRuntime.jsx("label",{htmlFor:e,children:t}),jsxRuntime.jsx("textarea",{id:e,value:m,onChange:c,onBlur:T,"aria-invalid":!!n,"aria-describedby":n?`${e}-error`:void 0,...a}),n&&jsxRuntime.jsx("span",{id:`${e}-error`,role:"alert",children:n.message})]})}exports.CheckboxField=re;exports.FormRewind=Z;exports.NumberField=te;exports.SelectField=ne;exports.TextField=ee;exports.TextareaField=oe;exports.useFormHistory=z;exports.useFormRewindContext=H;exports.validateAll=J;exports.validateField=B;
|
package/dist/index.mjs
CHANGED
|
@@ -1,282 +1 @@
|
|
|
1
|
-
import { useRef, useState, useCallback, useEffect }
|
|
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 {createContext,useState,useRef,useCallback,useEffect,useContext}from'react';import {jsx,jsxs}from'react/jsx-runtime';var ue=100,ce=300,ae=500,de=1;function pe(e){return !e||typeof e=="boolean"?null:{key:e.key,debounceMs:e.debounceMs??ae,version:e.version??de}}function K(){return typeof window<"u"&&!!window.localStorage}function me(e,t){try{if(!K())return null;let s=window.localStorage.getItem(e);if(!s)return null;let u=JSON.parse(s);return u.__v!==t?(window.localStorage.removeItem(e),null):u.__s}catch{return null}}function Y(e,t,s){try{if(!K())return;window.localStorage.setItem(e,JSON.stringify({__s:t,__v:s}));}catch{}}function ge(e){try{if(!K())return;window.localStorage.removeItem(e);}catch{}}function z(e,t={}){let{maxHistory:s=ue,debounceMs:u=ce,persist:i,keyboard:a=false,onUndo:r,onRedo:c,onSnapshot:T}=t,n=pe(i),[m,x]=useState(()=>{if(n){let l=me(n.key,n.version);if(l!==null)return l}return e}),d=useRef(m);d.current=m;let y=useRef([]),b=useRef([]),[U,S]=useState([]),[I,L]=useState([]),F=useRef(null),E=useRef(null),o=useRef(null),p=useRef(null),f=useCallback(()=>{b.current=[],L([]);},[]),R=useCallback(l=>{let g=[...y.current,l];g.length>s&&g.splice(0,g.length-s),y.current=g,S(g);},[s]);useEffect(()=>()=>{F.current!==null&&clearTimeout(F.current),p.current!==null&&clearTimeout(p.current);},[]);let M=useCallback(l=>{n&&(p.current!==null&&clearTimeout(p.current),p.current=setTimeout(()=>{Y(n.key,l,n.version),p.current=null;},n.debounceMs));},[n]),_=useCallback(l=>{n&&(p.current!==null&&(clearTimeout(p.current),p.current=null),Y(n.key,l,n.version));},[n]),se=useCallback((l,g)=>{let v=d.current,C=typeof l=="function"?l(v):l;if(!Object.is(v,C)){if(x(C),d.current=C,M(C),u<=0){let q={state:v,timestamp:Date.now(),label:g};R(q),f();return}E.current===null&&(o.current=v),E.current=C,F.current!==null&&clearTimeout(F.current),F.current=setTimeout(()=>{let q={state:o.current,timestamp:Date.now(),label:g};R(q),f(),F.current=null,E.current=null,o.current=null;},u);}},[u,R,M,f]),A=useCallback(()=>{if(F.current!==null&&(clearTimeout(F.current),F.current=null),E.current!==null&&o.current!==null){let C={state:o.current,timestamp:Date.now()};R(C),f(),E.current=null,o.current=null;}if(y.current.length===0)return;let l=y.current[y.current.length-1],g=y.current.slice(0,-1);y.current=g,S(g);let v={state:d.current,timestamp:Date.now()};b.current=[...b.current,v],L(b.current),x(l.state),d.current=l.state,_(l.state),r?.(l.state);},[R,r,_,f]),$=useCallback(()=>{if(b.current.length===0)return;let l=b.current[b.current.length-1],g=b.current.slice(0,-1);b.current=g,L(g);let v={state:d.current,timestamp:Date.now()};y.current=[...y.current,v],S(y.current),x(l.state),d.current=l.state,_(l.state),c?.(l.state);},[c,_]);useEffect(()=>{if(!a)return;let l=g=>{!(g.metaKey||g.ctrlKey)||g.key!=="z"||(g.preventDefault(),g.shiftKey?$():A());};return window.addEventListener("keydown",l),()=>window.removeEventListener("keydown",l)},[a,A,$]);let V=useCallback(()=>{F.current!==null&&(clearTimeout(F.current),F.current=null),E.current=null,o.current=null,y.current=[],b.current=[],S([]),L([]);},[]),ie=useCallback(l=>{let g=false;if(F.current!==null&&(clearTimeout(F.current),F.current=null),E.current!==null&&o.current!==null){let v={state:o.current,timestamp:Date.now()};R(v),f(),E.current=null,o.current=null,g=true;}if(g)l&&y.current.length>0&&(y.current[y.current.length-1].label=l,S([...y.current]));else {let v={state:d.current,timestamp:Date.now(),label:l};R(v),f(),T?.(v);}},[R,T,f]),le=useCallback(()=>{V(),n&&ge(n.key),x(e),d.current=e;},[V,e,n]);return {state:m,setState:se,undo:A,redo:$,canUndo:U.length>0,canRedo:I.length>0,clearHistory:V,clearDraft:le,snapshot:ie,past:U,future:I}}function N(e,t){return e&&typeof e=="object"&&"value"in e?e:{value:e,message:t}}function B(e,t){let u=typeof e=="number"?e:Number(e);if(t.required&&(typeof e=="string"?e.trim()==="":e==null||e===false))return {message:typeof t.required=="string"?t.required:"Required",type:"required"};if(t.pattern!=null){let{value:i,message:a}=N(t.pattern,"Invalid format");if(typeof e=="string"&&!i.test(e))return {message:a,type:"pattern"}}if(t.minLength!=null&&typeof e=="string"){let{value:i,message:a}=N(t.minLength,`Min ${t.minLength} characters`);if(e.length<i)return {message:a,type:"minLength"}}if(t.maxLength!=null&&typeof e=="string"){let{value:i,message:a}=N(t.maxLength,`Max ${t.maxLength} characters`);if(e.length>i)return {message:a,type:"maxLength"}}if(t.min!=null&&!isNaN(u)){let{value:i,message:a}=N(t.min,`Min ${t.min}`);if(u<i)return {message:a,type:"min"}}if(t.max!=null&&!isNaN(u)){let{value:i,message:a}=N(t.max,`Max ${t.max}`);if(u>i)return {message:a,type:"max"}}if(t.validate){let i=t.validate(e);if(i)return {message:i,type:"validate"}}return null}function J(e,t){let s={};for(let u in t){let i=B(e[u],t[u]);i&&(s[u]=i);}return s}var W=createContext(null);function H(){let e=useContext(W);if(!e)throw new Error("useFormRewindContext must be used within <FormRewind>");return e}function Z({initialState:e,children:t,onSubmit:s,maxHistory:u,debounceMs:i,persist:a,keyboard:r}){let c=z(e,{maxHistory:u,debounceMs:i,persist:a,keyboard:r}),[T,n]=useState({}),[m,x]=useState({}),d=useRef({}),y=useCallback((o,p)=>{d.current[o]=p??{};},[]),b=useCallback(o=>{delete d.current[o];},[]),U=useCallback((o,p)=>{n(f=>({...f,[o]:p}));},[]),S=useCallback(o=>{n(p=>{let f={...p};return delete f[o],f});},[]),I=useCallback(o=>{x(p=>({...p,[o]:true}));},[]),L=useCallback((o,p)=>{if(c.setState(f=>({...f,[o]:p}),o),m[o]&&d.current[o]){let f=B(p,d.current[o]);n(f?R=>({...R,[o]:f}):R=>{let M={...R};return delete M[o],M});}},[c.setState,m]),F=useCallback(o=>{o&&o.preventDefault();let p={};for(let R in d.current)p[R]=true;x(p);let f=J(c.state,d.current);return n(f),Object.keys(f).length===0&&s&&s(c.state),Object.keys(f).length===0},[c.state,s]),E={state:c.state,setState:L,errors:T,setError:U,clearError:S,touched:m,touch:I,fields:Object.fromEntries(Object.entries(d.current).map(([o,p])=>[o,{name:o,rules:p,touched:!!m[o]}])),registerField:y,unregisterField:b};return jsx(W.Provider,{value:E,children:jsx("form",{onSubmit:F,noValidate:true,children:t})})}function ee({name:e,label:t,rules:s,className:u,style:i,...a}){let r=H();useEffect(()=>(r.registerField(e,s),()=>r.unregisterField(e)),[e,s,r.registerField,r.unregisterField]);let c=x=>{r.setState(e,x.target.value);},T=()=>{r.touch(e);},n=r.errors[e],m=String(r.state[e]??"");return jsxs("div",{className:u,style:i,children:[t&&jsx("label",{htmlFor:e,children:t}),jsx("input",{id:e,type:"text",value:m,onChange:c,onBlur:T,"aria-invalid":!!n,"aria-describedby":n?`${e}-error`:void 0,...a}),n&&jsx("span",{id:`${e}-error`,role:"alert",children:n.message})]})}function te({name:e,label:t,rules:s,className:u,style:i,...a}){let r=H();useEffect(()=>(r.registerField(e,s),()=>r.unregisterField(e)),[e,s,r.registerField,r.unregisterField]);let c=x=>{let d=x.target.value===""?"":Number(x.target.value);r.setState(e,d);},T=()=>{r.touch(e);},n=r.errors[e],m=r.state[e];return jsxs("div",{className:u,style:i,children:[t&&jsx("label",{htmlFor:e,children:t}),jsx("input",{id:e,type:"number",value:m===""||m==null?"":Number(m),onChange:c,onBlur:T,"aria-invalid":!!n,"aria-describedby":n?`${e}-error`:void 0,...a}),n&&jsx("span",{id:`${e}-error`,role:"alert",children:n.message})]})}function re({name:e,label:t,rules:s,className:u,style:i,...a}){let r=H();useEffect(()=>(r.registerField(e,s),()=>r.unregisterField(e)),[e,s,r.registerField,r.unregisterField]);let c=m=>{r.setState(e,m.target.checked);},T=r.errors[e],n=!!r.state[e];return jsxs("div",{className:u,style:i,children:[jsxs("label",{children:[jsx("input",{type:"checkbox",checked:n,onChange:c,"aria-invalid":!!T,...a}),t]}),T&&jsx("span",{role:"alert",children:T.message})]})}function ne({name:e,label:t,rules:s,options:u,placeholder:i,className:a,style:r}){let c=H();useEffect(()=>(c.registerField(e,s),()=>c.unregisterField(e)),[e,s,c.registerField,c.unregisterField]);let T=d=>{c.setState(e,d.target.value);},n=()=>{c.touch(e);},m=c.errors[e],x=String(c.state[e]??"");return jsxs("div",{className:a,style:r,children:[t&&jsx("label",{htmlFor:e,children:t}),jsxs("select",{id:e,value:x,onChange:T,onBlur:n,"aria-invalid":!!m,"aria-describedby":m?`${e}-error`:void 0,children:[i&&jsx("option",{value:"",disabled:true,children:i}),u.map(d=>jsx("option",{value:d.value,children:d.label},d.value))]}),m&&jsx("span",{id:`${e}-error`,role:"alert",children:m.message})]})}function oe({name:e,label:t,rules:s,className:u,style:i,...a}){let r=H();useEffect(()=>(r.registerField(e,s),()=>r.unregisterField(e)),[e,s,r.registerField,r.unregisterField]);let c=x=>{r.setState(e,x.target.value);},T=()=>{r.touch(e);},n=r.errors[e],m=String(r.state[e]??"");return jsxs("div",{className:u,style:i,children:[t&&jsx("label",{htmlFor:e,children:t}),jsx("textarea",{id:e,value:m,onChange:c,onBlur:T,"aria-invalid":!!n,"aria-describedby":n?`${e}-error`:void 0,...a}),n&&jsx("span",{id:`${e}-error`,role:"alert",children:n.message})]})}export{re as CheckboxField,Z as FormRewind,te as NumberField,ne as SelectField,ee as TextField,oe as TextareaField,z as useFormHistory,H as useFormRewindContext,J as validateAll,B as validateField};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-form-rewind",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.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"]}
|