elementdrawing 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/elementdrawing.min.js +3 -0
- package/dist/elementdrawing.min.js.LICENSE.txt +8 -0
- package/dist/elementdrawing.min.js.map +1 -0
- package/dist/index.html +1 -0
- package/package.json +127 -0
- package/src/core/bridge.h +855 -0
- package/src/core/diff.c +900 -0
- package/src/core/element.c +1078 -0
- package/src/core/event.c +813 -0
- package/src/core/fiber.c +1027 -0
- package/src/core/hooks.c +919 -0
- package/src/core/renderer.c +963 -0
- package/src/core/scheduler.c +702 -0
- package/src/core/state.c +803 -0
- package/src/css/animations.css +779 -0
- package/src/css/base.css +615 -0
- package/src/css/components.css +1311 -0
- package/src/css/tailwind.css +370 -0
- package/src/css/themes.css +517 -0
- package/src/css/utilities.css +475 -0
- package/src/index.js +746 -0
- package/src/js/animation.js +655 -0
- package/src/js/dom.js +665 -0
- package/src/js/events.js +585 -0
- package/src/js/http.js +446 -0
- package/src/js/index.js +26 -0
- package/src/js/router.js +483 -0
- package/src/js/store.js +539 -0
- package/src/js/utils.js +593 -0
- package/src/js/validator.js +529 -0
- package/src/jsx/components/Accordion.jsx +210 -0
- package/src/jsx/components/Alert.jsx +169 -0
- package/src/jsx/components/Avatar.jsx +214 -0
- package/src/jsx/components/Badge.jsx +136 -0
- package/src/jsx/components/Breadcrumb.jsx +200 -0
- package/src/jsx/components/Button.jsx +188 -0
- package/src/jsx/components/Card.jsx +192 -0
- package/src/jsx/components/Carousel.jsx +278 -0
- package/src/jsx/components/Checkbox.jsx +215 -0
- package/src/jsx/components/Dialog.jsx +242 -0
- package/src/jsx/components/Drawer.jsx +190 -0
- package/src/jsx/components/Dropdown.jsx +268 -0
- package/src/jsx/components/Form.jsx +274 -0
- package/src/jsx/components/Input.jsx +285 -0
- package/src/jsx/components/Menu.jsx +276 -0
- package/src/jsx/components/Modal.jsx +274 -0
- package/src/jsx/components/Navbar.jsx +292 -0
- package/src/jsx/components/Pagination.jsx +268 -0
- package/src/jsx/components/Progress.jsx +252 -0
- package/src/jsx/components/Radio.jsx +208 -0
- package/src/jsx/components/Select.jsx +397 -0
- package/src/jsx/components/Sidebar.jsx +250 -0
- package/src/jsx/components/Slider.jsx +310 -0
- package/src/jsx/components/Spinner.jsx +198 -0
- package/src/jsx/components/Switch.jsx +201 -0
- package/src/jsx/components/Table.jsx +332 -0
- package/src/jsx/components/Tabs.jsx +227 -0
- package/src/jsx/components/Textarea.jsx +212 -0
- package/src/jsx/components/Toast.jsx +270 -0
- package/src/jsx/components/Tooltip.jsx +178 -0
- package/src/jsx/components/Typography.jsx +299 -0
- package/src/jsx/components/index.jsx +70 -0
- package/src/jsx/core/element.js +3 -0
- package/src/jsx/hooks/index.js +356 -0
- package/src/jsx/hooks/useCallback.js +472 -0
- package/src/jsx/hooks/useContext.js +586 -0
- package/src/jsx/hooks/useEffect.js +704 -0
- package/src/jsx/hooks/useLayoutEffect.js +508 -0
- package/src/jsx/hooks/useMemo.js +689 -0
- package/src/jsx/hooks/useReducer.js +729 -0
- package/src/jsx/hooks/useRef.js +542 -0
- package/src/jsx/hooks/useState.js +854 -0
- package/src/jsx/runtime/commit.js +903 -0
- package/src/jsx/runtime/createElement.js +860 -0
- package/src/jsx/runtime/index.js +356 -0
- package/src/jsx/runtime/reconcile.js +687 -0
- package/src/jsx/runtime/render.js +914 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useContext Hook Implementation
|
|
3
|
+
* ElementDrawing Framework - Context API with creation, provider/consumer,
|
|
4
|
+
* default values, subscription, nested contexts, selectors, observers,
|
|
5
|
+
* context versioning, and change notification optimization.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
// ─── Internal State ───────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
let currentlyRenderingComponent = null;
|
|
13
|
+
let hookIndex = 0;
|
|
14
|
+
let contextIdCounter = 0;
|
|
15
|
+
|
|
16
|
+
// ─── Context Versioning ───────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Track context value versions to optimize change notifications.
|
|
20
|
+
* When a provider value is the same version, consumers can skip re-renders.
|
|
21
|
+
*/
|
|
22
|
+
const contextVersions = new WeakMap();
|
|
23
|
+
|
|
24
|
+
function getContextVersion(context) {
|
|
25
|
+
return contextVersions.get(context) || 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function incrementContextVersion(context) {
|
|
29
|
+
const current = contextVersions.get(context) || 0;
|
|
30
|
+
contextVersions.set(context, current + 1);
|
|
31
|
+
return current + 1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── Context Creation ─────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create a new context with a default value.
|
|
38
|
+
*
|
|
39
|
+
* @param {*} defaultValue - The default value used when no Provider is found
|
|
40
|
+
* @returns {Object} Context object with Provider and Consumer
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* const ThemeContext = createContext('light');
|
|
44
|
+
* const LocaleContext = createContext({ language: 'en', region: 'US' });
|
|
45
|
+
*/
|
|
46
|
+
function createContext(defaultValue) {
|
|
47
|
+
const contextId = '__ED_CTX_' + (++contextIdCounter) + '__';
|
|
48
|
+
|
|
49
|
+
// Unique symbol for storing context value on providers
|
|
50
|
+
const valueKey = Symbol.for(contextId + '_value');
|
|
51
|
+
const subscribersKey = Symbol.for(contextId + '_subscribers');
|
|
52
|
+
const versionKey = Symbol.for(contextId + '_version');
|
|
53
|
+
|
|
54
|
+
// ─── Context Object ────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
const context = {
|
|
57
|
+
_id: contextId,
|
|
58
|
+
_valueKey: valueKey,
|
|
59
|
+
_subscribersKey: subscribersKey,
|
|
60
|
+
_versionKey: versionKey,
|
|
61
|
+
_defaultValue: defaultValue,
|
|
62
|
+
_isContext: true,
|
|
63
|
+
_displayName: null,
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Provider component that supplies context value to descendant consumers.
|
|
67
|
+
*
|
|
68
|
+
* @param {Object} props
|
|
69
|
+
* @param {*} props.value - The context value to provide
|
|
70
|
+
* @param {*} props.children - Child components
|
|
71
|
+
* @returns {Object} Virtual DOM node
|
|
72
|
+
*/
|
|
73
|
+
Provider: null, // Assigned below
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Consumer component that reads context value.
|
|
77
|
+
*
|
|
78
|
+
* @param {Object} props
|
|
79
|
+
* @param {Function} props.children - Render prop receiving context value
|
|
80
|
+
* @returns {*} Rendered result
|
|
81
|
+
*/
|
|
82
|
+
Consumer: null, // Assigned below
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get the current context value by traversing the component tree upward.
|
|
86
|
+
* @param {Object} component - Starting component
|
|
87
|
+
* @returns {*} Context value or default value
|
|
88
|
+
*/
|
|
89
|
+
_getValue(component) {
|
|
90
|
+
let current = component;
|
|
91
|
+
while (current) {
|
|
92
|
+
// Check if this component is a Provider for this context
|
|
93
|
+
if (current._contextProvider === contextId && current[valueKey] !== undefined) {
|
|
94
|
+
return current[valueKey];
|
|
95
|
+
}
|
|
96
|
+
current = current._parentComponent;
|
|
97
|
+
}
|
|
98
|
+
return defaultValue;
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get the version of the current context value.
|
|
103
|
+
* @param {Object} component - Starting component
|
|
104
|
+
* @returns {number} Version number
|
|
105
|
+
*/
|
|
106
|
+
_getVersion(component) {
|
|
107
|
+
let current = component;
|
|
108
|
+
while (current) {
|
|
109
|
+
if (current._contextProvider === contextId) {
|
|
110
|
+
return current[versionKey] || 0;
|
|
111
|
+
}
|
|
112
|
+
current = current._parentComponent;
|
|
113
|
+
}
|
|
114
|
+
return 0;
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Subscribe to context value changes.
|
|
119
|
+
* @param {Object} component - The subscribing component
|
|
120
|
+
* @param {Function} callback - Called with (newValue, oldValue)
|
|
121
|
+
* @returns {Function} Unsubscribe function
|
|
122
|
+
*/
|
|
123
|
+
_subscribe(component, callback) {
|
|
124
|
+
let current = component;
|
|
125
|
+
while (current) {
|
|
126
|
+
if (current._contextProvider === contextId) {
|
|
127
|
+
if (!current[subscribersKey]) {
|
|
128
|
+
current[subscribersKey] = new Set();
|
|
129
|
+
}
|
|
130
|
+
const sub = { component, callback };
|
|
131
|
+
current[subscribersKey].add(sub);
|
|
132
|
+
|
|
133
|
+
return function unsubscribe() {
|
|
134
|
+
if (current[subscribersKey]) {
|
|
135
|
+
current[subscribersKey].delete(sub);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
current = current._parentComponent;
|
|
140
|
+
}
|
|
141
|
+
return function noop() {};
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Subscribe with a selector for optimized re-renders.
|
|
146
|
+
* @param {Object} component - The subscribing component
|
|
147
|
+
* @param {Function} selector - Selector function
|
|
148
|
+
* @param {Function} callback - Called when selected value changes
|
|
149
|
+
* @returns {Function} Unsubscribe function
|
|
150
|
+
*/
|
|
151
|
+
_subscribeWithSelector(component, selector, callback) {
|
|
152
|
+
let lastSelectedValue = selector(context._getValue(component));
|
|
153
|
+
|
|
154
|
+
return context._subscribe(component, (newValue, oldValue) => {
|
|
155
|
+
const newSelectedValue = selector(newValue);
|
|
156
|
+
if (!Object.is(lastSelectedValue, newSelectedValue)) {
|
|
157
|
+
const prevSelected = lastSelectedValue;
|
|
158
|
+
lastSelectedValue = newSelectedValue;
|
|
159
|
+
callback(newSelectedValue, prevSelected);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Notify all subscribers when the Provider value changes.
|
|
166
|
+
* @param {Object} providerComponent - The Provider component
|
|
167
|
+
* @param {*} newValue - New context value
|
|
168
|
+
* @param {*} oldValue - Previous context value
|
|
169
|
+
*/
|
|
170
|
+
_notifySubscribers(providerComponent, newValue, oldValue) {
|
|
171
|
+
const subs = providerComponent[subscribersKey];
|
|
172
|
+
if (!subs) return;
|
|
173
|
+
|
|
174
|
+
subs.forEach(({ component, callback }) => {
|
|
175
|
+
try {
|
|
176
|
+
callback(newValue, oldValue);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error('[useContext] Subscriber notification error:', error);
|
|
179
|
+
if (component._handleError) {
|
|
180
|
+
component._handleError(error);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// ─── Provider Implementation ────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Provider component wraps children and supplies a context value.
|
|
191
|
+
* When the value prop changes, all subscribed consumers re-render.
|
|
192
|
+
*/
|
|
193
|
+
context.Provider = function Provider(props) {
|
|
194
|
+
const component = currentlyRenderingComponent;
|
|
195
|
+
|
|
196
|
+
// Mark this component as a provider for this context
|
|
197
|
+
if (component) {
|
|
198
|
+
component._contextProvider = contextId;
|
|
199
|
+
|
|
200
|
+
// Check if value changed
|
|
201
|
+
const prevValue = component[valueKey];
|
|
202
|
+
const newValue = props.value;
|
|
203
|
+
|
|
204
|
+
if (!Object.is(prevValue, newValue)) {
|
|
205
|
+
component[valueKey] = newValue;
|
|
206
|
+
|
|
207
|
+
// Increment version
|
|
208
|
+
const newVersion = (component[versionKey] || 0) + 1;
|
|
209
|
+
component[versionKey] = newVersion;
|
|
210
|
+
incrementContextVersion(context);
|
|
211
|
+
|
|
212
|
+
// Notify subscribers of value change
|
|
213
|
+
context._notifySubscribers(component, newValue, prevValue);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Initialize subscribers set
|
|
217
|
+
if (!component[subscribersKey]) {
|
|
218
|
+
component[subscribersKey] = new Set();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return props.children;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
context.Provider._context = context;
|
|
226
|
+
context.Provider.displayName = 'Context.Provider';
|
|
227
|
+
|
|
228
|
+
// ─── Consumer Implementation ────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Consumer component uses the render prop pattern to access context value.
|
|
232
|
+
*/
|
|
233
|
+
context.Consumer = function Consumer(props) {
|
|
234
|
+
const component = currentlyRenderingComponent;
|
|
235
|
+
let value = defaultValue;
|
|
236
|
+
|
|
237
|
+
if (component) {
|
|
238
|
+
value = context._getValue(component);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (typeof props.children === 'function') {
|
|
242
|
+
return props.children(value);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
console.warn(
|
|
246
|
+
'[useContext] Consumer children should be a function. ' +
|
|
247
|
+
'Received: ' + typeof props.children
|
|
248
|
+
);
|
|
249
|
+
return props.children;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
context.Consumer._context = context;
|
|
253
|
+
context.Consumer.displayName = 'Context.Consumer';
|
|
254
|
+
|
|
255
|
+
return context;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ─── useContext Hook ───────────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* useContext - Read context value and subscribe to changes.
|
|
262
|
+
*
|
|
263
|
+
* @param {Object} context - Context object created by createContext
|
|
264
|
+
* @returns {*} Current context value
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* const theme = useContext(ThemeContext);
|
|
268
|
+
* const { language } = useContext(LocaleContext);
|
|
269
|
+
*/
|
|
270
|
+
function useContext(context) {
|
|
271
|
+
const component = currentlyRenderingComponent;
|
|
272
|
+
if (!component) {
|
|
273
|
+
throw new Error('[useContext] Must be called within a component render phase');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!context || !context._isContext) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
'[useContext] Argument must be a context object created by createContext()'
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Initialize hooks array
|
|
283
|
+
if (!component._hooks) {
|
|
284
|
+
component._hooks = [];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const currentHookIndex = hookIndex;
|
|
288
|
+
let hook;
|
|
289
|
+
|
|
290
|
+
if (component._hooks[currentHookIndex]) {
|
|
291
|
+
// Re-render: reuse existing hook
|
|
292
|
+
hook = component._hooks[currentHookIndex];
|
|
293
|
+
} else {
|
|
294
|
+
// First render: create hook and subscribe to context
|
|
295
|
+
hook = {
|
|
296
|
+
type: 'context',
|
|
297
|
+
context,
|
|
298
|
+
_unsubscribe: null,
|
|
299
|
+
_lastValue: null,
|
|
300
|
+
_version: 0,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// Subscribe to context changes to trigger re-render
|
|
304
|
+
const unsubscribe = context._subscribe(component, (newValue, oldValue) => {
|
|
305
|
+
// Schedule re-render when context value changes
|
|
306
|
+
if (component._scheduleReRender) {
|
|
307
|
+
component._scheduleReRender();
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
hook._unsubscribe = unsubscribe;
|
|
311
|
+
|
|
312
|
+
component._hooks[currentHookIndex] = hook;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Get the current context value
|
|
316
|
+
const value = context._getValue(component);
|
|
317
|
+
hook._lastValue = value;
|
|
318
|
+
hook._version = context._getVersion(component);
|
|
319
|
+
|
|
320
|
+
hookIndex++;
|
|
321
|
+
return value;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ─── Context Selector Hook ────────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* useContextSelector - Read a specific slice of context using a selector.
|
|
328
|
+
* Only re-renders when the selected value changes, not on every context change.
|
|
329
|
+
*
|
|
330
|
+
* @param {Object} context - Context object created by createContext
|
|
331
|
+
* @param {Function} selector - Function that extracts a value from context
|
|
332
|
+
* @returns {*} Selected value from context
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* const theme = useContextSelector(ThemeContext, ctx => ctx.mode);
|
|
336
|
+
* const language = useContextSelector(LocaleContext, ctx => ctx.language);
|
|
337
|
+
*/
|
|
338
|
+
function useContextSelector(context, selector) {
|
|
339
|
+
const component = currentlyRenderingComponent;
|
|
340
|
+
if (!component) {
|
|
341
|
+
throw new Error('[useContextSelector] Must be called within a component render phase');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (!context || !context._isContext) {
|
|
345
|
+
throw new Error('[useContextSelector] First argument must be a context object');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (typeof selector !== 'function') {
|
|
349
|
+
throw new Error('[useContextSelector] Second argument must be a selector function');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Initialize hooks array
|
|
353
|
+
if (!component._hooks) {
|
|
354
|
+
component._hooks = [];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const currentHookIndex = hookIndex;
|
|
358
|
+
let hook;
|
|
359
|
+
|
|
360
|
+
if (component._hooks[currentHookIndex]) {
|
|
361
|
+
hook = component._hooks[currentHookIndex];
|
|
362
|
+
hook.selector = selector;
|
|
363
|
+
} else {
|
|
364
|
+
hook = {
|
|
365
|
+
type: 'context-selector',
|
|
366
|
+
context,
|
|
367
|
+
selector,
|
|
368
|
+
_unsubscribe: null,
|
|
369
|
+
_lastSelectedValue: undefined,
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// Subscribe with selector for optimized updates
|
|
373
|
+
const unsubscribe = context._subscribeWithSelector(
|
|
374
|
+
component,
|
|
375
|
+
selector,
|
|
376
|
+
(newSelected, oldSelected) => {
|
|
377
|
+
if (component._scheduleReRender) {
|
|
378
|
+
component._scheduleReRender();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
);
|
|
382
|
+
hook._unsubscribe = unsubscribe;
|
|
383
|
+
|
|
384
|
+
component._hooks[currentHookIndex] = hook;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Compute selected value
|
|
388
|
+
const fullValue = context._getValue(component);
|
|
389
|
+
const selectedValue = selector(fullValue);
|
|
390
|
+
hook._lastSelectedValue = selectedValue;
|
|
391
|
+
|
|
392
|
+
hookIndex++;
|
|
393
|
+
return selectedValue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ─── Context Observer Hook ────────────────────────────────────────────────────
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* useContextObserver - Observe context changes with a callback instead of
|
|
400
|
+
* triggering a re-render. Useful for logging, analytics, or side effects.
|
|
401
|
+
*
|
|
402
|
+
* @param {Object} context - Context object
|
|
403
|
+
* @param {Function} observer - Called with (newValue, oldValue) on changes
|
|
404
|
+
* @returns {*} Current context value
|
|
405
|
+
*
|
|
406
|
+
* @example
|
|
407
|
+
* const theme = useContextObserver(ThemeContext, (newTheme) => {
|
|
408
|
+
* analytics.track('theme_changed', { theme: newTheme });
|
|
409
|
+
* });
|
|
410
|
+
*/
|
|
411
|
+
function useContextObserver(context, observer) {
|
|
412
|
+
const component = currentlyRenderingComponent;
|
|
413
|
+
if (!component) {
|
|
414
|
+
throw new Error('[useContextObserver] Must be called within a component render phase');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (!context || !context._isContext) {
|
|
418
|
+
throw new Error('[useContextObserver] First argument must be a context object');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (typeof observer !== 'function') {
|
|
422
|
+
throw new Error('[useContextObserver] Second argument must be an observer function');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (!component._hooks) {
|
|
426
|
+
component._hooks = [];
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const currentHookIndex = hookIndex;
|
|
430
|
+
let hook;
|
|
431
|
+
|
|
432
|
+
if (component._hooks[currentHookIndex]) {
|
|
433
|
+
hook = component._hooks[currentHookIndex];
|
|
434
|
+
hook.observer = observer;
|
|
435
|
+
} else {
|
|
436
|
+
hook = {
|
|
437
|
+
type: 'context-observer',
|
|
438
|
+
context,
|
|
439
|
+
observer,
|
|
440
|
+
_unsubscribe: null,
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// Subscribe to context changes (observer doesn't trigger re-render)
|
|
444
|
+
const unsubscribe = context._subscribe(component, (newValue, oldValue) => {
|
|
445
|
+
try {
|
|
446
|
+
observer(newValue, oldValue);
|
|
447
|
+
} catch (error) {
|
|
448
|
+
console.error('[useContextObserver] Observer error:', error);
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
hook._unsubscribe = unsubscribe;
|
|
452
|
+
|
|
453
|
+
component._hooks[currentHookIndex] = hook;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const value = context._getValue(component);
|
|
457
|
+
hookIndex++;
|
|
458
|
+
return value;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ─── Nested Context Support ───────────────────────────────────────────────────
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Merge multiple contexts into a single object for convenience.
|
|
465
|
+
*
|
|
466
|
+
* @param {Object[]} contexts - Array of context objects
|
|
467
|
+
* @returns {Object} Object with current values from all contexts
|
|
468
|
+
*
|
|
469
|
+
* @example
|
|
470
|
+
* const { theme, locale } = useMergedContext([ThemeContext, LocaleContext]);
|
|
471
|
+
*/
|
|
472
|
+
function useMergedContext(contexts) {
|
|
473
|
+
if (!Array.isArray(contexts)) {
|
|
474
|
+
throw new Error('[useMergedContext] Expected an array of context objects');
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const values = {};
|
|
478
|
+
contexts.forEach((ctx) => {
|
|
479
|
+
const name = ctx._id.replace('__ED_CTX_', '').replace('__', '');
|
|
480
|
+
values[name] = useContext(ctx);
|
|
481
|
+
});
|
|
482
|
+
return values;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Create a composed context that derives its value from multiple parent contexts.
|
|
487
|
+
*
|
|
488
|
+
* @param {Object[]} contexts - Array of parent context objects
|
|
489
|
+
* @param {Function} compute - Function that receives parent values and returns composed value
|
|
490
|
+
* @returns {Object} New context object
|
|
491
|
+
*
|
|
492
|
+
* @example
|
|
493
|
+
* const ComposedCtx = composeContexts(
|
|
494
|
+
* [ThemeContext, LocaleContext],
|
|
495
|
+
* (theme, locale) => ({ theme, locale, isDark: theme === 'dark' })
|
|
496
|
+
* );
|
|
497
|
+
*/
|
|
498
|
+
function composeContexts(contexts, compute) {
|
|
499
|
+
const defaultValues = contexts.map((ctx) => ctx._defaultValue);
|
|
500
|
+
const composedDefault = compute(...defaultValues);
|
|
501
|
+
return createContext(composedDefault);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ─── Context Tree Utilities ───────────────────────────────────────────────────
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Find the nearest Provider for a given context in the component tree.
|
|
508
|
+
* @param {Object} component - Starting component
|
|
509
|
+
* @param {Object} context - Context to search for
|
|
510
|
+
* @returns {Object|null} Provider component or null
|
|
511
|
+
*/
|
|
512
|
+
function findNearestProvider(component, context) {
|
|
513
|
+
let current = component;
|
|
514
|
+
while (current) {
|
|
515
|
+
if (current._contextProvider === context._id) {
|
|
516
|
+
return current;
|
|
517
|
+
}
|
|
518
|
+
current = current._parentComponent;
|
|
519
|
+
}
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Get all context values from the component tree.
|
|
525
|
+
* @param {Object} component - Starting component
|
|
526
|
+
* @returns {Object} Map of context IDs to values
|
|
527
|
+
*/
|
|
528
|
+
function getAllContextValues(component) {
|
|
529
|
+
const values = {};
|
|
530
|
+
let current = component;
|
|
531
|
+
while (current) {
|
|
532
|
+
if (current._contextProvider && current[Symbol.for('__ED_CTX_' + current._contextProvider + '__value')]) {
|
|
533
|
+
values[current._contextProvider] = current[Symbol.for('__ED_CTX_' + current._contextProvider + '__value')];
|
|
534
|
+
}
|
|
535
|
+
current = current._parentComponent;
|
|
536
|
+
}
|
|
537
|
+
return values;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ─── Hook Context Management ──────────────────────────────────────────────────
|
|
541
|
+
|
|
542
|
+
function setCurrentComponent(component) {
|
|
543
|
+
currentlyRenderingComponent = component;
|
|
544
|
+
hookIndex = 0;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function resetHookIndex() {
|
|
548
|
+
hookIndex = 0;
|
|
549
|
+
currentlyRenderingComponent = null;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ─── Cleanup ──────────────────────────────────────────────────────────────────
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Clean up context subscriptions when a component unmounts.
|
|
556
|
+
* @param {Object} component - The unmounting component
|
|
557
|
+
*/
|
|
558
|
+
function cleanupContextSubscriptions(component) {
|
|
559
|
+
if (!component._hooks) return;
|
|
560
|
+
|
|
561
|
+
component._hooks.forEach((hook) => {
|
|
562
|
+
if (hook && (hook.type === 'context' || hook.type === 'context-selector' || hook.type === 'context-observer')) {
|
|
563
|
+
if (typeof hook._unsubscribe === 'function') {
|
|
564
|
+
hook._unsubscribe();
|
|
565
|
+
hook._unsubscribe = null;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
572
|
+
|
|
573
|
+
module.exports = {
|
|
574
|
+
createContext,
|
|
575
|
+
useContext,
|
|
576
|
+
useContextSelector,
|
|
577
|
+
useContextObserver,
|
|
578
|
+
useMergedContext,
|
|
579
|
+
composeContexts,
|
|
580
|
+
setCurrentComponent,
|
|
581
|
+
resetHookIndex,
|
|
582
|
+
findNearestProvider,
|
|
583
|
+
getAllContextValues,
|
|
584
|
+
cleanupContextSubscriptions,
|
|
585
|
+
getContextVersion,
|
|
586
|
+
};
|