cogsbox-state 0.5.472 → 0.5.474
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -18
- package/dist/CogsState.d.ts +98 -82
- package/dist/CogsState.d.ts.map +1 -1
- package/dist/CogsState.jsx +1030 -960
- package/dist/CogsState.jsx.map +1 -1
- package/dist/Components.d.ts.map +1 -1
- package/dist/Components.jsx +299 -219
- package/dist/Components.jsx.map +1 -1
- package/dist/PluginRunner.d.ts +10 -0
- package/dist/PluginRunner.d.ts.map +1 -0
- package/dist/PluginRunner.jsx +122 -0
- package/dist/PluginRunner.jsx.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -26
- package/dist/index.js.map +1 -1
- package/dist/pluginStore.d.ts +81 -0
- package/dist/pluginStore.d.ts.map +1 -0
- package/dist/pluginStore.js +52 -0
- package/dist/pluginStore.js.map +1 -0
- package/dist/plugins.d.ts +1323 -0
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +76 -0
- package/dist/plugins.js.map +1 -0
- package/dist/store.d.ts +50 -15
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +509 -470
- package/dist/store.js.map +1 -1
- package/dist/utility.d.ts +1 -1
- package/dist/utility.d.ts.map +1 -1
- package/dist/utility.js +12 -12
- package/dist/utility.js.map +1 -1
- package/dist/validation.d.ts +7 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +39 -0
- package/dist/validation.js.map +1 -0
- package/package.json +13 -3
- package/src/CogsState.tsx +657 -457
- package/src/Components.tsx +291 -194
- package/src/PluginRunner.tsx +203 -0
- package/src/index.ts +2 -0
- package/src/pluginStore.ts +176 -0
- package/src/plugins.ts +544 -0
- package/src/store.ts +748 -493
- package/src/utility.ts +31 -31
- package/src/validation.ts +84 -0
package/src/Components.tsx
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
FormElementParams,
|
|
3
|
+
StateObject,
|
|
4
|
+
UpdateTypeDetail,
|
|
5
|
+
type FormOptsType,
|
|
6
|
+
} from './CogsState';
|
|
7
|
+
import { pluginStore } from './pluginStore';
|
|
8
|
+
import { createMetadataContext, toDeconstructedMethods } from './plugins';
|
|
2
9
|
import React, {
|
|
3
10
|
memo,
|
|
4
11
|
RefObject,
|
|
@@ -7,16 +14,14 @@ import React, {
|
|
|
7
14
|
useLayoutEffect,
|
|
8
15
|
useRef,
|
|
9
16
|
useState,
|
|
17
|
+
useMemo,
|
|
10
18
|
} from 'react';
|
|
11
|
-
import {
|
|
12
|
-
formRefStore,
|
|
13
|
-
getGlobalStore,
|
|
14
|
-
ValidationError,
|
|
15
|
-
ValidationSeverity,
|
|
16
|
-
} from './store';
|
|
19
|
+
import { getGlobalStore, ValidationError, ValidationSeverity } from './store';
|
|
17
20
|
import { useInView } from 'react-intersection-observer';
|
|
18
21
|
import { v4 as uuidv4 } from 'uuid';
|
|
19
22
|
import { isDeepEqual } from './utility';
|
|
23
|
+
import { runValidation } from './validation';
|
|
24
|
+
|
|
20
25
|
const {
|
|
21
26
|
getInitialOptions,
|
|
22
27
|
|
|
@@ -30,6 +35,8 @@ const {
|
|
|
30
35
|
notifyPathSubscribers,
|
|
31
36
|
subscribeToPath,
|
|
32
37
|
} = getGlobalStore.getState();
|
|
38
|
+
const { stateHandlers, notifyFormUpdate } = pluginStore.getState();
|
|
39
|
+
|
|
33
40
|
export type ValidationWrapperProps = {
|
|
34
41
|
formOpts?: FormOptsType;
|
|
35
42
|
path: string[];
|
|
@@ -223,21 +230,31 @@ export function FormElementWrapper({
|
|
|
223
230
|
setState: any;
|
|
224
231
|
}) {
|
|
225
232
|
const componentId = useRef(uuidv4()).current;
|
|
226
|
-
const [, forceUpdate] = useState({});
|
|
227
233
|
|
|
234
|
+
const [, forceUpdate] = useState({});
|
|
235
|
+
const formElementRef = useRef<any>(null);
|
|
228
236
|
const stateKeyPathKey = [stateKey, ...path].join('.');
|
|
229
237
|
useRegisterComponent(stateKey, componentId, forceUpdate);
|
|
230
|
-
|
|
231
238
|
// Get the shadow node to access typeInfo and schema
|
|
232
239
|
const shadowNode = getGlobalStore.getState().getShadowNode(stateKey, path);
|
|
233
240
|
const typeInfo = shadowNode?._meta?.typeInfo;
|
|
234
|
-
const fieldSchema = typeInfo?.schema; // The actual Zod schema for this field
|
|
235
241
|
|
|
236
242
|
const globalStateValue = getShadowValue(stateKey, path);
|
|
237
243
|
const [localValue, setLocalValue] = useState<any>(globalStateValue);
|
|
238
244
|
const isCurrentlyDebouncing = useRef(false);
|
|
239
245
|
const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
240
246
|
|
|
247
|
+
// 2. Memoize the list of active form wrappers to avoid re-calculating on every render.
|
|
248
|
+
const activeFormWrappers = useMemo(() => {
|
|
249
|
+
return (
|
|
250
|
+
pluginStore
|
|
251
|
+
.getState()
|
|
252
|
+
.getPluginConfigsForState(stateKey)
|
|
253
|
+
// We only care about plugins that have defined a formWrapper
|
|
254
|
+
.filter((config) => typeof config.plugin.formWrapper === 'function')
|
|
255
|
+
);
|
|
256
|
+
}, [stateKey]);
|
|
257
|
+
|
|
241
258
|
useEffect(() => {
|
|
242
259
|
if (
|
|
243
260
|
!isCurrentlyDebouncing.current &&
|
|
@@ -248,6 +265,42 @@ export function FormElementWrapper({
|
|
|
248
265
|
}, [globalStateValue]);
|
|
249
266
|
|
|
250
267
|
useEffect(() => {
|
|
268
|
+
const { getShadowMetadata, setShadowMetadata } = getGlobalStore.getState();
|
|
269
|
+
|
|
270
|
+
// Initialize clientActivityState if needed
|
|
271
|
+
const currentMeta = getShadowMetadata(stateKey, path) || {};
|
|
272
|
+
if (!currentMeta.clientActivityState) {
|
|
273
|
+
currentMeta.clientActivityState = { elements: new Map() };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Detect element type from the ref
|
|
277
|
+
const detectElementType = () => {
|
|
278
|
+
const el = formElementRef.current;
|
|
279
|
+
if (!el) return 'input';
|
|
280
|
+
const tagName = el.tagName.toLowerCase();
|
|
281
|
+
if (tagName === 'textarea') return 'textarea';
|
|
282
|
+
if (tagName === 'select') return 'select';
|
|
283
|
+
if (tagName === 'input') {
|
|
284
|
+
const type = (el as HTMLInputElement).type;
|
|
285
|
+
if (type === 'checkbox') return 'checkbox';
|
|
286
|
+
if (type === 'radio') return 'radio';
|
|
287
|
+
if (type === 'range') return 'range';
|
|
288
|
+
if (type === 'file') return 'file';
|
|
289
|
+
}
|
|
290
|
+
return 'input';
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// Add this element to the Map
|
|
294
|
+
currentMeta.clientActivityState.elements.set(componentId, {
|
|
295
|
+
domRef: formElementRef,
|
|
296
|
+
elementType: detectElementType(),
|
|
297
|
+
inputType: formElementRef.current?.type,
|
|
298
|
+
mountedAt: Date.now(),
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
setShadowMetadata(stateKey, path, currentMeta);
|
|
302
|
+
|
|
303
|
+
// Subscribe to path updates
|
|
251
304
|
const unsubscribe = getGlobalStore
|
|
252
305
|
.getState()
|
|
253
306
|
.subscribeToPath(stateKeyPathKey, (newValue) => {
|
|
@@ -255,182 +308,28 @@ export function FormElementWrapper({
|
|
|
255
308
|
forceUpdate({});
|
|
256
309
|
}
|
|
257
310
|
});
|
|
311
|
+
|
|
312
|
+
// Cleanup
|
|
258
313
|
return () => {
|
|
259
314
|
unsubscribe();
|
|
315
|
+
|
|
260
316
|
if (debounceTimeoutRef.current) {
|
|
261
317
|
clearTimeout(debounceTimeoutRef.current);
|
|
262
318
|
isCurrentlyDebouncing.current = false;
|
|
263
319
|
}
|
|
264
|
-
};
|
|
265
|
-
}, []);
|
|
266
320
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
.getShadowMetadata(stateKey, []);
|
|
273
|
-
if (!rootMeta?.features?.validationEnabled) return;
|
|
274
|
-
|
|
275
|
-
const validationOptions = getInitialOptions(stateKey)?.validation;
|
|
276
|
-
|
|
277
|
-
if (!validationOptions) return;
|
|
278
|
-
|
|
279
|
-
const currentMeta = getShadowMetadata(stateKey, path) || {};
|
|
280
|
-
const currentStatus = currentMeta?.validation?.status;
|
|
281
|
-
|
|
282
|
-
let shouldValidate = false;
|
|
283
|
-
let severity: 'error' | 'warning' | undefined;
|
|
284
|
-
console.log('trigger', trigger, validationOptions);
|
|
285
|
-
if (trigger === 'onBlur' && validationOptions.onBlur) {
|
|
286
|
-
shouldValidate = true;
|
|
287
|
-
severity = validationOptions.onBlur ?? 'error';
|
|
288
|
-
} else if (trigger === 'onChange') {
|
|
289
|
-
if (validationOptions.onChange) {
|
|
290
|
-
shouldValidate = true;
|
|
291
|
-
severity = validationOptions.onChange;
|
|
292
|
-
} else if (currentStatus === 'INVALID') {
|
|
293
|
-
shouldValidate = true;
|
|
294
|
-
severity = 'warning';
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (!shouldValidate) return;
|
|
299
|
-
|
|
300
|
-
let validationResult: { success: boolean; message?: string } | null =
|
|
301
|
-
null;
|
|
302
|
-
console.log(
|
|
303
|
-
'shouldValidate 33',
|
|
304
|
-
path,
|
|
305
|
-
fieldSchema,
|
|
306
|
-
shouldValidate,
|
|
307
|
-
value,
|
|
308
|
-
typeof value
|
|
309
|
-
);
|
|
310
|
-
if (fieldSchema && shouldValidate) {
|
|
311
|
-
// Direct field validation using its own schema
|
|
312
|
-
const result = fieldSchema.safeParse(value);
|
|
313
|
-
|
|
314
|
-
if (!result.success) {
|
|
315
|
-
const errors =
|
|
316
|
-
'issues' in result.error
|
|
317
|
-
? result.error.issues
|
|
318
|
-
: (result.error as any).errors;
|
|
319
|
-
|
|
320
|
-
validationResult = {
|
|
321
|
-
success: false,
|
|
322
|
-
message: errors[0]?.message || 'Invalid value',
|
|
323
|
-
};
|
|
324
|
-
} else {
|
|
325
|
-
validationResult = { success: true };
|
|
326
|
-
}
|
|
327
|
-
} else {
|
|
328
|
-
// Fallback: validate using the entire schema
|
|
329
|
-
const zodSchema =
|
|
330
|
-
validationOptions.zodSchemaV4 || validationOptions.zodSchemaV3;
|
|
331
|
-
if (!zodSchema) return;
|
|
332
|
-
|
|
333
|
-
// Create a test state with the new value at the correct path
|
|
334
|
-
const fullState = getShadowValue(stateKey, []);
|
|
335
|
-
const testState = JSON.parse(JSON.stringify(fullState)); // Deep clone
|
|
336
|
-
|
|
337
|
-
// Set the value at the correct path
|
|
338
|
-
let current = testState;
|
|
339
|
-
for (let i = 0; i < path.length - 1; i++) {
|
|
340
|
-
if (!current[path[i]!]) current[path[i]!] = {};
|
|
341
|
-
current = current[path[i]!];
|
|
342
|
-
}
|
|
343
|
-
if (path.length > 0) {
|
|
344
|
-
current[path[path.length - 1]!] = value;
|
|
345
|
-
} else {
|
|
346
|
-
// Root level update
|
|
347
|
-
Object.assign(testState, value);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const result = zodSchema.safeParse(testState);
|
|
351
|
-
|
|
352
|
-
if (!result.success) {
|
|
353
|
-
const errors =
|
|
354
|
-
'issues' in result.error
|
|
355
|
-
? result.error.issues
|
|
356
|
-
: (result.error as any).errors;
|
|
357
|
-
|
|
358
|
-
// Find errors for this specific path
|
|
359
|
-
const pathErrors = errors.filter((error: any) => {
|
|
360
|
-
// Handle array paths with id: prefixes
|
|
361
|
-
if (path.some((p) => p.startsWith('id:'))) {
|
|
362
|
-
const parentPath = path[0]!.startsWith('id:')
|
|
363
|
-
? []
|
|
364
|
-
: path.slice(0, -1);
|
|
365
|
-
const arrayMeta = getGlobalStore
|
|
366
|
-
.getState()
|
|
367
|
-
.getShadowMetadata(stateKey, parentPath);
|
|
368
|
-
|
|
369
|
-
if (arrayMeta?.arrayKeys) {
|
|
370
|
-
const itemKey = path.slice(0, -1).join('.');
|
|
371
|
-
const itemIndex = arrayMeta.arrayKeys.findIndex(
|
|
372
|
-
(k) => k === path[path.length - 2]
|
|
373
|
-
);
|
|
374
|
-
const zodPath = [...parentPath, itemIndex, ...path.slice(-1)];
|
|
375
|
-
return JSON.stringify(error.path) === JSON.stringify(zodPath);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
return JSON.stringify(error.path) === JSON.stringify(path);
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
if (pathErrors.length > 0) {
|
|
383
|
-
validationResult = {
|
|
384
|
-
success: false,
|
|
385
|
-
message: pathErrors[0]?.message,
|
|
386
|
-
};
|
|
387
|
-
} else {
|
|
388
|
-
validationResult = { success: true };
|
|
389
|
-
}
|
|
390
|
-
} else {
|
|
391
|
-
validationResult = { success: true };
|
|
392
|
-
}
|
|
321
|
+
// Remove element from Map
|
|
322
|
+
const meta = getGlobalStore.getState().getShadowMetadata(stateKey, path);
|
|
323
|
+
if (meta?.clientActivityState?.elements) {
|
|
324
|
+
meta.clientActivityState.elements.delete(componentId);
|
|
325
|
+
setShadowMetadata(stateKey, path, meta);
|
|
393
326
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
if (validationResult) {
|
|
397
|
-
if (!validationResult.success) {
|
|
398
|
-
setShadowMetadata(stateKey, path, {
|
|
399
|
-
...currentMeta,
|
|
400
|
-
validation: {
|
|
401
|
-
status: 'INVALID',
|
|
402
|
-
errors: [
|
|
403
|
-
{
|
|
404
|
-
source: 'client' as const,
|
|
405
|
-
message: validationResult.message!,
|
|
406
|
-
severity: severity!,
|
|
407
|
-
},
|
|
408
|
-
],
|
|
409
|
-
lastValidated: Date.now(),
|
|
410
|
-
validatedValue: value,
|
|
411
|
-
},
|
|
412
|
-
});
|
|
413
|
-
} else {
|
|
414
|
-
setShadowMetadata(stateKey, path, {
|
|
415
|
-
...currentMeta,
|
|
416
|
-
validation: {
|
|
417
|
-
status: 'VALID',
|
|
418
|
-
errors: [],
|
|
419
|
-
lastValidated: Date.now(),
|
|
420
|
-
validatedValue: value,
|
|
421
|
-
},
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
forceUpdate({});
|
|
426
|
-
},
|
|
427
|
-
[stateKey, path, fieldSchema]
|
|
428
|
-
);
|
|
327
|
+
};
|
|
328
|
+
}, []);
|
|
429
329
|
|
|
430
330
|
const debouncedUpdate = useCallback(
|
|
431
331
|
(newValue: any) => {
|
|
432
|
-
//
|
|
433
|
-
|
|
332
|
+
// Type conversion logic (keep existing)
|
|
434
333
|
if (typeInfo) {
|
|
435
334
|
if (typeInfo.type === 'number' && typeof newValue === 'string') {
|
|
436
335
|
newValue =
|
|
@@ -448,10 +347,7 @@ export function FormElementWrapper({
|
|
|
448
347
|
newValue = new Date(newValue);
|
|
449
348
|
}
|
|
450
349
|
} else {
|
|
451
|
-
// Fallback to old behavior if no typeInfo
|
|
452
|
-
|
|
453
350
|
const currentType = typeof globalStateValue;
|
|
454
|
-
|
|
455
351
|
if (currentType === 'number' && typeof newValue === 'string') {
|
|
456
352
|
newValue = newValue === '' ? 0 : Number(newValue);
|
|
457
353
|
}
|
|
@@ -459,45 +355,164 @@ export function FormElementWrapper({
|
|
|
459
355
|
|
|
460
356
|
setLocalValue(newValue);
|
|
461
357
|
|
|
462
|
-
//
|
|
463
|
-
|
|
358
|
+
// Update input activity details
|
|
359
|
+
const { getShadowMetadata, setShadowMetadata } =
|
|
360
|
+
getGlobalStore.getState();
|
|
361
|
+
const meta = getShadowMetadata(stateKey, path);
|
|
362
|
+
if (meta?.clientActivityState?.elements?.has(componentId)) {
|
|
363
|
+
const element = meta.clientActivityState.elements.get(componentId);
|
|
364
|
+
if (element && element.currentActivity?.type === 'focus') {
|
|
365
|
+
element!.currentActivity.details = {
|
|
366
|
+
...element!.currentActivity.details,
|
|
367
|
+
value: newValue,
|
|
368
|
+
previousValue:
|
|
369
|
+
element!.currentActivity.details?.value || globalStateValue,
|
|
370
|
+
inputLength:
|
|
371
|
+
typeof newValue === 'string' ? newValue.length : undefined,
|
|
372
|
+
keystrokeCount:
|
|
373
|
+
(element!.currentActivity.details?.keystrokeCount || 0) + 1,
|
|
374
|
+
};
|
|
375
|
+
setShadowMetadata(stateKey, path, meta);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const element = meta?.clientActivityState?.elements?.get(componentId);
|
|
464
379
|
|
|
380
|
+
// Notify plugins
|
|
381
|
+
notifyFormUpdate({
|
|
382
|
+
stateKey,
|
|
383
|
+
activityType: 'input', // Changed from 'type'
|
|
384
|
+
path,
|
|
385
|
+
timestamp: Date.now(),
|
|
386
|
+
details: {
|
|
387
|
+
value: newValue,
|
|
388
|
+
inputLength:
|
|
389
|
+
typeof newValue === 'string' ? newValue.length : undefined,
|
|
390
|
+
isComposing: false, // You'd need to track this from the actual input event
|
|
391
|
+
isPasting: false, // You'd need to track this from paste events
|
|
392
|
+
keystrokeCount:
|
|
393
|
+
(element?.currentActivity?.details?.keystrokeCount || 0) + 1,
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
// Validation (keep existing)
|
|
397
|
+
const virtualOperation: UpdateTypeDetail = {
|
|
398
|
+
stateKey,
|
|
399
|
+
path,
|
|
400
|
+
newValue: newValue,
|
|
401
|
+
updateType: 'update',
|
|
402
|
+
timeStamp: Date.now(),
|
|
403
|
+
status: 'new',
|
|
404
|
+
oldValue: globalStateValue,
|
|
405
|
+
};
|
|
406
|
+
runValidation(virtualOperation, 'onChange');
|
|
407
|
+
|
|
408
|
+
// Debounce state update (keep existing)
|
|
465
409
|
isCurrentlyDebouncing.current = true;
|
|
466
|
-
|
|
467
410
|
if (debounceTimeoutRef.current) {
|
|
468
411
|
clearTimeout(debounceTimeoutRef.current);
|
|
469
412
|
}
|
|
470
413
|
|
|
471
414
|
const debounceTime = formOpts?.debounceTime ?? 200;
|
|
472
|
-
|
|
473
|
-
// Debounce only the state update, not the validation
|
|
474
415
|
debounceTimeoutRef.current = setTimeout(() => {
|
|
475
416
|
isCurrentlyDebouncing.current = false;
|
|
476
|
-
setState(newValue, path, {
|
|
417
|
+
setState(newValue, path, {
|
|
418
|
+
updateType: 'update',
|
|
419
|
+
validationTrigger: 'onChange',
|
|
420
|
+
});
|
|
477
421
|
}, debounceTime);
|
|
478
422
|
},
|
|
479
423
|
[
|
|
480
424
|
setState,
|
|
481
425
|
path,
|
|
482
426
|
formOpts?.debounceTime,
|
|
483
|
-
validateField,
|
|
484
427
|
typeInfo,
|
|
485
428
|
globalStateValue,
|
|
429
|
+
stateKey,
|
|
430
|
+
componentId,
|
|
486
431
|
]
|
|
487
432
|
);
|
|
488
433
|
|
|
434
|
+
const handleFocus = useCallback(() => {
|
|
435
|
+
const { getShadowMetadata, setShadowMetadata } = getGlobalStore.getState();
|
|
436
|
+
|
|
437
|
+
// Update element's current activity
|
|
438
|
+
const meta = getShadowMetadata(stateKey, path);
|
|
439
|
+
if (meta?.clientActivityState?.elements?.has(componentId)) {
|
|
440
|
+
const element = meta.clientActivityState.elements.get(componentId)!;
|
|
441
|
+
element.currentActivity = {
|
|
442
|
+
type: 'focus',
|
|
443
|
+
startTime: Date.now(),
|
|
444
|
+
details: {
|
|
445
|
+
value: localValue,
|
|
446
|
+
inputLength:
|
|
447
|
+
typeof localValue === 'string' ? localValue.length : undefined,
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
setShadowMetadata(stateKey, path, meta);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Notify plugins
|
|
454
|
+
notifyFormUpdate({
|
|
455
|
+
stateKey,
|
|
456
|
+
activityType: 'focus', // Changed from 'type'
|
|
457
|
+
path,
|
|
458
|
+
timestamp: Date.now(),
|
|
459
|
+
details: {
|
|
460
|
+
cursorPosition: formElementRef.current?.selectionStart,
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
}, [stateKey, path, componentId, localValue]);
|
|
489
464
|
const handleBlur = useCallback(() => {
|
|
490
|
-
|
|
465
|
+
const { getShadowMetadata, setShadowMetadata } = getGlobalStore.getState();
|
|
466
|
+
|
|
467
|
+
// Clear debounce if active
|
|
491
468
|
if (debounceTimeoutRef.current) {
|
|
492
469
|
clearTimeout(debounceTimeoutRef.current);
|
|
493
470
|
debounceTimeoutRef.current = null;
|
|
494
471
|
isCurrentlyDebouncing.current = false;
|
|
495
|
-
setState(localValue, path, {
|
|
472
|
+
setState(localValue, path, {
|
|
473
|
+
updateType: 'update',
|
|
474
|
+
validationTrigger: 'onBlur',
|
|
475
|
+
});
|
|
496
476
|
}
|
|
497
477
|
|
|
498
|
-
//
|
|
499
|
-
|
|
500
|
-
|
|
478
|
+
// Clear element's current activity
|
|
479
|
+
const meta = getShadowMetadata(stateKey, path);
|
|
480
|
+
if (meta?.clientActivityState?.elements?.has(componentId)) {
|
|
481
|
+
const element = meta.clientActivityState.elements.get(componentId)!;
|
|
482
|
+
element.currentActivity = undefined;
|
|
483
|
+
setShadowMetadata(stateKey, path, meta);
|
|
484
|
+
}
|
|
485
|
+
const focusStartTime =
|
|
486
|
+
meta?.clientActivityState?.elements?.get(componentId)?.currentActivity
|
|
487
|
+
?.startTime;
|
|
488
|
+
|
|
489
|
+
// Notify plugins
|
|
490
|
+
notifyFormUpdate({
|
|
491
|
+
stateKey,
|
|
492
|
+
activityType: 'blur', // Changed from 'type'
|
|
493
|
+
path,
|
|
494
|
+
timestamp: Date.now(),
|
|
495
|
+
duration: focusStartTime ? Date.now() - focusStartTime : undefined,
|
|
496
|
+
details: {
|
|
497
|
+
duration: focusStartTime ? Date.now() - focusStartTime : 0,
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// Run validation if configured
|
|
502
|
+
const validationOptions = getInitialOptions(stateKey)?.validation;
|
|
503
|
+
if (validationOptions?.onBlur) {
|
|
504
|
+
const virtualOperation: UpdateTypeDetail = {
|
|
505
|
+
stateKey,
|
|
506
|
+
path,
|
|
507
|
+
newValue: localValue,
|
|
508
|
+
updateType: 'update',
|
|
509
|
+
timeStamp: Date.now(),
|
|
510
|
+
status: 'new',
|
|
511
|
+
oldValue: globalStateValue,
|
|
512
|
+
};
|
|
513
|
+
runValidation(virtualOperation, 'onBlur');
|
|
514
|
+
}
|
|
515
|
+
}, [localValue, setState, path, stateKey, componentId, globalStateValue]);
|
|
501
516
|
|
|
502
517
|
const baseState = rebuildStateShape({
|
|
503
518
|
path: path,
|
|
@@ -513,10 +528,9 @@ export function FormElementWrapper({
|
|
|
513
528
|
onChange: (e: any) => {
|
|
514
529
|
debouncedUpdate(e.target.value);
|
|
515
530
|
},
|
|
531
|
+
onFocus: handleFocus,
|
|
516
532
|
onBlur: handleBlur,
|
|
517
|
-
ref:
|
|
518
|
-
.getState()
|
|
519
|
-
.getFormRef(stateKey + '.' + path.join('.')),
|
|
533
|
+
ref: formElementRef,
|
|
520
534
|
};
|
|
521
535
|
}
|
|
522
536
|
|
|
@@ -524,9 +538,25 @@ export function FormElementWrapper({
|
|
|
524
538
|
},
|
|
525
539
|
});
|
|
526
540
|
|
|
541
|
+
const initialElement = renderFn(stateWithInputProps);
|
|
542
|
+
|
|
543
|
+
const wrappedElement = activeFormWrappers.reduceRight(
|
|
544
|
+
(currentElement, config, index) => (
|
|
545
|
+
<PluginWrapper
|
|
546
|
+
stateKey={stateKey}
|
|
547
|
+
path={path}
|
|
548
|
+
pluginName={config.plugin.name}
|
|
549
|
+
wrapperDepth={activeFormWrappers.length - 1 - index}
|
|
550
|
+
>
|
|
551
|
+
{currentElement}
|
|
552
|
+
</PluginWrapper>
|
|
553
|
+
),
|
|
554
|
+
initialElement
|
|
555
|
+
);
|
|
556
|
+
|
|
527
557
|
return (
|
|
528
558
|
<ValidationWrapper formOpts={formOpts} path={path} stateKey={stateKey}>
|
|
529
|
-
{
|
|
559
|
+
{wrappedElement}
|
|
530
560
|
</ValidationWrapper>
|
|
531
561
|
);
|
|
532
562
|
}
|
|
@@ -637,3 +667,70 @@ export function IsolatedComponentWrapper({
|
|
|
637
667
|
|
|
638
668
|
return <>{renderFn(baseState)}</>;
|
|
639
669
|
}
|
|
670
|
+
|
|
671
|
+
// 1. Define the MINIMAL props needed.
|
|
672
|
+
type PluginWrapperProps = {
|
|
673
|
+
children: React.ReactNode;
|
|
674
|
+
stateKey: string;
|
|
675
|
+
path: string[];
|
|
676
|
+
pluginName: string;
|
|
677
|
+
wrapperDepth: number;
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
const PluginWrapper = memo(function PluginWrapper({
|
|
681
|
+
children,
|
|
682
|
+
stateKey,
|
|
683
|
+
path,
|
|
684
|
+
pluginName,
|
|
685
|
+
wrapperDepth,
|
|
686
|
+
}: PluginWrapperProps) {
|
|
687
|
+
const [, forceUpdate] = useState({});
|
|
688
|
+
|
|
689
|
+
useEffect(() => {
|
|
690
|
+
const fullPathKey = [stateKey, ...path].join('.');
|
|
691
|
+
const unsubscribe = getGlobalStore
|
|
692
|
+
.getState()
|
|
693
|
+
.subscribeToPath(fullPathKey, () => {
|
|
694
|
+
forceUpdate({});
|
|
695
|
+
});
|
|
696
|
+
return unsubscribe;
|
|
697
|
+
}, [stateKey, path]);
|
|
698
|
+
|
|
699
|
+
const plugin = pluginStore
|
|
700
|
+
.getState()
|
|
701
|
+
.registeredPlugins.find((p) => p.name === pluginName);
|
|
702
|
+
|
|
703
|
+
const stateHandler: StateObject<any> | undefined = pluginStore
|
|
704
|
+
.getState()
|
|
705
|
+
.stateHandlers.get(stateKey);
|
|
706
|
+
|
|
707
|
+
const typeInfo = getGlobalStore.getState().getShadowNode(stateKey, path)
|
|
708
|
+
?._meta?.typeInfo;
|
|
709
|
+
|
|
710
|
+
const options = pluginStore
|
|
711
|
+
.getState()
|
|
712
|
+
.pluginOptions.get(stateKey)
|
|
713
|
+
?.get(pluginName);
|
|
714
|
+
|
|
715
|
+
const hookData = pluginStore.getState().getHookResult(stateKey, pluginName);
|
|
716
|
+
|
|
717
|
+
if (!plugin?.formWrapper || !stateHandler) {
|
|
718
|
+
return <>{children}</>;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const metadataContext = createMetadataContext(stateKey, plugin.name);
|
|
722
|
+
const deconstructed = toDeconstructedMethods(stateHandler);
|
|
723
|
+
|
|
724
|
+
return plugin.formWrapper({
|
|
725
|
+
element: children,
|
|
726
|
+
path,
|
|
727
|
+
stateKey,
|
|
728
|
+
pluginName: plugin.name,
|
|
729
|
+
...deconstructed,
|
|
730
|
+
...metadataContext,
|
|
731
|
+
options,
|
|
732
|
+
hookData,
|
|
733
|
+
fieldType: typeInfo?.type,
|
|
734
|
+
wrapperDepth,
|
|
735
|
+
});
|
|
736
|
+
});
|