@symbo.ls/element 3.14.3 → 3.14.5
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/CHANGELOG.md +16 -0
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/src/cache.cjs +1 -0
- package/dist/cjs/src/create.cjs +6 -0
- package/dist/cjs/src/delegate.cjs +1 -0
- package/dist/cjs/src/dispose.cjs +1 -0
- package/dist/cjs/src/extends.cjs +1 -0
- package/dist/cjs/src/index.cjs +1 -0
- package/dist/cjs/src/methods.cjs +1 -0
- package/dist/cjs/src/registry.cjs +1 -0
- package/dist/cjs/src/update.cjs +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/src/cache.js +1 -0
- package/dist/esm/src/create.js +6 -0
- package/dist/esm/src/delegate.js +1 -0
- package/dist/esm/src/dispose.js +1 -0
- package/dist/esm/src/extends.js +1 -0
- package/dist/esm/src/index.js +1 -0
- package/dist/esm/src/methods.js +1 -0
- package/dist/esm/src/registry.js +1 -0
- package/dist/esm/src/update.js +1 -0
- package/package.json +29 -8
- package/index.js +0 -1
- package/src/cache.js +0 -124
- package/src/create.js +0 -1987
- package/src/delegate.js +0 -163
- package/src/dispose.js +0 -76
- package/src/extends.js +0 -123
- package/src/index.js +0 -8
- package/src/methods.js +0 -271
- package/src/registry.js +0 -79
- package/src/update.js +0 -132
package/src/create.js
DELETED
|
@@ -1,1987 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
import { createStore, isStore } from '@symbo.ls/signal'
|
|
4
|
-
import { createEffect, batch } from '@symbo.ls/signal'
|
|
5
|
-
import { cacheNode, detectTag } from './cache.js'
|
|
6
|
-
import { REGISTRY, LIFECYCLE_EVENTS } from './registry.js'
|
|
7
|
-
import { registerEvent } from './delegate.js'
|
|
8
|
-
import { applyExtends } from './extends.js'
|
|
9
|
-
import { addMethods } from './methods.js'
|
|
10
|
-
import { dispose } from './dispose.js'
|
|
11
|
-
import { replaceLiteralsWithObjectFields, exec } from '@symbo.ls/utils'
|
|
12
|
-
import { runPluginHook } from '@symbo.ls/utils/function.js'
|
|
13
|
-
import { applyStateMethods } from '@symbo.ls/state'
|
|
14
|
-
import { CSS_PROPS_REGISTRY } from 'css-in-props/src/props'
|
|
15
|
-
import { DEFAULT_CSS_PROPERTIES_LIST } from 'css-in-props/src/props/defaults'
|
|
16
|
-
import { css as atomicCss } from '@symbo.ls/css'
|
|
17
|
-
import { pushConfig, popConfig } from '@symbo.ls/scratch'
|
|
18
|
-
import { checkAttributeByTagName, applyAttrTransforms } from 'attrs-in-props'
|
|
19
|
-
|
|
20
|
-
let _uniqId = 1
|
|
21
|
-
|
|
22
|
-
// Detect onXxx event handler pattern
|
|
23
|
-
function isEventHandler(key) {
|
|
24
|
-
return key.length > 2 &&
|
|
25
|
-
key.charCodeAt(0) === 111 && // 'o'
|
|
26
|
-
key.charCodeAt(1) === 110 && // 'n'
|
|
27
|
-
key.charCodeAt(2) >= 65 && key.charCodeAt(2) <= 90 // A-Z
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Extract event name from onXxx key (onClick -> click, onMouseDown -> mouseDown)
|
|
31
|
-
function eventNameFrom(key) {
|
|
32
|
-
return key.charAt(2).toLowerCase() + key.slice(3)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Convert camelCase event name to lowercase DOM event (mouseDown -> mousedown)
|
|
36
|
-
function toDomEventName(name) {
|
|
37
|
-
return name.toLowerCase()
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Deep merge for child component definitions — override wins, but nested objects are recursively merged
|
|
41
|
-
function deepMergeChild(base, override) {
|
|
42
|
-
const result = { ...base }
|
|
43
|
-
for (const key in override) {
|
|
44
|
-
const baseVal = result[key]
|
|
45
|
-
const overVal = override[key]
|
|
46
|
-
if (baseVal && overVal &&
|
|
47
|
-
typeof baseVal === 'object' && typeof overVal === 'object' &&
|
|
48
|
-
!Array.isArray(baseVal) && !Array.isArray(overVal) &&
|
|
49
|
-
typeof baseVal !== 'function' && typeof overVal !== 'function') {
|
|
50
|
-
result[key] = deepMergeChild(baseVal, overVal)
|
|
51
|
-
} else {
|
|
52
|
-
result[key] = overVal
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return result
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// --- Create Element ---
|
|
59
|
-
|
|
60
|
-
export function create(definition, parent, key, options = {}, attachOptions) {
|
|
61
|
-
// Normalize definition
|
|
62
|
-
if (definition == null || definition === false) return null
|
|
63
|
-
if (typeof definition === 'string' || typeof definition === 'number') {
|
|
64
|
-
definition = { text: definition }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const element = Object.create(null)
|
|
69
|
-
element.key = key || definition.key || `_${_uniqId++}`
|
|
70
|
-
element.parent = parent || null
|
|
71
|
-
element.context = definition.context || parent?.context || options.context || {}
|
|
72
|
-
if (!element.context.__create) element.context.__create = create
|
|
73
|
-
|
|
74
|
-
// Internal ref
|
|
75
|
-
const ref = element.__ref = {
|
|
76
|
-
__if: true,
|
|
77
|
-
__children: [],
|
|
78
|
-
__exec: {},
|
|
79
|
-
__effects: [],
|
|
80
|
-
__eventCleanup: [],
|
|
81
|
-
root: parent?.__ref?.root || element,
|
|
82
|
-
path: parent?.__ref?.path ? [...parent.__ref.path, element.key] : [element.key]
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Auto-extend from key name: Key_suffix -> extends Key component
|
|
86
|
-
if (key && !definition.extends && !definition.extend) {
|
|
87
|
-
const underscoreIdx = key.indexOf('_')
|
|
88
|
-
if (underscoreIdx > 0) {
|
|
89
|
-
const baseName = key.substring(0, underscoreIdx)
|
|
90
|
-
const components = element.context?.components
|
|
91
|
-
if (components && (components[baseName] || components['smbls.' + baseName])) {
|
|
92
|
-
definition = { extends: baseName, ...definition }
|
|
93
|
-
} else if (
|
|
94
|
-
// FRAMEWORK-5: dev-mode warning. A key shaped like a multi-instance
|
|
95
|
-
// suffix (e.g. `Link_1`) clearly intends to auto-extend a base
|
|
96
|
-
// component, so silently rendering as a bare div is the kind of
|
|
97
|
-
// failure that takes a click test to discover. Warn loudly in dev
|
|
98
|
-
// so the typo / missing import is visible at mount time.
|
|
99
|
-
components &&
|
|
100
|
-
process.env.NODE_ENV !== 'production' &&
|
|
101
|
-
/^[A-Z]\w*_\d+$/.test(key)
|
|
102
|
-
) {
|
|
103
|
-
console.warn(
|
|
104
|
-
`[smbls] auto-extend: key "${key}" looks like a multi-instance ` +
|
|
105
|
-
`(${baseName}_N) but neither "${baseName}" nor "smbls.${baseName}" ` +
|
|
106
|
-
`is registered in context.components — element will render as a ` +
|
|
107
|
-
`plain <div>. Register the base component or rename the key.`
|
|
108
|
-
)
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Merge extends chain
|
|
114
|
-
applyExtends(element, definition, options)
|
|
115
|
-
|
|
116
|
-
// Copy properties from definition — deep merge child overrides, shallow merge known keys, override rest
|
|
117
|
-
const MERGE_KEYS = new Set(['childProps', 'attr', 'style', 'scope', 'data'])
|
|
118
|
-
|
|
119
|
-
// Capture props set by the extends chain before they get merged flat
|
|
120
|
-
const inheritedProps = (element.props && typeof element.props === 'object')
|
|
121
|
-
? element.props
|
|
122
|
-
: null
|
|
123
|
-
|
|
124
|
-
for (const prop in definition) {
|
|
125
|
-
if (prop === 'parent' || prop === 'node' || prop === '__ref' || prop === 'extends' || prop === 'context' || prop === 'props') continue
|
|
126
|
-
const val = definition[prop]
|
|
127
|
-
const existing = element[prop]
|
|
128
|
-
const bothObjects = existing && val &&
|
|
129
|
-
typeof existing === 'object' && typeof val === 'object' &&
|
|
130
|
-
!Array.isArray(existing) && !Array.isArray(val) &&
|
|
131
|
-
typeof existing !== 'function' && typeof val !== 'function'
|
|
132
|
-
if (bothObjects && (prop.charCodeAt(0) >= 65 && prop.charCodeAt(0) <= 90)) {
|
|
133
|
-
// Deep merge for uppercase child component overrides (definition wins)
|
|
134
|
-
element[prop] = deepMergeChild(existing, val)
|
|
135
|
-
} else if (bothObjects && MERGE_KEYS.has(prop)) {
|
|
136
|
-
element[prop] = { ...existing, ...val }
|
|
137
|
-
} else {
|
|
138
|
-
element[prop] = val
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Helper to merge a props-like object onto element directly
|
|
143
|
-
function mergePropsOntoElement (propsObj) {
|
|
144
|
-
for (const prop in propsObj) {
|
|
145
|
-
const val = propsObj[prop]
|
|
146
|
-
const existing = element[prop]
|
|
147
|
-
const bothObjects = existing && val &&
|
|
148
|
-
typeof existing === 'object' && typeof val === 'object' &&
|
|
149
|
-
!Array.isArray(existing) && !Array.isArray(val) &&
|
|
150
|
-
typeof existing !== 'function' && typeof val !== 'function'
|
|
151
|
-
if (bothObjects && (prop.charCodeAt(0) >= 65 && prop.charCodeAt(0) <= 90)) {
|
|
152
|
-
element[prop] = deepMergeChild(existing, val)
|
|
153
|
-
} else if (bothObjects && MERGE_KEYS.has(prop)) {
|
|
154
|
-
element[prop] = { ...existing, ...val }
|
|
155
|
-
} else {
|
|
156
|
-
element[prop] = val
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Merge inherited props from extends chain flat onto element
|
|
162
|
-
if (inheritedProps) mergePropsOntoElement(inheritedProps)
|
|
163
|
-
|
|
164
|
-
// Merge definition.props flat onto element (definition overrides inherited)
|
|
165
|
-
if (definition.props && typeof definition.props === 'object') {
|
|
166
|
-
mergePropsOntoElement(definition.props)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (definition.data !== undefined) element.data = definition.data
|
|
170
|
-
|
|
171
|
-
if (definition.classlist !== undefined) element.classlist = definition.classlist
|
|
172
|
-
if (definition.text !== undefined) element.text = definition.text
|
|
173
|
-
if (definition.html !== undefined) element.html = definition.html
|
|
174
|
-
// The scope chain at any element resolves keys in this order:
|
|
175
|
-
// own (definition.scope) → parent.scope → … → root.scope → context.globalScope
|
|
176
|
-
// Direct reads like `el.scope.LANG_META` walk the prototype chain, so a
|
|
177
|
-
// component that doesn't declare its own scope still sees parent scopes
|
|
178
|
-
// and the project's globalScope as fallbacks. Without `globalScope` at the
|
|
179
|
-
// bottom of the chain, every project pattern that puts shared constants
|
|
180
|
-
// in `globalScope.js` and reads them via `el.scope.X` was silently broken
|
|
181
|
-
// — the user's reactive function got `undefined` and either threw or
|
|
182
|
-
// rendered nothing. The frank-rewriter's `__scope` helper already chains
|
|
183
|
-
// through globalScope; this brings the runtime `el.scope` to parity.
|
|
184
|
-
// Capture scope set by the extends chain (applyExtends ran above) BEFORE
|
|
185
|
-
// we rebuild element.scope. Otherwise scope blocks declared on the base
|
|
186
|
-
// component (e.g. `Navbar.scope = { LANG_META }`) get blown away when
|
|
187
|
-
// a consumer extends without redeclaring scope (`AppNavbar: { extends:
|
|
188
|
-
// 'Navbar' }`). FA-L24.
|
|
189
|
-
const inheritedFromExtends = element.scope && typeof element.scope === 'object' ? element.scope : null
|
|
190
|
-
const ancestorScope = parent?.scope
|
|
191
|
-
const globalScope =
|
|
192
|
-
(element.context && typeof element.context.globalScope === 'object' && element.context.globalScope) || null
|
|
193
|
-
const baseScope = ancestorScope ||
|
|
194
|
-
(globalScope ? Object.create(globalScope) : null)
|
|
195
|
-
if (definition.scope !== undefined || inheritedFromExtends) {
|
|
196
|
-
const target = baseScope ? Object.create(baseScope) : {}
|
|
197
|
-
if (inheritedFromExtends) Object.assign(target, inheritedFromExtends)
|
|
198
|
-
if (definition.scope !== undefined) Object.assign(target, definition.scope)
|
|
199
|
-
element.scope = target
|
|
200
|
-
} else if (baseScope) {
|
|
201
|
-
element.scope = Object.create(baseScope)
|
|
202
|
-
}
|
|
203
|
-
if (definition.if !== undefined) element.if = definition.if
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
// Add methods
|
|
207
|
-
addMethods(element)
|
|
208
|
-
|
|
209
|
-
// Create state (reactive store)
|
|
210
|
-
createElementState(element, parent, definition)
|
|
211
|
-
|
|
212
|
-
// Evaluate `if` condition
|
|
213
|
-
if (element.if != null) {
|
|
214
|
-
const ifResult = typeof element.if === 'function'
|
|
215
|
-
? element.if(element, element.state, element.context)
|
|
216
|
-
: element.if
|
|
217
|
-
ref.__if = !!ifResult
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (!ref.__if) {
|
|
221
|
-
if (parent) parent[element.key] = element
|
|
222
|
-
// Register only the `if` effect so the element can appear later when the
|
|
223
|
-
// condition becomes true. Other effects will be registered inside the
|
|
224
|
-
// effect's first-time activation branch.
|
|
225
|
-
if (typeof definition.if === 'function') {
|
|
226
|
-
registerEffects(element, { if: definition.if })
|
|
227
|
-
}
|
|
228
|
-
return element
|
|
229
|
-
} else if (typeof definition.if === 'function') {
|
|
230
|
-
// Element starts visible — mark as initialized so the `if` effect's
|
|
231
|
-
// re-activation branch (false→true) doesn't re-run full creation.
|
|
232
|
-
ref.__ifInitialized = true
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Detect tag
|
|
236
|
-
if (!element.tag) element.tag = detectTag(element)
|
|
237
|
-
|
|
238
|
-
// Fire onInit
|
|
239
|
-
triggerLifecycle('Init', element, options)
|
|
240
|
-
|
|
241
|
-
// Create DOM node
|
|
242
|
-
const node = element.node = cacheNode(element)
|
|
243
|
-
if (!node) return element
|
|
244
|
-
|
|
245
|
-
triggerLifecycle('AttachNode', element, options)
|
|
246
|
-
|
|
247
|
-
// Set key as data attribute for dev debugging
|
|
248
|
-
if (node.setAttribute && element.key) {
|
|
249
|
-
node.setAttribute('data-key', element.key)
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Stamp data-smbls-app on the app's root node so its design-system CSS
|
|
253
|
-
// variables (scoped to `[data-smbls-app="<key>"]`) cascade through its
|
|
254
|
-
// subtree without leaking to sibling apps in the same document.
|
|
255
|
-
if (
|
|
256
|
-
node.setAttribute &&
|
|
257
|
-
element.context?.scopeSelector &&
|
|
258
|
-
element.context.scopeSelector !== ':root' &&
|
|
259
|
-
element.context.key &&
|
|
260
|
-
element.key === element.context.key
|
|
261
|
-
) {
|
|
262
|
-
node.setAttribute('data-smbls-app', element.context.key)
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Apply id attribute
|
|
266
|
-
if (element.id && node.setAttribute) {
|
|
267
|
-
node.id = element.id
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Register reactive effects for dynamic properties
|
|
271
|
-
registerEffects(element, definition)
|
|
272
|
-
|
|
273
|
-
// Apply static mixins (text, html, style, attr, data, classlist)
|
|
274
|
-
applyStaticMixins(element)
|
|
275
|
-
|
|
276
|
-
// Apply attrs-in-props (src, href, etc. from element properties)
|
|
277
|
-
applyAttrsInProps(element)
|
|
278
|
-
|
|
279
|
-
// Register events (onXxx handlers directly on element)
|
|
280
|
-
registerEvents(element, options)
|
|
281
|
-
|
|
282
|
-
// FB-render-runtime: previously a thrown error inside createChildren()
|
|
283
|
-
// would silently abort the element-mount walk, leaving the page with a
|
|
284
|
-
// 0-child root div and no console output. Wrap with try/catch + log so
|
|
285
|
-
// operators can see what crashed.
|
|
286
|
-
try {
|
|
287
|
-
createChildren(element, options)
|
|
288
|
-
} catch (childErr) {
|
|
289
|
-
if (typeof console !== 'undefined' && console.error) {
|
|
290
|
-
const elPath = element.__ref?.path?.join('.') || element.key || '<root>'
|
|
291
|
-
console.error(
|
|
292
|
-
`[smbls] createChildren failed at ${elPath}:`,
|
|
293
|
-
childErr?.message || childErr,
|
|
294
|
-
childErr?.stack ? '\n' + childErr.stack.split('\n').slice(0, 6).join('\n') : ''
|
|
295
|
-
)
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Assign node to DOM
|
|
300
|
-
assignNode(element, parent, attachOptions)
|
|
301
|
-
|
|
302
|
-
// Track in parent
|
|
303
|
-
if (parent) {
|
|
304
|
-
parent[element.key] = element
|
|
305
|
-
if (parent.__ref?.__children) {
|
|
306
|
-
if (!parent.__ref.__children.includes(element.key)) {
|
|
307
|
-
parent.__ref.__children.push(element.key)
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Evaluate extends-inherited html functions (e.g. Svg component).
|
|
313
|
-
// Guard: if the function returns a function (e.g. an extended component
|
|
314
|
-
// forwards `value` which itself is a function getter), skip setting
|
|
315
|
-
// innerHTML — otherwise we'd serialize the function source into the DOM
|
|
316
|
-
// (visible as `(el, s) => …` text inside textareas / inputs).
|
|
317
|
-
if (typeof element.html === 'function' && element.node && !element.node.innerHTML) {
|
|
318
|
-
try {
|
|
319
|
-
const val = element.html(element, element.state, element.context)
|
|
320
|
-
if (val != null && typeof val !== 'function') element.node.innerHTML = val
|
|
321
|
-
} catch (e) { /* skip if html function errors */ }
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
triggerLifecycle('Create', element, options)
|
|
325
|
-
triggerLifecycle('Complete', element, options)
|
|
326
|
-
triggerLifecycle('Render', element, options)
|
|
327
|
-
triggerLifecycle('RenderRouter', element, options)
|
|
328
|
-
|
|
329
|
-
// Start onFrame loop if defined
|
|
330
|
-
// FT-FRAMEWORK-3: Previously this returned early when `!element.node.isConnected`,
|
|
331
|
-
// killing the loop. During an initial top-down mount, the first rAF often
|
|
332
|
-
// fires before the root has been attached to document.body — so deeply
|
|
333
|
-
// nested children's onFrame loops self-terminated and never restarted.
|
|
334
|
-
// Now we reschedule unconditionally and gate only the handler call on
|
|
335
|
-
// `isConnected`. The loop stops only when the element is disposed
|
|
336
|
-
// (`element.node === null`).
|
|
337
|
-
if (typeof element.onFrame === 'function') {
|
|
338
|
-
const frameHandler = element.onFrame
|
|
339
|
-
const frameDsConfig = element.context?.designSystem
|
|
340
|
-
const tick = () => {
|
|
341
|
-
if (!element.node) return
|
|
342
|
-
if (element.node.isConnected) {
|
|
343
|
-
if (frameDsConfig) pushConfig(frameDsConfig)
|
|
344
|
-
try { frameHandler(element, element.state, element.context) } catch (e) { console.error('[DOMQL] onFrame error:', e) }
|
|
345
|
-
finally {
|
|
346
|
-
if (frameDsConfig) popConfig()
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
requestAnimationFrame(tick)
|
|
350
|
-
}
|
|
351
|
-
requestAnimationFrame(tick)
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
return element
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// --- State Creation ---
|
|
358
|
-
|
|
359
|
-
function createElementState(element, parent, definition) {
|
|
360
|
-
// Prefer the local definition's `state`, but fall back to anything that
|
|
361
|
-
// applyExtends already merged onto element.state from the extends chain.
|
|
362
|
-
// Without this fallback, a component like `{ extends: 'Dashboard' }` where
|
|
363
|
-
// Dashboard defines `state: { useJson: true }` would lose its state because
|
|
364
|
-
// the local definition has no `state` field, and we'd silently inherit
|
|
365
|
-
// parent state instead.
|
|
366
|
-
// Treat an empty plain-object `state: {}` declaration as "not provided"
|
|
367
|
-
// when this is the root element AND context.state has content. Without
|
|
368
|
-
// this, scaffolded `app.js` files that ship with `state: {}` boilerplate
|
|
369
|
-
// would silently override context.state (loaded from `state.js`) and
|
|
370
|
-
// leave the root state empty at runtime — see chatbot template where
|
|
371
|
-
// `s.root.navTabs` resolved undefined despite a populated state.js.
|
|
372
|
-
// Explicit non-empty objects + non-root elements bypass this rewrite.
|
|
373
|
-
const isRootCandidate = !parent
|
|
374
|
-
const isEmptyObjectState = definition.state &&
|
|
375
|
-
typeof definition.state === 'object' &&
|
|
376
|
-
!Array.isArray(definition.state) &&
|
|
377
|
-
Object.keys(definition.state).length === 0
|
|
378
|
-
const useContextStateFallback = isRootCandidate && isEmptyObjectState && element.context?.state
|
|
379
|
-
|
|
380
|
-
const stateDef = (definition.state !== undefined && !useContextStateFallback)
|
|
381
|
-
? definition.state
|
|
382
|
-
: (element.state && !isStore(element.state) ? element.state : undefined)
|
|
383
|
-
|
|
384
|
-
if (stateDef === undefined || stateDef === null) {
|
|
385
|
-
if (parent?.state) {
|
|
386
|
-
element.state = parent.state
|
|
387
|
-
applyStateMethods(element, element.state)
|
|
388
|
-
} else if (element.context?.state) {
|
|
389
|
-
// Root element: use prepared state from context
|
|
390
|
-
const ctxState = element.context.state
|
|
391
|
-
const stateData = { ...ctxState }
|
|
392
|
-
|
|
393
|
-
// Merge polyglot translations for active language into root state
|
|
394
|
-
const poly = element.context?.polyglot
|
|
395
|
-
if (poly?.translations) {
|
|
396
|
-
const lang = ctxState.lang || poly.defaultLang || 'en'
|
|
397
|
-
const langT = poly.translations[lang]
|
|
398
|
-
if (langT && typeof langT === 'object') {
|
|
399
|
-
for (const k in langT) {
|
|
400
|
-
if (stateData[k] === undefined) stateData[k] = langT[k]
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
// Also merge top-level translation keys (skip language code keys)
|
|
404
|
-
const langs = new Set(poly.languages || [])
|
|
405
|
-
for (const k in poly.translations) {
|
|
406
|
-
if (!langs.has(k) && stateData[k] === undefined) {
|
|
407
|
-
stateData[k] = poly.translations[k]
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
element.state = isStore(stateData) ? stateData : createStore(stateData)
|
|
413
|
-
element.state.__element = element
|
|
414
|
-
element.state.root = element.state
|
|
415
|
-
applyStateMethods(element, element.state)
|
|
416
|
-
} else {
|
|
417
|
-
element.state = {}
|
|
418
|
-
// Self-reference root so consumer code can rely on `s.root` always
|
|
419
|
-
// being defined, matching the root-element branch above. Non-enumerable
|
|
420
|
-
// so Object.keys / for…in / JSON.stringify skip it (this is a plain
|
|
421
|
-
// object — unlike a store proxy, which hides root from ownKeys via
|
|
422
|
-
// the proxy trap).
|
|
423
|
-
Object.defineProperty(element.state, 'root', {
|
|
424
|
-
value: element.state, enumerable: false, configurable: true, writable: true
|
|
425
|
-
})
|
|
426
|
-
applyStateMethods(element, element.state)
|
|
427
|
-
}
|
|
428
|
-
return
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
if (typeof stateDef === 'string') {
|
|
432
|
-
element.state = resolveStatePath(stateDef, parent)
|
|
433
|
-
// Propagate root reference to plain sub-state objects (v3 compat: s.root).
|
|
434
|
-
// Final fallback to self-ref keeps `s.root` defined even for orphan
|
|
435
|
-
// string-bound state (no parent, or parent has no state).
|
|
436
|
-
if (element.state && typeof element.state === 'object' && !isStore(element.state)) {
|
|
437
|
-
const rootRef = parent?.state?.root || parent?.state || element.state
|
|
438
|
-
Object.defineProperty(element.state, 'root', {
|
|
439
|
-
value: rootRef, enumerable: false, configurable: true, writable: true
|
|
440
|
-
})
|
|
441
|
-
}
|
|
442
|
-
applyStateMethods(element, element.state)
|
|
443
|
-
return
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
if (typeof stateDef === 'function') {
|
|
447
|
-
const resolved = stateDef(element, parent?.state)
|
|
448
|
-
element.state = createStore(resolved || {})
|
|
449
|
-
} else {
|
|
450
|
-
element.state = createStore({ ...stateDef })
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
element.state.__element = element
|
|
454
|
-
if (parent?.state) {
|
|
455
|
-
element.state.parent = parent.state
|
|
456
|
-
// Inherit root reference from parent chain
|
|
457
|
-
element.state.root = parent.state.root || parent.state
|
|
458
|
-
} else {
|
|
459
|
-
// Root element: self-reference for v3 compat (state.root === state)
|
|
460
|
-
element.state.root = element.state
|
|
461
|
-
}
|
|
462
|
-
applyStateMethods(element, element.state)
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
function resolvePath(obj, path) {
|
|
466
|
-
if (!path) return obj
|
|
467
|
-
const parts = path.split('/')
|
|
468
|
-
let result = obj
|
|
469
|
-
for (const part of parts) {
|
|
470
|
-
if (result === undefined || result === null) return {}
|
|
471
|
-
result = result[part]
|
|
472
|
-
}
|
|
473
|
-
return result ?? {}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function resolveStatePath(path, parent) {
|
|
477
|
-
if (!parent) return {}
|
|
478
|
-
if (path === '.') return parent.state || {}
|
|
479
|
-
if (path.startsWith('../')) {
|
|
480
|
-
const key = path.slice(3)
|
|
481
|
-
return key ? resolvePath(parent.state, key) : parent.state ?? {}
|
|
482
|
-
}
|
|
483
|
-
if (path.startsWith('~/')) {
|
|
484
|
-
const rootState = parent.state?.root || parent.state
|
|
485
|
-
const key = path.slice(2)
|
|
486
|
-
return key ? resolvePath(rootState, key) : rootState ?? {}
|
|
487
|
-
}
|
|
488
|
-
// Check parent state first (supports nested paths like 'product/0')
|
|
489
|
-
if (parent.state?.[path] !== undefined) return parent.state[path]
|
|
490
|
-
const nested = resolvePath(parent.state, path)
|
|
491
|
-
if (nested !== undefined && nested !== null && Object.keys(nested).length > 0) return nested
|
|
492
|
-
|
|
493
|
-
// Fall back to polyglot translations for current language
|
|
494
|
-
const poly = parent.context?.polyglot
|
|
495
|
-
if (poly?.translations) {
|
|
496
|
-
const lang = parent.state?.root?.lang || poly.defaultLang || 'en'
|
|
497
|
-
const langTranslations = poly.translations[lang]
|
|
498
|
-
if (langTranslations?.[path] !== undefined) return langTranslations[path]
|
|
499
|
-
// Check top-level translations
|
|
500
|
-
if (poly.translations[path] !== undefined) return poly.translations[path]
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
return {}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// --- Reactive Effects ---
|
|
507
|
-
|
|
508
|
-
function wrapCustomPropFunctions(element) {
|
|
509
|
-
const ref = element.__ref
|
|
510
|
-
const tag = element.tag || 'div'
|
|
511
|
-
const ds = element.context?.designSystem
|
|
512
|
-
|
|
513
|
-
// Two-pass: collect first, install second. Lets sibling refs like
|
|
514
|
-
// `a: fn`, `b: (el) => el.a + 1` work regardless of declaration order
|
|
515
|
-
// because every peer is already a value-producing getter once we begin
|
|
516
|
-
// evaluating any of them.
|
|
517
|
-
const targets = []
|
|
518
|
-
const ownProps = Object.keys(element)
|
|
519
|
-
for (let i = 0; i < ownProps.length; i++) {
|
|
520
|
-
const prop = ownProps[i]
|
|
521
|
-
if (typeof element[prop] !== 'function') continue
|
|
522
|
-
if (REGISTRY.has(prop)) continue
|
|
523
|
-
const firstChar = prop.charCodeAt(0)
|
|
524
|
-
if (firstChar >= 65 && firstChar <= 90) continue // child component
|
|
525
|
-
if (isEventHandler(prop)) continue
|
|
526
|
-
if (CSS_PROPS_REGISTRY[prop]) continue // handled by CSS effect loop
|
|
527
|
-
if (DEFAULT_CSS_PROPERTIES_LIST && DEFAULT_CSS_PROPERTIES_LIST.has(prop)) continue
|
|
528
|
-
if (checkAttributeByTagName(tag, prop)) continue // handled by attr effect loop
|
|
529
|
-
targets.push({ prop, fn: element[prop] })
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
for (let i = 0; i < targets.length; i++) {
|
|
533
|
-
const { prop, fn } = targets[i]
|
|
534
|
-
ref.__exec[prop] = fn
|
|
535
|
-
Object.defineProperty(element, prop, {
|
|
536
|
-
get () {
|
|
537
|
-
// Re-run fn on each read. Signal dependency tracking still works:
|
|
538
|
-
// whatever effect is reading el[prop] subscribes to state signals
|
|
539
|
-
// touched inside fn, so it re-fires on state changes. We skip a
|
|
540
|
-
// memoizing layer (createComputed) so fn never sees a peer custom
|
|
541
|
-
// prop in its raw-function form during this pass's eager init.
|
|
542
|
-
if (ds) pushConfig(ds)
|
|
543
|
-
try {
|
|
544
|
-
return fn(element, element.state, element.context)
|
|
545
|
-
} catch (e) {
|
|
546
|
-
return undefined
|
|
547
|
-
} finally {
|
|
548
|
-
if (ds) popConfig()
|
|
549
|
-
}
|
|
550
|
-
},
|
|
551
|
-
set (val) {
|
|
552
|
-
delete ref.__exec[prop]
|
|
553
|
-
Object.defineProperty(element, prop, {
|
|
554
|
-
value: val, writable: true, configurable: true, enumerable: true
|
|
555
|
-
})
|
|
556
|
-
},
|
|
557
|
-
configurable: true,
|
|
558
|
-
enumerable: true
|
|
559
|
-
})
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
function registerEffects(element, definition) {
|
|
564
|
-
const ref = element.__ref
|
|
565
|
-
|
|
566
|
-
// Custom-prop function reactivity (FRAMEWORK-1).
|
|
567
|
-
// Replace user-defined function-valued props (lowercase, not framework-
|
|
568
|
-
// reserved, not CSS, not HTML attribute, not event/lifecycle) with a getter
|
|
569
|
-
// that calls the function on read. Reads via `el.X` — including
|
|
570
|
-
// `el.parent.X` from descendants — return the evaluated value, and signal
|
|
571
|
-
// dependency tracking propagates through dependent effects (`text:`,
|
|
572
|
-
// `width:`, `href:`, etc.). Without this wrap, dependent effects coerce the
|
|
573
|
-
// un-invoked function via `Function.prototype.toString` and ship the
|
|
574
|
-
// arrow-function source into the DOM (e.g. "(el, s) => 8 / 10").
|
|
575
|
-
wrapCustomPropFunctions(element)
|
|
576
|
-
|
|
577
|
-
// Text effect
|
|
578
|
-
if (typeof element.text === 'function') {
|
|
579
|
-
const fn = element.text
|
|
580
|
-
const effect = createEffect(() => {
|
|
581
|
-
try {
|
|
582
|
-
let val = fn(element, element.state, element.context)
|
|
583
|
-
if (element.node && val != null && typeof val !== 'function' && typeof val !== 'object') {
|
|
584
|
-
// Resolve {{ }} templates in function return values
|
|
585
|
-
if (typeof val === 'string' && val.includes('{{')) {
|
|
586
|
-
val = replaceLiteralsWithObjectFields.call(element, val, element.state)
|
|
587
|
-
}
|
|
588
|
-
if (element.tag === 'string') {
|
|
589
|
-
element.node.nodeValue = val
|
|
590
|
-
} else {
|
|
591
|
-
element.node.textContent = val
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
} catch (e) { /* text effect may fail before state is populated */ }
|
|
595
|
-
}, { depth: ref.path.length })
|
|
596
|
-
ref.__effects.push(effect)
|
|
597
|
-
ref.__exec.text = fn
|
|
598
|
-
} else if (typeof element.text === 'string' && element.text.includes('{{')) {
|
|
599
|
-
const template = element.text
|
|
600
|
-
const effect = createEffect(() => {
|
|
601
|
-
const val = replaceLiteralsWithObjectFields.call(element, template, element.state)
|
|
602
|
-
if (val != null) element.text = val // Update element.text with resolved value
|
|
603
|
-
if (element.node) {
|
|
604
|
-
if (element.tag === 'string') {
|
|
605
|
-
element.node.nodeValue = val ?? ''
|
|
606
|
-
} else {
|
|
607
|
-
element.node.textContent = val ?? ''
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
}, { depth: ref.path.length })
|
|
611
|
-
ref.__effects.push(effect)
|
|
612
|
-
ref.__exec.text = template
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// Reactive effects for function-valued CSS properties (CSS_PROPS_REGISTRY and DEFAULT_CSS)
|
|
616
|
-
for (const prop in element) {
|
|
617
|
-
if (!Object.prototype.hasOwnProperty.call(element, prop)) continue
|
|
618
|
-
if (typeof element[prop] !== 'function') continue
|
|
619
|
-
if (REGISTRY.has(prop)) continue
|
|
620
|
-
if (isEventHandler(prop)) continue
|
|
621
|
-
if (ref.__exec[prop]) continue
|
|
622
|
-
|
|
623
|
-
if (CSS_PROPS_REGISTRY[prop]) {
|
|
624
|
-
const fn = element[prop]
|
|
625
|
-
const cssProp = prop
|
|
626
|
-
const effect = createEffect(() => {
|
|
627
|
-
// Push this element's design system config so CSS_PROPS_REGISTRY
|
|
628
|
-
// handlers (which internally call getActiveConfig() for token resolution)
|
|
629
|
-
// see the right config in multi-app setups (canvas editor vs. iframe project).
|
|
630
|
-
// The document is on the config, so getActiveDocument() resolves it too.
|
|
631
|
-
const ds = element.context?.designSystem
|
|
632
|
-
if (ds) pushConfig(ds)
|
|
633
|
-
try {
|
|
634
|
-
const val = fn(element, element.state, element.context)
|
|
635
|
-
const cssResult = CSS_PROPS_REGISTRY[cssProp](val, element, element.state, element.context)
|
|
636
|
-
if (cssResult && element.node) {
|
|
637
|
-
applyStyles(element.node, cssResult)
|
|
638
|
-
} else if (element.node && (cssProp === 'show' || cssProp === 'hide')) {
|
|
639
|
-
element.node.style.display = ''
|
|
640
|
-
}
|
|
641
|
-
} finally {
|
|
642
|
-
if (ds) popConfig()
|
|
643
|
-
}
|
|
644
|
-
}, { depth: ref.path.length })
|
|
645
|
-
ref.__effects.push(effect)
|
|
646
|
-
ref.__exec[prop] = fn
|
|
647
|
-
} else if (DEFAULT_CSS_PROPERTIES_LIST && DEFAULT_CSS_PROPERTIES_LIST.has(prop)) {
|
|
648
|
-
const fn = element[prop]
|
|
649
|
-
const cssProp = prop
|
|
650
|
-
const effect = createEffect(() => {
|
|
651
|
-
const ds = element.context?.designSystem
|
|
652
|
-
if (ds) pushConfig(ds)
|
|
653
|
-
try {
|
|
654
|
-
const val = fn(element, element.state, element.context)
|
|
655
|
-
if (element.node) {
|
|
656
|
-
if (val != null) applyStyles(element.node, { [cssProp]: val })
|
|
657
|
-
else element.node.style[cssProp] = ''
|
|
658
|
-
}
|
|
659
|
-
} finally {
|
|
660
|
-
if (ds) popConfig()
|
|
661
|
-
}
|
|
662
|
-
}, { depth: ref.path.length })
|
|
663
|
-
ref.__effects.push(effect)
|
|
664
|
-
ref.__exec[prop] = fn
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
// HTML effect.
|
|
670
|
-
// Guard: skip when the html function returns a function (forwarded reactive
|
|
671
|
-
// getter from an extended component). Avoids serializing the function source
|
|
672
|
-
// into innerHTML — see same guard in the eager-pass block above.
|
|
673
|
-
if (typeof definition.html === 'function') {
|
|
674
|
-
const fn = definition.html
|
|
675
|
-
const effect = createEffect(() => {
|
|
676
|
-
const val = fn(element, element.state, element.context)
|
|
677
|
-
if (element.node && val != null && typeof val !== 'function') {
|
|
678
|
-
element.node.innerHTML = val
|
|
679
|
-
}
|
|
680
|
-
}, { depth: ref.path.length })
|
|
681
|
-
ref.__effects.push(effect)
|
|
682
|
-
ref.__exec.html = fn
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
// Value effect — reactively set node.value on input/textarea/select.
|
|
686
|
-
// Preserves cursor position for the currently focused input so a re-render
|
|
687
|
-
// mid-typing does not yank the caret to the end.
|
|
688
|
-
if (typeof definition.value === 'function') {
|
|
689
|
-
const fn = definition.value
|
|
690
|
-
const effect = createEffect(() => {
|
|
691
|
-
try {
|
|
692
|
-
const val = fn(element, element.state, element.context)
|
|
693
|
-
if (!element.node || !('value' in element.node)) return
|
|
694
|
-
const next = (val == null) ? '' : String(val)
|
|
695
|
-
if (element.node.value === next) return
|
|
696
|
-
const focused = (typeof document !== 'undefined') && document.activeElement === element.node
|
|
697
|
-
const selStart = focused && typeof element.node.selectionStart === 'number'
|
|
698
|
-
? element.node.selectionStart : null
|
|
699
|
-
const selEnd = focused && typeof element.node.selectionEnd === 'number'
|
|
700
|
-
? element.node.selectionEnd : null
|
|
701
|
-
element.node.value = next
|
|
702
|
-
if (focused && selStart != null && selEnd != null) {
|
|
703
|
-
try { element.node.setSelectionRange(selStart, selEnd) } catch (e) {}
|
|
704
|
-
}
|
|
705
|
-
} catch (e) { /* value effect may fail before state is populated */ }
|
|
706
|
-
}, { depth: ref.path.length })
|
|
707
|
-
ref.__effects.push(effect)
|
|
708
|
-
ref.__exec.value = fn
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
// Style effect
|
|
712
|
-
if (typeof definition.style === 'function') {
|
|
713
|
-
const fn = definition.style
|
|
714
|
-
const effect = createEffect(() => {
|
|
715
|
-
const styles = fn(element, element.state, element.context)
|
|
716
|
-
applyStyles(element.node, styles)
|
|
717
|
-
}, { depth: ref.path.length })
|
|
718
|
-
ref.__effects.push(effect)
|
|
719
|
-
ref.__exec.style = fn
|
|
720
|
-
} else if (element.style && typeof element.style === 'object') {
|
|
721
|
-
// Nested-object style: function values per-key get individual effects.
|
|
722
|
-
// Static keys go through applyStaticMixins → applyStyles at mount.
|
|
723
|
-
// Without this branch, `style: { color: (el, s) => s.theme }` runs
|
|
724
|
-
// once at mount and never re-evaluates, while flat top-level
|
|
725
|
-
// `color: fn` IS reactive — surprising parity gap (FA-L23).
|
|
726
|
-
for (const styleKey in element.style) {
|
|
727
|
-
const styleFn = element.style[styleKey]
|
|
728
|
-
if (typeof styleFn !== 'function') continue
|
|
729
|
-
const effect = createEffect(() => {
|
|
730
|
-
try {
|
|
731
|
-
const val = styleFn(element, element.state, element.context)
|
|
732
|
-
if (element.node) applyStyles(element.node, { [styleKey]: val })
|
|
733
|
-
} catch (e) { /* style fn may fail before state is populated */ }
|
|
734
|
-
}, { depth: ref.path.length })
|
|
735
|
-
ref.__effects.push(effect)
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
// Attribute effects for function-valued attrs (check element.attr which includes extends)
|
|
740
|
-
if (element.attr && typeof element.attr === 'object') {
|
|
741
|
-
for (const attrName in element.attr) {
|
|
742
|
-
if (typeof element.attr[attrName] === 'function') {
|
|
743
|
-
const fn = element.attr[attrName]
|
|
744
|
-
const name = attrName
|
|
745
|
-
const effect = createEffect(() => {
|
|
746
|
-
try {
|
|
747
|
-
const val = fn(element, element.state, element.context)
|
|
748
|
-
if (element.node?.setAttribute) {
|
|
749
|
-
if (val == null || val === false) {
|
|
750
|
-
element.node.removeAttribute(name)
|
|
751
|
-
} else if (typeof val !== 'function' && typeof val !== 'object') {
|
|
752
|
-
element.node.setAttribute(name, val === true ? '' : val)
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
} catch (e) {
|
|
756
|
-
// Attr function may fail if element properties aren't ready yet
|
|
757
|
-
}
|
|
758
|
-
}, { depth: ref.path.length })
|
|
759
|
-
ref.__effects.push(effect)
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
// Reactive nested aria.X — `aria: { label: (el, s) => s.title }` re-runs
|
|
765
|
-
// on every state change. Static aria.X values are flattened to top-level
|
|
766
|
-
// attrs by `filterAttributesByTagName` (attrs-in-props/index.js), so we
|
|
767
|
-
// only wire effects for function values here.
|
|
768
|
-
if (element.aria && typeof element.aria === 'object') {
|
|
769
|
-
for (const ariaKey in element.aria) {
|
|
770
|
-
const ariaFn = element.aria[ariaKey]
|
|
771
|
-
if (typeof ariaFn !== 'function') continue
|
|
772
|
-
const attrName = 'aria-' + ariaKey.replace(/[A-Z]/g, m => '-' + m.toLowerCase())
|
|
773
|
-
const effect = createEffect(() => {
|
|
774
|
-
try {
|
|
775
|
-
const val = ariaFn(element, element.state, element.context)
|
|
776
|
-
if (!element.node?.setAttribute) return
|
|
777
|
-
if (val == null || val === false) {
|
|
778
|
-
element.node.removeAttribute(attrName)
|
|
779
|
-
} else if (typeof val !== 'function' && typeof val !== 'object') {
|
|
780
|
-
element.node.setAttribute(attrName, val === true ? '' : val)
|
|
781
|
-
}
|
|
782
|
-
} catch (e) { /* aria fn may fail before state is populated */ }
|
|
783
|
-
}, { depth: ref.path.length })
|
|
784
|
-
ref.__effects.push(effect)
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
// Reactive nested data.X — same shape as aria, kebab-cases the key.
|
|
789
|
-
if (element.data && typeof element.data === 'object') {
|
|
790
|
-
for (const dataKey in element.data) {
|
|
791
|
-
const dataFn = element.data[dataKey]
|
|
792
|
-
if (typeof dataFn !== 'function') continue
|
|
793
|
-
const kebab = dataKey.replace(/[A-Z]/g, m => '-' + m.toLowerCase())
|
|
794
|
-
const attrName = 'data-' + kebab
|
|
795
|
-
const effect = createEffect(() => {
|
|
796
|
-
try {
|
|
797
|
-
const val = dataFn(element, element.state, element.context)
|
|
798
|
-
if (!element.node?.setAttribute) return
|
|
799
|
-
if (val == null || val === false) {
|
|
800
|
-
element.node.removeAttribute(attrName)
|
|
801
|
-
} else if (typeof val !== 'function' && typeof val !== 'object') {
|
|
802
|
-
element.node.setAttribute(attrName, val === true ? '' : val)
|
|
803
|
-
}
|
|
804
|
-
} catch (e) { /* data fn may fail before state is populated */ }
|
|
805
|
-
}, { depth: ref.path.length })
|
|
806
|
-
ref.__effects.push(effect)
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
// Reactive classlist / class — `class: { active: (el, s) => s.isOpen }`
|
|
811
|
-
// toggles on state change. Static keys are applied at mount by
|
|
812
|
-
// applyStaticMixins; only function-valued keys need effects here.
|
|
813
|
-
const classObj = (element.classlist && typeof element.classlist === 'object' && !Array.isArray(element.classlist))
|
|
814
|
-
? element.classlist
|
|
815
|
-
: (element.class && typeof element.class === 'object' && !Array.isArray(element.class))
|
|
816
|
-
? element.class
|
|
817
|
-
: null
|
|
818
|
-
if (classObj) {
|
|
819
|
-
for (const className in classObj) {
|
|
820
|
-
const classFn = classObj[className]
|
|
821
|
-
if (typeof classFn !== 'function') continue
|
|
822
|
-
const effect = createEffect(() => {
|
|
823
|
-
try {
|
|
824
|
-
const val = classFn(element, element.state, element.context)
|
|
825
|
-
if (!element.node?.classList) return
|
|
826
|
-
// Mirror the static-path logic: `val: 'custom-name'` → toggle that
|
|
827
|
-
// string class on; falsy → toggle base key off; truthy non-string →
|
|
828
|
-
// toggle base key on.
|
|
829
|
-
if (typeof val === 'string') {
|
|
830
|
-
element.node.classList.add(val)
|
|
831
|
-
} else if (val) {
|
|
832
|
-
element.node.classList.add(className)
|
|
833
|
-
} else {
|
|
834
|
-
element.node.classList.remove(className)
|
|
835
|
-
}
|
|
836
|
-
} catch (e) { /* class fn may fail before state is populated */ }
|
|
837
|
-
}, { depth: ref.path.length })
|
|
838
|
-
ref.__effects.push(effect)
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
// If condition effect
|
|
843
|
-
if (typeof definition.if === 'function') {
|
|
844
|
-
const fn = definition.if
|
|
845
|
-
const effect = createEffect(() => {
|
|
846
|
-
const ifResult = fn(element, element.state, element.context)
|
|
847
|
-
const wasVisible = ref.__if
|
|
848
|
-
ref.__if = !!ifResult
|
|
849
|
-
|
|
850
|
-
if (wasVisible && !ref.__if && element.node) {
|
|
851
|
-
element.node.remove()
|
|
852
|
-
} else if (!wasVisible && ref.__if) {
|
|
853
|
-
// First-time activation: run the full creation steps that were skipped
|
|
854
|
-
// when initial `if` was false. This builds the element's node, children,
|
|
855
|
-
// attrs, and events the first time the condition becomes true.
|
|
856
|
-
if (!ref.__ifInitialized) {
|
|
857
|
-
ref.__ifInitialized = true
|
|
858
|
-
if (!element.tag) element.tag = detectTag(element)
|
|
859
|
-
triggerLifecycle('Init', element, {})
|
|
860
|
-
if (!element.node) element.node = cacheNode(element)
|
|
861
|
-
if (!element.node) return
|
|
862
|
-
triggerLifecycle('AttachNode', element, {})
|
|
863
|
-
if (element.node.setAttribute && element.key) {
|
|
864
|
-
element.node.setAttribute('data-key', element.key)
|
|
865
|
-
}
|
|
866
|
-
if (element.id && element.node.setAttribute) {
|
|
867
|
-
element.node.id = element.id
|
|
868
|
-
}
|
|
869
|
-
// Register reactive effects for dynamic props (excluding `if` to avoid recursion)
|
|
870
|
-
const prevIf = definition.if
|
|
871
|
-
delete definition.if
|
|
872
|
-
try { registerEffects(element, definition) } finally { definition.if = prevIf }
|
|
873
|
-
applyStaticMixins(element)
|
|
874
|
-
applyAttrsInProps(element)
|
|
875
|
-
registerEvents(element, {})
|
|
876
|
-
createChildren(element, {})
|
|
877
|
-
assignNode(element, element.parent)
|
|
878
|
-
} else {
|
|
879
|
-
// Subsequent re-activations: just re-attach existing node
|
|
880
|
-
if (!element.node) element.node = cacheNode(element)
|
|
881
|
-
// Find the next sibling in DOMQL tree that has a node, to insert before it
|
|
882
|
-
const parent = element.parent
|
|
883
|
-
const parentNode = parent?.node
|
|
884
|
-
if (parentNode && element.node) {
|
|
885
|
-
let nextSibNode = null
|
|
886
|
-
const keys = Object.keys(parent)
|
|
887
|
-
const myIdx = keys.indexOf(element.key)
|
|
888
|
-
for (let i = myIdx + 1; i < keys.length; i++) {
|
|
889
|
-
const sib = parent[keys[i]]
|
|
890
|
-
if (sib?.node?.parentNode === parentNode) {
|
|
891
|
-
nextSibNode = sib.node
|
|
892
|
-
break
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
if (nextSibNode) {
|
|
896
|
-
parentNode.insertBefore(element.node, nextSibNode)
|
|
897
|
-
} else {
|
|
898
|
-
parentNode.appendChild(element.node)
|
|
899
|
-
}
|
|
900
|
-
} else {
|
|
901
|
-
assignNode(element, element.parent)
|
|
902
|
-
}
|
|
903
|
-
applyStaticMixins(element)
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
}, { depth: ref.path.length })
|
|
907
|
-
ref.__effects.push(effect)
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
// CSS-in-props effects (dynamic CSS properties as functions)
|
|
911
|
-
for (const prop in element) {
|
|
912
|
-
if (!Object.prototype.hasOwnProperty.call(element, prop)) continue
|
|
913
|
-
if (REGISTRY.has(prop)) continue
|
|
914
|
-
if (typeof element[prop] !== 'function') continue
|
|
915
|
-
|
|
916
|
-
// Skip child components (Uppercase)
|
|
917
|
-
const firstChar = prop.charCodeAt(0)
|
|
918
|
-
if (firstChar >= 65 && firstChar <= 90) continue
|
|
919
|
-
|
|
920
|
-
// Skip onXxx event handlers
|
|
921
|
-
if (isEventHandler(prop)) continue
|
|
922
|
-
|
|
923
|
-
const fn = element[prop]
|
|
924
|
-
ref.__exec[prop] = fn
|
|
925
|
-
|
|
926
|
-
// Reactive effects for function-valued HTML attributes (src, href, etc.)
|
|
927
|
-
// Skip props already handled as CSS properties
|
|
928
|
-
const tag = element.tag || 'div'
|
|
929
|
-
if (checkAttributeByTagName(tag, prop) && !CSS_PROPS_REGISTRY[prop] && !(DEFAULT_CSS_PROPERTIES_LIST && DEFAULT_CSS_PROPERTIES_LIST.has(prop))) {
|
|
930
|
-
const attrName = prop
|
|
931
|
-
const effect = createEffect(() => {
|
|
932
|
-
try {
|
|
933
|
-
const val = fn(element, element.state, element.context)
|
|
934
|
-
if (element.node?.setAttribute) {
|
|
935
|
-
if (val == null || val === false) {
|
|
936
|
-
element.node.removeAttribute(attrName)
|
|
937
|
-
} else if (typeof val !== 'function' && typeof val !== 'object') {
|
|
938
|
-
// Use property setter for src on iframes/media to trigger navigation
|
|
939
|
-
if (attrName === 'src' && 'src' in element.node) {
|
|
940
|
-
element.node.src = val
|
|
941
|
-
} else {
|
|
942
|
-
element.node.setAttribute(attrName, val === true ? '' : val)
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
} catch (e) { /* attr function may fail before state is populated */ }
|
|
947
|
-
}, { depth: ref.path.length })
|
|
948
|
-
ref.__effects.push(effect)
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
// Reactive conditionals (.prop / !prop blocks)
|
|
953
|
-
registerConditionalEffects(element)
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
// --- Static Mixin Application ---
|
|
957
|
-
|
|
958
|
-
function applyStaticMixins(element) {
|
|
959
|
-
const { node } = element
|
|
960
|
-
if (!node) return
|
|
961
|
-
|
|
962
|
-
// Text (skip template literals — handled by reactive effect)
|
|
963
|
-
if (typeof element.text === 'string' && !element.text.includes('{{')) {
|
|
964
|
-
if (element.tag === 'string') {
|
|
965
|
-
node.nodeValue = element.text
|
|
966
|
-
} else {
|
|
967
|
-
node.textContent = element.text
|
|
968
|
-
}
|
|
969
|
-
} else if (typeof element.text === 'number') {
|
|
970
|
-
node.textContent = element.text
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
// CSS-in-props static transforms (show, hide, padding, flexAlign, etc.)
|
|
974
|
-
// Conditionals (.prop / !prop) are handled reactively in registerConditionalEffects
|
|
975
|
-
applyCssInProps(element)
|
|
976
|
-
|
|
977
|
-
// HTML
|
|
978
|
-
if (typeof element.html === 'string') {
|
|
979
|
-
node.innerHTML = element.html
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
// Static value for input/textarea/select. The reactive effect path
|
|
983
|
-
// (see registerEffects) handles function-valued `value:` props.
|
|
984
|
-
if (
|
|
985
|
-
element.value != null &&
|
|
986
|
-
typeof element.value !== 'function' &&
|
|
987
|
-
'value' in node
|
|
988
|
-
) {
|
|
989
|
-
const next = String(element.value)
|
|
990
|
-
if (node.value !== next) node.value = next
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
// Style
|
|
994
|
-
if (element.style && typeof element.style === 'object') {
|
|
995
|
-
applyStyles(node, element.style)
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
// Attributes
|
|
999
|
-
if (element.attr && typeof element.attr === 'object') {
|
|
1000
|
-
for (const key in element.attr) {
|
|
1001
|
-
const val = element.attr[key]
|
|
1002
|
-
if (typeof val === 'function' || typeof val === 'object') continue
|
|
1003
|
-
if (val == null || val === false) continue
|
|
1004
|
-
if (node.setAttribute) {
|
|
1005
|
-
node.setAttribute(key, val === true ? '' : val)
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
// Data attributes — same guard shape as the attr loop above. Function-
|
|
1011
|
-
// valued data.X is handled by the reactive-data effect in registerEffects;
|
|
1012
|
-
// writing the function reference into `dataset` here would render as a
|
|
1013
|
-
// stringified function literal in the DOM until something else overwrote
|
|
1014
|
-
// it. Skip null/false/object values too.
|
|
1015
|
-
if (element.data && typeof element.data === 'object') {
|
|
1016
|
-
for (const key in element.data) {
|
|
1017
|
-
const val = element.data[key]
|
|
1018
|
-
if (typeof val === 'function' || typeof val === 'object') continue
|
|
1019
|
-
if (val == null || val === false) continue
|
|
1020
|
-
if (node.dataset) node.dataset[key] = val === true ? '' : val
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
// Classlist
|
|
1025
|
-
if (element.classlist || element.class) {
|
|
1026
|
-
const cls = element.classlist || element.class
|
|
1027
|
-
if (typeof cls === 'string') {
|
|
1028
|
-
cls.split(' ').forEach(c => c && node.classList.add(c))
|
|
1029
|
-
} else if (typeof cls === 'object') {
|
|
1030
|
-
for (const name in cls) {
|
|
1031
|
-
const val = typeof cls[name] === 'function'
|
|
1032
|
-
? cls[name](element, element.state, element.context)
|
|
1033
|
-
: cls[name]
|
|
1034
|
-
if (val) node.classList.add(typeof val === 'string' ? val : name)
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
function applyStyles(node, styles) {
|
|
1041
|
-
if (!node || !styles || !node.style) return
|
|
1042
|
-
|
|
1043
|
-
let cssRules = null
|
|
1044
|
-
|
|
1045
|
-
for (const prop in styles) {
|
|
1046
|
-
const val = styles[prop]
|
|
1047
|
-
if (val == null) continue
|
|
1048
|
-
|
|
1049
|
-
// Handle &: selectors — generate CSS rules
|
|
1050
|
-
if (prop.charAt(0) === '&' && typeof val === 'object') {
|
|
1051
|
-
if (!cssRules) cssRules = {}
|
|
1052
|
-
cssRules[prop] = val
|
|
1053
|
-
continue
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
try {
|
|
1057
|
-
const str = String(val)
|
|
1058
|
-
// Reactive writes go through `setProperty` as plain inline (no
|
|
1059
|
-
// priority). Inline already beats class-rule selectors in the cascade
|
|
1060
|
-
// — the smbls atomic CSS engine never emits `!important`, so a plain
|
|
1061
|
-
// inline value wins on its own. The previous `'important'` priority
|
|
1062
|
-
// was a cargo-culted band-aid for an apparent stale-getComputedStyle
|
|
1063
|
-
// observation; in practice it caused CSS transitions on the same
|
|
1064
|
-
// element to leave stuck `Animation` objects (currentTime:0,
|
|
1065
|
-
// fill:'backwards') because Chrome's transition state machine
|
|
1066
|
-
// mishandles a mid-transition switch to/from `!important`.
|
|
1067
|
-
// If a value carries an explicit `!important` suffix from upstream
|
|
1068
|
-
// (rare — design tokens etc.), honor it.
|
|
1069
|
-
const cssProp = prop.replace(/[A-Z]/g, m => '-' + m.toLowerCase())
|
|
1070
|
-
const trimmed = str.trim()
|
|
1071
|
-
const isImportant = /\s*!important\s*$/.test(trimmed)
|
|
1072
|
-
const cleanVal = trimmed.replace(/\s*!important\s*$/, '')
|
|
1073
|
-
node.style.setProperty(cssProp, cleanVal, isImportant ? 'important' : '')
|
|
1074
|
-
} catch (e) {
|
|
1075
|
-
// Invalid style value
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
// Inject CSS rules for &: selectors
|
|
1080
|
-
if (cssRules) {
|
|
1081
|
-
injectScopedCSS(node, cssRules)
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
let _scopedStyleId = 0
|
|
1086
|
-
|
|
1087
|
-
function injectScopedCSS(node, rules) {
|
|
1088
|
-
const id = `_ds${_scopedStyleId++}`
|
|
1089
|
-
node.classList.add(id)
|
|
1090
|
-
|
|
1091
|
-
const doc = node.ownerDocument
|
|
1092
|
-
let styleEl = doc.querySelector('style[data-smbls-scoped]')
|
|
1093
|
-
if (!styleEl) {
|
|
1094
|
-
styleEl = doc.createElement('style')
|
|
1095
|
-
styleEl.setAttribute('data-smbls-scoped', '')
|
|
1096
|
-
doc.head.appendChild(styleEl)
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
let css = ''
|
|
1100
|
-
for (const selector in rules) {
|
|
1101
|
-
// Replace & with scoped class, and auto-scope unscoped comma-separated parts
|
|
1102
|
-
const scopedClass = '.' + id
|
|
1103
|
-
const resolvedSelector = selector.replace(/&/g, scopedClass)
|
|
1104
|
-
.split(',')
|
|
1105
|
-
.map(s => {
|
|
1106
|
-
const t = s.trim()
|
|
1107
|
-
return t.includes(scopedClass) ? t : scopedClass + t
|
|
1108
|
-
})
|
|
1109
|
-
.join(', ')
|
|
1110
|
-
const val = rules[selector]
|
|
1111
|
-
|
|
1112
|
-
if (typeof val === 'object') {
|
|
1113
|
-
// Check for nested selectors (e.g. '& [dropdown]' inside '&:focus-within')
|
|
1114
|
-
let props = ''
|
|
1115
|
-
let nested = ''
|
|
1116
|
-
for (const k in val) {
|
|
1117
|
-
const v = val[k]
|
|
1118
|
-
if (typeof v === 'object') {
|
|
1119
|
-
// `&` in the nested key references the outer's resolved selector
|
|
1120
|
-
// (standard CSS-nesting semantics). `&.X` chains on the outer,
|
|
1121
|
-
// `& .X` is a descendant of the outer, `&:hover` / `&[disabled]`
|
|
1122
|
-
// chain a pseudo/attr selector — all of these work because the
|
|
1123
|
-
// replace preserves the user's whitespace and punctuation between
|
|
1124
|
-
// `&` and the rest. When the nested key has no `&`, treat it as
|
|
1125
|
-
// an implicit descendant (preserves keys like `'> h1'` rendering
|
|
1126
|
-
// as `outer > h1`).
|
|
1127
|
-
const fullSelector = k.includes('&')
|
|
1128
|
-
? resolvedSelector.split(', ').map(s => k.replace(/&/g, s)).join(', ')
|
|
1129
|
-
: resolvedSelector.split(', ').map(s => `${s} ${k}`).join(', ')
|
|
1130
|
-
let nestedProps = ''
|
|
1131
|
-
for (const nk in v) {
|
|
1132
|
-
nestedProps += `${nk.replace(/[A-Z]/g, m => '-' + m.toLowerCase())}: ${v[nk]};`
|
|
1133
|
-
}
|
|
1134
|
-
nested += `${fullSelector} { ${nestedProps} }\n`
|
|
1135
|
-
} else {
|
|
1136
|
-
props += `${k.replace(/[A-Z]/g, m => '-' + m.toLowerCase())}: ${v};`
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
if (props) css += `${resolvedSelector} { ${props} }\n`
|
|
1140
|
-
if (nested) css += nested
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
if (css) styleEl.textContent += css
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
// --- Conditionals ---
|
|
1148
|
-
// Reactive `.propName` (truthy) and `!propName` (falsy) blocks.
|
|
1149
|
-
//
|
|
1150
|
-
// Each conditional becomes its own `createEffect` so signal-driven predicates
|
|
1151
|
-
// (state reads inside a function-valued prop) re-fire it. Block values are
|
|
1152
|
-
// applied via inline styles (with CSS_PROPS_REGISTRY token resolution) and
|
|
1153
|
-
// reverted by clearing the inline keys when the predicate flips off — the
|
|
1154
|
-
// static class never bakes in the conditional values, so there is nothing to
|
|
1155
|
-
// unmerge on revert.
|
|
1156
|
-
//
|
|
1157
|
-
// Imperative updates to a non-signal predicate (e.g. `element.update({ isActive })`
|
|
1158
|
-
// where `isActive` is a plain element prop, not in state) re-run via
|
|
1159
|
-
// `__conditionalRunners`, which `update.js` invokes after applying the prop.
|
|
1160
|
-
|
|
1161
|
-
function registerConditionalEffects(element) {
|
|
1162
|
-
const ref = element.__ref
|
|
1163
|
-
ref.__conditionalRunners = []
|
|
1164
|
-
|
|
1165
|
-
for (const key in element) {
|
|
1166
|
-
if (!Object.prototype.hasOwnProperty.call(element, key)) continue
|
|
1167
|
-
const c0 = key.charAt(0)
|
|
1168
|
-
if (c0 !== '.' && c0 !== '!') continue
|
|
1169
|
-
|
|
1170
|
-
const propName = key.slice(1)
|
|
1171
|
-
if (!propName) continue
|
|
1172
|
-
|
|
1173
|
-
const block = element[key]
|
|
1174
|
-
if (!block || typeof block !== 'object') continue
|
|
1175
|
-
|
|
1176
|
-
const negate = c0 === '!'
|
|
1177
|
-
const local = { lastMatch: false, appliedKeys: null }
|
|
1178
|
-
|
|
1179
|
-
const evaluate = () => {
|
|
1180
|
-
if (!element.node) return
|
|
1181
|
-
|
|
1182
|
-
// Resolution order matches css-in-props transformer
|
|
1183
|
-
// (`applyConditionalCaseProps`): element prop → state → context.cases.
|
|
1184
|
-
// Reading `element.state[propName]` inside the effect subscribes to the
|
|
1185
|
-
// signal so state-only predicates re-fire on `state.update`.
|
|
1186
|
-
let resolved
|
|
1187
|
-
const propVal = element[propName]
|
|
1188
|
-
if (propVal === undefined) {
|
|
1189
|
-
if (element.state && propName in element.state) {
|
|
1190
|
-
resolved = element.state[propName]
|
|
1191
|
-
} else {
|
|
1192
|
-
const caseFn = element.context?.cases?.[propName]
|
|
1193
|
-
if (caseFn === undefined) return
|
|
1194
|
-
resolved = typeof caseFn === 'function' ? caseFn.call(element, element) : caseFn
|
|
1195
|
-
}
|
|
1196
|
-
} else if (typeof propVal === 'function') {
|
|
1197
|
-
resolved = propVal(element, element.state, element.context)
|
|
1198
|
-
} else {
|
|
1199
|
-
resolved = propVal
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
const match = negate ? !resolved : !!resolved
|
|
1203
|
-
|
|
1204
|
-
if (match === local.lastMatch) return
|
|
1205
|
-
local.lastMatch = match
|
|
1206
|
-
|
|
1207
|
-
const ds = element.context?.designSystem
|
|
1208
|
-
if (ds) pushConfig(ds)
|
|
1209
|
-
try {
|
|
1210
|
-
if (match) {
|
|
1211
|
-
const styles = {}
|
|
1212
|
-
for (const prop in block) {
|
|
1213
|
-
if (!Object.prototype.hasOwnProperty.call(block, prop)) continue
|
|
1214
|
-
const raw = typeof block[prop] === 'function'
|
|
1215
|
-
? block[prop](element, element.state, element.context)
|
|
1216
|
-
: block[prop]
|
|
1217
|
-
if (raw == null) continue
|
|
1218
|
-
|
|
1219
|
-
if (CSS_PROPS_REGISTRY[prop]) {
|
|
1220
|
-
const r = CSS_PROPS_REGISTRY[prop](raw, element, element.state, element.context)
|
|
1221
|
-
if (r) {
|
|
1222
|
-
const arr = Array.isArray(r) ? r : [r]
|
|
1223
|
-
for (const o of arr) if (o) Object.assign(styles, o)
|
|
1224
|
-
}
|
|
1225
|
-
} else if (DEFAULT_CSS_PROPERTIES_LIST && DEFAULT_CSS_PROPERTIES_LIST.has(prop)) {
|
|
1226
|
-
styles[prop] = raw
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
const keys = Object.keys(styles)
|
|
1230
|
-
if (keys.length) {
|
|
1231
|
-
applyStyles(element.node, styles)
|
|
1232
|
-
local.appliedKeys = keys
|
|
1233
|
-
} else {
|
|
1234
|
-
local.appliedKeys = null
|
|
1235
|
-
}
|
|
1236
|
-
} else if (local.appliedKeys) {
|
|
1237
|
-
for (const k of local.appliedKeys) element.node.style[k] = ''
|
|
1238
|
-
local.appliedKeys = null
|
|
1239
|
-
}
|
|
1240
|
-
} finally {
|
|
1241
|
-
if (ds) popConfig()
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
ref.__conditionalRunners.push(evaluate)
|
|
1246
|
-
|
|
1247
|
-
const effect = createEffect(evaluate, { depth: ref.path.length })
|
|
1248
|
-
ref.__effects.push(effect)
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
// --- CSS-in-props ---
|
|
1253
|
-
// Processes element properties through CSS_PROPS_REGISTRY transforms
|
|
1254
|
-
|
|
1255
|
-
const __APPLY_CSS_STACK = new WeakSet()
|
|
1256
|
-
function applyCssInProps(element) {
|
|
1257
|
-
const { node } = element
|
|
1258
|
-
if (!node) return
|
|
1259
|
-
|
|
1260
|
-
if (__APPLY_CSS_STACK.has(element)) {
|
|
1261
|
-
console.error('[applyCssInProps] re-entry detected for element', element.key, 'prop path:', element.__ref?.path)
|
|
1262
|
-
return
|
|
1263
|
-
}
|
|
1264
|
-
__APPLY_CSS_STACK.add(element)
|
|
1265
|
-
|
|
1266
|
-
const ds = element.context?.designSystem
|
|
1267
|
-
if (ds) pushConfig(ds)
|
|
1268
|
-
|
|
1269
|
-
try {
|
|
1270
|
-
const cssStyles = {}
|
|
1271
|
-
let hasStyles = false
|
|
1272
|
-
|
|
1273
|
-
for (const prop in element) {
|
|
1274
|
-
if (!Object.prototype.hasOwnProperty.call(element, prop)) continue
|
|
1275
|
-
if (typeof element[prop] === 'function') continue
|
|
1276
|
-
if (REGISTRY.has(prop)) continue
|
|
1277
|
-
|
|
1278
|
-
const firstChar = prop.charCodeAt(0)
|
|
1279
|
-
if (firstChar >= 65 && firstChar <= 90) continue
|
|
1280
|
-
if (isEventHandler(prop)) continue
|
|
1281
|
-
|
|
1282
|
-
if (CSS_PROPS_REGISTRY[prop]) {
|
|
1283
|
-
const val = element[prop]
|
|
1284
|
-
const cssResult = CSS_PROPS_REGISTRY[prop](val, element, element.state, element.context)
|
|
1285
|
-
if (cssResult) {
|
|
1286
|
-
if (Array.isArray(cssResult)) {
|
|
1287
|
-
for (const r of cssResult) if (r) Object.assign(cssStyles, r)
|
|
1288
|
-
} else {
|
|
1289
|
-
Object.assign(cssStyles, cssResult)
|
|
1290
|
-
}
|
|
1291
|
-
hasStyles = true
|
|
1292
|
-
}
|
|
1293
|
-
} else if (DEFAULT_CSS_PROPERTIES_LIST && DEFAULT_CSS_PROPERTIES_LIST.has(prop)) {
|
|
1294
|
-
const val = element[prop]
|
|
1295
|
-
if (val != null && typeof val !== 'object') {
|
|
1296
|
-
cssStyles[prop] = val
|
|
1297
|
-
hasStyles = true
|
|
1298
|
-
}
|
|
1299
|
-
} else if (prop.charCodeAt(0) === 45) {
|
|
1300
|
-
// FT-FRAMEWORK-4: any dash-prefixed CSS key — both custom properties
|
|
1301
|
-
// (`--var-name`) and vendor-prefixed properties
|
|
1302
|
-
// (`-webkit-text-stroke`, `-moz-osx-font-smoothing`, …) — pass
|
|
1303
|
-
// straight through the atomic CSS engine. Previously only `--`
|
|
1304
|
-
// matched; `-webkit-text-stroke: 1px darkgrey` at the top level was
|
|
1305
|
-
// silently dropped, forcing consumers to wrap it in `style: { ... }`.
|
|
1306
|
-
const val = element[prop]
|
|
1307
|
-
if (val != null && typeof val !== 'object') {
|
|
1308
|
-
cssStyles[prop] = val
|
|
1309
|
-
hasStyles = true
|
|
1310
|
-
}
|
|
1311
|
-
} else if (prop.charAt(0) === '&') {
|
|
1312
|
-
// Top-level scope-anchored selector — e.g. `'& .token': { '&.keyword': {color} }`
|
|
1313
|
-
// — flows straight to the atomic CSS engine, which substitutes `&` with
|
|
1314
|
-
// the generated class. The `:` and `@` branches below already do this for
|
|
1315
|
-
// pseudo-classes and at-rules; without this branch, top-level `&` keys
|
|
1316
|
-
// were silently dropped and projects had to wrap every scope rule in a
|
|
1317
|
-
// `style: {}` block (or inject a `<style>` child as a workaround).
|
|
1318
|
-
const val = element[prop]
|
|
1319
|
-
if (val && typeof val === 'object') {
|
|
1320
|
-
cssStyles[prop] = val
|
|
1321
|
-
hasStyles = true
|
|
1322
|
-
}
|
|
1323
|
-
} else if (prop.charAt(0) === '@' || prop.charAt(0) === ':') {
|
|
1324
|
-
// Selector-based CSS: @dark, @media, :hover
|
|
1325
|
-
const val = element[prop]
|
|
1326
|
-
if (val && typeof val === 'object') {
|
|
1327
|
-
const resolved = {}
|
|
1328
|
-
for (const k in val) {
|
|
1329
|
-
let v = val[k]
|
|
1330
|
-
// Evaluate function-valued CSS within selector blocks (e.g. @dark background)
|
|
1331
|
-
if (typeof v === 'function') v = v(element, element.state, element.context)
|
|
1332
|
-
if (v == null || typeof v === 'function') continue
|
|
1333
|
-
if (CSS_PROPS_REGISTRY[k]) {
|
|
1334
|
-
const result = CSS_PROPS_REGISTRY[k](v, element, element.state, element.context)
|
|
1335
|
-
if (result) {
|
|
1336
|
-
if (Array.isArray(result)) { for (const r of result) if (r) Object.assign(resolved, r) }
|
|
1337
|
-
else Object.assign(resolved, result)
|
|
1338
|
-
}
|
|
1339
|
-
} else {
|
|
1340
|
-
resolved[k] = v
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
if (Object.keys(resolved).length) {
|
|
1344
|
-
// Per-element theme selectors (`@dark`, `@light`, or any custom
|
|
1345
|
-
// scheme `@name`) all compile to a single
|
|
1346
|
-
// `[data-theme="<scheme>"] &`
|
|
1347
|
-
// rule. The !important marker ensures the themed value wins over
|
|
1348
|
-
// the default (the unconditional property on the same element —
|
|
1349
|
-
// e.g. `background: 'neutral100'`).
|
|
1350
|
-
//
|
|
1351
|
-
// `globalTheme: 'auto'` is handled outside the CSS: `scratch.set`
|
|
1352
|
-
// watches `prefers-color-scheme` and writes `data-theme` on the
|
|
1353
|
-
// scope root to follow OS changes live. That keeps a single CSS
|
|
1354
|
-
// selector pattern — no `@media` duplicates to parse — and lets
|
|
1355
|
-
// custom themes like `@ocean` use the same mechanism.
|
|
1356
|
-
//
|
|
1357
|
-
// We intentionally treat `@dark`/`@light` as theme selectors even
|
|
1358
|
-
// if `designSystem.media.dark` / `.light` is defined — projects
|
|
1359
|
-
// historically mapped them to `[data-theme="…"]` there, and
|
|
1360
|
-
// routing through the media-shortcut branch would emit invalid
|
|
1361
|
-
// `@media [data-theme="…"]` rules.
|
|
1362
|
-
const isThemeSelector = prop.length > 1 && prop.charAt(0) === '@' &&
|
|
1363
|
-
!prop.startsWith('@media') && !prop.startsWith('@keyframes') && !prop.startsWith('@supports') &&
|
|
1364
|
-
(prop === '@dark' || prop === '@light' ||
|
|
1365
|
-
!element.context?.designSystem?.media?.[prop.slice(1)])
|
|
1366
|
-
if (isThemeSelector) {
|
|
1367
|
-
const important = {}
|
|
1368
|
-
for (const rk in resolved) {
|
|
1369
|
-
const rv = resolved[rk]
|
|
1370
|
-
important[rk] = typeof rv === 'string' && !rv.includes('!important') ? rv + ' !important' : rv
|
|
1371
|
-
}
|
|
1372
|
-
const scheme = prop.slice(1)
|
|
1373
|
-
cssStyles[`[data-theme="${scheme}"] &`] = important
|
|
1374
|
-
} else {
|
|
1375
|
-
// Resolve design system media shortcuts (@mobileL -> @media (max-width: 768px))
|
|
1376
|
-
let cssProp = prop
|
|
1377
|
-
if (prop.charAt(0) === '@' && !prop.startsWith('@media') && !prop.startsWith('@keyframes')) {
|
|
1378
|
-
const mediaKey = prop.slice(1)
|
|
1379
|
-
const mediaVal = element.context?.designSystem?.media?.[mediaKey] ||
|
|
1380
|
-
element.context?.designSystem?.media?.[mediaKey]
|
|
1381
|
-
if (mediaVal) cssProp = `@media ${mediaVal}`
|
|
1382
|
-
}
|
|
1383
|
-
// Separate flat CSS props from nested selector objects
|
|
1384
|
-
const flat = {}
|
|
1385
|
-
for (const rk in resolved) {
|
|
1386
|
-
const rv = resolved[rk]
|
|
1387
|
-
if (rv && typeof rv === 'object' && !Array.isArray(rv)) {
|
|
1388
|
-
// Nested selector (e.g. '> hgroup > h6': { color: 'blue' })
|
|
1389
|
-
// Resolve CSS tokens in nested values
|
|
1390
|
-
const nestedResolved = {}
|
|
1391
|
-
for (const nk in rv) {
|
|
1392
|
-
if (CSS_PROPS_REGISTRY[nk]) {
|
|
1393
|
-
const nr = CSS_PROPS_REGISTRY[nk](rv[nk], element, element.state, element.context)
|
|
1394
|
-
if (nr) {
|
|
1395
|
-
if (Array.isArray(nr)) { for (const r of nr) if (r) Object.assign(nestedResolved, r) }
|
|
1396
|
-
else Object.assign(nestedResolved, nr)
|
|
1397
|
-
}
|
|
1398
|
-
} else {
|
|
1399
|
-
nestedResolved[nk] = rv[nk]
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
// Pseudo-selectors attach directly, combinators need space
|
|
1403
|
-
const sep = rk.startsWith(':') ? '' : ' '
|
|
1404
|
-
// Distribute nested selector across comma-separated parts
|
|
1405
|
-
if (cssProp.includes(',')) {
|
|
1406
|
-
const distributed = cssProp.split(',').map(s => `${s.trim()}${sep}${rk}`).join(', ')
|
|
1407
|
-
cssStyles[distributed] = nestedResolved
|
|
1408
|
-
} else {
|
|
1409
|
-
cssStyles[`${cssProp}${sep}${rk}`] = nestedResolved
|
|
1410
|
-
}
|
|
1411
|
-
} else {
|
|
1412
|
-
flat[rk] = rv
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
if (Object.keys(flat).length) cssStyles[cssProp] = flat
|
|
1416
|
-
}
|
|
1417
|
-
hasStyles = true
|
|
1418
|
-
}
|
|
1419
|
-
hasStyles = true
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
if (hasStyles) {
|
|
1425
|
-
// Document already set by the outer push at the top of applyCssInProps
|
|
1426
|
-
|
|
1427
|
-
// Apply display:none !important as inline style (for show/hide to work reliably)
|
|
1428
|
-
if (cssStyles.display && String(cssStyles.display).includes('!important')) {
|
|
1429
|
-
node.style.setProperty('display', String(cssStyles.display).replace(/\s*!important\s*$/, ''), 'important')
|
|
1430
|
-
delete cssStyles.display
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
const className = atomicCss(cssStyles, null, element.context?.cssPrefix)
|
|
1434
|
-
if (className && node.classList) {
|
|
1435
|
-
className.split(' ').forEach(c => c && node.classList.add(c))
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
// Enforce explicit display when it conflicts with generated display from flow/align
|
|
1439
|
-
// Also handle hide:true / show:false (which set display:none via CSS_PROPS_REGISTRY)
|
|
1440
|
-
const effectiveDisplay = element.display != null && typeof element.display !== 'function'
|
|
1441
|
-
? element.display
|
|
1442
|
-
: (element.hide === true || element.show === false) ? 'none' : null
|
|
1443
|
-
if (effectiveDisplay != null) {
|
|
1444
|
-
const generatedDisplay = cssStyles.display
|
|
1445
|
-
if (generatedDisplay && generatedDisplay !== effectiveDisplay) {
|
|
1446
|
-
if (effectiveDisplay === 'none') {
|
|
1447
|
-
// For display:none, remove conflicting atomic display classes instead of inline style
|
|
1448
|
-
// (inline style would block conditional show/hide from updating later)
|
|
1449
|
-
const toRemove = []
|
|
1450
|
-
for (const cls of node.classList) {
|
|
1451
|
-
if (cls.startsWith('_d-') && cls !== '_d-none') toRemove.push(cls)
|
|
1452
|
-
}
|
|
1453
|
-
for (const cls of toRemove) node.classList.remove(cls)
|
|
1454
|
-
node.classList.add('_d-none')
|
|
1455
|
-
} else {
|
|
1456
|
-
// For grid/block/inline, inline style is safe
|
|
1457
|
-
node.style.display = element.display
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
} finally {
|
|
1463
|
-
if (ds) popConfig()
|
|
1464
|
-
__APPLY_CSS_STACK.delete(element)
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
// --- Attrs-in-props ---
|
|
1469
|
-
// Applies HTML attributes from element properties based on tag
|
|
1470
|
-
|
|
1471
|
-
function applyAttrsInProps(element) {
|
|
1472
|
-
const { node } = element
|
|
1473
|
-
if (!node || !node.setAttribute) return
|
|
1474
|
-
|
|
1475
|
-
// Apply STATIC nested aria.X / data.X values at mount. Function values
|
|
1476
|
-
// are owned by the reactive aria/data effects in registerEffects — those
|
|
1477
|
-
// re-run on state changes. Without this loop, aria/data static values
|
|
1478
|
-
// declared as nested objects (`aria: { label: 'Search' }`) silently
|
|
1479
|
-
// dropped — `applyStaticMixins` already promoted top-level data.X to
|
|
1480
|
-
// dataset and the parallel aria branch had no equivalent.
|
|
1481
|
-
if (element.aria && typeof element.aria === 'object') {
|
|
1482
|
-
for (const ariaKey in element.aria) {
|
|
1483
|
-
const v = element.aria[ariaKey]
|
|
1484
|
-
if (typeof v === 'function' || typeof v === 'object') continue
|
|
1485
|
-
if (v == null || v === false) continue
|
|
1486
|
-
const attrName = 'aria-' + ariaKey.replace(/[A-Z]/g, m => '-' + m.toLowerCase())
|
|
1487
|
-
node.setAttribute(attrName, v === true ? '' : v)
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
// Apply attribute transforms (src, href, poster, etc. with file/template resolution)
|
|
1492
|
-
const transformed = applyAttrTransforms(element)
|
|
1493
|
-
for (const attr in transformed) {
|
|
1494
|
-
const val = transformed[attr]
|
|
1495
|
-
if (val != null && val !== false && typeof val !== 'function' && typeof val !== 'object') {
|
|
1496
|
-
node.setAttribute(attr, val === true ? '' : val)
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
const tag = element.tag || 'div'
|
|
1501
|
-
for (const prop in element) {
|
|
1502
|
-
if (!Object.prototype.hasOwnProperty.call(element, prop)) continue
|
|
1503
|
-
if (typeof element[prop] === 'function') continue
|
|
1504
|
-
if (REGISTRY.has(prop)) continue
|
|
1505
|
-
if (prop === 'on') continue
|
|
1506
|
-
if (transformed[prop] !== undefined) continue
|
|
1507
|
-
|
|
1508
|
-
const firstChar = prop.charCodeAt(0)
|
|
1509
|
-
if (firstChar >= 65 && firstChar <= 90) continue
|
|
1510
|
-
if (isEventHandler(prop)) continue
|
|
1511
|
-
|
|
1512
|
-
// Skip CSS properties — already handled by applyCssInProps via CSS.
|
|
1513
|
-
// Setting design system tokens (e.g. 'A') as SVG/HTML attributes causes browser errors.
|
|
1514
|
-
if (CSS_PROPS_REGISTRY[prop] || (DEFAULT_CSS_PROPERTIES_LIST && DEFAULT_CSS_PROPERTIES_LIST.has(prop))) continue
|
|
1515
|
-
|
|
1516
|
-
// Check if this prop is a valid HTML attribute for this tag.
|
|
1517
|
-
// Note: do NOT pre-guard with `HTML_ATTRIBUTES[prop]` — that map is keyed
|
|
1518
|
-
// by tag name, not attribute name, so the guard would only ever pass for
|
|
1519
|
-
// props whose name happens to also be a tag (e.g. `a`, `div`). The proper
|
|
1520
|
-
// check is checkAttributeByTagName, which inspects the tag's attribute list.
|
|
1521
|
-
if (checkAttributeByTagName(tag, prop)) {
|
|
1522
|
-
let val = element[prop]
|
|
1523
|
-
if (val == null || val === false) continue
|
|
1524
|
-
|
|
1525
|
-
// Resolve {{ }} template literals
|
|
1526
|
-
if (typeof val === 'string' && val.includes('{{')) {
|
|
1527
|
-
val = replaceLiteralsWithObjectFields(val, element, element.state, element.context)
|
|
1528
|
-
element[prop] = val
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
if (val == null || val === false || typeof val === 'function' || typeof val === 'object') continue
|
|
1532
|
-
node.setAttribute(prop, val === true ? '' : val)
|
|
1533
|
-
}
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
// --- Events ---
|
|
1538
|
-
// v3 syntax: onXxx handlers directly on the component (no `on: {}` wrapper)
|
|
1539
|
-
|
|
1540
|
-
function registerEvents(element, options) {
|
|
1541
|
-
for (const key in element) {
|
|
1542
|
-
if (!Object.prototype.hasOwnProperty.call(element, key)) continue
|
|
1543
|
-
if (!isEventHandler(key)) continue
|
|
1544
|
-
|
|
1545
|
-
let handler = element[key]
|
|
1546
|
-
|
|
1547
|
-
// String shortcut: onClick: 'fnName' → el.call('fnName', ev).
|
|
1548
|
-
// Only valid JS identifiers — arbitrary strings like 'not a function' fall
|
|
1549
|
-
// through to the typeof-function check below and are skipped.
|
|
1550
|
-
if (typeof handler === 'string' && /^[A-Za-z_$][\w$]*$/.test(handler)) {
|
|
1551
|
-
const fnName = handler
|
|
1552
|
-
handler = function stringHandlerShortcut (ev) {
|
|
1553
|
-
return element.call(fnName, ev)
|
|
1554
|
-
}
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
if (typeof handler !== 'function') continue
|
|
1558
|
-
|
|
1559
|
-
const eventName = eventNameFrom(key)
|
|
1560
|
-
if (LIFECYCLE_EVENTS.has(eventName)) continue
|
|
1561
|
-
|
|
1562
|
-
registerEvent(toDomEventName(eventName), element, handler)
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
// --- Children ---
|
|
1567
|
-
|
|
1568
|
-
// Deep merge childProps with childDef — nested objects are merged, not replaced
|
|
1569
|
-
function deepMergeChildProps(base, override) {
|
|
1570
|
-
const result = { ...base }
|
|
1571
|
-
for (const key in override) {
|
|
1572
|
-
const bVal = result[key]
|
|
1573
|
-
const oVal = override[key]
|
|
1574
|
-
if (bVal && oVal && typeof bVal === 'object' && typeof oVal === 'object' &&
|
|
1575
|
-
!Array.isArray(bVal) && !Array.isArray(oVal) &&
|
|
1576
|
-
typeof bVal !== 'function' && typeof oVal !== 'function') {
|
|
1577
|
-
result[key] = deepMergeChildProps(bVal, oVal)
|
|
1578
|
-
} else {
|
|
1579
|
-
result[key] = oVal
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
return result
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
// Renders resolved children (array, object, or string state ref) into the element's DOM
|
|
1586
|
-
function renderChildren(element, children, options) {
|
|
1587
|
-
if (!children || !element.node) return
|
|
1588
|
-
|
|
1589
|
-
const childExtends = element.childExtends
|
|
1590
|
-
|
|
1591
|
-
if (Array.isArray(children)) {
|
|
1592
|
-
const doc = element.context?.document || globalThis.document
|
|
1593
|
-
const fragment = doc?.createDocumentFragment()
|
|
1594
|
-
if (!fragment) return
|
|
1595
|
-
|
|
1596
|
-
const childPropsRaw = element.childProps
|
|
1597
|
-
const childrenAs = element.childrenAs
|
|
1598
|
-
|
|
1599
|
-
for (let i = 0; i < children.length; i++) {
|
|
1600
|
-
let childDef = children[i]
|
|
1601
|
-
const rawChild = childDef
|
|
1602
|
-
|
|
1603
|
-
if (typeof childDef === 'string' || typeof childDef === 'number') {
|
|
1604
|
-
childDef = { text: childDef }
|
|
1605
|
-
}
|
|
1606
|
-
|
|
1607
|
-
// Resolve function-valued childProps per child
|
|
1608
|
-
let childProps = childPropsRaw
|
|
1609
|
-
if (typeof childPropsRaw === 'function') {
|
|
1610
|
-
try {
|
|
1611
|
-
childProps = childPropsRaw(element, childDef)
|
|
1612
|
-
} catch (e) {
|
|
1613
|
-
childProps = null
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
if (childrenAs === 'state') {
|
|
1618
|
-
// For string/number children, create state with both value and text for v3 compat
|
|
1619
|
-
let stateVal = rawChild
|
|
1620
|
-
if (typeof rawChild === 'string' || typeof rawChild === 'number') {
|
|
1621
|
-
stateVal = { value: rawChild, text: rawChild }
|
|
1622
|
-
}
|
|
1623
|
-
childDef = childProps ? { ...childProps, state: stateVal } : { state: stateVal }
|
|
1624
|
-
} else if (childProps) {
|
|
1625
|
-
childDef = deepMergeChildProps(childProps, childDef)
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
if (childExtends && !childDef.ignoreChildExtends) {
|
|
1629
|
-
if (childDef.extends) {
|
|
1630
|
-
const own = Array.isArray(childDef.extends) ? childDef.extends : [childDef.extends]
|
|
1631
|
-
const parent = Array.isArray(childExtends) ? childExtends : [childExtends]
|
|
1632
|
-
childDef = { ...childDef, extends: [...parent, ...own] }
|
|
1633
|
-
} else {
|
|
1634
|
-
childDef = { extends: childExtends, ...childDef }
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
const childKey = childDef?.key || `${i}`
|
|
1639
|
-
const child = create(childDef, element, childKey, options)
|
|
1640
|
-
if (child?.node) fragment.appendChild(child.node)
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
element.node.appendChild(fragment)
|
|
1644
|
-
return
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
if (typeof children === 'string') {
|
|
1648
|
-
const stateVal = resolveStatePath(children, element)
|
|
1649
|
-
if (Array.isArray(stateVal)) {
|
|
1650
|
-
renderChildren(element, stateVal, options)
|
|
1651
|
-
return
|
|
1652
|
-
}
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
|
-
if (typeof children === 'object') {
|
|
1656
|
-
for (const childKey in children) {
|
|
1657
|
-
if (REGISTRY.has(childKey)) continue
|
|
1658
|
-
|
|
1659
|
-
let childDef = children[childKey]
|
|
1660
|
-
if (childDef == null || childDef === false) continue
|
|
1661
|
-
|
|
1662
|
-
if (typeof childDef === 'string' || typeof childDef === 'number') {
|
|
1663
|
-
childDef = { text: childDef }
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
if (element.childProps) {
|
|
1667
|
-
childDef = deepMergeChildProps(element.childProps, childDef)
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
if (childExtends && !childDef.ignoreChildExtends) {
|
|
1671
|
-
if (childDef.extends) {
|
|
1672
|
-
const own = Array.isArray(childDef.extends) ? childDef.extends : [childDef.extends]
|
|
1673
|
-
const parent = Array.isArray(childExtends) ? childExtends : [childExtends]
|
|
1674
|
-
childDef = { ...childDef, extends: [...parent, ...own] }
|
|
1675
|
-
} else {
|
|
1676
|
-
childDef = { extends: childExtends, ...childDef }
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
create(childDef, element, childKey, options)
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
function createChildren(element, options) {
|
|
1686
|
-
let { children, content } = element
|
|
1687
|
-
const childExtends = element.childExtends
|
|
1688
|
-
|
|
1689
|
-
// Resolve function-valued children with reactive tracking
|
|
1690
|
-
if (typeof children === 'function') {
|
|
1691
|
-
const childrenFn = children
|
|
1692
|
-
children = childrenFn(element, element.state, element.context)
|
|
1693
|
-
element.children = children
|
|
1694
|
-
|
|
1695
|
-
// Reactive effect: keyed reconciliation on update.
|
|
1696
|
-
//
|
|
1697
|
-
// Previously we wiped every child and rebuilt from scratch whenever
|
|
1698
|
-
// the children factory produced a new result. That destroyed any
|
|
1699
|
-
// DOM state inside the subtree — most visibly, an `<input>` being
|
|
1700
|
-
// typed into would lose focus after every keystroke because its
|
|
1701
|
-
// node was detached and replaced.
|
|
1702
|
-
//
|
|
1703
|
-
// Now we diff the incoming array against `ref.__children`:
|
|
1704
|
-
// • keys still present → update the child's reactive state in
|
|
1705
|
-
// place so its subscribers re-render,
|
|
1706
|
-
// but the DOM node (and its focus /
|
|
1707
|
-
// selection state) is preserved.
|
|
1708
|
-
// • keys gone → remove + delete.
|
|
1709
|
-
// • new keys → create and append.
|
|
1710
|
-
//
|
|
1711
|
-
// Non-array results and non-`childrenAs: 'state'` children still
|
|
1712
|
-
// fall through to the full rebuild path since we can't safely
|
|
1713
|
-
// update their internals in place.
|
|
1714
|
-
const ref = element.__ref
|
|
1715
|
-
let initialized = false
|
|
1716
|
-
const effect = createEffect(() => {
|
|
1717
|
-
const result = childrenFn(element, element.state, element.context)
|
|
1718
|
-
if (!initialized) { initialized = true; return }
|
|
1719
|
-
if (result === element.children) return
|
|
1720
|
-
|
|
1721
|
-
element.children = result
|
|
1722
|
-
|
|
1723
|
-
// Fast path: keyed reconciliation for array children + childrenAs=state.
|
|
1724
|
-
// This is the only case where we can safely update in place, because
|
|
1725
|
-
// each child owns a reactive store whose fields we can patch.
|
|
1726
|
-
const isArr = Array.isArray(result)
|
|
1727
|
-
const canReconcile = isArr && element.childrenAs === 'state'
|
|
1728
|
-
|
|
1729
|
-
if (canReconcile) {
|
|
1730
|
-
// Compute the new key order. Key resolution mirrors renderChildren:
|
|
1731
|
-
// a `childProps` factory can lift `key` to the top level of the
|
|
1732
|
-
// synthesized childDef; otherwise fall back to `rawChild.key`,
|
|
1733
|
-
// finally falling back to the positional index.
|
|
1734
|
-
const childPropsRaw = element.childProps
|
|
1735
|
-
const newOrder = []
|
|
1736
|
-
const itemByKey = new Map()
|
|
1737
|
-
for (let i = 0; i < result.length; i++) {
|
|
1738
|
-
const rawChild = result[i]
|
|
1739
|
-
let childProps = childPropsRaw
|
|
1740
|
-
if (typeof childPropsRaw === 'function') {
|
|
1741
|
-
try { childProps = childPropsRaw(element, rawChild) } catch { childProps = null }
|
|
1742
|
-
}
|
|
1743
|
-
const key = (childProps && childProps.key) || (rawChild && rawChild.key) || `${i}`
|
|
1744
|
-
newOrder.push(key)
|
|
1745
|
-
itemByKey.set(key, { rawChild, childProps })
|
|
1746
|
-
}
|
|
1747
|
-
|
|
1748
|
-
const newKeySet = new Set(newOrder)
|
|
1749
|
-
// Remove children whose keys are no longer present.
|
|
1750
|
-
for (let i = ref.__children.length - 1; i >= 0; i--) {
|
|
1751
|
-
const k = ref.__children[i]
|
|
1752
|
-
if (!newKeySet.has(k)) {
|
|
1753
|
-
const child = element[k]
|
|
1754
|
-
if (child?.remove) child.remove()
|
|
1755
|
-
delete element[k]
|
|
1756
|
-
ref.__children.splice(i, 1)
|
|
1757
|
-
}
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
// Upsert children in order. Existing keys reuse their node and
|
|
1761
|
-
// patch their state; new keys are created and appended.
|
|
1762
|
-
const childExtends = element.childExtends
|
|
1763
|
-
const doc = element.context?.document || globalThis.document
|
|
1764
|
-
const fragment = doc?.createDocumentFragment()
|
|
1765
|
-
for (const key of newOrder) {
|
|
1766
|
-
const entry = itemByKey.get(key)
|
|
1767
|
-
const rawChild = entry.rawChild
|
|
1768
|
-
const existing = element[key]
|
|
1769
|
-
|
|
1770
|
-
if (existing && existing.state) {
|
|
1771
|
-
// Patch the existing store with the new item's fields via
|
|
1772
|
-
// `state.replace(obj)` — unlike `update` (which only merges
|
|
1773
|
-
// new keys) `replace` also REMOVES any prior keys that
|
|
1774
|
-
// aren't on the new object. Critical for list reconcilers:
|
|
1775
|
-
// without replace semantics, a row that went from parent
|
|
1776
|
-
// summary → sub-row would inherit the old `_isParent: true`
|
|
1777
|
-
// flag and render both roles simultaneously.
|
|
1778
|
-
//
|
|
1779
|
-
// Signals fire for added / changed / removed keys, so the
|
|
1780
|
-
// child's `text` / `hide` / `style` gates re-evaluate and
|
|
1781
|
-
// the DOM node (+ any focused input inside) stays untouched.
|
|
1782
|
-
if (rawChild && typeof rawChild === 'object') {
|
|
1783
|
-
const st = existing.state
|
|
1784
|
-
if (typeof st.replace === 'function') {
|
|
1785
|
-
st.replace(rawChild)
|
|
1786
|
-
} else if (typeof st.update === 'function') {
|
|
1787
|
-
// Fallback for stores without replace(): emulate it by
|
|
1788
|
-
// nulling keys not in the new object, then update().
|
|
1789
|
-
for (const k2 in st) {
|
|
1790
|
-
if (k2 === '__element' || k2 === 'root' || k2 === 'parent' || k2 === 'update') continue
|
|
1791
|
-
if (!Object.prototype.hasOwnProperty.call(rawChild, k2)) {
|
|
1792
|
-
try { st[k2] = undefined } catch {}
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
|
-
st.update(rawChild)
|
|
1796
|
-
} else {
|
|
1797
|
-
for (const k2 in rawChild) st[k2] = rawChild[k2]
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
continue
|
|
1801
|
-
}
|
|
1802
|
-
|
|
1803
|
-
// New child — build the childDef the same way renderChildren does,
|
|
1804
|
-
// then create + append.
|
|
1805
|
-
let stateVal = rawChild
|
|
1806
|
-
if (typeof rawChild === 'string' || typeof rawChild === 'number') {
|
|
1807
|
-
stateVal = { value: rawChild, text: rawChild }
|
|
1808
|
-
}
|
|
1809
|
-
const childProps = entry.childProps
|
|
1810
|
-
let childDef = childProps ? { ...childProps, state: stateVal } : { state: stateVal }
|
|
1811
|
-
|
|
1812
|
-
if (childExtends && !childDef.ignoreChildExtends) {
|
|
1813
|
-
if (childDef.extends) {
|
|
1814
|
-
const own = Array.isArray(childDef.extends) ? childDef.extends : [childDef.extends]
|
|
1815
|
-
const parent = Array.isArray(childExtends) ? childExtends : [childExtends]
|
|
1816
|
-
childDef = { ...childDef, extends: [...parent, ...own] }
|
|
1817
|
-
} else {
|
|
1818
|
-
childDef = { extends: childExtends, ...childDef }
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
const child = create(childDef, element, key, options)
|
|
1823
|
-
if (child?.node && fragment) fragment.appendChild(child.node)
|
|
1824
|
-
}
|
|
1825
|
-
if (fragment && fragment.childNodes.length && element.node) {
|
|
1826
|
-
element.node.appendChild(fragment)
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
|
-
// Keep __children in the new order for stable future reconciliations.
|
|
1830
|
-
ref.__children = newOrder
|
|
1831
|
-
return
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
|
-
// Fallback: full rebuild (non-array results, or childrenAs not set).
|
|
1835
|
-
for (const key of [...ref.__children]) {
|
|
1836
|
-
const child = element[key]
|
|
1837
|
-
if (child?.remove) child.remove()
|
|
1838
|
-
delete element[key]
|
|
1839
|
-
}
|
|
1840
|
-
ref.__children = []
|
|
1841
|
-
renderChildren(element, result, options)
|
|
1842
|
-
}, { depth: ref.path.length })
|
|
1843
|
-
ref.__effects.push(effect)
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
renderChildren(element, children || content, options)
|
|
1847
|
-
|
|
1848
|
-
if (children || content) return
|
|
1849
|
-
|
|
1850
|
-
// Iterate element properties for child components (Uppercase keys)
|
|
1851
|
-
const components = element.context?.components || {}
|
|
1852
|
-
|
|
1853
|
-
// v3 compat: resolve XxxExtends shorthand
|
|
1854
|
-
// Pattern: StarIconExtends: 'Icon', name: 'star', fontSize: 'X2'
|
|
1855
|
-
// Becomes: StarIcon: { extends: 'Icon', name: 'star', fontSize: 'X2' }
|
|
1856
|
-
{
|
|
1857
|
-
const keys = Object.keys(element)
|
|
1858
|
-
for (let i = 0; i < keys.length; i++) {
|
|
1859
|
-
const prop = keys[i]
|
|
1860
|
-
if (!prop.endsWith('Extends') || typeof element[prop] !== 'string') continue
|
|
1861
|
-
const childName = prop.slice(0, -7)
|
|
1862
|
-
if (!childName || childName.charCodeAt(0) < 65 || childName.charCodeAt(0) > 90) continue
|
|
1863
|
-
|
|
1864
|
-
const childDef = { extends: element[prop] }
|
|
1865
|
-
delete element[prop]
|
|
1866
|
-
|
|
1867
|
-
// Collect subsequent lowercase properties until next Uppercase key or end
|
|
1868
|
-
for (let j = i + 1; j < keys.length; j++) {
|
|
1869
|
-
const nextKey = keys[j]
|
|
1870
|
-
const fc = nextKey.charCodeAt(0)
|
|
1871
|
-
if (fc >= 65 && fc <= 90) break // Stop at next child/component key
|
|
1872
|
-
if (REGISTRY.has(nextKey)) break // Stop at framework keys
|
|
1873
|
-
if (typeof element[nextKey] === 'function' && (nextKey.startsWith('on') || nextKey.startsWith('.'))) break
|
|
1874
|
-
const val = element[nextKey]
|
|
1875
|
-
if (val !== undefined) {
|
|
1876
|
-
childDef[nextKey] = val
|
|
1877
|
-
delete element[nextKey]
|
|
1878
|
-
}
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1881
|
-
// Merge with existing child def if present
|
|
1882
|
-
if (element[childName] && typeof element[childName] === 'object') {
|
|
1883
|
-
element[childName] = { ...childDef, ...element[childName] }
|
|
1884
|
-
} else {
|
|
1885
|
-
element[childName] = childDef
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
for (const prop in element) {
|
|
1891
|
-
if (!Object.prototype.hasOwnProperty.call(element, prop)) continue
|
|
1892
|
-
if (REGISTRY.has(prop)) continue
|
|
1893
|
-
if (typeof element[prop] !== 'object' || element[prop] === null) continue
|
|
1894
|
-
|
|
1895
|
-
const firstChar = prop.charCodeAt(0)
|
|
1896
|
-
if (firstChar < 65 || firstChar > 90) continue
|
|
1897
|
-
|
|
1898
|
-
let childDef = element[prop]
|
|
1899
|
-
if (childDef.__ref) continue // Already created
|
|
1900
|
-
|
|
1901
|
-
// Auto-extend from context component if key matches and no extends set
|
|
1902
|
-
if (!childDef.extends && !childDef.extend) {
|
|
1903
|
-
const comp = components[prop] || components[`smbls.${prop}`]
|
|
1904
|
-
if (comp) {
|
|
1905
|
-
childDef = { extends: comp, ...childDef }
|
|
1906
|
-
}
|
|
1907
|
-
}
|
|
1908
|
-
|
|
1909
|
-
// Apply childProps (e.g. fontSize, padding from parent's childProps)
|
|
1910
|
-
if (element.childProps) {
|
|
1911
|
-
childDef = { ...element.childProps, ...childDef }
|
|
1912
|
-
}
|
|
1913
|
-
|
|
1914
|
-
if (childExtends && !childDef.ignoreChildExtends) {
|
|
1915
|
-
if (childDef.extends) {
|
|
1916
|
-
const own = Array.isArray(childDef.extends) ? childDef.extends : [childDef.extends]
|
|
1917
|
-
const parent = Array.isArray(childExtends) ? childExtends : [childExtends]
|
|
1918
|
-
childDef = { ...childDef, extends: [...parent, ...own] }
|
|
1919
|
-
} else {
|
|
1920
|
-
childDef = { extends: childExtends, ...childDef }
|
|
1921
|
-
}
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
|
-
create(childDef, element, prop, options)
|
|
1925
|
-
}
|
|
1926
|
-
}
|
|
1927
|
-
|
|
1928
|
-
// --- DOM Assignment ---
|
|
1929
|
-
|
|
1930
|
-
function assignNode(element, parent, attachOptions) {
|
|
1931
|
-
const { node } = element
|
|
1932
|
-
if (!node) return
|
|
1933
|
-
|
|
1934
|
-
// Expose DOMQL ref on DOM node in dev mode (use $0.ref in DevTools)
|
|
1935
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
1936
|
-
node.ref = element
|
|
1937
|
-
}
|
|
1938
|
-
|
|
1939
|
-
// Support both DOMQL elements (.node) and raw DOM nodes (root case)
|
|
1940
|
-
const parentNode = parent?.node || (parent?.nodeType ? parent : null)
|
|
1941
|
-
if (!parentNode) return
|
|
1942
|
-
|
|
1943
|
-
if (attachOptions?.position === 'before' && attachOptions.node) {
|
|
1944
|
-
parentNode.insertBefore(node, attachOptions.node)
|
|
1945
|
-
} else if (attachOptions?.position === 'after' && attachOptions.node) {
|
|
1946
|
-
const nextSib = attachOptions.node.nextSibling
|
|
1947
|
-
if (nextSib) {
|
|
1948
|
-
parentNode.insertBefore(node, nextSib)
|
|
1949
|
-
} else {
|
|
1950
|
-
parentNode.appendChild(node)
|
|
1951
|
-
}
|
|
1952
|
-
} else {
|
|
1953
|
-
parentNode.appendChild(node)
|
|
1954
|
-
}
|
|
1955
|
-
}
|
|
1956
|
-
|
|
1957
|
-
function triggerLifecycle(eventSuffix, element, options) {
|
|
1958
|
-
const hookName = eventSuffix.charAt(0).toLowerCase() + eventSuffix.slice(1)
|
|
1959
|
-
if (element.context?.plugins?.length) runPluginHook(hookName, element, options)
|
|
1960
|
-
|
|
1961
|
-
const key = 'on' + eventSuffix
|
|
1962
|
-
const handler = element[key]
|
|
1963
|
-
if (typeof handler === 'function') {
|
|
1964
|
-
// Scope config to this element's context so lifecycle handlers that
|
|
1965
|
-
// inject CSS or resolve tokens use the right app. The document is
|
|
1966
|
-
// stored on the config, so getActiveDocument() resolves it too.
|
|
1967
|
-
const ds = element.context?.designSystem
|
|
1968
|
-
if (ds) pushConfig(ds)
|
|
1969
|
-
try {
|
|
1970
|
-
const result = handler(element, element.state, element.context, options)
|
|
1971
|
-
if (result === false) return false
|
|
1972
|
-
} catch (err) {
|
|
1973
|
-
// Route the error through the plugin error hook so observers
|
|
1974
|
-
// (e.g. the analyze plugin) see it with full element context.
|
|
1975
|
-
// Falls back to console.error when no plugin captures it.
|
|
1976
|
-
const handled = element.context?.plugins?.length &&
|
|
1977
|
-
runPluginHook('error', element, { hook: key, error: err })
|
|
1978
|
-
if (!handled) console.error(`[DOMQL] Lifecycle "${key}" error:`, err)
|
|
1979
|
-
if (element.context?.strictMode) throw err
|
|
1980
|
-
} finally {
|
|
1981
|
-
if (ds) popConfig()
|
|
1982
|
-
}
|
|
1983
|
-
}
|
|
1984
|
-
return true
|
|
1985
|
-
}
|
|
1986
|
-
|
|
1987
|
-
export default create
|