@allhailai/formfoundry-core 1.2.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/LICENSE +21 -0
- package/dist/index.cjs +2972 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1112 -0
- package/dist/index.d.ts +1112 -0
- package/dist/index.js +2905 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2972 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var React4 = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
|
|
6
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
|
|
8
|
+
var React4__default = /*#__PURE__*/_interopDefault(React4);
|
|
9
|
+
|
|
10
|
+
// src/node/events.ts
|
|
11
|
+
function createEventEmitter() {
|
|
12
|
+
const listeners2 = /* @__PURE__ */ new Map();
|
|
13
|
+
const emitter = {
|
|
14
|
+
on(event, listener) {
|
|
15
|
+
if (!listeners2.has(event)) {
|
|
16
|
+
listeners2.set(event, /* @__PURE__ */ new Set());
|
|
17
|
+
}
|
|
18
|
+
listeners2.get(event).add(listener);
|
|
19
|
+
return () => {
|
|
20
|
+
listeners2.get(event)?.delete(listener);
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
once(event, listener) {
|
|
24
|
+
const wrapper = (payload) => {
|
|
25
|
+
listener(payload);
|
|
26
|
+
listeners2.get(event)?.delete(wrapper);
|
|
27
|
+
};
|
|
28
|
+
return emitter.on(event, wrapper);
|
|
29
|
+
},
|
|
30
|
+
emit(event, payload) {
|
|
31
|
+
const eventListeners = listeners2.get(event);
|
|
32
|
+
if (!eventListeners) return;
|
|
33
|
+
for (const listener of eventListeners) {
|
|
34
|
+
try {
|
|
35
|
+
listener(payload);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
if (process.env.NODE_ENV !== "production") {
|
|
38
|
+
console.error("[FormFoundry] Event listener error:", e);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
off(event) {
|
|
44
|
+
if (event) {
|
|
45
|
+
listeners2.delete(event);
|
|
46
|
+
} else {
|
|
47
|
+
listeners2.clear();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
return emitter;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/node/globalRegistry.ts
|
|
55
|
+
var globalNodeMap = /* @__PURE__ */ new Map();
|
|
56
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
57
|
+
function subscribeToRegistry(fn) {
|
|
58
|
+
listeners.add(fn);
|
|
59
|
+
return () => listeners.delete(fn);
|
|
60
|
+
}
|
|
61
|
+
function getNode(id) {
|
|
62
|
+
return globalNodeMap.get(id);
|
|
63
|
+
}
|
|
64
|
+
function registerNode(node) {
|
|
65
|
+
if (process.env.NODE_ENV !== "production" && globalNodeMap.has(node.id)) {
|
|
66
|
+
const existing = globalNodeMap.get(node.id);
|
|
67
|
+
if (existing !== node) {
|
|
68
|
+
console.warn(
|
|
69
|
+
`[FormFoundry] Node with id "${node.id}" is already registered globally. The previous node will be overwritten.`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
globalNodeMap.set(node.id, node);
|
|
74
|
+
listeners.forEach((fn) => fn("registered", node.id, node));
|
|
75
|
+
}
|
|
76
|
+
function unregisterNode(id) {
|
|
77
|
+
const node = globalNodeMap.get(id);
|
|
78
|
+
globalNodeMap.delete(id);
|
|
79
|
+
if (node) {
|
|
80
|
+
listeners.forEach((fn) => fn("unregistered", id, node));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function clearGlobalRegistry() {
|
|
84
|
+
const entries = [...globalNodeMap.entries()];
|
|
85
|
+
globalNodeMap.clear();
|
|
86
|
+
for (const [id, node] of entries) {
|
|
87
|
+
listeners.forEach((fn) => fn("unregistered", id, node));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function clearRegistryListeners() {
|
|
91
|
+
listeners.clear();
|
|
92
|
+
}
|
|
93
|
+
function getRegisteredNodeIds() {
|
|
94
|
+
return Array.from(globalNodeMap.keys());
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/node/hooks.ts
|
|
98
|
+
function createHookDispatcher() {
|
|
99
|
+
const middlewares = [];
|
|
100
|
+
return {
|
|
101
|
+
use(middleware) {
|
|
102
|
+
middlewares.push(middleware);
|
|
103
|
+
return () => {
|
|
104
|
+
const index = middlewares.indexOf(middleware);
|
|
105
|
+
if (index !== -1) {
|
|
106
|
+
middlewares.splice(index, 1);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
dispatch(payload) {
|
|
111
|
+
if (middlewares.length === 0) return payload;
|
|
112
|
+
let index = 0;
|
|
113
|
+
const next = (current) => {
|
|
114
|
+
if (index >= middlewares.length) return current;
|
|
115
|
+
const middleware = middlewares[index++];
|
|
116
|
+
return middleware(current, next);
|
|
117
|
+
};
|
|
118
|
+
return next(payload);
|
|
119
|
+
},
|
|
120
|
+
// BUG-34 fix: async dispatch that awaits each middleware result
|
|
121
|
+
async dispatchAsync(payload) {
|
|
122
|
+
if (middlewares.length === 0) return payload;
|
|
123
|
+
let current = payload;
|
|
124
|
+
for (const middleware of middlewares) {
|
|
125
|
+
const result = middleware(current, (p) => p);
|
|
126
|
+
current = result instanceof Promise ? await result : result;
|
|
127
|
+
}
|
|
128
|
+
return current;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function createHooks() {
|
|
133
|
+
return {
|
|
134
|
+
init: createHookDispatcher(),
|
|
135
|
+
input: createHookDispatcher(),
|
|
136
|
+
commit: createHookDispatcher(),
|
|
137
|
+
prop: createHookDispatcher(),
|
|
138
|
+
message: createHookDispatcher(),
|
|
139
|
+
submit: createHookDispatcher()
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/node/ledger.ts
|
|
144
|
+
function createLedger(store) {
|
|
145
|
+
const counters = [];
|
|
146
|
+
let unsubscribe = null;
|
|
147
|
+
function recalculate() {
|
|
148
|
+
for (const counter of counters) {
|
|
149
|
+
counter.count = 0;
|
|
150
|
+
for (const msg of store.messages.values()) {
|
|
151
|
+
if (counter.filter(msg)) {
|
|
152
|
+
counter.count++;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
unsubscribe = store.on(() => {
|
|
158
|
+
recalculate();
|
|
159
|
+
});
|
|
160
|
+
const ledger = {
|
|
161
|
+
count(name, filter) {
|
|
162
|
+
const counter = { name, filter, count: 0 };
|
|
163
|
+
for (const msg of store.messages.values()) {
|
|
164
|
+
if (filter(msg)) {
|
|
165
|
+
counter.count++;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
counters.push(counter);
|
|
169
|
+
return counter;
|
|
170
|
+
},
|
|
171
|
+
counters() {
|
|
172
|
+
return counters;
|
|
173
|
+
},
|
|
174
|
+
recalculate,
|
|
175
|
+
destroy() {
|
|
176
|
+
if (unsubscribe) {
|
|
177
|
+
unsubscribe();
|
|
178
|
+
unsubscribe = null;
|
|
179
|
+
}
|
|
180
|
+
counters.length = 0;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
return ledger;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/node/messages.ts
|
|
187
|
+
function createMessage(partial) {
|
|
188
|
+
return {
|
|
189
|
+
type: "validation",
|
|
190
|
+
blocking: true,
|
|
191
|
+
visible: true,
|
|
192
|
+
meta: {},
|
|
193
|
+
...partial
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function createMessageStore() {
|
|
197
|
+
const messages = /* @__PURE__ */ new Map();
|
|
198
|
+
const listeners2 = /* @__PURE__ */ new Set();
|
|
199
|
+
let batchDepth = 0;
|
|
200
|
+
let batchDirty = false;
|
|
201
|
+
const notify = () => {
|
|
202
|
+
if (batchDepth > 0) {
|
|
203
|
+
batchDirty = true;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
for (const listener of listeners2) {
|
|
207
|
+
listener(store);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
const store = {
|
|
211
|
+
get messages() {
|
|
212
|
+
return messages;
|
|
213
|
+
},
|
|
214
|
+
set(message) {
|
|
215
|
+
messages.set(message.key, message);
|
|
216
|
+
notify();
|
|
217
|
+
},
|
|
218
|
+
remove(key) {
|
|
219
|
+
if (messages.delete(key)) {
|
|
220
|
+
notify();
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
get(key) {
|
|
224
|
+
return messages.get(key);
|
|
225
|
+
},
|
|
226
|
+
has(key) {
|
|
227
|
+
return messages.has(key);
|
|
228
|
+
},
|
|
229
|
+
clear(type) {
|
|
230
|
+
if (type) {
|
|
231
|
+
for (const [key, msg] of messages) {
|
|
232
|
+
if (msg.type === type) {
|
|
233
|
+
messages.delete(key);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
messages.clear();
|
|
238
|
+
}
|
|
239
|
+
notify();
|
|
240
|
+
},
|
|
241
|
+
filter(type) {
|
|
242
|
+
const result = [];
|
|
243
|
+
for (const msg of messages.values()) {
|
|
244
|
+
if (msg.type === type) {
|
|
245
|
+
result.push(msg);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return result;
|
|
249
|
+
},
|
|
250
|
+
visible() {
|
|
251
|
+
const result = [];
|
|
252
|
+
for (const msg of messages.values()) {
|
|
253
|
+
if (msg.visible) {
|
|
254
|
+
result.push(msg);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return result;
|
|
258
|
+
},
|
|
259
|
+
hasBlocking() {
|
|
260
|
+
for (const msg of messages.values()) {
|
|
261
|
+
if (msg.blocking) return true;
|
|
262
|
+
}
|
|
263
|
+
return false;
|
|
264
|
+
},
|
|
265
|
+
blocking() {
|
|
266
|
+
const result = [];
|
|
267
|
+
for (const msg of messages.values()) {
|
|
268
|
+
if (msg.blocking) {
|
|
269
|
+
result.push(msg);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return result;
|
|
273
|
+
},
|
|
274
|
+
on(listener) {
|
|
275
|
+
listeners2.add(listener);
|
|
276
|
+
return () => {
|
|
277
|
+
listeners2.delete(listener);
|
|
278
|
+
};
|
|
279
|
+
},
|
|
280
|
+
batch(fn) {
|
|
281
|
+
batchDepth++;
|
|
282
|
+
try {
|
|
283
|
+
fn();
|
|
284
|
+
} finally {
|
|
285
|
+
batchDepth--;
|
|
286
|
+
if (batchDepth === 0 && batchDirty) {
|
|
287
|
+
batchDirty = false;
|
|
288
|
+
notify();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
return store;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/node/index.ts
|
|
297
|
+
var nodeCounter = 0;
|
|
298
|
+
function generateNodeId(type) {
|
|
299
|
+
return `${type}_${++nodeCounter}`;
|
|
300
|
+
}
|
|
301
|
+
function recomputeValue(node) {
|
|
302
|
+
if (node.type === "group") {
|
|
303
|
+
const obj = {};
|
|
304
|
+
for (const child of node.children) {
|
|
305
|
+
obj[child.name] = child.value;
|
|
306
|
+
}
|
|
307
|
+
node.value = obj;
|
|
308
|
+
node._value = obj;
|
|
309
|
+
} else if (node.type === "list") {
|
|
310
|
+
node.value = node.children.map((child) => child.value);
|
|
311
|
+
node._value = node.value;
|
|
312
|
+
}
|
|
313
|
+
if (node.parent) {
|
|
314
|
+
recomputeValue(node.parent);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async function hydrateChildren(node, initialValue, emitEvents = false) {
|
|
318
|
+
if (node.type === "group" && initialValue !== null && typeof initialValue === "object" && !Array.isArray(initialValue)) {
|
|
319
|
+
const values = initialValue;
|
|
320
|
+
for (const child of node.children) {
|
|
321
|
+
if (child.name in values) {
|
|
322
|
+
if (emitEvents) {
|
|
323
|
+
await child.input(values[child.name]);
|
|
324
|
+
} else {
|
|
325
|
+
child.value = values[child.name];
|
|
326
|
+
child._value = values[child.name];
|
|
327
|
+
if (child.config.defaultValue === void 0) {
|
|
328
|
+
child.config.defaultValue = values[child.name];
|
|
329
|
+
}
|
|
330
|
+
if (child.type !== "input") {
|
|
331
|
+
hydrateChildren(child, child.value);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} else if (node.type === "list" && Array.isArray(initialValue)) {
|
|
337
|
+
for (let i = 0; i < node.children.length && i < initialValue.length; i++) {
|
|
338
|
+
const child = node.children[i];
|
|
339
|
+
if (emitEvents) {
|
|
340
|
+
await child.input(initialValue[i]);
|
|
341
|
+
} else {
|
|
342
|
+
child.value = initialValue[i];
|
|
343
|
+
child._value = initialValue[i];
|
|
344
|
+
if (child.config.defaultValue === void 0) {
|
|
345
|
+
child.config.defaultValue = initialValue[i];
|
|
346
|
+
}
|
|
347
|
+
if (child.type !== "input") {
|
|
348
|
+
hydrateChildren(child, child.value);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function resolveConfig(node, key) {
|
|
355
|
+
if (key in node.config) {
|
|
356
|
+
return node.config[key];
|
|
357
|
+
}
|
|
358
|
+
if (node.parent) {
|
|
359
|
+
return resolveConfig(node.parent, key);
|
|
360
|
+
}
|
|
361
|
+
return void 0;
|
|
362
|
+
}
|
|
363
|
+
function createNode(options = {}) {
|
|
364
|
+
const {
|
|
365
|
+
type = "input",
|
|
366
|
+
name,
|
|
367
|
+
value,
|
|
368
|
+
id,
|
|
369
|
+
config = {},
|
|
370
|
+
children: initialChildren = [],
|
|
371
|
+
parent = null,
|
|
372
|
+
plugins = [],
|
|
373
|
+
props = {}
|
|
374
|
+
} = options;
|
|
375
|
+
const nodeId = id ?? generateNodeId(type);
|
|
376
|
+
const nodeName = name ?? nodeId;
|
|
377
|
+
const store = createMessageStore();
|
|
378
|
+
const hooks = createHooks();
|
|
379
|
+
const events = createEventEmitter();
|
|
380
|
+
const ledger = createLedger(store);
|
|
381
|
+
const pluginCleanups = [];
|
|
382
|
+
const pluginCleanupsByChild = /* @__PURE__ */ new Map();
|
|
383
|
+
let pendingCount = 0;
|
|
384
|
+
let settleResolvers = [];
|
|
385
|
+
function trackPending() {
|
|
386
|
+
pendingCount++;
|
|
387
|
+
}
|
|
388
|
+
function resolvePending() {
|
|
389
|
+
pendingCount--;
|
|
390
|
+
if (pendingCount <= 0) {
|
|
391
|
+
pendingCount = 0;
|
|
392
|
+
const resolvers = settleResolvers;
|
|
393
|
+
settleResolvers = [];
|
|
394
|
+
queueMicrotask(() => {
|
|
395
|
+
if (pendingCount <= 0) {
|
|
396
|
+
for (const resolve of resolvers) {
|
|
397
|
+
resolve();
|
|
398
|
+
}
|
|
399
|
+
} else {
|
|
400
|
+
settleResolvers = [...resolvers, ...settleResolvers];
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const node = {
|
|
406
|
+
id: nodeId,
|
|
407
|
+
name: nodeName,
|
|
408
|
+
type,
|
|
409
|
+
// Tree
|
|
410
|
+
parent,
|
|
411
|
+
children: [],
|
|
412
|
+
// Value
|
|
413
|
+
value: value ?? (type === "group" ? {} : type === "list" ? [] : void 0),
|
|
414
|
+
_value: value ?? (type === "group" ? {} : type === "list" ? [] : void 0),
|
|
415
|
+
// State
|
|
416
|
+
touched: false,
|
|
417
|
+
dirty: false,
|
|
418
|
+
disabled: false,
|
|
419
|
+
// Derived (computed from message store)
|
|
420
|
+
get valid() {
|
|
421
|
+
return !store.hasBlocking();
|
|
422
|
+
},
|
|
423
|
+
get errors() {
|
|
424
|
+
return store.filter("validation").filter((m) => m.visible).map((m) => m.value);
|
|
425
|
+
},
|
|
426
|
+
// Subsystems
|
|
427
|
+
store,
|
|
428
|
+
hooks,
|
|
429
|
+
events,
|
|
430
|
+
ledger,
|
|
431
|
+
config,
|
|
432
|
+
props,
|
|
433
|
+
// Methods
|
|
434
|
+
async input(newValue) {
|
|
435
|
+
trackPending();
|
|
436
|
+
try {
|
|
437
|
+
const transformedValue = await hooks.input.dispatchAsync(newValue);
|
|
438
|
+
node._value = transformedValue;
|
|
439
|
+
const committedValue = await hooks.commit.dispatchAsync(transformedValue);
|
|
440
|
+
node.value = committedValue;
|
|
441
|
+
if (node.type !== "input" && committedValue != null) {
|
|
442
|
+
await hydrateChildren(node, committedValue, true);
|
|
443
|
+
}
|
|
444
|
+
if (node.parent) {
|
|
445
|
+
recomputeValue(node.parent);
|
|
446
|
+
}
|
|
447
|
+
events.emit("input", committedValue);
|
|
448
|
+
events.emit("commit", committedValue);
|
|
449
|
+
} finally {
|
|
450
|
+
resolvePending();
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
addChild(child, index) {
|
|
454
|
+
child.parent = node;
|
|
455
|
+
if (index !== void 0 && index >= 0 && index <= node.children.length) {
|
|
456
|
+
node.children.splice(index, 0, child);
|
|
457
|
+
} else {
|
|
458
|
+
node.children.push(child);
|
|
459
|
+
}
|
|
460
|
+
const parentPlugins = config.plugins ?? plugins;
|
|
461
|
+
const childCleanups = [];
|
|
462
|
+
for (const plugin of parentPlugins) {
|
|
463
|
+
const cleanup = plugin(child);
|
|
464
|
+
if (cleanup) childCleanups.push(cleanup);
|
|
465
|
+
}
|
|
466
|
+
if (childCleanups.length > 0) {
|
|
467
|
+
pluginCleanupsByChild.set(child.id, childCleanups);
|
|
468
|
+
}
|
|
469
|
+
const preAddValue = node.type === "group" ? node.value : null;
|
|
470
|
+
recomputeValue(node);
|
|
471
|
+
if (preAddValue && typeof preAddValue === "object" && child.name in preAddValue) {
|
|
472
|
+
const existingVal = preAddValue[child.name];
|
|
473
|
+
if (existingVal !== void 0) {
|
|
474
|
+
const shouldHydrate = child.type !== "input" || child.value === void 0;
|
|
475
|
+
if (shouldHydrate) {
|
|
476
|
+
child.value = existingVal;
|
|
477
|
+
child._value = existingVal;
|
|
478
|
+
if (child.type !== "input") {
|
|
479
|
+
hydrateChildren(child, existingVal);
|
|
480
|
+
}
|
|
481
|
+
recomputeValue(node);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
removeChild(child) {
|
|
487
|
+
const index = node.children.indexOf(child);
|
|
488
|
+
if (index !== -1) {
|
|
489
|
+
node.children.splice(index, 1);
|
|
490
|
+
child.parent = null;
|
|
491
|
+
const childCleanups = pluginCleanupsByChild.get(child.id);
|
|
492
|
+
if (childCleanups) {
|
|
493
|
+
for (const cleanup of childCleanups) {
|
|
494
|
+
cleanup();
|
|
495
|
+
}
|
|
496
|
+
pluginCleanupsByChild.delete(child.id);
|
|
497
|
+
}
|
|
498
|
+
recomputeValue(node);
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
destroy() {
|
|
502
|
+
events.emit("destroying", void 0);
|
|
503
|
+
for (const cleanup of pluginCleanups) {
|
|
504
|
+
cleanup();
|
|
505
|
+
}
|
|
506
|
+
if (node.parent) {
|
|
507
|
+
node.parent.removeChild(node);
|
|
508
|
+
}
|
|
509
|
+
const childrenToDestroy = [...node.children];
|
|
510
|
+
node.children = [];
|
|
511
|
+
for (const child of childrenToDestroy) {
|
|
512
|
+
child.destroy();
|
|
513
|
+
child.parent = null;
|
|
514
|
+
}
|
|
515
|
+
for (const cleanups of pluginCleanupsByChild.values()) {
|
|
516
|
+
for (const cleanup of cleanups) {
|
|
517
|
+
cleanup();
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
pluginCleanupsByChild.clear();
|
|
521
|
+
ledger.destroy();
|
|
522
|
+
events.off();
|
|
523
|
+
unregisterNode(node.id);
|
|
524
|
+
},
|
|
525
|
+
walk(fn) {
|
|
526
|
+
fn(node);
|
|
527
|
+
for (const child of node.children) {
|
|
528
|
+
child.walk(fn);
|
|
529
|
+
}
|
|
530
|
+
},
|
|
531
|
+
at(path) {
|
|
532
|
+
const segments = path.split(".");
|
|
533
|
+
let current = node;
|
|
534
|
+
for (const segment of segments) {
|
|
535
|
+
if (!current || current.type === "input") return void 0;
|
|
536
|
+
current = current.children.find((c) => c.name === segment);
|
|
537
|
+
}
|
|
538
|
+
return current;
|
|
539
|
+
},
|
|
540
|
+
reset(resetValue) {
|
|
541
|
+
node.touched = false;
|
|
542
|
+
node.dirty = false;
|
|
543
|
+
node.store.clear("validation");
|
|
544
|
+
node.store.clear("error");
|
|
545
|
+
if (node.type === "group" && node.children.length > 0) {
|
|
546
|
+
const obj = typeof resetValue === "object" && resetValue !== null && !Array.isArray(resetValue) ? resetValue : void 0;
|
|
547
|
+
for (const child of node.children) {
|
|
548
|
+
child.reset(obj?.[child.name]);
|
|
549
|
+
}
|
|
550
|
+
recomputeValue(node);
|
|
551
|
+
} else if (node.type === "list" && node.children.length > 0) {
|
|
552
|
+
const arr = Array.isArray(resetValue) ? resetValue : void 0;
|
|
553
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
554
|
+
node.children[i].reset(arr?.[i]);
|
|
555
|
+
}
|
|
556
|
+
recomputeValue(node);
|
|
557
|
+
} else {
|
|
558
|
+
const defaultVal = resetValue ?? node.config.defaultValue ?? (node.type === "group" ? {} : node.type === "list" ? [] : void 0);
|
|
559
|
+
node.value = defaultVal;
|
|
560
|
+
node._value = defaultVal;
|
|
561
|
+
}
|
|
562
|
+
if (node.parent) {
|
|
563
|
+
recomputeValue(node.parent);
|
|
564
|
+
}
|
|
565
|
+
events.emit("reset", node.value);
|
|
566
|
+
},
|
|
567
|
+
/**
|
|
568
|
+
* Wait for all currently in-flight `input()` calls to complete.
|
|
569
|
+
*
|
|
570
|
+
* **Design note (G1):** If no `input()` calls are pending, `settle()`
|
|
571
|
+
* resolves immediately. This is intentional — it means "wait for
|
|
572
|
+
* current async work", not "wait for the next input". If you need to
|
|
573
|
+
* wait for a future input, subscribe to the `'commit'` event instead.
|
|
574
|
+
*
|
|
575
|
+
* Uses a microtask-deferred resolver (G2 fix) so that re-entrant
|
|
576
|
+
* `input()` calls triggered during resolver callbacks are captured
|
|
577
|
+
* before settlement completes.
|
|
578
|
+
*/
|
|
579
|
+
async settle() {
|
|
580
|
+
if (pendingCount <= 0) return;
|
|
581
|
+
return new Promise((resolve) => {
|
|
582
|
+
settleResolvers.push(resolve);
|
|
583
|
+
});
|
|
584
|
+
},
|
|
585
|
+
async submit() {
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
for (const child of initialChildren) {
|
|
589
|
+
node.addChild(child);
|
|
590
|
+
}
|
|
591
|
+
if (value !== void 0 && type !== "input") {
|
|
592
|
+
hydrateChildren(node, value);
|
|
593
|
+
} else if (type !== "input") {
|
|
594
|
+
recomputeValue(node);
|
|
595
|
+
}
|
|
596
|
+
const allPlugins = [...config.plugins ?? [], ...plugins];
|
|
597
|
+
for (const plugin of allPlugins) {
|
|
598
|
+
const cleanup = plugin(node);
|
|
599
|
+
if (cleanup) pluginCleanups.push(cleanup);
|
|
600
|
+
}
|
|
601
|
+
hooks.init.dispatch(node);
|
|
602
|
+
registerNode(node);
|
|
603
|
+
events.emit("created", node);
|
|
604
|
+
return node;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/i18n/index.ts
|
|
608
|
+
function interpolate(template, ctx) {
|
|
609
|
+
return template.replace(/\{label\}/g, ctx.label).replace(/\{name\}/g, ctx.name).replace(/\{args\.(\d+)\}/g, (_, i) => String(ctx.args[Number(i)] ?? "")).replace(/\{(\d+)\}/g, (_, i) => String(ctx.args[Number(i)] ?? ""));
|
|
610
|
+
}
|
|
611
|
+
function resolveLocaleMessage(locale, ruleName, ctx) {
|
|
612
|
+
const msg = locale.messages[ruleName];
|
|
613
|
+
if (!msg) return `${ctx.label} is invalid.`;
|
|
614
|
+
if (typeof msg === "function") {
|
|
615
|
+
return msg(ctx);
|
|
616
|
+
}
|
|
617
|
+
return interpolate(msg, ctx);
|
|
618
|
+
}
|
|
619
|
+
var localeRegistry = /* @__PURE__ */ new Map();
|
|
620
|
+
var activeLocaleCode = "en";
|
|
621
|
+
function registerLocale(locale) {
|
|
622
|
+
localeRegistry.set(locale.code, locale);
|
|
623
|
+
}
|
|
624
|
+
function setActiveLocale(code) {
|
|
625
|
+
if (!localeRegistry.has(code)) {
|
|
626
|
+
if (process.env.NODE_ENV !== "production") {
|
|
627
|
+
console.warn(
|
|
628
|
+
`[FormFoundry i18n] Locale "${code}" is not registered. Available: ${Array.from(localeRegistry.keys()).join(", ") || "(none)"}`
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
activeLocaleCode = code;
|
|
633
|
+
}
|
|
634
|
+
function getActiveLocale() {
|
|
635
|
+
return localeRegistry.get(activeLocaleCode);
|
|
636
|
+
}
|
|
637
|
+
function getLocale(code) {
|
|
638
|
+
return localeRegistry.get(code);
|
|
639
|
+
}
|
|
640
|
+
function getRegisteredLocales() {
|
|
641
|
+
return Array.from(localeRegistry.keys());
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// src/validation/messages.ts
|
|
645
|
+
var defaultMessages = {
|
|
646
|
+
required: "This field is required.",
|
|
647
|
+
email: "Please enter a valid email address.",
|
|
648
|
+
url: "Please enter a valid URL.",
|
|
649
|
+
min: (args) => `Must be at least ${args[0] ?? "?"}.`,
|
|
650
|
+
max: (args) => `Must be at most ${args[0] ?? "?"}.`,
|
|
651
|
+
minLength: (args) => `Must be at least ${args[0] ?? "?"} characters.`,
|
|
652
|
+
maxLength: (args) => `Must be at most ${args[0] ?? "?"} characters.`,
|
|
653
|
+
matches: "This field does not match the required pattern.",
|
|
654
|
+
confirm: "This field does not match.",
|
|
655
|
+
alpha: "This field must contain only alphabetical characters.",
|
|
656
|
+
alphanumeric: "This field must contain only alphanumeric characters.",
|
|
657
|
+
date: "Please enter a valid date.",
|
|
658
|
+
between: (args) => `Must be between ${args[0] ?? "?"} and ${args[1] ?? "?"}.`,
|
|
659
|
+
number: "This field must be a number.",
|
|
660
|
+
accepted: "This field must be accepted.",
|
|
661
|
+
alpha_spaces: "This field must contain only letters and spaces.",
|
|
662
|
+
contains_alpha: "This field must contain at least one letter.",
|
|
663
|
+
contains_alphanumeric: "This field must contain at least one letter or number.",
|
|
664
|
+
contains_alpha_spaces: "This field must contain at least one letter or space.",
|
|
665
|
+
contains_lowercase: "This field must contain at least one lowercase letter.",
|
|
666
|
+
contains_uppercase: "This field must contain at least one uppercase letter.",
|
|
667
|
+
contains_numeric: "This field must contain at least one number.",
|
|
668
|
+
contains_symbol: "This field must contain at least one symbol.",
|
|
669
|
+
date_after: (args) => `Must be after ${args[0] ?? "today"}.`,
|
|
670
|
+
date_before: (args) => `Must be before ${args[0] ?? "today"}.`,
|
|
671
|
+
date_between: (args) => `Must be between ${args[0] ?? "?"} and ${args[1] ?? "?"}.`,
|
|
672
|
+
date_format: (args) => `Must match the format ${args[0] ?? "YYYY-MM-DD"}.`,
|
|
673
|
+
ends_with: (args) => `Must end with ${args.join(", ") || "?"}.`,
|
|
674
|
+
is: (args) => `Must be one of: ${args.join(", ") || "?"}.`,
|
|
675
|
+
length: (args) => args[1] ? `Must be between ${args[0]} and ${args[1]} characters.` : `Must be exactly ${args[0] ?? "?"} characters.`,
|
|
676
|
+
lowercase: "This field must be all lowercase.",
|
|
677
|
+
not: (args) => `Must not be: ${args.join(", ") || "?"}.`,
|
|
678
|
+
require_one: "At least one of these fields is required.",
|
|
679
|
+
starts_with: (args) => `Must start with ${args.join(", ") || "?"}.`,
|
|
680
|
+
symbol: "This field must contain only symbols.",
|
|
681
|
+
uppercase: "This field must be all uppercase."
|
|
682
|
+
};
|
|
683
|
+
function resolveMessage(ruleName, args, customMessages, label, name) {
|
|
684
|
+
const custom = customMessages?.[ruleName];
|
|
685
|
+
if (custom) {
|
|
686
|
+
return typeof custom === "function" ? custom(args) : custom;
|
|
687
|
+
}
|
|
688
|
+
const activeLocale = getActiveLocale();
|
|
689
|
+
if (activeLocale) {
|
|
690
|
+
const ctx = {
|
|
691
|
+
label: label ?? name ?? "This field",
|
|
692
|
+
name: name ?? "",
|
|
693
|
+
args,
|
|
694
|
+
values: {}
|
|
695
|
+
};
|
|
696
|
+
const localeMsg = activeLocale.messages[ruleName];
|
|
697
|
+
if (localeMsg) {
|
|
698
|
+
return resolveLocaleMessage(activeLocale, ruleName, ctx);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
const source = defaultMessages[ruleName];
|
|
702
|
+
if (typeof source === "function") return source(args);
|
|
703
|
+
if (typeof source === "string") return source;
|
|
704
|
+
return "This field is invalid.";
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/validation/parseRules.ts
|
|
708
|
+
function parseRules(spec) {
|
|
709
|
+
if (typeof spec === "string") {
|
|
710
|
+
return parseStringRules(spec);
|
|
711
|
+
}
|
|
712
|
+
if (Array.isArray(spec)) {
|
|
713
|
+
return parseArrayRules(spec);
|
|
714
|
+
}
|
|
715
|
+
return [];
|
|
716
|
+
}
|
|
717
|
+
function parseStringRules(spec) {
|
|
718
|
+
if (!spec.trim()) return [];
|
|
719
|
+
const rules = spec.split("|");
|
|
720
|
+
return rules.map(parseStringRule).filter(Boolean);
|
|
721
|
+
}
|
|
722
|
+
function parseStringRule(rule) {
|
|
723
|
+
let remaining = rule.trim();
|
|
724
|
+
if (!remaining) return null;
|
|
725
|
+
const hints = {};
|
|
726
|
+
while (remaining.length > 0) {
|
|
727
|
+
const debounceMatch = remaining.match(/^\((\d+)\)/);
|
|
728
|
+
if (debounceMatch) {
|
|
729
|
+
hints.debounce = parseInt(debounceMatch[1], 10);
|
|
730
|
+
remaining = remaining.slice(debounceMatch[0].length);
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
if (remaining.startsWith("+")) {
|
|
734
|
+
hints.empty = true;
|
|
735
|
+
remaining = remaining.slice(1);
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
if (remaining.startsWith("*")) {
|
|
739
|
+
hints.force = true;
|
|
740
|
+
remaining = remaining.slice(1);
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
if (remaining.startsWith("?")) {
|
|
744
|
+
hints.optional = true;
|
|
745
|
+
remaining = remaining.slice(1);
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
break;
|
|
749
|
+
}
|
|
750
|
+
const colonIndex = remaining.indexOf(":");
|
|
751
|
+
if (colonIndex === -1) {
|
|
752
|
+
return { name: remaining, args: [], hints };
|
|
753
|
+
}
|
|
754
|
+
const name = remaining.slice(0, colonIndex);
|
|
755
|
+
const argsString = remaining.slice(colonIndex + 1);
|
|
756
|
+
const args = argsString.split(",").map((a) => a.trim());
|
|
757
|
+
return { name, args, hints };
|
|
758
|
+
}
|
|
759
|
+
function parseArrayRules(spec) {
|
|
760
|
+
const rules = [];
|
|
761
|
+
for (const item of spec) {
|
|
762
|
+
if (typeof item === "string") {
|
|
763
|
+
rules.push(...parseStringRules(item));
|
|
764
|
+
} else if (typeof item === "function") {
|
|
765
|
+
rules.push({
|
|
766
|
+
name: item.name || `custom_${rules.length}`,
|
|
767
|
+
args: [],
|
|
768
|
+
hints: {},
|
|
769
|
+
handler: item
|
|
770
|
+
});
|
|
771
|
+
} else if (Array.isArray(item)) {
|
|
772
|
+
const [name, ...args] = item;
|
|
773
|
+
if (typeof name === "string") {
|
|
774
|
+
rules.push({ name, args, hints: {} });
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return rules;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// src/validation/rules.ts
|
|
782
|
+
function isEmpty(value) {
|
|
783
|
+
if (value === null || value === void 0 || value === "") return true;
|
|
784
|
+
if (Array.isArray(value) && value.length === 0) return true;
|
|
785
|
+
return false;
|
|
786
|
+
}
|
|
787
|
+
function toNumber(value) {
|
|
788
|
+
if (typeof value === "number") return value;
|
|
789
|
+
if (typeof value === "string") {
|
|
790
|
+
const parsed = Number(value);
|
|
791
|
+
return isNaN(parsed) ? null : parsed;
|
|
792
|
+
}
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
function toString(value) {
|
|
796
|
+
if (typeof value === "string") return value;
|
|
797
|
+
if (value === null || value === void 0) return "";
|
|
798
|
+
return String(value);
|
|
799
|
+
}
|
|
800
|
+
function getMessage(ruleName, args) {
|
|
801
|
+
const msgFactory = defaultMessages[ruleName];
|
|
802
|
+
if (typeof msgFactory === "function") {
|
|
803
|
+
return msgFactory(args.map(String));
|
|
804
|
+
}
|
|
805
|
+
if (typeof msgFactory === "string") {
|
|
806
|
+
return msgFactory;
|
|
807
|
+
}
|
|
808
|
+
return `This field is invalid.`;
|
|
809
|
+
}
|
|
810
|
+
var required = (value, args) => {
|
|
811
|
+
if (isEmpty(value)) return getMessage("required", args);
|
|
812
|
+
if (typeof value === "boolean" && !value) return getMessage("required", args);
|
|
813
|
+
return null;
|
|
814
|
+
};
|
|
815
|
+
var email = (value, args) => {
|
|
816
|
+
const str = toString(value);
|
|
817
|
+
if (!str) return null;
|
|
818
|
+
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
819
|
+
return re.test(str) ? null : getMessage("email", args);
|
|
820
|
+
};
|
|
821
|
+
var url = (value, args) => {
|
|
822
|
+
const str = toString(value);
|
|
823
|
+
if (!str) return null;
|
|
824
|
+
try {
|
|
825
|
+
new URL(str);
|
|
826
|
+
return null;
|
|
827
|
+
} catch {
|
|
828
|
+
return getMessage("url", args);
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
var min = (value, args) => {
|
|
832
|
+
const num = toNumber(value);
|
|
833
|
+
const threshold = toNumber(args[0]);
|
|
834
|
+
if (num === null || threshold === null) return null;
|
|
835
|
+
return num >= threshold ? null : getMessage("min", args);
|
|
836
|
+
};
|
|
837
|
+
var max = (value, args) => {
|
|
838
|
+
const num = toNumber(value);
|
|
839
|
+
const threshold = toNumber(args[0]);
|
|
840
|
+
if (num === null || threshold === null) return null;
|
|
841
|
+
return num <= threshold ? null : getMessage("max", args);
|
|
842
|
+
};
|
|
843
|
+
var minLength = (value, args) => {
|
|
844
|
+
const str = toString(value);
|
|
845
|
+
if (!str) return null;
|
|
846
|
+
const threshold = toNumber(args[0]);
|
|
847
|
+
if (threshold === null) return null;
|
|
848
|
+
return str.length >= threshold ? null : getMessage("minLength", args);
|
|
849
|
+
};
|
|
850
|
+
var maxLength = (value, args) => {
|
|
851
|
+
const str = toString(value);
|
|
852
|
+
if (!str) return null;
|
|
853
|
+
const threshold = toNumber(args[0]);
|
|
854
|
+
if (threshold === null) return null;
|
|
855
|
+
return str.length <= threshold ? null : getMessage("maxLength", args);
|
|
856
|
+
};
|
|
857
|
+
var matches = (value, args) => {
|
|
858
|
+
const str = toString(value);
|
|
859
|
+
if (!str) return null;
|
|
860
|
+
const pattern = args[0];
|
|
861
|
+
if (pattern instanceof RegExp) {
|
|
862
|
+
return pattern.test(str) ? null : getMessage("matches", args);
|
|
863
|
+
}
|
|
864
|
+
if (typeof pattern === "string") {
|
|
865
|
+
const re = new RegExp(pattern);
|
|
866
|
+
return re.test(str) ? null : getMessage("matches", args);
|
|
867
|
+
}
|
|
868
|
+
return null;
|
|
869
|
+
};
|
|
870
|
+
var confirm = (value, args, context) => {
|
|
871
|
+
const siblingName = typeof args[0] === "string" ? args[0] : `${context.name}_confirm`;
|
|
872
|
+
const siblingValue = context.values[siblingName];
|
|
873
|
+
return value === siblingValue ? null : getMessage("confirm", args);
|
|
874
|
+
};
|
|
875
|
+
var alpha = (value, args) => {
|
|
876
|
+
const str = toString(value);
|
|
877
|
+
if (!str) return null;
|
|
878
|
+
return /^[a-zA-Z]+$/.test(str) ? null : getMessage("alpha", args);
|
|
879
|
+
};
|
|
880
|
+
var alphanumeric = (value, args) => {
|
|
881
|
+
const str = toString(value);
|
|
882
|
+
if (!str) return null;
|
|
883
|
+
return /^[a-zA-Z0-9]+$/.test(str) ? null : getMessage("alphanumeric", args);
|
|
884
|
+
};
|
|
885
|
+
var date = (value, args) => {
|
|
886
|
+
const str = toString(value);
|
|
887
|
+
if (!str) return null;
|
|
888
|
+
const d = new Date(str);
|
|
889
|
+
return isNaN(d.getTime()) ? getMessage("date", args) : null;
|
|
890
|
+
};
|
|
891
|
+
var between = (value, args) => {
|
|
892
|
+
const num = toNumber(value);
|
|
893
|
+
const lo = toNumber(args[0]);
|
|
894
|
+
const hi = toNumber(args[1]);
|
|
895
|
+
if (num === null || lo === null || hi === null) return null;
|
|
896
|
+
return num >= lo && num <= hi ? null : getMessage("between", args);
|
|
897
|
+
};
|
|
898
|
+
var number = (value, args) => {
|
|
899
|
+
const str = toString(value);
|
|
900
|
+
if (!str) return null;
|
|
901
|
+
return isNaN(Number(str)) ? getMessage("number", args) : null;
|
|
902
|
+
};
|
|
903
|
+
var accepted = (value, args) => {
|
|
904
|
+
const v = typeof value === "string" ? value.toLowerCase() : value;
|
|
905
|
+
if (v === true || v === "yes" || v === "on" || v === "1" || v === 1) return null;
|
|
906
|
+
return getMessage("accepted", args);
|
|
907
|
+
};
|
|
908
|
+
var alpha_spaces = (value, args) => {
|
|
909
|
+
const str = toString(value);
|
|
910
|
+
if (!str) return null;
|
|
911
|
+
return /^[a-zA-Z\s]+$/.test(str) ? null : getMessage("alpha_spaces", args);
|
|
912
|
+
};
|
|
913
|
+
var contains_alpha = (value, args) => {
|
|
914
|
+
const str = toString(value);
|
|
915
|
+
if (!str) return null;
|
|
916
|
+
return /[a-zA-Z]/.test(str) ? null : getMessage("contains_alpha", args);
|
|
917
|
+
};
|
|
918
|
+
var contains_alphanumeric = (value, args) => {
|
|
919
|
+
const str = toString(value);
|
|
920
|
+
if (!str) return null;
|
|
921
|
+
return /[a-zA-Z0-9]/.test(str) ? null : getMessage("contains_alphanumeric", args);
|
|
922
|
+
};
|
|
923
|
+
var contains_alpha_spaces = (value, args) => {
|
|
924
|
+
const str = toString(value);
|
|
925
|
+
if (!str) return null;
|
|
926
|
+
return /[a-zA-Z\s]/.test(str) ? null : getMessage("contains_alpha_spaces", args);
|
|
927
|
+
};
|
|
928
|
+
var contains_lowercase = (value, args) => {
|
|
929
|
+
const str = toString(value);
|
|
930
|
+
if (!str) return null;
|
|
931
|
+
return /[a-z]/.test(str) ? null : getMessage("contains_lowercase", args);
|
|
932
|
+
};
|
|
933
|
+
var contains_uppercase = (value, args) => {
|
|
934
|
+
const str = toString(value);
|
|
935
|
+
if (!str) return null;
|
|
936
|
+
return /[A-Z]/.test(str) ? null : getMessage("contains_uppercase", args);
|
|
937
|
+
};
|
|
938
|
+
var contains_numeric = (value, args) => {
|
|
939
|
+
const str = toString(value);
|
|
940
|
+
if (!str) return null;
|
|
941
|
+
return /\d/.test(str) ? null : getMessage("contains_numeric", args);
|
|
942
|
+
};
|
|
943
|
+
var contains_symbol = (value, args) => {
|
|
944
|
+
const str = toString(value);
|
|
945
|
+
if (!str) return null;
|
|
946
|
+
return /[^a-zA-Z0-9\s]/.test(str) ? null : getMessage("contains_symbol", args);
|
|
947
|
+
};
|
|
948
|
+
var date_after = (value, args) => {
|
|
949
|
+
const str = toString(value);
|
|
950
|
+
if (!str) return null;
|
|
951
|
+
const d = new Date(str);
|
|
952
|
+
if (isNaN(d.getTime())) return getMessage("date_after", args);
|
|
953
|
+
const after = args[0] ? new Date(String(args[0])) : /* @__PURE__ */ new Date();
|
|
954
|
+
if (isNaN(after.getTime())) return null;
|
|
955
|
+
return d > after ? null : getMessage("date_after", args);
|
|
956
|
+
};
|
|
957
|
+
var date_before = (value, args) => {
|
|
958
|
+
const str = toString(value);
|
|
959
|
+
if (!str) return null;
|
|
960
|
+
const d = new Date(str);
|
|
961
|
+
if (isNaN(d.getTime())) return getMessage("date_before", args);
|
|
962
|
+
const before = args[0] ? new Date(String(args[0])) : /* @__PURE__ */ new Date();
|
|
963
|
+
if (isNaN(before.getTime())) return null;
|
|
964
|
+
return d < before ? null : getMessage("date_before", args);
|
|
965
|
+
};
|
|
966
|
+
var date_between = (value, args) => {
|
|
967
|
+
const str = toString(value);
|
|
968
|
+
if (!str) return null;
|
|
969
|
+
const d = new Date(str);
|
|
970
|
+
if (isNaN(d.getTime())) return getMessage("date_between", args);
|
|
971
|
+
const lo = args[0] ? new Date(String(args[0])) : null;
|
|
972
|
+
const hi = args[1] ? new Date(String(args[1])) : null;
|
|
973
|
+
if (!lo || !hi || isNaN(lo.getTime()) || isNaN(hi.getTime())) return null;
|
|
974
|
+
return d >= lo && d <= hi ? null : getMessage("date_between", args);
|
|
975
|
+
};
|
|
976
|
+
var date_format = (value, args) => {
|
|
977
|
+
const str = toString(value);
|
|
978
|
+
if (!str) return null;
|
|
979
|
+
const format = typeof args[0] === "string" ? args[0] : "YYYY-MM-DD";
|
|
980
|
+
const regex = format.replace(/YYYY/g, "\\d{4}").replace(/MM/g, "\\d{2}").replace(/DD/g, "\\d{2}").replace(/HH/g, "\\d{2}").replace(/mm/g, "\\d{2}").replace(/ss/g, "\\d{2}");
|
|
981
|
+
return new RegExp(`^${regex}$`).test(str) ? null : getMessage("date_format", args);
|
|
982
|
+
};
|
|
983
|
+
var ends_with = (value, args) => {
|
|
984
|
+
const str = toString(value);
|
|
985
|
+
if (!str) return null;
|
|
986
|
+
const suffixes = args.map(String);
|
|
987
|
+
return suffixes.some((s) => str.endsWith(s)) ? null : getMessage("ends_with", args);
|
|
988
|
+
};
|
|
989
|
+
var is = (value, args) => {
|
|
990
|
+
const str = toString(value);
|
|
991
|
+
return args.some((arg) => String(arg) === str) ? null : getMessage("is", args);
|
|
992
|
+
};
|
|
993
|
+
var length = (value, args) => {
|
|
994
|
+
const str = toString(value);
|
|
995
|
+
if (!str) return null;
|
|
996
|
+
const len = str.length;
|
|
997
|
+
const lo = toNumber(args[0]);
|
|
998
|
+
const hi = args[1] !== void 0 ? toNumber(args[1]) : lo;
|
|
999
|
+
if (lo === null) return null;
|
|
1000
|
+
if (hi === null) return len === lo ? null : getMessage("length", args);
|
|
1001
|
+
return len >= lo && len <= hi ? null : getMessage("length", args);
|
|
1002
|
+
};
|
|
1003
|
+
var lowercase = (value, args) => {
|
|
1004
|
+
const str = toString(value);
|
|
1005
|
+
if (!str) return null;
|
|
1006
|
+
return str === str.toLowerCase() ? null : getMessage("lowercase", args);
|
|
1007
|
+
};
|
|
1008
|
+
var not = (value, args) => {
|
|
1009
|
+
const str = toString(value);
|
|
1010
|
+
return args.some((arg) => String(arg) === str) ? getMessage("not", args) : null;
|
|
1011
|
+
};
|
|
1012
|
+
var require_one = (value, args, context) => {
|
|
1013
|
+
const fieldNames = args.map(String);
|
|
1014
|
+
const hasSome = fieldNames.some((name) => {
|
|
1015
|
+
const v = context.values[name];
|
|
1016
|
+
return v !== null && v !== void 0 && v !== "";
|
|
1017
|
+
});
|
|
1018
|
+
return hasSome ? null : getMessage("require_one", args);
|
|
1019
|
+
};
|
|
1020
|
+
var starts_with = (value, args) => {
|
|
1021
|
+
const str = toString(value);
|
|
1022
|
+
if (!str) return null;
|
|
1023
|
+
const prefixes = args.map(String);
|
|
1024
|
+
return prefixes.some((p) => str.startsWith(p)) ? null : getMessage("starts_with", args);
|
|
1025
|
+
};
|
|
1026
|
+
var symbol = (value, args) => {
|
|
1027
|
+
const str = toString(value);
|
|
1028
|
+
if (!str) return null;
|
|
1029
|
+
return /^[^a-zA-Z0-9\s]+$/.test(str) ? null : getMessage("symbol", args);
|
|
1030
|
+
};
|
|
1031
|
+
var uppercase = (value, args) => {
|
|
1032
|
+
const str = toString(value);
|
|
1033
|
+
if (!str) return null;
|
|
1034
|
+
return str === str.toUpperCase() ? null : getMessage("uppercase", args);
|
|
1035
|
+
};
|
|
1036
|
+
var builtInRules = {
|
|
1037
|
+
required,
|
|
1038
|
+
email,
|
|
1039
|
+
url,
|
|
1040
|
+
min,
|
|
1041
|
+
max,
|
|
1042
|
+
minLength,
|
|
1043
|
+
maxLength,
|
|
1044
|
+
matches,
|
|
1045
|
+
confirm,
|
|
1046
|
+
alpha,
|
|
1047
|
+
alphanumeric,
|
|
1048
|
+
date,
|
|
1049
|
+
between,
|
|
1050
|
+
number,
|
|
1051
|
+
accepted,
|
|
1052
|
+
alpha_spaces,
|
|
1053
|
+
contains_alpha,
|
|
1054
|
+
contains_alphanumeric,
|
|
1055
|
+
contains_alpha_spaces,
|
|
1056
|
+
contains_lowercase,
|
|
1057
|
+
contains_uppercase,
|
|
1058
|
+
contains_numeric,
|
|
1059
|
+
contains_symbol,
|
|
1060
|
+
date_after,
|
|
1061
|
+
date_before,
|
|
1062
|
+
date_between,
|
|
1063
|
+
date_format,
|
|
1064
|
+
ends_with,
|
|
1065
|
+
is,
|
|
1066
|
+
length,
|
|
1067
|
+
lowercase,
|
|
1068
|
+
not,
|
|
1069
|
+
require_one,
|
|
1070
|
+
starts_with,
|
|
1071
|
+
symbol,
|
|
1072
|
+
uppercase
|
|
1073
|
+
};
|
|
1074
|
+
|
|
1075
|
+
// src/validation/registry.ts
|
|
1076
|
+
var customRules = /* @__PURE__ */ new Map();
|
|
1077
|
+
function registerRule(name, rule) {
|
|
1078
|
+
customRules.set(name, rule);
|
|
1079
|
+
}
|
|
1080
|
+
function getRule(name) {
|
|
1081
|
+
return customRules.get(name) ?? builtInRules[name];
|
|
1082
|
+
}
|
|
1083
|
+
function hasRule(name) {
|
|
1084
|
+
return customRules.has(name) || name in builtInRules;
|
|
1085
|
+
}
|
|
1086
|
+
function unregisterRule(name) {
|
|
1087
|
+
customRules.delete(name);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// src/validation/zodAdapter.ts
|
|
1091
|
+
function validateWithZod(schema, value) {
|
|
1092
|
+
const result = schema.safeParse(value);
|
|
1093
|
+
if (result.success) return [];
|
|
1094
|
+
return result.error.issues.map((issue) => {
|
|
1095
|
+
const path = issue.path.join(".");
|
|
1096
|
+
return createMessage({
|
|
1097
|
+
key: `zod_${path || "root"}_${issue.code}`,
|
|
1098
|
+
type: "validation",
|
|
1099
|
+
value: issue.message,
|
|
1100
|
+
blocking: true,
|
|
1101
|
+
visible: true,
|
|
1102
|
+
meta: {
|
|
1103
|
+
zodCode: issue.code,
|
|
1104
|
+
path: issue.path
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
function validateFormWithZod(schema, values) {
|
|
1110
|
+
const result = schema.safeParse(values);
|
|
1111
|
+
if (result.success) return {};
|
|
1112
|
+
const errors = {};
|
|
1113
|
+
for (const issue of result.error.issues) {
|
|
1114
|
+
const fieldName = issue.path[0];
|
|
1115
|
+
if (typeof fieldName === "string") {
|
|
1116
|
+
if (!errors[fieldName]) {
|
|
1117
|
+
errors[fieldName] = [];
|
|
1118
|
+
}
|
|
1119
|
+
errors[fieldName].push(issue.message);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
return errors;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// src/validation/index.ts
|
|
1126
|
+
async function runValidation(spec, value, context, customMessages) {
|
|
1127
|
+
const rules = parseRules(spec);
|
|
1128
|
+
const messages = [];
|
|
1129
|
+
const valueIsEmpty = isEmpty2(value);
|
|
1130
|
+
for (const rule of rules) {
|
|
1131
|
+
if (valueIsEmpty && !rule.hints.empty && rule.name !== "required") {
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
const error = await executeRule(rule, value, context, customMessages);
|
|
1135
|
+
if (error) {
|
|
1136
|
+
messages.push(error);
|
|
1137
|
+
if (!rule.hints.force) {
|
|
1138
|
+
break;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
return messages;
|
|
1143
|
+
}
|
|
1144
|
+
async function executeRule(rule, value, context, customMessages) {
|
|
1145
|
+
const handler = rule.handler ?? getRule(rule.name);
|
|
1146
|
+
if (!handler) {
|
|
1147
|
+
console.warn(`[FormFoundry] Unknown validation rule: "${rule.name}"`);
|
|
1148
|
+
return null;
|
|
1149
|
+
}
|
|
1150
|
+
const errorString = await handler(value, rule.args, context);
|
|
1151
|
+
if (errorString === null) return null;
|
|
1152
|
+
const source = customMessages?.[rule.name];
|
|
1153
|
+
let messageValue = errorString;
|
|
1154
|
+
if (source) {
|
|
1155
|
+
messageValue = typeof source === "function" ? source(rule.args.map(String)) : source;
|
|
1156
|
+
}
|
|
1157
|
+
return createMessage({
|
|
1158
|
+
key: `rule_${rule.name}`,
|
|
1159
|
+
type: "validation",
|
|
1160
|
+
value: messageValue,
|
|
1161
|
+
blocking: !rule.hints.optional,
|
|
1162
|
+
visible: true,
|
|
1163
|
+
meta: {
|
|
1164
|
+
ruleName: rule.name,
|
|
1165
|
+
ruleArgs: rule.args
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
function isEmpty2(value) {
|
|
1170
|
+
if (value === null || value === void 0 || value === "") return true;
|
|
1171
|
+
if (Array.isArray(value) && value.length === 0) return true;
|
|
1172
|
+
return false;
|
|
1173
|
+
}
|
|
1174
|
+
var defaultMeta = {
|
|
1175
|
+
isSubmitting: false,
|
|
1176
|
+
isValid: true,
|
|
1177
|
+
errors: {},
|
|
1178
|
+
register: () => {
|
|
1179
|
+
if (process.env.NODE_ENV !== "production") {
|
|
1180
|
+
console.warn("[FormFoundry] register called outside of a <Form> component");
|
|
1181
|
+
}
|
|
1182
|
+
},
|
|
1183
|
+
unregister: () => {
|
|
1184
|
+
},
|
|
1185
|
+
getNode: () => void 0,
|
|
1186
|
+
triggerValidation: async () => true,
|
|
1187
|
+
showErrors: false,
|
|
1188
|
+
config: {},
|
|
1189
|
+
formNode: null,
|
|
1190
|
+
reset: () => {
|
|
1191
|
+
},
|
|
1192
|
+
submitCount: 0,
|
|
1193
|
+
setExternalErrors: () => {
|
|
1194
|
+
},
|
|
1195
|
+
clearExternalErrors: () => {
|
|
1196
|
+
},
|
|
1197
|
+
isDisabled: false
|
|
1198
|
+
};
|
|
1199
|
+
var FormMetaContext = React4.createContext(defaultMeta);
|
|
1200
|
+
function useFormMeta() {
|
|
1201
|
+
return React4.useContext(FormMetaContext);
|
|
1202
|
+
}
|
|
1203
|
+
var defaultValues = {
|
|
1204
|
+
values: {},
|
|
1205
|
+
getValue: () => void 0,
|
|
1206
|
+
setValue: () => {
|
|
1207
|
+
if (process.env.NODE_ENV !== "production") {
|
|
1208
|
+
console.warn("[FormFoundry] setValue called outside of a <Form> component");
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
var FormValuesContext = React4.createContext(defaultValues);
|
|
1213
|
+
function useFormValues() {
|
|
1214
|
+
return React4.useContext(FormValuesContext);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// src/context/useFormContext.ts
|
|
1218
|
+
function useFormContext() {
|
|
1219
|
+
return {
|
|
1220
|
+
values: useFormValues(),
|
|
1221
|
+
meta: useFormMeta()
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
function useFormFoundryField(options) {
|
|
1225
|
+
const {
|
|
1226
|
+
name,
|
|
1227
|
+
validation,
|
|
1228
|
+
validationBehavior: fieldBehavior,
|
|
1229
|
+
defaultValue,
|
|
1230
|
+
plugins,
|
|
1231
|
+
disabled = false,
|
|
1232
|
+
label,
|
|
1233
|
+
inputType,
|
|
1234
|
+
selectOptions,
|
|
1235
|
+
dependsOn
|
|
1236
|
+
} = options;
|
|
1237
|
+
const reactId = React4.useId();
|
|
1238
|
+
const fieldId = `formfoundry-${name}-${reactId}`;
|
|
1239
|
+
const { values, getValue, setValue } = useFormValues();
|
|
1240
|
+
const meta = useFormMeta();
|
|
1241
|
+
const validationBehavior = fieldBehavior ?? meta.config.validationBehavior ?? "blur";
|
|
1242
|
+
const nodeRef = React4.useRef(null);
|
|
1243
|
+
if (!nodeRef.current) {
|
|
1244
|
+
nodeRef.current = createNode({
|
|
1245
|
+
type: "input",
|
|
1246
|
+
name,
|
|
1247
|
+
id: fieldId,
|
|
1248
|
+
value: getValue(name) ?? defaultValue,
|
|
1249
|
+
config: {
|
|
1250
|
+
validation,
|
|
1251
|
+
validationBehavior,
|
|
1252
|
+
defaultValue,
|
|
1253
|
+
plugins,
|
|
1254
|
+
// Store label and inputType for print renderer access
|
|
1255
|
+
label,
|
|
1256
|
+
inputType: inputType ?? "text",
|
|
1257
|
+
options: selectOptions
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
const node = nodeRef.current;
|
|
1262
|
+
if (label !== void 0) {
|
|
1263
|
+
node.config.label = label;
|
|
1264
|
+
}
|
|
1265
|
+
if (inputType !== void 0) {
|
|
1266
|
+
node.config.inputType = inputType;
|
|
1267
|
+
}
|
|
1268
|
+
if (selectOptions !== void 0) {
|
|
1269
|
+
node.config.options = selectOptions;
|
|
1270
|
+
}
|
|
1271
|
+
if (node.name !== name) {
|
|
1272
|
+
const oldName = node.name;
|
|
1273
|
+
node.name = name;
|
|
1274
|
+
meta.unregister(oldName);
|
|
1275
|
+
meta.register(name, node);
|
|
1276
|
+
}
|
|
1277
|
+
const [touched, setTouched] = React4.useState(false);
|
|
1278
|
+
const [dirty, setDirty] = React4.useState(false);
|
|
1279
|
+
const [errorStrings, setErrorStrings] = React4.useState([]);
|
|
1280
|
+
const hasBlurredRef = React4.useRef(false);
|
|
1281
|
+
const debounceTimerRef = React4.useRef(null);
|
|
1282
|
+
const valuesRef = React4.useRef(values);
|
|
1283
|
+
valuesRef.current = values;
|
|
1284
|
+
const getValueRef = React4.useRef(getValue);
|
|
1285
|
+
getValueRef.current = getValue;
|
|
1286
|
+
const validationGenRef = React4.useRef(0);
|
|
1287
|
+
const defaultValueRef = React4.useRef(defaultValue);
|
|
1288
|
+
defaultValueRef.current = defaultValue;
|
|
1289
|
+
const defaultValueJsonRef = React4.useRef(JSON.stringify(defaultValue));
|
|
1290
|
+
defaultValueJsonRef.current = JSON.stringify(defaultValue);
|
|
1291
|
+
const validate = React4.useCallback(async () => {
|
|
1292
|
+
const gen = ++validationGenRef.current;
|
|
1293
|
+
if (!validation) {
|
|
1294
|
+
setErrorStrings([]);
|
|
1295
|
+
node.store.clear("validation");
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
if (isZodSchema(validation)) {
|
|
1299
|
+
const msgs2 = validateWithZod(validation, getValueRef.current(name));
|
|
1300
|
+
if (gen !== validationGenRef.current) return;
|
|
1301
|
+
node.store.batch(() => {
|
|
1302
|
+
node.store.clear("validation");
|
|
1303
|
+
for (const msg of msgs2) {
|
|
1304
|
+
node.store.set(msg);
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1307
|
+
setErrorStrings(msgs2.filter((m) => m.visible).map((m) => m.value));
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
const msgs = await runValidation(
|
|
1311
|
+
validation,
|
|
1312
|
+
getValueRef.current(name),
|
|
1313
|
+
{
|
|
1314
|
+
values: valuesRef.current,
|
|
1315
|
+
name,
|
|
1316
|
+
label
|
|
1317
|
+
},
|
|
1318
|
+
meta.config.messages
|
|
1319
|
+
);
|
|
1320
|
+
if (gen !== validationGenRef.current) return;
|
|
1321
|
+
node.store.batch(() => {
|
|
1322
|
+
node.store.clear("validation");
|
|
1323
|
+
for (const msg of msgs) {
|
|
1324
|
+
node.store.set(msg);
|
|
1325
|
+
}
|
|
1326
|
+
});
|
|
1327
|
+
setErrorStrings(msgs.filter((m) => m.visible).map((m) => m.value));
|
|
1328
|
+
}, [validation, name, label, meta.config.messages, node]);
|
|
1329
|
+
React4.useEffect(() => {
|
|
1330
|
+
meta.register(name, node);
|
|
1331
|
+
return () => {
|
|
1332
|
+
meta.unregister(name);
|
|
1333
|
+
};
|
|
1334
|
+
}, [name, node, meta]);
|
|
1335
|
+
React4.useEffect(() => {
|
|
1336
|
+
meta.updateValidateCallback?.(name, validate);
|
|
1337
|
+
}, [name, validate, meta]);
|
|
1338
|
+
const onChange = React4.useCallback(
|
|
1339
|
+
(newValue) => {
|
|
1340
|
+
setValue(name, newValue);
|
|
1341
|
+
void node.input(newValue).then(() => {
|
|
1342
|
+
const committedValue = node.value;
|
|
1343
|
+
if (committedValue !== newValue) {
|
|
1344
|
+
setValue(name, committedValue);
|
|
1345
|
+
}
|
|
1346
|
+
const def = defaultValueRef.current ?? void 0;
|
|
1347
|
+
let isDirty;
|
|
1348
|
+
if (def !== null && typeof def === "object") {
|
|
1349
|
+
isDirty = JSON.stringify(committedValue) !== defaultValueJsonRef.current;
|
|
1350
|
+
} else {
|
|
1351
|
+
isDirty = committedValue !== def;
|
|
1352
|
+
}
|
|
1353
|
+
setDirty(isDirty);
|
|
1354
|
+
node.dirty = isDirty;
|
|
1355
|
+
});
|
|
1356
|
+
if (validationBehavior === "live" || validationBehavior === "blur" && hasBlurredRef.current || validationBehavior === "dirty" && newValue !== (defaultValueRef.current ?? void 0) || validationBehavior === "submit" && meta.submitCount > 0) {
|
|
1357
|
+
if (debounceTimerRef.current) {
|
|
1358
|
+
clearTimeout(debounceTimerRef.current);
|
|
1359
|
+
}
|
|
1360
|
+
const debounceMs = meta.config.validationDebounce ?? 150;
|
|
1361
|
+
debounceTimerRef.current = setTimeout(() => {
|
|
1362
|
+
void validate();
|
|
1363
|
+
}, debounceMs);
|
|
1364
|
+
}
|
|
1365
|
+
},
|
|
1366
|
+
[name, setValue, validationBehavior, validate, meta.config.validationDebounce, meta.submitCount, node]
|
|
1367
|
+
);
|
|
1368
|
+
const onBlur = React4.useCallback(() => {
|
|
1369
|
+
setTouched(true);
|
|
1370
|
+
node.touched = true;
|
|
1371
|
+
hasBlurredRef.current = true;
|
|
1372
|
+
if (validationBehavior === "blur") {
|
|
1373
|
+
void validate();
|
|
1374
|
+
}
|
|
1375
|
+
}, [validationBehavior, validate, node]);
|
|
1376
|
+
const depsKey = dependsOn?.join(",") ?? "";
|
|
1377
|
+
const depsValues = dependsOn?.map((dep) => JSON.stringify(getValue(dep))).join("|") ?? "";
|
|
1378
|
+
React4.useEffect(() => {
|
|
1379
|
+
if (!dependsOn || dependsOn.length === 0) return;
|
|
1380
|
+
if (touched || hasBlurredRef.current) {
|
|
1381
|
+
void validate();
|
|
1382
|
+
}
|
|
1383
|
+
}, [depsKey, depsValues, validate]);
|
|
1384
|
+
React4.useEffect(() => {
|
|
1385
|
+
return () => {
|
|
1386
|
+
if (debounceTimerRef.current) {
|
|
1387
|
+
clearTimeout(debounceTimerRef.current);
|
|
1388
|
+
}
|
|
1389
|
+
};
|
|
1390
|
+
}, []);
|
|
1391
|
+
const value = getValue(name) ?? defaultValue;
|
|
1392
|
+
const valid = errorStrings.length === 0;
|
|
1393
|
+
const error = errorStrings[0] ?? null;
|
|
1394
|
+
const showErrors = touched || meta.showErrors;
|
|
1395
|
+
const fieldDisabled = disabled || meta.isDisabled;
|
|
1396
|
+
const fieldProps = React4.useMemo(
|
|
1397
|
+
() => ({
|
|
1398
|
+
value: value ?? "",
|
|
1399
|
+
onChange,
|
|
1400
|
+
onBlur,
|
|
1401
|
+
"aria-invalid": showErrors && !valid ? true : void 0,
|
|
1402
|
+
"aria-describedby": showErrors && error ? `${fieldId}-error` : void 0,
|
|
1403
|
+
id: fieldId,
|
|
1404
|
+
name,
|
|
1405
|
+
disabled: fieldDisabled || void 0
|
|
1406
|
+
}),
|
|
1407
|
+
[value, onChange, onBlur, showErrors, valid, error, fieldId, name, fieldDisabled]
|
|
1408
|
+
);
|
|
1409
|
+
return {
|
|
1410
|
+
value,
|
|
1411
|
+
error: showErrors ? error : null,
|
|
1412
|
+
errors: showErrors ? errorStrings : [],
|
|
1413
|
+
touched,
|
|
1414
|
+
dirty,
|
|
1415
|
+
valid,
|
|
1416
|
+
onChange,
|
|
1417
|
+
onBlur,
|
|
1418
|
+
fieldProps,
|
|
1419
|
+
id: fieldId,
|
|
1420
|
+
node
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
function isZodSchema(validation) {
|
|
1424
|
+
return typeof validation === "object" && validation !== null && "_def" in validation && "safeParse" in validation;
|
|
1425
|
+
}
|
|
1426
|
+
function subscribeToNode(node, onChange) {
|
|
1427
|
+
const unsubs = [
|
|
1428
|
+
node.events.on("commit", onChange),
|
|
1429
|
+
node.events.on("reset", onChange),
|
|
1430
|
+
node.events.on("message-added", onChange),
|
|
1431
|
+
node.events.on("message-removed", onChange)
|
|
1432
|
+
];
|
|
1433
|
+
return () => unsubs.forEach((u) => u());
|
|
1434
|
+
}
|
|
1435
|
+
function useFormNode(id) {
|
|
1436
|
+
const [, forceRender] = React4.useState(0);
|
|
1437
|
+
const nodeUnsubRef = React4.useRef(null);
|
|
1438
|
+
const triggerRender = React4.useCallback(() => {
|
|
1439
|
+
forceRender((n) => n + 1);
|
|
1440
|
+
}, []);
|
|
1441
|
+
const subscribeToNodeEvents = React4.useCallback(
|
|
1442
|
+
(node) => {
|
|
1443
|
+
nodeUnsubRef.current?.();
|
|
1444
|
+
nodeUnsubRef.current = subscribeToNode(node, triggerRender);
|
|
1445
|
+
},
|
|
1446
|
+
[triggerRender]
|
|
1447
|
+
);
|
|
1448
|
+
React4.useEffect(() => {
|
|
1449
|
+
if (!id) return;
|
|
1450
|
+
const node = getNode(id);
|
|
1451
|
+
if (node) {
|
|
1452
|
+
subscribeToNodeEvents(node);
|
|
1453
|
+
}
|
|
1454
|
+
const unsubRegistry = subscribeToRegistry((event, registeredId, registeredNode) => {
|
|
1455
|
+
if (registeredId !== id) return;
|
|
1456
|
+
if (event === "registered") {
|
|
1457
|
+
subscribeToNodeEvents(registeredNode);
|
|
1458
|
+
triggerRender();
|
|
1459
|
+
} else if (event === "unregistered") {
|
|
1460
|
+
nodeUnsubRef.current?.();
|
|
1461
|
+
nodeUnsubRef.current = null;
|
|
1462
|
+
triggerRender();
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1465
|
+
return () => {
|
|
1466
|
+
unsubRegistry();
|
|
1467
|
+
nodeUnsubRef.current?.();
|
|
1468
|
+
nodeUnsubRef.current = null;
|
|
1469
|
+
};
|
|
1470
|
+
}, [id, triggerRender, subscribeToNodeEvents]);
|
|
1471
|
+
return id ? getNode(id) : void 0;
|
|
1472
|
+
}
|
|
1473
|
+
function useFormNodes(ids) {
|
|
1474
|
+
const [, forceRender] = React4.useState(0);
|
|
1475
|
+
const nodeUnsubsRef = React4.useRef(/* @__PURE__ */ new Map());
|
|
1476
|
+
const triggerRender = React4.useCallback(() => {
|
|
1477
|
+
forceRender((n) => n + 1);
|
|
1478
|
+
}, []);
|
|
1479
|
+
const idsKey = ids.join(",");
|
|
1480
|
+
React4.useEffect(() => {
|
|
1481
|
+
for (const unsub of nodeUnsubsRef.current.values()) {
|
|
1482
|
+
unsub();
|
|
1483
|
+
}
|
|
1484
|
+
nodeUnsubsRef.current.clear();
|
|
1485
|
+
const idSet = new Set(ids);
|
|
1486
|
+
for (const id of ids) {
|
|
1487
|
+
const node = getNode(id);
|
|
1488
|
+
if (node) {
|
|
1489
|
+
nodeUnsubsRef.current.set(id, subscribeToNode(node, triggerRender));
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
const unsubRegistry = subscribeToRegistry((event, registeredId, registeredNode) => {
|
|
1493
|
+
if (!idSet.has(registeredId)) return;
|
|
1494
|
+
if (event === "registered") {
|
|
1495
|
+
nodeUnsubsRef.current.get(registeredId)?.();
|
|
1496
|
+
nodeUnsubsRef.current.set(
|
|
1497
|
+
registeredId,
|
|
1498
|
+
subscribeToNode(registeredNode, triggerRender)
|
|
1499
|
+
);
|
|
1500
|
+
triggerRender();
|
|
1501
|
+
} else if (event === "unregistered") {
|
|
1502
|
+
nodeUnsubsRef.current.get(registeredId)?.();
|
|
1503
|
+
nodeUnsubsRef.current.delete(registeredId);
|
|
1504
|
+
triggerRender();
|
|
1505
|
+
}
|
|
1506
|
+
});
|
|
1507
|
+
return () => {
|
|
1508
|
+
unsubRegistry();
|
|
1509
|
+
for (const unsub of nodeUnsubsRef.current.values()) {
|
|
1510
|
+
unsub();
|
|
1511
|
+
}
|
|
1512
|
+
nodeUnsubsRef.current.clear();
|
|
1513
|
+
};
|
|
1514
|
+
}, [idsKey, triggerRender]);
|
|
1515
|
+
const result = /* @__PURE__ */ new Map();
|
|
1516
|
+
for (const id of ids) {
|
|
1517
|
+
result.set(id, getNode(id));
|
|
1518
|
+
}
|
|
1519
|
+
return result;
|
|
1520
|
+
}
|
|
1521
|
+
function useFormPrint(defaultOptions) {
|
|
1522
|
+
const { values } = useFormValues();
|
|
1523
|
+
const meta = useFormMeta();
|
|
1524
|
+
const printContainerRef = React4.useRef(null);
|
|
1525
|
+
const getFieldDescriptors = React4.useCallback(
|
|
1526
|
+
(_mode) => {
|
|
1527
|
+
const descriptors = [];
|
|
1528
|
+
const formNode = meta.formNode;
|
|
1529
|
+
if (!formNode) return descriptors;
|
|
1530
|
+
for (const child of formNode.children) {
|
|
1531
|
+
const name = child.name;
|
|
1532
|
+
const config = child.config ?? {};
|
|
1533
|
+
const label = config.label;
|
|
1534
|
+
const type = child.type === "input" ? config.inputType ?? "text" : child.type;
|
|
1535
|
+
if (child.type !== "input") continue;
|
|
1536
|
+
const descriptor = {
|
|
1537
|
+
name,
|
|
1538
|
+
label: label ?? name,
|
|
1539
|
+
type,
|
|
1540
|
+
value: values[name],
|
|
1541
|
+
options: config.options
|
|
1542
|
+
};
|
|
1543
|
+
descriptors.push(descriptor);
|
|
1544
|
+
}
|
|
1545
|
+
return descriptors;
|
|
1546
|
+
},
|
|
1547
|
+
[meta.formNode, values]
|
|
1548
|
+
);
|
|
1549
|
+
const printForm = React4.useCallback(
|
|
1550
|
+
(overrides) => {
|
|
1551
|
+
const options = { ...defaultOptions, ...overrides };
|
|
1552
|
+
const mode = options.mode ?? "filled";
|
|
1553
|
+
const fields = getFieldDescriptors(mode);
|
|
1554
|
+
injectPrintCSS();
|
|
1555
|
+
const container = document.createElement("div");
|
|
1556
|
+
container.setAttribute("data-formfoundry-print-root", "");
|
|
1557
|
+
container.style.display = "none";
|
|
1558
|
+
const resolvedMode = mode === "current" ? "filled" : mode;
|
|
1559
|
+
container.innerHTML = buildPrintHTML(fields, resolvedMode, options);
|
|
1560
|
+
document.body.appendChild(container);
|
|
1561
|
+
printContainerRef.current = container;
|
|
1562
|
+
container.style.display = "block";
|
|
1563
|
+
requestAnimationFrame(() => {
|
|
1564
|
+
window.print();
|
|
1565
|
+
setTimeout(() => {
|
|
1566
|
+
if (printContainerRef.current) {
|
|
1567
|
+
document.body.removeChild(printContainerRef.current);
|
|
1568
|
+
printContainerRef.current = null;
|
|
1569
|
+
}
|
|
1570
|
+
}, 100);
|
|
1571
|
+
});
|
|
1572
|
+
},
|
|
1573
|
+
[defaultOptions, getFieldDescriptors]
|
|
1574
|
+
);
|
|
1575
|
+
return { printForm, getFieldDescriptors };
|
|
1576
|
+
}
|
|
1577
|
+
function buildPrintHTML(fields, mode, options) {
|
|
1578
|
+
const header = options.title || options.subtitle ? `<div class="ff-print__header">
|
|
1579
|
+
${options.title ? `<h1 class="ff-print__title">${escapeHtml(options.title)}</h1>` : ""}
|
|
1580
|
+
${options.subtitle ? `<p class="ff-print__subtitle">${escapeHtml(options.subtitle)}</p>` : ""}
|
|
1581
|
+
</div>` : "";
|
|
1582
|
+
const fieldRows = fields.map((field) => {
|
|
1583
|
+
const label = escapeHtml(field.label || field.name);
|
|
1584
|
+
const isTextarea = field.type === "textarea";
|
|
1585
|
+
const fieldClass = `ff-print__field${isTextarea ? " ff-print__field--textarea" : ""}`;
|
|
1586
|
+
const valueHtml = mode === "blank" ? buildBlankValue(field) : buildFilledValue(field);
|
|
1587
|
+
return `<li class="${fieldClass}">
|
|
1588
|
+
<span class="ff-print__label">${label}:</span>
|
|
1589
|
+
<span class="ff-print__value">${valueHtml}</span>
|
|
1590
|
+
</li>`;
|
|
1591
|
+
}).join("\n");
|
|
1592
|
+
const footer = options.footer ? `<div class="ff-print__footer">${escapeHtml(options.footer)}</div>` : "";
|
|
1593
|
+
return `<div class="ff-print${options.className ? ` ${options.className}` : ""}">
|
|
1594
|
+
${header}
|
|
1595
|
+
<ul class="ff-print__fields">${fieldRows}</ul>
|
|
1596
|
+
${footer}
|
|
1597
|
+
</div>`;
|
|
1598
|
+
}
|
|
1599
|
+
function buildBlankValue(field) {
|
|
1600
|
+
switch (field.type) {
|
|
1601
|
+
case "checkbox":
|
|
1602
|
+
case "switch":
|
|
1603
|
+
return '<span class="ff-print__blank-box"></span>';
|
|
1604
|
+
case "radio":
|
|
1605
|
+
return `<span class="ff-print__blank-options">
|
|
1606
|
+
${(field.options ?? []).map(
|
|
1607
|
+
(opt) => `<span class="ff-print__blank-option"><span class="ff-print__blank-radio"></span>${escapeHtml(opt.label)}</span>`
|
|
1608
|
+
).join("")}
|
|
1609
|
+
</span>`;
|
|
1610
|
+
case "select":
|
|
1611
|
+
case "checkboxgroup":
|
|
1612
|
+
return `<span class="ff-print__blank-options">
|
|
1613
|
+
${(field.options ?? []).map(
|
|
1614
|
+
(opt) => `<span class="ff-print__blank-option"><span class="ff-print__blank-box"></span>${escapeHtml(opt.label)}</span>`
|
|
1615
|
+
).join("")}
|
|
1616
|
+
</span>`;
|
|
1617
|
+
case "textarea":
|
|
1618
|
+
return '<div class="ff-print__blank-textarea"></div>';
|
|
1619
|
+
default:
|
|
1620
|
+
return '<span class="ff-print__blank-line"></span>';
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
function buildFilledValue(field) {
|
|
1624
|
+
const { value, type } = field;
|
|
1625
|
+
switch (type) {
|
|
1626
|
+
case "checkbox":
|
|
1627
|
+
case "switch":
|
|
1628
|
+
return `<span class="ff-print__filled-check">${value ? "\u2611" : "\u2610"}</span>`;
|
|
1629
|
+
case "radio":
|
|
1630
|
+
case "select": {
|
|
1631
|
+
const selected = field.options?.find(
|
|
1632
|
+
(opt) => String(opt.value) === String(value)
|
|
1633
|
+
);
|
|
1634
|
+
return `<span class="ff-print__filled-value">${escapeHtml(selected?.label ?? String(value ?? "\u2014"))}</span>`;
|
|
1635
|
+
}
|
|
1636
|
+
case "checkboxgroup": {
|
|
1637
|
+
const selectedValues = Array.isArray(value) ? value : [];
|
|
1638
|
+
const labels = (field.options ?? []).filter((opt) => selectedValues.includes(opt.value)).map((opt) => opt.label);
|
|
1639
|
+
return `<span class="ff-print__filled-value">${escapeHtml(labels.length > 0 ? labels.join(", ") : "\u2014")}</span>`;
|
|
1640
|
+
}
|
|
1641
|
+
case "textarea":
|
|
1642
|
+
return `<div class="ff-print__filled-textarea">${escapeHtml(String(value ?? ""))}</div>`;
|
|
1643
|
+
case "file":
|
|
1644
|
+
return `<span class="ff-print__filled-value">${value ? escapeHtml(String(value)) : "No file selected"}</span>`;
|
|
1645
|
+
case "hidden":
|
|
1646
|
+
return "";
|
|
1647
|
+
default:
|
|
1648
|
+
return `<span class="ff-print__filled-value">${value != null && value !== "" ? escapeHtml(String(value)) : "\u2014"}</span>`;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
function escapeHtml(str) {
|
|
1652
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1653
|
+
}
|
|
1654
|
+
var PRINT_STYLE_ID = "formfoundry-print-css";
|
|
1655
|
+
function injectPrintCSS() {
|
|
1656
|
+
if (typeof document === "undefined") return;
|
|
1657
|
+
if (document.getElementById(PRINT_STYLE_ID)) return;
|
|
1658
|
+
const style = document.createElement("style");
|
|
1659
|
+
style.id = PRINT_STYLE_ID;
|
|
1660
|
+
style.textContent = PRINT_CSS;
|
|
1661
|
+
document.head.appendChild(style);
|
|
1662
|
+
}
|
|
1663
|
+
var PRINT_CSS = `
|
|
1664
|
+
@media print {
|
|
1665
|
+
body > *:not([data-formfoundry-print-root]) {
|
|
1666
|
+
display: none !important;
|
|
1667
|
+
}
|
|
1668
|
+
[data-formfoundry-print-root] {
|
|
1669
|
+
display: block !important;
|
|
1670
|
+
position: static !important;
|
|
1671
|
+
width: 100% !important;
|
|
1672
|
+
background: white !important;
|
|
1673
|
+
color: black !important;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
.ff-print {
|
|
1677
|
+
font-family: 'Georgia', 'Times New Roman', serif;
|
|
1678
|
+
font-size: 12pt;
|
|
1679
|
+
line-height: 1.5;
|
|
1680
|
+
color: #000;
|
|
1681
|
+
background: #fff;
|
|
1682
|
+
padding: 0.5in;
|
|
1683
|
+
max-width: 8.5in;
|
|
1684
|
+
margin: 0 auto;
|
|
1685
|
+
}
|
|
1686
|
+
.ff-print__header {
|
|
1687
|
+
text-align: center;
|
|
1688
|
+
margin-bottom: 1.5rem;
|
|
1689
|
+
padding-bottom: 0.75rem;
|
|
1690
|
+
border-bottom: 2px solid #000;
|
|
1691
|
+
}
|
|
1692
|
+
.ff-print__title {
|
|
1693
|
+
font-size: 18pt;
|
|
1694
|
+
font-weight: 700;
|
|
1695
|
+
margin: 0 0 0.25rem 0;
|
|
1696
|
+
}
|
|
1697
|
+
.ff-print__subtitle {
|
|
1698
|
+
font-size: 10pt;
|
|
1699
|
+
color: #555;
|
|
1700
|
+
margin: 0;
|
|
1701
|
+
font-style: italic;
|
|
1702
|
+
}
|
|
1703
|
+
.ff-print__fields {
|
|
1704
|
+
list-style: none;
|
|
1705
|
+
padding: 0;
|
|
1706
|
+
margin: 0;
|
|
1707
|
+
}
|
|
1708
|
+
.ff-print__field {
|
|
1709
|
+
display: flex;
|
|
1710
|
+
align-items: baseline;
|
|
1711
|
+
padding: 0.4rem 0;
|
|
1712
|
+
border-bottom: 1px solid #e5e5e5;
|
|
1713
|
+
break-inside: avoid;
|
|
1714
|
+
page-break-inside: avoid;
|
|
1715
|
+
}
|
|
1716
|
+
.ff-print__field--textarea {
|
|
1717
|
+
flex-direction: column;
|
|
1718
|
+
align-items: stretch;
|
|
1719
|
+
}
|
|
1720
|
+
.ff-print__label {
|
|
1721
|
+
font-weight: 600;
|
|
1722
|
+
font-size: 10pt;
|
|
1723
|
+
min-width: 160px;
|
|
1724
|
+
flex-shrink: 0;
|
|
1725
|
+
color: #333;
|
|
1726
|
+
}
|
|
1727
|
+
.ff-print__value {
|
|
1728
|
+
flex: 1;
|
|
1729
|
+
font-size: 11pt;
|
|
1730
|
+
}
|
|
1731
|
+
.ff-print__blank-line {
|
|
1732
|
+
flex: 1;
|
|
1733
|
+
border-bottom: 1px solid #999;
|
|
1734
|
+
min-height: 1.2em;
|
|
1735
|
+
margin-left: 0.5rem;
|
|
1736
|
+
}
|
|
1737
|
+
.ff-print__blank-box {
|
|
1738
|
+
display: inline-block;
|
|
1739
|
+
width: 14px;
|
|
1740
|
+
height: 14px;
|
|
1741
|
+
border: 1.5px solid #666;
|
|
1742
|
+
vertical-align: middle;
|
|
1743
|
+
margin-right: 0.25rem;
|
|
1744
|
+
border-radius: 2px;
|
|
1745
|
+
}
|
|
1746
|
+
.ff-print__blank-radio {
|
|
1747
|
+
display: inline-block;
|
|
1748
|
+
width: 14px;
|
|
1749
|
+
height: 14px;
|
|
1750
|
+
border: 1.5px solid #666;
|
|
1751
|
+
vertical-align: middle;
|
|
1752
|
+
margin-right: 0.25rem;
|
|
1753
|
+
border-radius: 50%;
|
|
1754
|
+
}
|
|
1755
|
+
.ff-print__blank-textarea {
|
|
1756
|
+
border: 1px solid #999;
|
|
1757
|
+
min-height: 4em;
|
|
1758
|
+
margin-top: 0.35rem;
|
|
1759
|
+
border-radius: 2px;
|
|
1760
|
+
}
|
|
1761
|
+
.ff-print__blank-options {
|
|
1762
|
+
display: flex;
|
|
1763
|
+
flex-direction: column;
|
|
1764
|
+
gap: 0.25rem;
|
|
1765
|
+
margin-left: 0.5rem;
|
|
1766
|
+
}
|
|
1767
|
+
.ff-print__blank-option {
|
|
1768
|
+
display: flex;
|
|
1769
|
+
align-items: center;
|
|
1770
|
+
gap: 0.35rem;
|
|
1771
|
+
font-size: 10pt;
|
|
1772
|
+
}
|
|
1773
|
+
.ff-print__filled-value {
|
|
1774
|
+
font-size: 11pt;
|
|
1775
|
+
margin-left: 0.5rem;
|
|
1776
|
+
}
|
|
1777
|
+
.ff-print__filled-check {
|
|
1778
|
+
font-size: 13pt;
|
|
1779
|
+
margin-left: 0.5rem;
|
|
1780
|
+
}
|
|
1781
|
+
.ff-print__filled-textarea {
|
|
1782
|
+
white-space: pre-wrap;
|
|
1783
|
+
font-size: 10pt;
|
|
1784
|
+
margin-top: 0.35rem;
|
|
1785
|
+
padding: 0.35rem;
|
|
1786
|
+
background: #fafafa;
|
|
1787
|
+
border: 1px solid #e0e0e0;
|
|
1788
|
+
border-radius: 2px;
|
|
1789
|
+
min-height: 2em;
|
|
1790
|
+
}
|
|
1791
|
+
.ff-print__footer {
|
|
1792
|
+
margin-top: 2rem;
|
|
1793
|
+
padding-top: 0.75rem;
|
|
1794
|
+
border-top: 1px solid #ccc;
|
|
1795
|
+
font-size: 9pt;
|
|
1796
|
+
color: #888;
|
|
1797
|
+
text-align: center;
|
|
1798
|
+
}
|
|
1799
|
+
.ff-print--preview {
|
|
1800
|
+
border: 1px solid #ddd;
|
|
1801
|
+
border-radius: 8px;
|
|
1802
|
+
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
|
1803
|
+
margin: 1rem 0;
|
|
1804
|
+
}
|
|
1805
|
+
`;
|
|
1806
|
+
var InputRegistryContext = React4.createContext(null);
|
|
1807
|
+
function useInputRegistry() {
|
|
1808
|
+
const registry = React4.useContext(InputRegistryContext);
|
|
1809
|
+
if (!registry) {
|
|
1810
|
+
throw new Error(
|
|
1811
|
+
"[FormFoundry] useInputRegistry must be used within a <FormFoundryProvider>. Wrap your application or form tree with <FormFoundryProvider>."
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
return registry;
|
|
1815
|
+
}
|
|
1816
|
+
function createRegistry(entries) {
|
|
1817
|
+
const map = /* @__PURE__ */ new Map();
|
|
1818
|
+
if (entries) {
|
|
1819
|
+
for (const [type, component] of Object.entries(entries)) {
|
|
1820
|
+
map.set(type, component);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
return {
|
|
1824
|
+
register(type, component) {
|
|
1825
|
+
map.set(type, component);
|
|
1826
|
+
},
|
|
1827
|
+
get(type) {
|
|
1828
|
+
return map.get(type);
|
|
1829
|
+
},
|
|
1830
|
+
has(type) {
|
|
1831
|
+
return map.has(type);
|
|
1832
|
+
},
|
|
1833
|
+
keys() {
|
|
1834
|
+
return [...map.keys()];
|
|
1835
|
+
}
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
function createDefaultRegistry() {
|
|
1839
|
+
return createRegistry();
|
|
1840
|
+
}
|
|
1841
|
+
var FormFoundryProviderContext = React4.createContext({});
|
|
1842
|
+
function useFormFoundryProvider() {
|
|
1843
|
+
return React4.useContext(FormFoundryProviderContext);
|
|
1844
|
+
}
|
|
1845
|
+
function FormFoundryProvider({
|
|
1846
|
+
config,
|
|
1847
|
+
locale,
|
|
1848
|
+
registry,
|
|
1849
|
+
plugins,
|
|
1850
|
+
children
|
|
1851
|
+
}) {
|
|
1852
|
+
const inputRegistry = React4.useMemo(
|
|
1853
|
+
() => registry ?? createDefaultRegistry(),
|
|
1854
|
+
[registry]
|
|
1855
|
+
);
|
|
1856
|
+
const providerValue = React4.useMemo(
|
|
1857
|
+
() => ({
|
|
1858
|
+
config: config ? { ...config, locale } : locale ? { locale } : void 0,
|
|
1859
|
+
plugins
|
|
1860
|
+
}),
|
|
1861
|
+
[config, locale, plugins]
|
|
1862
|
+
);
|
|
1863
|
+
return /* @__PURE__ */ jsxRuntime.jsx(FormFoundryProviderContext.Provider, { value: providerValue, children: /* @__PURE__ */ jsxRuntime.jsx(InputRegistryContext.Provider, { value: inputRegistry, children }) });
|
|
1864
|
+
}
|
|
1865
|
+
function formReducer(state, action) {
|
|
1866
|
+
switch (action.type) {
|
|
1867
|
+
case "SET_VALUE":
|
|
1868
|
+
return {
|
|
1869
|
+
...state,
|
|
1870
|
+
values: { ...state.values, [action.name]: action.value }
|
|
1871
|
+
};
|
|
1872
|
+
case "SET_VALUES":
|
|
1873
|
+
return { ...state, values: action.values };
|
|
1874
|
+
case "SET_SUBMITTING":
|
|
1875
|
+
return { ...state, isSubmitting: action.isSubmitting };
|
|
1876
|
+
case "SHOW_ERRORS":
|
|
1877
|
+
return { ...state, showErrors: true };
|
|
1878
|
+
case "RESET":
|
|
1879
|
+
return {
|
|
1880
|
+
values: action.values,
|
|
1881
|
+
isSubmitting: false,
|
|
1882
|
+
showErrors: false,
|
|
1883
|
+
submitCount: 0,
|
|
1884
|
+
externalErrors: {}
|
|
1885
|
+
};
|
|
1886
|
+
case "INCREMENT_SUBMIT_COUNT":
|
|
1887
|
+
return { ...state, submitCount: state.submitCount + 1 };
|
|
1888
|
+
case "SET_EXTERNAL_ERRORS":
|
|
1889
|
+
return { ...state, externalErrors: action.errors };
|
|
1890
|
+
case "CLEAR_EXTERNAL_ERRORS":
|
|
1891
|
+
return { ...state, externalErrors: {} };
|
|
1892
|
+
default:
|
|
1893
|
+
return state;
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
function Form({
|
|
1897
|
+
id: formId,
|
|
1898
|
+
ref,
|
|
1899
|
+
onSubmit,
|
|
1900
|
+
onSubmitRaw,
|
|
1901
|
+
defaultValues: defaultValues2 = {},
|
|
1902
|
+
schema,
|
|
1903
|
+
resolver,
|
|
1904
|
+
plugins,
|
|
1905
|
+
validationBehavior,
|
|
1906
|
+
showErrors: showErrorsProp,
|
|
1907
|
+
disableOnSubmit = true,
|
|
1908
|
+
as: Component = "form",
|
|
1909
|
+
children,
|
|
1910
|
+
className
|
|
1911
|
+
}) {
|
|
1912
|
+
const providerConfig = useFormFoundryProvider();
|
|
1913
|
+
const [state, dispatch] = React4.useReducer(formReducer, {
|
|
1914
|
+
values: defaultValues2,
|
|
1915
|
+
isSubmitting: false,
|
|
1916
|
+
showErrors: false,
|
|
1917
|
+
submitCount: 0,
|
|
1918
|
+
externalErrors: {}
|
|
1919
|
+
});
|
|
1920
|
+
const [validationVersion, setValidationVersion] = React4.useState(0);
|
|
1921
|
+
const nodesRef = React4.useRef(/* @__PURE__ */ new Map());
|
|
1922
|
+
const validateCallbacksRef = React4.useRef(/* @__PURE__ */ new Map());
|
|
1923
|
+
const formNodeRef = React4.useRef(null);
|
|
1924
|
+
if (!formNodeRef.current) {
|
|
1925
|
+
formNodeRef.current = createNode({
|
|
1926
|
+
type: "group",
|
|
1927
|
+
name: "__form__",
|
|
1928
|
+
id: formId,
|
|
1929
|
+
value: defaultValues2,
|
|
1930
|
+
config: {
|
|
1931
|
+
validationBehavior: validationBehavior ?? providerConfig.config?.validationBehavior,
|
|
1932
|
+
plugins: [...providerConfig.plugins ?? [], ...plugins ?? []]
|
|
1933
|
+
}
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
const defaultValuesRef = React4.useRef(defaultValues2);
|
|
1937
|
+
defaultValuesRef.current = defaultValues2;
|
|
1938
|
+
const valuesContext = React4.useMemo(
|
|
1939
|
+
() => ({
|
|
1940
|
+
values: state.values,
|
|
1941
|
+
getValue: (name) => state.values[name],
|
|
1942
|
+
setValue: (name, value) => {
|
|
1943
|
+
dispatch({ type: "SET_VALUE", name, value });
|
|
1944
|
+
}
|
|
1945
|
+
}),
|
|
1946
|
+
[state.values]
|
|
1947
|
+
);
|
|
1948
|
+
const valuesRef = React4.useRef(state.values);
|
|
1949
|
+
valuesRef.current = state.values;
|
|
1950
|
+
const externalErrorsRef = React4.useRef(state.externalErrors);
|
|
1951
|
+
externalErrorsRef.current = state.externalErrors;
|
|
1952
|
+
const register = React4.useCallback((name, node) => {
|
|
1953
|
+
nodesRef.current.set(name, node);
|
|
1954
|
+
if (formNodeRef.current && !formNodeRef.current.children.includes(node)) {
|
|
1955
|
+
formNodeRef.current.addChild(node);
|
|
1956
|
+
}
|
|
1957
|
+
}, []);
|
|
1958
|
+
const updateValidateCallback = React4.useCallback((name, validate) => {
|
|
1959
|
+
const wrappedValidate = async () => {
|
|
1960
|
+
await validate();
|
|
1961
|
+
setValidationVersion((v) => v + 1);
|
|
1962
|
+
};
|
|
1963
|
+
validateCallbacksRef.current.set(name, wrappedValidate);
|
|
1964
|
+
}, []);
|
|
1965
|
+
const unregister = React4.useCallback((name) => {
|
|
1966
|
+
const node = nodesRef.current.get(name);
|
|
1967
|
+
if (node) {
|
|
1968
|
+
formNodeRef.current?.removeChild(node);
|
|
1969
|
+
}
|
|
1970
|
+
nodesRef.current.delete(name);
|
|
1971
|
+
validateCallbacksRef.current.delete(name);
|
|
1972
|
+
}, []);
|
|
1973
|
+
const getNode2 = React4.useCallback(
|
|
1974
|
+
(name) => nodesRef.current.get(name),
|
|
1975
|
+
[]
|
|
1976
|
+
);
|
|
1977
|
+
const triggerValidation = React4.useCallback(
|
|
1978
|
+
async (name) => {
|
|
1979
|
+
if (name) {
|
|
1980
|
+
const validateFn = validateCallbacksRef.current.get(name);
|
|
1981
|
+
if (validateFn) {
|
|
1982
|
+
await validateFn();
|
|
1983
|
+
}
|
|
1984
|
+
const node = nodesRef.current.get(name);
|
|
1985
|
+
return node ? node.valid : true;
|
|
1986
|
+
}
|
|
1987
|
+
const validationPromises = [];
|
|
1988
|
+
for (const validateFn of validateCallbacksRef.current.values()) {
|
|
1989
|
+
validationPromises.push(validateFn());
|
|
1990
|
+
}
|
|
1991
|
+
await Promise.all(validationPromises);
|
|
1992
|
+
let allValid = true;
|
|
1993
|
+
for (const node of nodesRef.current.values()) {
|
|
1994
|
+
if (!node.valid) {
|
|
1995
|
+
allValid = false;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
if (schema) {
|
|
1999
|
+
const formErrors = validateFormWithZod(schema, valuesRef.current);
|
|
2000
|
+
if (Object.keys(formErrors).length > 0) {
|
|
2001
|
+
allValid = false;
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
if (resolver) {
|
|
2005
|
+
const result = await resolver(valuesRef.current);
|
|
2006
|
+
if (Object.keys(result.errors).length > 0) {
|
|
2007
|
+
allValid = false;
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
return allValid;
|
|
2011
|
+
},
|
|
2012
|
+
[schema, resolver]
|
|
2013
|
+
);
|
|
2014
|
+
const reset = React4.useCallback((values) => {
|
|
2015
|
+
const resetValues = values ?? defaultValuesRef.current;
|
|
2016
|
+
for (const [fieldName, node] of nodesRef.current) {
|
|
2017
|
+
node.reset(resetValues[fieldName]);
|
|
2018
|
+
}
|
|
2019
|
+
dispatch({ type: "RESET", values: resetValues });
|
|
2020
|
+
}, []);
|
|
2021
|
+
const setExternalErrors = React4.useCallback((errors) => {
|
|
2022
|
+
const normalized = {};
|
|
2023
|
+
for (const [key, val] of Object.entries(errors)) {
|
|
2024
|
+
normalized[key] = Array.isArray(val) ? val : [val];
|
|
2025
|
+
}
|
|
2026
|
+
dispatch({ type: "SET_EXTERNAL_ERRORS", errors: normalized });
|
|
2027
|
+
}, []);
|
|
2028
|
+
const clearExternalErrors = React4.useCallback(() => {
|
|
2029
|
+
dispatch({ type: "CLEAR_EXTERNAL_ERRORS" });
|
|
2030
|
+
}, []);
|
|
2031
|
+
const aggregatedErrors = React4.useMemo(() => {
|
|
2032
|
+
const errors = {};
|
|
2033
|
+
for (const [name, node] of nodesRef.current) {
|
|
2034
|
+
if (node.errors.length > 0) {
|
|
2035
|
+
errors[name] = [...node.errors];
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
for (const [name, extErrors] of Object.entries(state.externalErrors)) {
|
|
2039
|
+
if (!errors[name]) {
|
|
2040
|
+
errors[name] = [];
|
|
2041
|
+
}
|
|
2042
|
+
errors[name].push(...extErrors);
|
|
2043
|
+
}
|
|
2044
|
+
return errors;
|
|
2045
|
+
}, [state.values, state.externalErrors, validationVersion]);
|
|
2046
|
+
const isValid = React4.useMemo(() => {
|
|
2047
|
+
for (const node of nodesRef.current.values()) {
|
|
2048
|
+
if (!node.valid) return false;
|
|
2049
|
+
}
|
|
2050
|
+
return Object.keys(state.externalErrors).length === 0;
|
|
2051
|
+
}, [state.values, state.externalErrors, validationVersion]);
|
|
2052
|
+
const isDisabled = disableOnSubmit && state.isSubmitting;
|
|
2053
|
+
const metaContext = React4.useMemo(
|
|
2054
|
+
() => ({
|
|
2055
|
+
isSubmitting: state.isSubmitting,
|
|
2056
|
+
isValid,
|
|
2057
|
+
errors: aggregatedErrors,
|
|
2058
|
+
register,
|
|
2059
|
+
unregister,
|
|
2060
|
+
updateValidateCallback,
|
|
2061
|
+
getNode: getNode2,
|
|
2062
|
+
triggerValidation,
|
|
2063
|
+
showErrors: state.showErrors || (showErrorsProp ?? false),
|
|
2064
|
+
config: {
|
|
2065
|
+
...providerConfig.config,
|
|
2066
|
+
validationBehavior: validationBehavior ?? providerConfig.config?.validationBehavior
|
|
2067
|
+
},
|
|
2068
|
+
formNode: formNodeRef.current,
|
|
2069
|
+
reset,
|
|
2070
|
+
submitCount: state.submitCount,
|
|
2071
|
+
setExternalErrors,
|
|
2072
|
+
clearExternalErrors,
|
|
2073
|
+
isDisabled
|
|
2074
|
+
}),
|
|
2075
|
+
[
|
|
2076
|
+
state.isSubmitting,
|
|
2077
|
+
state.showErrors,
|
|
2078
|
+
state.submitCount,
|
|
2079
|
+
state.externalErrors,
|
|
2080
|
+
isValid,
|
|
2081
|
+
isDisabled,
|
|
2082
|
+
aggregatedErrors,
|
|
2083
|
+
register,
|
|
2084
|
+
unregister,
|
|
2085
|
+
updateValidateCallback,
|
|
2086
|
+
getNode2,
|
|
2087
|
+
triggerValidation,
|
|
2088
|
+
showErrorsProp,
|
|
2089
|
+
providerConfig.config,
|
|
2090
|
+
validationBehavior,
|
|
2091
|
+
reset,
|
|
2092
|
+
setExternalErrors,
|
|
2093
|
+
clearExternalErrors
|
|
2094
|
+
]
|
|
2095
|
+
);
|
|
2096
|
+
const performSubmit = React4.useCallback(
|
|
2097
|
+
async () => {
|
|
2098
|
+
dispatch({ type: "INCREMENT_SUBMIT_COUNT" });
|
|
2099
|
+
onSubmitRaw?.(valuesRef.current);
|
|
2100
|
+
dispatch({ type: "SHOW_ERRORS" });
|
|
2101
|
+
const valid = await triggerValidation();
|
|
2102
|
+
if (!valid) return;
|
|
2103
|
+
dispatch({ type: "SET_SUBMITTING", isSubmitting: true });
|
|
2104
|
+
try {
|
|
2105
|
+
const formNode = formNodeRef.current;
|
|
2106
|
+
const currentValues = valuesRef.current;
|
|
2107
|
+
const values = formNode ? formNode.hooks.submit.dispatch({ ...currentValues }) : currentValues;
|
|
2108
|
+
await onSubmit(values);
|
|
2109
|
+
} finally {
|
|
2110
|
+
dispatch({ type: "SET_SUBMITTING", isSubmitting: false });
|
|
2111
|
+
}
|
|
2112
|
+
},
|
|
2113
|
+
[triggerValidation, onSubmit, onSubmitRaw]
|
|
2114
|
+
);
|
|
2115
|
+
const handleSubmit = React4.useCallback(
|
|
2116
|
+
async (e) => {
|
|
2117
|
+
e.preventDefault();
|
|
2118
|
+
await performSubmit();
|
|
2119
|
+
},
|
|
2120
|
+
[performSubmit]
|
|
2121
|
+
);
|
|
2122
|
+
React4.useEffect(() => {
|
|
2123
|
+
const formNode = formNodeRef.current;
|
|
2124
|
+
if (formNode) {
|
|
2125
|
+
formNode.submit = performSubmit;
|
|
2126
|
+
}
|
|
2127
|
+
}, [performSubmit]);
|
|
2128
|
+
const { printForm } = useFormPrint();
|
|
2129
|
+
React4.useImperativeHandle(ref, () => ({
|
|
2130
|
+
submit: performSubmit,
|
|
2131
|
+
reset,
|
|
2132
|
+
getValues: () => ({ ...valuesRef.current }),
|
|
2133
|
+
isValid: () => {
|
|
2134
|
+
for (const node of nodesRef.current.values()) {
|
|
2135
|
+
if (!node.valid) return false;
|
|
2136
|
+
}
|
|
2137
|
+
return Object.keys(externalErrorsRef.current).length === 0;
|
|
2138
|
+
},
|
|
2139
|
+
printForm
|
|
2140
|
+
}), [performSubmit, reset, printForm]);
|
|
2141
|
+
return /* @__PURE__ */ jsxRuntime.jsx(FormValuesContext.Provider, { value: valuesContext, children: /* @__PURE__ */ jsxRuntime.jsx(FormMetaContext.Provider, { value: metaContext, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2142
|
+
Component,
|
|
2143
|
+
{
|
|
2144
|
+
onSubmit: Component === "form" ? handleSubmit : void 0,
|
|
2145
|
+
className,
|
|
2146
|
+
noValidate: Component === "form" ? true : void 0,
|
|
2147
|
+
children
|
|
2148
|
+
}
|
|
2149
|
+
) }) });
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
// src/schema/expressions.ts
|
|
2153
|
+
var EXPRESSION_PREFIX = "$: ";
|
|
2154
|
+
function isExpression(value) {
|
|
2155
|
+
return typeof value === "string" && value.startsWith(EXPRESSION_PREFIX);
|
|
2156
|
+
}
|
|
2157
|
+
function isVariableRef(value) {
|
|
2158
|
+
return typeof value === "string" && value.startsWith("$") && !value.startsWith("$: ") && !value.startsWith("$el") && !value.startsWith("$cmp") && !value.startsWith("$formfoundry");
|
|
2159
|
+
}
|
|
2160
|
+
function tokenize(expr) {
|
|
2161
|
+
const tokens = [];
|
|
2162
|
+
let i = 0;
|
|
2163
|
+
while (i < expr.length) {
|
|
2164
|
+
const ch = expr[i];
|
|
2165
|
+
if (/\s/.test(ch)) {
|
|
2166
|
+
i++;
|
|
2167
|
+
continue;
|
|
2168
|
+
}
|
|
2169
|
+
if (/[0-9]/.test(ch) || ch === "." && i + 1 < expr.length && /[0-9]/.test(expr[i + 1])) {
|
|
2170
|
+
let num = "";
|
|
2171
|
+
while (i < expr.length && /[0-9.]/.test(expr[i])) {
|
|
2172
|
+
num += expr[i];
|
|
2173
|
+
i++;
|
|
2174
|
+
}
|
|
2175
|
+
tokens.push({ type: "number", value: parseFloat(num) });
|
|
2176
|
+
continue;
|
|
2177
|
+
}
|
|
2178
|
+
if (ch === "'" || ch === '"') {
|
|
2179
|
+
const quote = ch;
|
|
2180
|
+
i++;
|
|
2181
|
+
let str = "";
|
|
2182
|
+
while (i < expr.length && expr[i] !== quote) {
|
|
2183
|
+
if (expr[i] === "\\" && i + 1 < expr.length) {
|
|
2184
|
+
i++;
|
|
2185
|
+
str += expr[i];
|
|
2186
|
+
} else {
|
|
2187
|
+
str += expr[i];
|
|
2188
|
+
}
|
|
2189
|
+
i++;
|
|
2190
|
+
}
|
|
2191
|
+
i++;
|
|
2192
|
+
tokens.push({ type: "string", value: str });
|
|
2193
|
+
continue;
|
|
2194
|
+
}
|
|
2195
|
+
if (ch === "$") {
|
|
2196
|
+
i++;
|
|
2197
|
+
let varName = "";
|
|
2198
|
+
while (i < expr.length && /[a-zA-Z0-9_]/.test(expr[i])) {
|
|
2199
|
+
varName += expr[i];
|
|
2200
|
+
i++;
|
|
2201
|
+
}
|
|
2202
|
+
while (i < expr.length && expr[i] === "." && i + 1 < expr.length && /[a-zA-Z_]/.test(expr[i + 1])) {
|
|
2203
|
+
varName += ".";
|
|
2204
|
+
i++;
|
|
2205
|
+
while (i < expr.length && /[a-zA-Z0-9_]/.test(expr[i])) {
|
|
2206
|
+
varName += expr[i];
|
|
2207
|
+
i++;
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
tokens.push({ type: "variable", value: varName });
|
|
2211
|
+
continue;
|
|
2212
|
+
}
|
|
2213
|
+
if (expr.slice(i, i + 4) === "true" && (i + 4 >= expr.length || !/[a-zA-Z0-9_]/.test(expr[i + 4]))) {
|
|
2214
|
+
tokens.push({ type: "boolean", value: true });
|
|
2215
|
+
i += 4;
|
|
2216
|
+
continue;
|
|
2217
|
+
}
|
|
2218
|
+
if (expr.slice(i, i + 5) === "false" && (i + 5 >= expr.length || !/[a-zA-Z0-9_]/.test(expr[i + 5]))) {
|
|
2219
|
+
tokens.push({ type: "boolean", value: false });
|
|
2220
|
+
i += 5;
|
|
2221
|
+
continue;
|
|
2222
|
+
}
|
|
2223
|
+
if (expr.slice(i, i + 3) === "===" || expr.slice(i, i + 3) === "!==") {
|
|
2224
|
+
tokens.push({ type: "operator", value: expr.slice(i, i + 3) });
|
|
2225
|
+
i += 3;
|
|
2226
|
+
continue;
|
|
2227
|
+
}
|
|
2228
|
+
if (expr.slice(i, i + 2) === "&&" || expr.slice(i, i + 2) === "||" || expr.slice(i, i + 2) === ">=" || expr.slice(i, i + 2) === "<=") {
|
|
2229
|
+
tokens.push({ type: "operator", value: expr.slice(i, i + 2) });
|
|
2230
|
+
i += 2;
|
|
2231
|
+
continue;
|
|
2232
|
+
}
|
|
2233
|
+
if (ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === ">" || ch === "<") {
|
|
2234
|
+
tokens.push({ type: "operator", value: ch });
|
|
2235
|
+
i++;
|
|
2236
|
+
continue;
|
|
2237
|
+
}
|
|
2238
|
+
if (ch === ".") {
|
|
2239
|
+
tokens.push({ type: "dot", value: "." });
|
|
2240
|
+
i++;
|
|
2241
|
+
continue;
|
|
2242
|
+
}
|
|
2243
|
+
if (ch === "?") {
|
|
2244
|
+
tokens.push({ type: "ternary_q", value: "?" });
|
|
2245
|
+
i++;
|
|
2246
|
+
continue;
|
|
2247
|
+
}
|
|
2248
|
+
if (ch === ":") {
|
|
2249
|
+
tokens.push({ type: "ternary_c", value: ":" });
|
|
2250
|
+
i++;
|
|
2251
|
+
continue;
|
|
2252
|
+
}
|
|
2253
|
+
if (ch === "(" || ch === ")") {
|
|
2254
|
+
tokens.push({ type: "paren", value: ch });
|
|
2255
|
+
i++;
|
|
2256
|
+
continue;
|
|
2257
|
+
}
|
|
2258
|
+
if (ch === "!" && (i + 1 >= expr.length || expr[i + 1] !== "=")) {
|
|
2259
|
+
tokens.push({ type: "not", value: "!" });
|
|
2260
|
+
i++;
|
|
2261
|
+
continue;
|
|
2262
|
+
}
|
|
2263
|
+
i++;
|
|
2264
|
+
}
|
|
2265
|
+
return tokens;
|
|
2266
|
+
}
|
|
2267
|
+
var Parser = class {
|
|
2268
|
+
constructor(tokens) {
|
|
2269
|
+
this.tokens = tokens;
|
|
2270
|
+
}
|
|
2271
|
+
pos = 0;
|
|
2272
|
+
parse() {
|
|
2273
|
+
const ast = this.parseTernary();
|
|
2274
|
+
return ast;
|
|
2275
|
+
}
|
|
2276
|
+
peek() {
|
|
2277
|
+
return this.tokens[this.pos];
|
|
2278
|
+
}
|
|
2279
|
+
consume() {
|
|
2280
|
+
return this.tokens[this.pos++];
|
|
2281
|
+
}
|
|
2282
|
+
parseTernary() {
|
|
2283
|
+
let node = this.parseOr();
|
|
2284
|
+
if (this.peek()?.type === "ternary_q") {
|
|
2285
|
+
this.consume();
|
|
2286
|
+
const consequent = this.parseTernary();
|
|
2287
|
+
if (this.peek()?.type === "ternary_c") {
|
|
2288
|
+
this.consume();
|
|
2289
|
+
}
|
|
2290
|
+
const alternate = this.parseTernary();
|
|
2291
|
+
node = { kind: "ternary", condition: node, consequent, alternate };
|
|
2292
|
+
}
|
|
2293
|
+
return node;
|
|
2294
|
+
}
|
|
2295
|
+
parseOr() {
|
|
2296
|
+
let node = this.parseAnd();
|
|
2297
|
+
while (this.peek()?.type === "operator" && this.peek()?.value === "||") {
|
|
2298
|
+
const op = this.consume().value;
|
|
2299
|
+
const right = this.parseAnd();
|
|
2300
|
+
node = { kind: "binary", op, left: node, right };
|
|
2301
|
+
}
|
|
2302
|
+
return node;
|
|
2303
|
+
}
|
|
2304
|
+
parseAnd() {
|
|
2305
|
+
let node = this.parseEquality();
|
|
2306
|
+
while (this.peek()?.type === "operator" && this.peek()?.value === "&&") {
|
|
2307
|
+
const op = this.consume().value;
|
|
2308
|
+
const right = this.parseEquality();
|
|
2309
|
+
node = { kind: "binary", op, left: node, right };
|
|
2310
|
+
}
|
|
2311
|
+
return node;
|
|
2312
|
+
}
|
|
2313
|
+
parseEquality() {
|
|
2314
|
+
let node = this.parseComparison();
|
|
2315
|
+
while (this.peek()?.type === "operator" && (this.peek()?.value === "===" || this.peek()?.value === "!==")) {
|
|
2316
|
+
const op = this.consume().value;
|
|
2317
|
+
const right = this.parseComparison();
|
|
2318
|
+
node = { kind: "binary", op, left: node, right };
|
|
2319
|
+
}
|
|
2320
|
+
return node;
|
|
2321
|
+
}
|
|
2322
|
+
parseComparison() {
|
|
2323
|
+
let node = this.parseAdditive();
|
|
2324
|
+
while (this.peek()?.type === "operator" && (this.peek()?.value === ">" || this.peek()?.value === "<" || this.peek()?.value === ">=" || this.peek()?.value === "<=")) {
|
|
2325
|
+
const op = this.consume().value;
|
|
2326
|
+
const right = this.parseAdditive();
|
|
2327
|
+
node = { kind: "binary", op, left: node, right };
|
|
2328
|
+
}
|
|
2329
|
+
return node;
|
|
2330
|
+
}
|
|
2331
|
+
parseAdditive() {
|
|
2332
|
+
let node = this.parseMultiplicative();
|
|
2333
|
+
while (this.peek()?.type === "operator" && (this.peek()?.value === "+" || this.peek()?.value === "-")) {
|
|
2334
|
+
const op = this.consume().value;
|
|
2335
|
+
const right = this.parseMultiplicative();
|
|
2336
|
+
node = { kind: "binary", op, left: node, right };
|
|
2337
|
+
}
|
|
2338
|
+
return node;
|
|
2339
|
+
}
|
|
2340
|
+
parseMultiplicative() {
|
|
2341
|
+
let node = this.parseUnary();
|
|
2342
|
+
while (this.peek()?.type === "operator" && (this.peek()?.value === "*" || this.peek()?.value === "/" || this.peek()?.value === "%")) {
|
|
2343
|
+
const op = this.consume().value;
|
|
2344
|
+
const right = this.parseUnary();
|
|
2345
|
+
node = { kind: "binary", op, left: node, right };
|
|
2346
|
+
}
|
|
2347
|
+
return node;
|
|
2348
|
+
}
|
|
2349
|
+
parseUnary() {
|
|
2350
|
+
if (this.peek()?.type === "not") {
|
|
2351
|
+
this.consume();
|
|
2352
|
+
const operand = this.parseUnary();
|
|
2353
|
+
return { kind: "unary", op: "!", operand };
|
|
2354
|
+
}
|
|
2355
|
+
return this.parsePrimary();
|
|
2356
|
+
}
|
|
2357
|
+
parsePrimary() {
|
|
2358
|
+
const token = this.peek();
|
|
2359
|
+
if (!token) {
|
|
2360
|
+
return { kind: "literal", value: void 0 };
|
|
2361
|
+
}
|
|
2362
|
+
if (token.type === "paren" && token.value === "(") {
|
|
2363
|
+
this.consume();
|
|
2364
|
+
const node = this.parseTernary();
|
|
2365
|
+
if (this.peek()?.type === "paren" && this.peek()?.value === ")") {
|
|
2366
|
+
this.consume();
|
|
2367
|
+
}
|
|
2368
|
+
return node;
|
|
2369
|
+
}
|
|
2370
|
+
if (token.type === "number" || token.type === "string" || token.type === "boolean") {
|
|
2371
|
+
this.consume();
|
|
2372
|
+
return { kind: "literal", value: token.value };
|
|
2373
|
+
}
|
|
2374
|
+
if (token.type === "variable") {
|
|
2375
|
+
this.consume();
|
|
2376
|
+
const path = token.value.split(".");
|
|
2377
|
+
return { kind: "variable", path };
|
|
2378
|
+
}
|
|
2379
|
+
this.consume();
|
|
2380
|
+
return { kind: "literal", value: void 0 };
|
|
2381
|
+
}
|
|
2382
|
+
};
|
|
2383
|
+
function resolvePath(data, path) {
|
|
2384
|
+
let current = data;
|
|
2385
|
+
for (const segment of path) {
|
|
2386
|
+
if (current == null || typeof current !== "object") return void 0;
|
|
2387
|
+
current = current[segment];
|
|
2388
|
+
}
|
|
2389
|
+
return current;
|
|
2390
|
+
}
|
|
2391
|
+
function evaluate(ast, data) {
|
|
2392
|
+
switch (ast.kind) {
|
|
2393
|
+
case "literal":
|
|
2394
|
+
return ast.value;
|
|
2395
|
+
case "variable":
|
|
2396
|
+
return resolvePath(data, ast.path);
|
|
2397
|
+
case "unary": {
|
|
2398
|
+
const val = evaluate(ast.operand, data);
|
|
2399
|
+
if (ast.op === "!") return !val;
|
|
2400
|
+
return val;
|
|
2401
|
+
}
|
|
2402
|
+
case "binary": {
|
|
2403
|
+
const left = evaluate(ast.left, data);
|
|
2404
|
+
const right = evaluate(ast.right, data);
|
|
2405
|
+
switch (ast.op) {
|
|
2406
|
+
case "+": {
|
|
2407
|
+
if (typeof left === "string" || typeof right === "string") {
|
|
2408
|
+
return String(left ?? "") + String(right ?? "");
|
|
2409
|
+
}
|
|
2410
|
+
return left + right;
|
|
2411
|
+
}
|
|
2412
|
+
case "-":
|
|
2413
|
+
return left - right;
|
|
2414
|
+
case "*":
|
|
2415
|
+
return left * right;
|
|
2416
|
+
case "/":
|
|
2417
|
+
return right !== 0 ? left / right : 0;
|
|
2418
|
+
case "%":
|
|
2419
|
+
return right !== 0 ? left % right : 0;
|
|
2420
|
+
case "===":
|
|
2421
|
+
return left === right;
|
|
2422
|
+
case "!==":
|
|
2423
|
+
return left !== right;
|
|
2424
|
+
case ">":
|
|
2425
|
+
return left > right;
|
|
2426
|
+
case "<":
|
|
2427
|
+
return left < right;
|
|
2428
|
+
case ">=":
|
|
2429
|
+
return left >= right;
|
|
2430
|
+
case "<=":
|
|
2431
|
+
return left <= right;
|
|
2432
|
+
case "&&":
|
|
2433
|
+
return left && right;
|
|
2434
|
+
case "||":
|
|
2435
|
+
return left || right;
|
|
2436
|
+
default:
|
|
2437
|
+
return void 0;
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
case "ternary": {
|
|
2441
|
+
const condition = evaluate(ast.condition, data);
|
|
2442
|
+
return condition ? evaluate(ast.consequent, data) : evaluate(ast.alternate, data);
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
function evaluateExpression(expression, data) {
|
|
2447
|
+
const raw = expression.startsWith(EXPRESSION_PREFIX) ? expression.slice(EXPRESSION_PREFIX.length) : expression;
|
|
2448
|
+
const tokens = tokenize(raw);
|
|
2449
|
+
const parser = new Parser(tokens);
|
|
2450
|
+
const ast = parser.parse();
|
|
2451
|
+
return evaluate(ast, data);
|
|
2452
|
+
}
|
|
2453
|
+
function resolveVariableRef(ref, data) {
|
|
2454
|
+
const path = ref.slice(1).split(".");
|
|
2455
|
+
return resolvePath(data, path);
|
|
2456
|
+
}
|
|
2457
|
+
function resolveSchemaValue(value, data) {
|
|
2458
|
+
if (isExpression(value)) {
|
|
2459
|
+
return evaluateExpression(value, data);
|
|
2460
|
+
}
|
|
2461
|
+
if (isVariableRef(value)) {
|
|
2462
|
+
return resolveVariableRef(value, data);
|
|
2463
|
+
}
|
|
2464
|
+
return value;
|
|
2465
|
+
}
|
|
2466
|
+
var SchemaDataContext = React4.createContext({
|
|
2467
|
+
values: {},
|
|
2468
|
+
data: {},
|
|
2469
|
+
merged: {}
|
|
2470
|
+
});
|
|
2471
|
+
function useSchemaData() {
|
|
2472
|
+
return React4.useContext(SchemaDataContext);
|
|
2473
|
+
}
|
|
2474
|
+
function SchemaRenderer({ schema, library, data = {} }) {
|
|
2475
|
+
const registry = useInputRegistry();
|
|
2476
|
+
const { values } = useFormValues();
|
|
2477
|
+
const schemaDataContext = React4.useMemo(
|
|
2478
|
+
() => ({
|
|
2479
|
+
values,
|
|
2480
|
+
data,
|
|
2481
|
+
merged: { ...values, ...data }
|
|
2482
|
+
}),
|
|
2483
|
+
[values, data]
|
|
2484
|
+
);
|
|
2485
|
+
return /* @__PURE__ */ jsxRuntime.jsx(SchemaDataContext.Provider, { value: schemaDataContext, children: schema.map((node, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
2486
|
+
SchemaNodeRenderer,
|
|
2487
|
+
{
|
|
2488
|
+
node,
|
|
2489
|
+
registry,
|
|
2490
|
+
library
|
|
2491
|
+
},
|
|
2492
|
+
getNodeKey(node, index)
|
|
2493
|
+
)) });
|
|
2494
|
+
}
|
|
2495
|
+
function SchemaNodeRenderer({
|
|
2496
|
+
node,
|
|
2497
|
+
registry,
|
|
2498
|
+
library
|
|
2499
|
+
}) {
|
|
2500
|
+
const { merged } = useSchemaData();
|
|
2501
|
+
if ("for" in node && Array.isArray(node.for)) {
|
|
2502
|
+
return renderLoop(node, registry, library, merged);
|
|
2503
|
+
}
|
|
2504
|
+
if ("then" in node && "if" in node && !("$formfoundry" in node) && !("$el" in node) && !("$cmp" in node)) {
|
|
2505
|
+
return renderConditional(
|
|
2506
|
+
node,
|
|
2507
|
+
registry,
|
|
2508
|
+
library,
|
|
2509
|
+
merged
|
|
2510
|
+
);
|
|
2511
|
+
}
|
|
2512
|
+
if ("if" in node && typeof node.if === "function" && !node.if(merged)) {
|
|
2513
|
+
return null;
|
|
2514
|
+
}
|
|
2515
|
+
if ("$formfoundry" in node) {
|
|
2516
|
+
return renderFormFoundryInput(node, registry, merged);
|
|
2517
|
+
}
|
|
2518
|
+
if ("$el" in node) {
|
|
2519
|
+
return renderHtmlElement(
|
|
2520
|
+
node,
|
|
2521
|
+
registry,
|
|
2522
|
+
library,
|
|
2523
|
+
merged
|
|
2524
|
+
);
|
|
2525
|
+
}
|
|
2526
|
+
if ("$cmp" in node) {
|
|
2527
|
+
return renderComponent(
|
|
2528
|
+
node,
|
|
2529
|
+
registry,
|
|
2530
|
+
library,
|
|
2531
|
+
merged
|
|
2532
|
+
);
|
|
2533
|
+
}
|
|
2534
|
+
return null;
|
|
2535
|
+
}
|
|
2536
|
+
function renderFormFoundryInput(node, registry, merged = {}) {
|
|
2537
|
+
const { $formfoundry, if: _if, ...rawProps } = node;
|
|
2538
|
+
const Component = registry.get($formfoundry);
|
|
2539
|
+
if (!Component) {
|
|
2540
|
+
if (process.env.NODE_ENV !== "production") {
|
|
2541
|
+
console.warn(
|
|
2542
|
+
`[FormFoundry] No input registered for type "${$formfoundry}". Available types: ${registry.keys().join(", ") || "(none)"}`
|
|
2543
|
+
);
|
|
2544
|
+
}
|
|
2545
|
+
return null;
|
|
2546
|
+
}
|
|
2547
|
+
const props = resolveObjectExpressions(rawProps, merged);
|
|
2548
|
+
return /* @__PURE__ */ jsxRuntime.jsx(Component, { ...props });
|
|
2549
|
+
}
|
|
2550
|
+
function renderHtmlElement(node, registry, library, merged = {}) {
|
|
2551
|
+
const { $el, attrs: rawAttrs, children, if: _if } = node;
|
|
2552
|
+
const attrs = rawAttrs ? resolveObjectExpressions(rawAttrs, merged) : void 0;
|
|
2553
|
+
const childContent = resolveChildren(children, registry, library, merged);
|
|
2554
|
+
return React4__default.default.createElement($el, attrs, childContent);
|
|
2555
|
+
}
|
|
2556
|
+
function renderComponent(node, registry, library, merged = {}) {
|
|
2557
|
+
const { $cmp, props: rawProps, children, if: _if } = node;
|
|
2558
|
+
const Component = library?.[$cmp];
|
|
2559
|
+
if (!Component) {
|
|
2560
|
+
if (process.env.NODE_ENV !== "production") {
|
|
2561
|
+
console.warn(
|
|
2562
|
+
`[FormFoundry] No component found for "$cmp: ${$cmp}" in library.`
|
|
2563
|
+
);
|
|
2564
|
+
}
|
|
2565
|
+
return null;
|
|
2566
|
+
}
|
|
2567
|
+
const resolvedProps = rawProps ? resolveObjectExpressions(rawProps, merged) : {};
|
|
2568
|
+
const childContent = resolveChildren(children, registry, library, merged);
|
|
2569
|
+
return /* @__PURE__ */ jsxRuntime.jsx(Component, { ...resolvedProps, children: childContent });
|
|
2570
|
+
}
|
|
2571
|
+
function renderConditional(node, registry, library, merged) {
|
|
2572
|
+
const branch = node.if(merged) ? node.then : node.else;
|
|
2573
|
+
if (!branch) return null;
|
|
2574
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: branch.map((child, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
2575
|
+
SchemaNodeRenderer,
|
|
2576
|
+
{
|
|
2577
|
+
node: child,
|
|
2578
|
+
registry,
|
|
2579
|
+
library
|
|
2580
|
+
},
|
|
2581
|
+
getNodeKey(child, index)
|
|
2582
|
+
)) });
|
|
2583
|
+
}
|
|
2584
|
+
function renderLoop(node, registry, library, merged) {
|
|
2585
|
+
const [itemName, indexName, source] = node.for;
|
|
2586
|
+
const items = typeof source === "string" ? merged[source] ?? [] : source ?? [];
|
|
2587
|
+
if (!Array.isArray(items)) return null;
|
|
2588
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: items.map((item, idx) => {
|
|
2589
|
+
const loopData = {
|
|
2590
|
+
...merged,
|
|
2591
|
+
[itemName]: item,
|
|
2592
|
+
[indexName]: idx
|
|
2593
|
+
};
|
|
2594
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2595
|
+
SchemaDataContext.Provider,
|
|
2596
|
+
{
|
|
2597
|
+
value: { values: {}, data: loopData, merged: loopData },
|
|
2598
|
+
children: node.children.map((child, childIdx) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
2599
|
+
SchemaNodeRenderer,
|
|
2600
|
+
{
|
|
2601
|
+
node: child,
|
|
2602
|
+
registry,
|
|
2603
|
+
library
|
|
2604
|
+
},
|
|
2605
|
+
getNodeKey(child, childIdx)
|
|
2606
|
+
))
|
|
2607
|
+
},
|
|
2608
|
+
`loop_${idx}`
|
|
2609
|
+
);
|
|
2610
|
+
}) });
|
|
2611
|
+
}
|
|
2612
|
+
function resolveObjectExpressions(obj, data) {
|
|
2613
|
+
const resolved = {};
|
|
2614
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
2615
|
+
resolved[key] = resolveSchemaValue(value, data);
|
|
2616
|
+
}
|
|
2617
|
+
return resolved;
|
|
2618
|
+
}
|
|
2619
|
+
function resolveChildren(children, registry, library, merged) {
|
|
2620
|
+
if (children === void 0) return null;
|
|
2621
|
+
if (typeof children === "string") {
|
|
2622
|
+
if (isExpression(children) || isVariableRef(children)) {
|
|
2623
|
+
return String(resolveSchemaValue(children, merged) ?? "");
|
|
2624
|
+
}
|
|
2625
|
+
return children;
|
|
2626
|
+
}
|
|
2627
|
+
return children.map((child, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
2628
|
+
SchemaNodeRenderer,
|
|
2629
|
+
{
|
|
2630
|
+
node: child,
|
|
2631
|
+
registry,
|
|
2632
|
+
library
|
|
2633
|
+
},
|
|
2634
|
+
getNodeKey(child, index)
|
|
2635
|
+
));
|
|
2636
|
+
}
|
|
2637
|
+
function getNodeKey(node, index) {
|
|
2638
|
+
if ("name" in node && typeof node.name === "string") {
|
|
2639
|
+
return node.name;
|
|
2640
|
+
}
|
|
2641
|
+
if ("$el" in node) {
|
|
2642
|
+
return `el_${index}`;
|
|
2643
|
+
}
|
|
2644
|
+
if ("$cmp" in node) {
|
|
2645
|
+
return `cmp_${node.$cmp}_${index}`;
|
|
2646
|
+
}
|
|
2647
|
+
return `node_${index}`;
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
// src/plugins/index.ts
|
|
2651
|
+
function applyPlugins(node, plugins) {
|
|
2652
|
+
const cleanups = [];
|
|
2653
|
+
for (const plugin of plugins) {
|
|
2654
|
+
const cleanup = plugin(node);
|
|
2655
|
+
if (cleanup) {
|
|
2656
|
+
cleanups.push(cleanup);
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
return cleanups;
|
|
2660
|
+
}
|
|
2661
|
+
function createDebugPlugin(prefix = "[FormFoundry]") {
|
|
2662
|
+
return (node) => {
|
|
2663
|
+
const unsubscribe = node.hooks.commit.use((value, next) => {
|
|
2664
|
+
console.log(`${prefix} ${node.name} committed:`, value);
|
|
2665
|
+
return next(value);
|
|
2666
|
+
});
|
|
2667
|
+
return unsubscribe;
|
|
2668
|
+
};
|
|
2669
|
+
}
|
|
2670
|
+
function createAutoTrimPlugin() {
|
|
2671
|
+
return (node) => {
|
|
2672
|
+
const unsubscribe = node.hooks.commit.use((value, next) => {
|
|
2673
|
+
if (typeof value === "string") {
|
|
2674
|
+
return next(value.trim());
|
|
2675
|
+
}
|
|
2676
|
+
return next(value);
|
|
2677
|
+
});
|
|
2678
|
+
return unsubscribe;
|
|
2679
|
+
};
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
// src/resolvers/zodResolver.ts
|
|
2683
|
+
function zodResolver(schema) {
|
|
2684
|
+
return async (values) => {
|
|
2685
|
+
const result = schema.safeParse(values);
|
|
2686
|
+
if (result.success) {
|
|
2687
|
+
return { errors: {} };
|
|
2688
|
+
}
|
|
2689
|
+
const errors = {};
|
|
2690
|
+
for (const issue of result.error.issues) {
|
|
2691
|
+
const fieldName = issue.path[0];
|
|
2692
|
+
if (typeof fieldName === "string") {
|
|
2693
|
+
if (!errors[fieldName]) {
|
|
2694
|
+
errors[fieldName] = [];
|
|
2695
|
+
}
|
|
2696
|
+
errors[fieldName].push(issue.message);
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
return { errors };
|
|
2700
|
+
};
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
// src/i18n/en.ts
|
|
2704
|
+
var en = {
|
|
2705
|
+
code: "en",
|
|
2706
|
+
name: "English",
|
|
2707
|
+
messages: {
|
|
2708
|
+
// ─── Core rules ────────────────────────────────────────────────────
|
|
2709
|
+
required: "{label} is required.",
|
|
2710
|
+
email: "{label} must be a valid email address.",
|
|
2711
|
+
url: "{label} must be a valid URL.",
|
|
2712
|
+
min: "{label} must be at least {0}.",
|
|
2713
|
+
max: "{label} must be at most {0}.",
|
|
2714
|
+
minLength: "{label} must be at least {0} characters.",
|
|
2715
|
+
maxLength: "{label} must be at most {0} characters.",
|
|
2716
|
+
matches: "{label} does not match the required pattern.",
|
|
2717
|
+
confirm: "{label} does not match.",
|
|
2718
|
+
alpha: "{label} must contain only alphabetical characters.",
|
|
2719
|
+
alphanumeric: "{label} must contain only alphanumeric characters.",
|
|
2720
|
+
date: "{label} must be a valid date.",
|
|
2721
|
+
between: "{label} must be between {0} and {1}.",
|
|
2722
|
+
number: "{label} must be a number.",
|
|
2723
|
+
// ─── Extended rules ────────────────────────────────────────────────
|
|
2724
|
+
accepted: "{label} must be accepted.",
|
|
2725
|
+
alpha_spaces: "{label} must contain only letters and spaces.",
|
|
2726
|
+
contains_alpha: "{label} must contain at least one letter.",
|
|
2727
|
+
contains_alphanumeric: "{label} must contain at least one letter or number.",
|
|
2728
|
+
contains_alpha_spaces: "{label} must contain at least one letter or space.",
|
|
2729
|
+
contains_lowercase: "{label} must contain at least one lowercase letter.",
|
|
2730
|
+
contains_uppercase: "{label} must contain at least one uppercase letter.",
|
|
2731
|
+
contains_numeric: "{label} must contain at least one number.",
|
|
2732
|
+
contains_symbol: "{label} must contain at least one symbol.",
|
|
2733
|
+
date_after: "{label} must be after {0}.",
|
|
2734
|
+
date_before: "{label} must be before {0}.",
|
|
2735
|
+
date_between: "{label} must be between {0} and {1}.",
|
|
2736
|
+
date_format: "{label} must match the format {0}.",
|
|
2737
|
+
ends_with: ({ label, args }) => `${label} must end with ${args.join(", ") || "?"}.`,
|
|
2738
|
+
is: ({ label, args }) => `${label} must be one of: ${args.join(", ") || "?"}.`,
|
|
2739
|
+
length: ({ label, args }) => args[1] ? `${label} must be between ${args[0]} and ${args[1]} characters.` : `${label} must be exactly ${args[0] ?? "?"} characters.`,
|
|
2740
|
+
lowercase: "{label} must be all lowercase.",
|
|
2741
|
+
not: ({ label, args }) => `${label} must not be: ${args.join(", ") || "?"}.`,
|
|
2742
|
+
require_one: "At least one of these fields is required.",
|
|
2743
|
+
starts_with: ({ label, args }) => `${label} must start with ${args.join(", ") || "?"}.`,
|
|
2744
|
+
symbol: "{label} must contain only symbols.",
|
|
2745
|
+
uppercase: "{label} must be all uppercase."
|
|
2746
|
+
}
|
|
2747
|
+
};
|
|
2748
|
+
function FormPrintButton({
|
|
2749
|
+
options,
|
|
2750
|
+
children = "Print",
|
|
2751
|
+
className,
|
|
2752
|
+
disabled
|
|
2753
|
+
}) {
|
|
2754
|
+
const { printForm } = useFormPrint(options);
|
|
2755
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2756
|
+
"button",
|
|
2757
|
+
{
|
|
2758
|
+
type: "button",
|
|
2759
|
+
onClick: () => printForm(),
|
|
2760
|
+
className,
|
|
2761
|
+
disabled,
|
|
2762
|
+
"data-formfoundry": "print-button",
|
|
2763
|
+
children
|
|
2764
|
+
}
|
|
2765
|
+
);
|
|
2766
|
+
}
|
|
2767
|
+
function FormPrintView({
|
|
2768
|
+
fields,
|
|
2769
|
+
mode,
|
|
2770
|
+
title,
|
|
2771
|
+
subtitle,
|
|
2772
|
+
footer,
|
|
2773
|
+
className,
|
|
2774
|
+
preview = false
|
|
2775
|
+
}) {
|
|
2776
|
+
const resolvedMode = mode === "current" ? "filled" : mode;
|
|
2777
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `ff-print${preview ? " ff-print--preview" : ""}${className ? ` ${className}` : ""}`, children: [
|
|
2778
|
+
(title || subtitle) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-print__header", children: [
|
|
2779
|
+
title && /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "ff-print__title", children: title }),
|
|
2780
|
+
subtitle && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ff-print__subtitle", children: subtitle })
|
|
2781
|
+
] }),
|
|
2782
|
+
/* @__PURE__ */ jsxRuntime.jsx("ul", { className: "ff-print__fields", children: fields.map((field) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
2783
|
+
PrintField,
|
|
2784
|
+
{
|
|
2785
|
+
field,
|
|
2786
|
+
mode: resolvedMode
|
|
2787
|
+
},
|
|
2788
|
+
field.name
|
|
2789
|
+
)) }),
|
|
2790
|
+
footer && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-print__footer", children: footer })
|
|
2791
|
+
] });
|
|
2792
|
+
}
|
|
2793
|
+
function PrintField({ field, mode }) {
|
|
2794
|
+
const isTextarea = field.type === "textarea";
|
|
2795
|
+
const label = field.label || field.name;
|
|
2796
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: `ff-print__field${isTextarea ? " ff-print__field--textarea" : ""}`, children: [
|
|
2797
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ff-print__label", children: [
|
|
2798
|
+
label,
|
|
2799
|
+
":"
|
|
2800
|
+
] }),
|
|
2801
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-print__value", children: mode === "blank" ? renderBlank(field) : renderFilled(field) })
|
|
2802
|
+
] });
|
|
2803
|
+
}
|
|
2804
|
+
function renderBlank(field) {
|
|
2805
|
+
switch (field.type) {
|
|
2806
|
+
case "checkbox":
|
|
2807
|
+
case "switch":
|
|
2808
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-print__blank-box" });
|
|
2809
|
+
case "radio":
|
|
2810
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-print__blank-options", children: (field.options ?? []).map((opt) => /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ff-print__blank-option", children: [
|
|
2811
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-print__blank-radio" }),
|
|
2812
|
+
opt.label
|
|
2813
|
+
] }, opt.value)) });
|
|
2814
|
+
case "select":
|
|
2815
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-print__blank-options", children: (field.options ?? []).map((opt) => /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ff-print__blank-option", children: [
|
|
2816
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-print__blank-box" }),
|
|
2817
|
+
opt.label
|
|
2818
|
+
] }, opt.value)) });
|
|
2819
|
+
case "textarea":
|
|
2820
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-print__blank-textarea" });
|
|
2821
|
+
case "checkboxgroup":
|
|
2822
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-print__blank-options", children: (field.options ?? []).map((opt) => /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ff-print__blank-option", children: [
|
|
2823
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-print__blank-box" }),
|
|
2824
|
+
opt.label
|
|
2825
|
+
] }, opt.value)) });
|
|
2826
|
+
// text, email, password, url, tel, number, date, color, slider, file, hidden
|
|
2827
|
+
default:
|
|
2828
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-print__blank-line" });
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
function renderFilled(field) {
|
|
2832
|
+
const { value, type } = field;
|
|
2833
|
+
switch (type) {
|
|
2834
|
+
case "checkbox":
|
|
2835
|
+
case "switch":
|
|
2836
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-print__filled-check", children: value ? "\u2611" : "\u2610" });
|
|
2837
|
+
case "radio":
|
|
2838
|
+
case "select": {
|
|
2839
|
+
const selected = field.options?.find(
|
|
2840
|
+
(opt) => String(opt.value) === String(value)
|
|
2841
|
+
);
|
|
2842
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-print__filled-value", children: selected?.label ?? String(value ?? "\u2014") });
|
|
2843
|
+
}
|
|
2844
|
+
case "checkboxgroup": {
|
|
2845
|
+
const selectedValues = Array.isArray(value) ? value : [];
|
|
2846
|
+
const selectedLabels = (field.options ?? []).filter((opt) => selectedValues.includes(opt.value)).map((opt) => opt.label);
|
|
2847
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-print__filled-value", children: selectedLabels.length > 0 ? selectedLabels.join(", ") : "\u2014" });
|
|
2848
|
+
}
|
|
2849
|
+
case "textarea":
|
|
2850
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-print__filled-textarea", children: String(value ?? "") });
|
|
2851
|
+
case "file":
|
|
2852
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-print__filled-value", children: value ? String(value) : "No file selected" });
|
|
2853
|
+
case "color":
|
|
2854
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-print__filled-value", children: value ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2855
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2856
|
+
"span",
|
|
2857
|
+
{
|
|
2858
|
+
style: {
|
|
2859
|
+
display: "inline-block",
|
|
2860
|
+
width: 14,
|
|
2861
|
+
height: 14,
|
|
2862
|
+
backgroundColor: String(value),
|
|
2863
|
+
border: "1px solid #999",
|
|
2864
|
+
verticalAlign: "middle",
|
|
2865
|
+
marginRight: 6,
|
|
2866
|
+
borderRadius: 2
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
),
|
|
2870
|
+
String(value)
|
|
2871
|
+
] }) : "\u2014" });
|
|
2872
|
+
case "hidden":
|
|
2873
|
+
return null;
|
|
2874
|
+
// text, email, password, url, tel, number, date, slider
|
|
2875
|
+
default:
|
|
2876
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-print__filled-value", children: value != null && value !== "" ? String(value) : "\u2014" });
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
function PrintOverlay({
|
|
2880
|
+
fields,
|
|
2881
|
+
mode,
|
|
2882
|
+
title,
|
|
2883
|
+
subtitle,
|
|
2884
|
+
footer,
|
|
2885
|
+
className,
|
|
2886
|
+
open = true
|
|
2887
|
+
}) {
|
|
2888
|
+
if (!open || fields.length === 0) return null;
|
|
2889
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2890
|
+
"div",
|
|
2891
|
+
{
|
|
2892
|
+
"data-formfoundry-print-root": "",
|
|
2893
|
+
style: { display: "none" },
|
|
2894
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2895
|
+
FormPrintView,
|
|
2896
|
+
{
|
|
2897
|
+
fields,
|
|
2898
|
+
mode,
|
|
2899
|
+
title,
|
|
2900
|
+
subtitle,
|
|
2901
|
+
footer,
|
|
2902
|
+
className
|
|
2903
|
+
}
|
|
2904
|
+
)
|
|
2905
|
+
}
|
|
2906
|
+
);
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
exports.EXPRESSION_PREFIX = EXPRESSION_PREFIX;
|
|
2910
|
+
exports.Form = Form;
|
|
2911
|
+
exports.FormFoundryProvider = FormFoundryProvider;
|
|
2912
|
+
exports.FormMetaContext = FormMetaContext;
|
|
2913
|
+
exports.FormPrintButton = FormPrintButton;
|
|
2914
|
+
exports.FormPrintView = FormPrintView;
|
|
2915
|
+
exports.FormValuesContext = FormValuesContext;
|
|
2916
|
+
exports.InputRegistryContext = InputRegistryContext;
|
|
2917
|
+
exports.PrintOverlay = PrintOverlay;
|
|
2918
|
+
exports.SchemaRenderer = SchemaRenderer;
|
|
2919
|
+
exports.applyPlugins = applyPlugins;
|
|
2920
|
+
exports.builtInRules = builtInRules;
|
|
2921
|
+
exports.clearGlobalRegistry = clearGlobalRegistry;
|
|
2922
|
+
exports.clearRegistryListeners = clearRegistryListeners;
|
|
2923
|
+
exports.createAutoTrimPlugin = createAutoTrimPlugin;
|
|
2924
|
+
exports.createDebugPlugin = createDebugPlugin;
|
|
2925
|
+
exports.createDefaultRegistry = createDefaultRegistry;
|
|
2926
|
+
exports.createEventEmitter = createEventEmitter;
|
|
2927
|
+
exports.createHooks = createHooks;
|
|
2928
|
+
exports.createLedger = createLedger;
|
|
2929
|
+
exports.createMessage = createMessage;
|
|
2930
|
+
exports.createMessageStore = createMessageStore;
|
|
2931
|
+
exports.createNode = createNode;
|
|
2932
|
+
exports.createRegistry = createRegistry;
|
|
2933
|
+
exports.defaultMessages = defaultMessages;
|
|
2934
|
+
exports.enLocale = en;
|
|
2935
|
+
exports.evaluateExpression = evaluateExpression;
|
|
2936
|
+
exports.getActiveLocale = getActiveLocale;
|
|
2937
|
+
exports.getLocale = getLocale;
|
|
2938
|
+
exports.getNode = getNode;
|
|
2939
|
+
exports.getRegisteredLocales = getRegisteredLocales;
|
|
2940
|
+
exports.getRegisteredNodeIds = getRegisteredNodeIds;
|
|
2941
|
+
exports.getRule = getRule;
|
|
2942
|
+
exports.hasRule = hasRule;
|
|
2943
|
+
exports.interpolate = interpolate;
|
|
2944
|
+
exports.isExpression = isExpression;
|
|
2945
|
+
exports.isVariableRef = isVariableRef;
|
|
2946
|
+
exports.parseRules = parseRules;
|
|
2947
|
+
exports.registerLocale = registerLocale;
|
|
2948
|
+
exports.registerNode = registerNode;
|
|
2949
|
+
exports.registerRule = registerRule;
|
|
2950
|
+
exports.resolveConfig = resolveConfig;
|
|
2951
|
+
exports.resolveLocaleMessage = resolveLocaleMessage;
|
|
2952
|
+
exports.resolveMessage = resolveMessage;
|
|
2953
|
+
exports.resolveSchemaValue = resolveSchemaValue;
|
|
2954
|
+
exports.resolveVariableRef = resolveVariableRef;
|
|
2955
|
+
exports.runValidation = runValidation;
|
|
2956
|
+
exports.setActiveLocale = setActiveLocale;
|
|
2957
|
+
exports.subscribeToRegistry = subscribeToRegistry;
|
|
2958
|
+
exports.unregisterNode = unregisterNode;
|
|
2959
|
+
exports.unregisterRule = unregisterRule;
|
|
2960
|
+
exports.useFormContext = useFormContext;
|
|
2961
|
+
exports.useFormFoundryField = useFormFoundryField;
|
|
2962
|
+
exports.useFormMeta = useFormMeta;
|
|
2963
|
+
exports.useFormNode = useFormNode;
|
|
2964
|
+
exports.useFormNodes = useFormNodes;
|
|
2965
|
+
exports.useFormPrint = useFormPrint;
|
|
2966
|
+
exports.useFormValues = useFormValues;
|
|
2967
|
+
exports.useInputRegistry = useInputRegistry;
|
|
2968
|
+
exports.validateFormWithZod = validateFormWithZod;
|
|
2969
|
+
exports.validateWithZod = validateWithZod;
|
|
2970
|
+
exports.zodResolver = zodResolver;
|
|
2971
|
+
//# sourceMappingURL=index.cjs.map
|
|
2972
|
+
//# sourceMappingURL=index.cjs.map
|