1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|
import { useState } from "react";
import { evaluateMathExpression } from "@/utils/math";
function looksLikeExpression({ input }: { input: string }): boolean {
const trimmed = input.trim();
if (!trimmed) return false;
if (/[+*/]/.test(input)) return true;
const minusIndex = trimmed.indexOf("-");
return minusIndex > 0;
}
export function usePropertyDraft<T>({
displayValue: sourceDisplay,
parse,
onPreview,
onCommit,
onStartEditing,
supportsExpressions = true,
}: {
displayValue: string;
parse: (input: string) => T | null;
onPreview: (value: T) => void;
onCommit: () => void;
onStartEditing?: () => void;
supportsExpressions?: boolean;
}) {
const [isEditing, setIsEditing] = useState(false);
const [draft, setDraft] = useState("");
return {
displayValue: isEditing ? draft : sourceDisplay,
scrubTo: (value: number) => {
const parsed = parse(String(value));
if (parsed !== null) onPreview(parsed);
},
commitScrub: onCommit,
onFocus: () => {
setIsEditing(true);
setDraft(sourceDisplay);
onStartEditing?.();
},
onChange: (
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
const nextDraft = event.target.value;
setDraft(nextDraft);
const parsed = parse(nextDraft);
if (parsed !== null) {
onPreview(parsed);
}
},
onBlur: (
event: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
const nextDraft = event.target.value;
if (supportsExpressions && looksLikeExpression({ input: nextDraft })) {
const evaluated = evaluateMathExpression({ input: nextDraft });
if (evaluated !== null) {
const parsed = parse(String(evaluated));
if (parsed !== null) onPreview(parsed);
}
}
onCommit();
setIsEditing(false);
setDraft("");
},
};
}
|