cogsbox-state 0.5.473 → 0.5.475-canary.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.
@@ -5,7 +5,11 @@ import {
5
5
  type FormOptsType,
6
6
  } from './CogsState';
7
7
  import { pluginStore } from './pluginStore';
8
- import { createMetadataContext, toDeconstructedMethods } from './plugins';
8
+ import {
9
+ createMetadataContext,
10
+ createScopedMetadataContext,
11
+ toDeconstructedMethods,
12
+ } from './plugins';
9
13
  import React, {
10
14
  memo,
11
15
  RefObject,
@@ -78,6 +82,34 @@ export function ValidationWrapper({
78
82
  : warningMessages.length > 0
79
83
  ? 'warning'
80
84
  : undefined;
85
+ const { registeredPlugins } = pluginStore.getState();
86
+ const pluginsApi: any = {};
87
+
88
+ // We iterate over ALL registered plugins in the app.
89
+ registeredPlugins.forEach((plugin) => {
90
+ // A plugin is considered "active" for this state key if its name
91
+ // exists as a key in the options (e.g., options.syncPlugin exists).
92
+ if (thisStateOpts && thisStateOpts.hasOwnProperty(plugin.name)) {
93
+ const pluginName = plugin.name;
94
+
95
+ // Now we can safely build the API for this active plugin.
96
+ const hookData = pluginStore
97
+ .getState()
98
+ .getHookResult(stateKey, pluginName);
99
+
100
+ const scopedMetadata = createScopedMetadataContext(
101
+ stateKey,
102
+ pluginName,
103
+ path
104
+ );
105
+
106
+ pluginsApi[pluginName] = {
107
+ hookData,
108
+ getFieldMetaData: scopedMetadata.getFieldMetaData,
109
+ setFieldMetaData: scopedMetadata.setFieldMetaData,
110
+ };
111
+ }
112
+ });
81
113
  return (
82
114
  <>
83
115
  {thisStateOpts?.formElements?.validation &&
@@ -96,6 +128,7 @@ export function ValidationWrapper({
96
128
  allErrors: errors,
97
129
  path: path,
98
130
  getData: () => getShadowValue(stateKey!, path),
131
+ plugins: pluginsApi,
99
132
  })
100
133
  ) : (
101
134
  <React.Fragment key={path.toString()}>{children}</React.Fragment>
@@ -244,17 +277,6 @@ export function FormElementWrapper({
244
277
  const isCurrentlyDebouncing = useRef(false);
245
278
  const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
246
279
 
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
-
258
280
  useEffect(() => {
259
281
  if (
260
282
  !isCurrentlyDebouncing.current &&
@@ -375,15 +397,24 @@ export function FormElementWrapper({
375
397
  setShadowMetadata(stateKey, path, meta);
376
398
  }
377
399
  }
400
+ const element = meta?.clientActivityState?.elements?.get(componentId);
378
401
 
379
402
  // Notify plugins
380
403
  notifyFormUpdate({
381
404
  stateKey,
382
- type: 'input',
405
+ activityType: 'input', // Changed from 'type'
383
406
  path,
384
- value: newValue,
407
+ timestamp: Date.now(),
408
+ details: {
409
+ value: newValue,
410
+ inputLength:
411
+ typeof newValue === 'string' ? newValue.length : undefined,
412
+ isComposing: false, // You'd need to track this from the actual input event
413
+ isPasting: false, // You'd need to track this from paste events
414
+ keystrokeCount:
415
+ (element?.currentActivity?.details?.keystrokeCount || 0) + 1,
416
+ },
385
417
  });
386
-
387
418
  // Validation (keep existing)
388
419
  const virtualOperation: UpdateTypeDetail = {
389
420
  stateKey,
@@ -444,9 +475,12 @@ export function FormElementWrapper({
444
475
  // Notify plugins
445
476
  notifyFormUpdate({
446
477
  stateKey,
447
- type: 'focus',
478
+ activityType: 'focus', // Changed from 'type'
448
479
  path,
449
- value: localValue,
480
+ timestamp: Date.now(),
481
+ details: {
482
+ cursorPosition: formElementRef.current?.selectionStart,
483
+ },
450
484
  });
451
485
  }, [stateKey, path, componentId, localValue]);
452
486
  const handleBlur = useCallback(() => {
@@ -470,13 +504,20 @@ export function FormElementWrapper({
470
504
  element.currentActivity = undefined;
471
505
  setShadowMetadata(stateKey, path, meta);
472
506
  }
507
+ const focusStartTime =
508
+ meta?.clientActivityState?.elements?.get(componentId)?.currentActivity
509
+ ?.startTime;
473
510
 
474
511
  // Notify plugins
475
512
  notifyFormUpdate({
476
513
  stateKey,
477
- type: 'blur',
514
+ activityType: 'blur', // Changed from 'type'
478
515
  path,
479
- value: localValue,
516
+ timestamp: Date.now(),
517
+ duration: focusStartTime ? Date.now() - focusStartTime : undefined,
518
+ details: {
519
+ duration: focusStartTime ? Date.now() - focusStartTime : 0,
520
+ },
480
521
  });
481
522
 
482
523
  // Run validation if configured
@@ -521,23 +562,9 @@ export function FormElementWrapper({
521
562
 
522
563
  const initialElement = renderFn(stateWithInputProps);
523
564
 
524
- const wrappedElement = activeFormWrappers.reduceRight(
525
- (currentElement, config, index) => (
526
- <PluginWrapper
527
- stateKey={stateKey}
528
- path={path}
529
- pluginName={config.plugin.name}
530
- wrapperDepth={activeFormWrappers.length - 1 - index}
531
- >
532
- {currentElement}
533
- </PluginWrapper>
534
- ),
535
- initialElement
536
- );
537
-
538
565
  return (
539
566
  <ValidationWrapper formOpts={formOpts} path={path} stateKey={stateKey}>
540
- {wrappedElement}
567
+ {initialElement}
541
568
  </ValidationWrapper>
542
569
  );
543
570
  }
@@ -549,18 +576,15 @@ export function useRegisterComponent(
549
576
  const fullComponentId = `${stateKey}////${componentId}`;
550
577
 
551
578
  useLayoutEffect(() => {
552
- // Call the safe, centralized function to register
553
579
  registerComponent(stateKey, fullComponentId, {
554
580
  forceUpdate: () => forceUpdate({}),
555
581
  paths: new Set(),
556
582
  reactiveType: ['component'],
557
583
  });
558
-
559
- // The cleanup now calls the safe, centralized unregister function
560
584
  return () => {
561
585
  unregisterComponent(stateKey, fullComponentId);
562
586
  };
563
- }, [stateKey, fullComponentId]); // Dependencies are stable and correct
587
+ }, [stateKey, fullComponentId]);
564
588
  }
565
589
 
566
590
  const useImageLoaded = (ref: RefObject<HTMLElement>): boolean => {
@@ -699,16 +723,20 @@ const PluginWrapper = memo(function PluginWrapper({
699
723
  return <>{children}</>;
700
724
  }
701
725
 
702
- const metadataContext = createMetadataContext(stateKey, plugin.name);
703
726
  const deconstructed = toDeconstructedMethods(stateHandler);
704
-
727
+ const scopedMetadataContext = createScopedMetadataContext(
728
+ stateKey,
729
+ plugin.name,
730
+ path
731
+ );
705
732
  return plugin.formWrapper({
706
733
  element: children,
707
734
  path,
708
735
  stateKey,
709
736
  pluginName: plugin.name,
710
737
  ...deconstructed,
711
- ...metadataContext,
738
+
739
+ ...scopedMetadataContext,
712
740
  options,
713
741
  hookData,
714
742
  fieldType: typeInfo?.type,
@@ -1,10 +1,14 @@
1
1
  import React, { useEffect, useMemo, useState, useRef, useReducer } from 'react';
2
- import { pluginStore } from './pluginStore';
2
+ import { ClientActivityEvent, pluginStore } from './pluginStore';
3
3
  import { isDeepEqual } from './utility';
4
- import { createMetadataContext, toDeconstructedMethods } from './plugins';
4
+ import {
5
+ createMetadataContext,
6
+ createScopedMetadataContext,
7
+ toDeconstructedMethods,
8
+ } from './plugins';
5
9
  import type { CogsPlugin } from './plugins';
6
10
  import type { StateObject, UpdateTypeDetail } from './CogsState';
7
- import { FormEventType } from './store';
11
+ import { ClientActivityState, FormEventType } from './store';
8
12
 
9
13
  const { setHookResult, removeHookResult } = pluginStore.getState();
10
14
 
@@ -99,6 +103,13 @@ const PluginInstance = React.memo(
99
103
 
100
104
  const handleUpdate = (update: UpdateTypeDetail) => {
101
105
  if (update.stateKey === stateKey) {
106
+ // Create a new, SCOPED context for this specific path
107
+ const scopedMetadata = createScopedMetadataContext(
108
+ stateKey,
109
+ plugin.name,
110
+ update.path
111
+ );
112
+
102
113
  plugin.onUpdate!({
103
114
  stateKey,
104
115
  pluginName: plugin.name,
@@ -107,7 +118,7 @@ const PluginInstance = React.memo(
107
118
  options,
108
119
  hookData: hookDataRef.current,
109
120
  ...deconstructed,
110
- ...metadataContext,
121
+ ...scopedMetadata, // <-- Use the new scoped context
111
122
  });
112
123
  }
113
124
  };
@@ -116,29 +127,31 @@ const PluginInstance = React.memo(
116
127
  .getState()
117
128
  .subscribeToUpdates(handleUpdate);
118
129
  return unsubscribe;
119
- }, [stateKey, plugin, options, deconstructed, metadataContext]);
130
+ }, [stateKey, plugin, options, deconstructed]);
120
131
 
121
132
  useEffect(() => {
122
133
  if (!plugin.onFormUpdate) return;
123
134
 
124
135
  const handleFormUpdate = (
125
- event: FormEventType & { stateKey: string }
136
+ event: ClientActivityEvent // Use the proper type
126
137
  ) => {
127
138
  if (event.stateKey === stateKey) {
128
- const path = event.path;
139
+ // Create a new, SCOPED context for this specific path
140
+ const scopedMetadata = createScopedMetadataContext(
141
+ stateKey,
142
+ plugin.name,
143
+ event.path
144
+ );
145
+
129
146
  plugin.onFormUpdate!({
130
147
  stateKey,
131
148
  pluginName: plugin.name,
132
- path,
133
- event: {
134
- type: event.type,
135
- value: event.value,
136
- path,
137
- },
149
+ path: event.path,
150
+ event: event,
138
151
  options,
139
152
  hookData: hookDataRef.current,
140
153
  ...deconstructed,
141
- ...metadataContext,
154
+ ...scopedMetadata, // <-- Use the new scoped context
142
155
  });
143
156
  }
144
157
  };
@@ -147,7 +160,7 @@ const PluginInstance = React.memo(
147
160
  .getState()
148
161
  .subscribeToFormUpdates(handleFormUpdate);
149
162
  return unsubscribe;
150
- }, [stateKey, plugin, options, deconstructed, metadataContext]);
163
+ }, [stateKey, plugin, options, deconstructed]);
151
164
 
152
165
  return null;
153
166
  }
@@ -1,6 +1,40 @@
1
1
  import { create } from 'zustand';
2
2
  import type { PluginData, StateObject, UpdateTypeDetail } from './CogsState';
3
3
  import type { CogsPlugin } from './plugins';
4
+ export type ClientActivityEvent = {
5
+ stateKey: string;
6
+ path: string[];
7
+ timestamp: number;
8
+ duration?: number;
9
+ } & (
10
+ | { activityType: 'focus'; details: { cursorPosition?: number } }
11
+ | { activityType: 'blur'; details: { duration: number } }
12
+ | {
13
+ activityType: 'input';
14
+ details: {
15
+ value: any;
16
+ inputLength?: number;
17
+ isComposing?: boolean;
18
+ isPasting?: boolean;
19
+ keystrokeCount?: number;
20
+ };
21
+ }
22
+ | {
23
+ activityType: 'select';
24
+ details: {
25
+ selectionStart: number;
26
+ selectionEnd: number;
27
+ selectedText?: string;
28
+ };
29
+ }
30
+ | { activityType: 'hover_enter'; details: { cursorPosition?: number } }
31
+ | { activityType: 'hover_exit'; details: { duration: number } }
32
+ | {
33
+ activityType: 'scroll';
34
+ details: { scrollTop: number; scrollLeft: number };
35
+ }
36
+ | { activityType: 'cursor_move'; details: { cursorPosition: number } }
37
+ );
4
38
 
5
39
  type PluginRegistryStore = {
6
40
  stateHandlers: Map<string, StateObject<any>>; // stateKey -> handler
@@ -27,28 +61,11 @@ type PluginRegistryStore = {
27
61
  callback: (update: UpdateTypeDetail) => void
28
62
  ) => () => void;
29
63
  notifyUpdate: (update: UpdateTypeDetail) => void;
30
- formUpdateSubscribers: Set<
31
- (event: {
32
- stateKey: string;
33
- type: 'focus' | 'blur' | 'input';
34
- path: string[];
35
- value?: any;
36
- }) => void
37
- >;
64
+ formUpdateSubscribers: Set<(event: ClientActivityEvent) => void>;
38
65
  subscribeToFormUpdates: (
39
- callback: (event: {
40
- stateKey: string;
41
- type: 'focus' | 'blur' | 'input';
42
- path: string[];
43
- value?: any;
44
- }) => void
66
+ callback: (event: ClientActivityEvent) => void
45
67
  ) => () => void;
46
- notifyFormUpdate: (event: {
47
- stateKey: string;
48
- type: 'focus' | 'blur' | 'input';
49
- path: string[];
50
- value?: any;
51
- }) => void;
68
+ notifyFormUpdate: (event: ClientActivityEvent) => void;
52
69
  hookResults: Map<string, Map<string, any>>; // stateKey -> pluginName -> hook
53
70
  setHookResult: (stateKey: string, pluginName: string, data: any) => void;
54
71
  getHookResult: (stateKey: string, pluginName: string) => any | undefined;