@symbo.ls/element 3.14.0 → 3.14.1
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/package.json +6 -3
- package/src/cache.js +1 -1
- package/src/create.js +174 -15
- package/src/methods.js +5 -1
- package/src/update.js +10 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbo.ls/element",
|
|
3
|
-
"version": "3.14.
|
|
4
|
-
"description": "Core element creation, update, and disposal engine for smbls
|
|
3
|
+
"version": "3.14.1",
|
|
4
|
+
"description": "Core element creation, update, and disposal engine for smbls v3.14",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
7
7
|
"module": "./index.js",
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
"css-in-props": "^3.14.0",
|
|
17
17
|
"attrs-in-props": "^3.14.0"
|
|
18
18
|
},
|
|
19
|
-
"files": [
|
|
19
|
+
"files": [
|
|
20
|
+
"src",
|
|
21
|
+
"index.js"
|
|
22
|
+
],
|
|
20
23
|
"license": "CC-BY-NC-4.0"
|
|
21
24
|
}
|
package/src/cache.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
// Node caching - cloneNode from cached prototypes instead of createElement
|
|
4
|
-
// This is already a v3 optimization that works well -- preserved in
|
|
4
|
+
// This is already a v3 optimization that works well -- preserved in v3.14
|
|
5
5
|
|
|
6
6
|
const SVG_NS = 'http://www.w3.org/2000/svg'
|
|
7
7
|
|
package/src/create.js
CHANGED
|
@@ -155,12 +155,27 @@ export function create(definition, parent, key, options = {}, attachOptions) {
|
|
|
155
155
|
if (definition.classlist !== undefined) element.classlist = definition.classlist
|
|
156
156
|
if (definition.text !== undefined) element.text = definition.text
|
|
157
157
|
if (definition.html !== undefined) element.html = definition.html
|
|
158
|
+
// The scope chain at any element resolves keys in this order:
|
|
159
|
+
// own (definition.scope) → parent.scope → … → root.scope → context.globalScope
|
|
160
|
+
// Direct reads like `el.scope.LANG_META` walk the prototype chain, so a
|
|
161
|
+
// component that doesn't declare its own scope still sees parent scopes
|
|
162
|
+
// and the project's globalScope as fallbacks. Without `globalScope` at the
|
|
163
|
+
// bottom of the chain, every project pattern that puts shared constants
|
|
164
|
+
// in `globalScope.js` and reads them via `el.scope.X` was silently broken
|
|
165
|
+
// — the user's reactive function got `undefined` and either threw or
|
|
166
|
+
// rendered nothing. The frank-rewriter's `__scope` helper already chains
|
|
167
|
+
// through globalScope; this brings the runtime `el.scope` to parity.
|
|
168
|
+
const ancestorScope = parent?.scope
|
|
169
|
+
const globalScope =
|
|
170
|
+
(element.context && typeof element.context.globalScope === 'object' && element.context.globalScope) || null
|
|
171
|
+
const baseScope = ancestorScope ||
|
|
172
|
+
(globalScope ? Object.create(globalScope) : null)
|
|
158
173
|
if (definition.scope !== undefined) {
|
|
159
|
-
element.scope =
|
|
160
|
-
? Object.assign(Object.create(
|
|
174
|
+
element.scope = baseScope
|
|
175
|
+
? Object.assign(Object.create(baseScope), definition.scope)
|
|
161
176
|
: definition.scope
|
|
162
|
-
} else if (
|
|
163
|
-
element.scope = Object.create(
|
|
177
|
+
} else if (baseScope) {
|
|
178
|
+
element.scope = Object.create(baseScope)
|
|
164
179
|
}
|
|
165
180
|
if (definition.if !== undefined) element.if = definition.if
|
|
166
181
|
|
|
@@ -316,7 +331,21 @@ function createElementState(element, parent, definition) {
|
|
|
316
331
|
// Dashboard defines `state: { useJson: true }` would lose its state because
|
|
317
332
|
// the local definition has no `state` field, and we'd silently inherit
|
|
318
333
|
// parent state instead.
|
|
319
|
-
|
|
334
|
+
// Treat an empty plain-object `state: {}` declaration as "not provided"
|
|
335
|
+
// when this is the root element AND context.state has content. Without
|
|
336
|
+
// this, scaffolded `app.js` files that ship with `state: {}` boilerplate
|
|
337
|
+
// would silently override context.state (loaded from `state.js`) and
|
|
338
|
+
// leave the root state empty at runtime — see chatbot template where
|
|
339
|
+
// `s.root.navTabs` resolved undefined despite a populated state.js.
|
|
340
|
+
// Explicit non-empty objects + non-root elements bypass this rewrite.
|
|
341
|
+
const isRootCandidate = !parent
|
|
342
|
+
const isEmptyObjectState = definition.state &&
|
|
343
|
+
typeof definition.state === 'object' &&
|
|
344
|
+
!Array.isArray(definition.state) &&
|
|
345
|
+
Object.keys(definition.state).length === 0
|
|
346
|
+
const useContextStateFallback = isRootCandidate && isEmptyObjectState && element.context?.state
|
|
347
|
+
|
|
348
|
+
const stateDef = (definition.state !== undefined && !useContextStateFallback)
|
|
320
349
|
? definition.state
|
|
321
350
|
: (element.state && !isStore(element.state) ? element.state : undefined)
|
|
322
351
|
|
|
@@ -590,6 +619,23 @@ function registerEffects(element, definition) {
|
|
|
590
619
|
}, { depth: ref.path.length })
|
|
591
620
|
ref.__effects.push(effect)
|
|
592
621
|
ref.__exec.style = fn
|
|
622
|
+
} else if (element.style && typeof element.style === 'object') {
|
|
623
|
+
// Nested-object style: function values per-key get individual effects.
|
|
624
|
+
// Static keys go through applyStaticMixins → applyStyles at mount.
|
|
625
|
+
// Without this branch, `style: { color: (el, s) => s.theme }` runs
|
|
626
|
+
// once at mount and never re-evaluates, while flat top-level
|
|
627
|
+
// `color: fn` IS reactive — surprising parity gap (FA-L23).
|
|
628
|
+
for (const styleKey in element.style) {
|
|
629
|
+
const styleFn = element.style[styleKey]
|
|
630
|
+
if (typeof styleFn !== 'function') continue
|
|
631
|
+
const effect = createEffect(() => {
|
|
632
|
+
try {
|
|
633
|
+
const val = styleFn(element, element.state, element.context)
|
|
634
|
+
if (element.node) applyStyles(element.node, { [styleKey]: val })
|
|
635
|
+
} catch (e) { /* style fn may fail before state is populated */ }
|
|
636
|
+
}, { depth: ref.path.length })
|
|
637
|
+
ref.__effects.push(effect)
|
|
638
|
+
}
|
|
593
639
|
}
|
|
594
640
|
|
|
595
641
|
// Attribute effects for function-valued attrs (check element.attr which includes extends)
|
|
@@ -617,6 +663,84 @@ function registerEffects(element, definition) {
|
|
|
617
663
|
}
|
|
618
664
|
}
|
|
619
665
|
|
|
666
|
+
// Reactive nested aria.X — `aria: { label: (el, s) => s.title }` re-runs
|
|
667
|
+
// on every state change. Static aria.X values are flattened to top-level
|
|
668
|
+
// attrs by `filterAttributesByTagName` (attrs-in-props/index.js), so we
|
|
669
|
+
// only wire effects for function values here.
|
|
670
|
+
if (element.aria && typeof element.aria === 'object') {
|
|
671
|
+
for (const ariaKey in element.aria) {
|
|
672
|
+
const ariaFn = element.aria[ariaKey]
|
|
673
|
+
if (typeof ariaFn !== 'function') continue
|
|
674
|
+
const attrName = 'aria-' + ariaKey.replace(/[A-Z]/g, m => '-' + m.toLowerCase())
|
|
675
|
+
const effect = createEffect(() => {
|
|
676
|
+
try {
|
|
677
|
+
const val = ariaFn(element, element.state, element.context)
|
|
678
|
+
if (!element.node?.setAttribute) return
|
|
679
|
+
if (val == null || val === false) {
|
|
680
|
+
element.node.removeAttribute(attrName)
|
|
681
|
+
} else if (typeof val !== 'function' && typeof val !== 'object') {
|
|
682
|
+
element.node.setAttribute(attrName, val === true ? '' : val)
|
|
683
|
+
}
|
|
684
|
+
} catch (e) { /* aria fn may fail before state is populated */ }
|
|
685
|
+
}, { depth: ref.path.length })
|
|
686
|
+
ref.__effects.push(effect)
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Reactive nested data.X — same shape as aria, kebab-cases the key.
|
|
691
|
+
if (element.data && typeof element.data === 'object') {
|
|
692
|
+
for (const dataKey in element.data) {
|
|
693
|
+
const dataFn = element.data[dataKey]
|
|
694
|
+
if (typeof dataFn !== 'function') continue
|
|
695
|
+
const kebab = dataKey.replace(/[A-Z]/g, m => '-' + m.toLowerCase())
|
|
696
|
+
const attrName = 'data-' + kebab
|
|
697
|
+
const effect = createEffect(() => {
|
|
698
|
+
try {
|
|
699
|
+
const val = dataFn(element, element.state, element.context)
|
|
700
|
+
if (!element.node?.setAttribute) return
|
|
701
|
+
if (val == null || val === false) {
|
|
702
|
+
element.node.removeAttribute(attrName)
|
|
703
|
+
} else if (typeof val !== 'function' && typeof val !== 'object') {
|
|
704
|
+
element.node.setAttribute(attrName, val === true ? '' : val)
|
|
705
|
+
}
|
|
706
|
+
} catch (e) { /* data fn may fail before state is populated */ }
|
|
707
|
+
}, { depth: ref.path.length })
|
|
708
|
+
ref.__effects.push(effect)
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Reactive classlist / class — `class: { active: (el, s) => s.isOpen }`
|
|
713
|
+
// toggles on state change. Static keys are applied at mount by
|
|
714
|
+
// applyStaticMixins; only function-valued keys need effects here.
|
|
715
|
+
const classObj = (element.classlist && typeof element.classlist === 'object' && !Array.isArray(element.classlist))
|
|
716
|
+
? element.classlist
|
|
717
|
+
: (element.class && typeof element.class === 'object' && !Array.isArray(element.class))
|
|
718
|
+
? element.class
|
|
719
|
+
: null
|
|
720
|
+
if (classObj) {
|
|
721
|
+
for (const className in classObj) {
|
|
722
|
+
const classFn = classObj[className]
|
|
723
|
+
if (typeof classFn !== 'function') continue
|
|
724
|
+
const effect = createEffect(() => {
|
|
725
|
+
try {
|
|
726
|
+
const val = classFn(element, element.state, element.context)
|
|
727
|
+
if (!element.node?.classList) return
|
|
728
|
+
// Mirror the static-path logic: `val: 'custom-name'` → toggle that
|
|
729
|
+
// string class on; falsy → toggle base key off; truthy non-string →
|
|
730
|
+
// toggle base key on.
|
|
731
|
+
if (typeof val === 'string') {
|
|
732
|
+
element.node.classList.add(val)
|
|
733
|
+
} else if (val) {
|
|
734
|
+
element.node.classList.add(className)
|
|
735
|
+
} else {
|
|
736
|
+
element.node.classList.remove(className)
|
|
737
|
+
}
|
|
738
|
+
} catch (e) { /* class fn may fail before state is populated */ }
|
|
739
|
+
}, { depth: ref.path.length })
|
|
740
|
+
ref.__effects.push(effect)
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
620
744
|
// If condition effect
|
|
621
745
|
if (typeof definition.if === 'function') {
|
|
622
746
|
const fn = definition.if
|
|
@@ -785,10 +909,17 @@ function applyStaticMixins(element) {
|
|
|
785
909
|
}
|
|
786
910
|
}
|
|
787
911
|
|
|
788
|
-
// Data attributes
|
|
912
|
+
// Data attributes — same guard shape as the attr loop above. Function-
|
|
913
|
+
// valued data.X is handled by the reactive-data effect in registerEffects;
|
|
914
|
+
// writing the function reference into `dataset` here would render as a
|
|
915
|
+
// stringified function literal in the DOM until something else overwrote
|
|
916
|
+
// it. Skip null/false/object values too.
|
|
789
917
|
if (element.data && typeof element.data === 'object') {
|
|
790
918
|
for (const key in element.data) {
|
|
791
|
-
|
|
919
|
+
const val = element.data[key]
|
|
920
|
+
if (typeof val === 'function' || typeof val === 'object') continue
|
|
921
|
+
if (val == null || val === false) continue
|
|
922
|
+
if (node.dataset) node.dataset[key] = val === true ? '' : val
|
|
792
923
|
}
|
|
793
924
|
}
|
|
794
925
|
|
|
@@ -826,12 +957,22 @@ function applyStyles(node, styles) {
|
|
|
826
957
|
|
|
827
958
|
try {
|
|
828
959
|
const str = String(val)
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
960
|
+
// Reactive writes go through `setProperty` as plain inline (no
|
|
961
|
+
// priority). Inline already beats class-rule selectors in the cascade
|
|
962
|
+
// — the smbls atomic CSS engine never emits `!important`, so a plain
|
|
963
|
+
// inline value wins on its own. The previous `'important'` priority
|
|
964
|
+
// was a cargo-culted band-aid for an apparent stale-getComputedStyle
|
|
965
|
+
// observation; in practice it caused CSS transitions on the same
|
|
966
|
+
// element to leave stuck `Animation` objects (currentTime:0,
|
|
967
|
+
// fill:'backwards') because Chrome's transition state machine
|
|
968
|
+
// mishandles a mid-transition switch to/from `!important`.
|
|
969
|
+
// If a value carries an explicit `!important` suffix from upstream
|
|
970
|
+
// (rare — design tokens etc.), honor it.
|
|
971
|
+
const cssProp = prop.replace(/[A-Z]/g, m => '-' + m.toLowerCase())
|
|
972
|
+
const trimmed = str.trim()
|
|
973
|
+
const isImportant = /\s*!important\s*$/.test(trimmed)
|
|
974
|
+
const cleanVal = trimmed.replace(/\s*!important\s*$/, '')
|
|
975
|
+
node.style.setProperty(cssProp, cleanVal, isImportant ? 'important' : '')
|
|
835
976
|
} catch (e) {
|
|
836
977
|
// Invalid style value
|
|
837
978
|
}
|
|
@@ -1208,6 +1349,22 @@ function applyAttrsInProps(element) {
|
|
|
1208
1349
|
const { node } = element
|
|
1209
1350
|
if (!node || !node.setAttribute) return
|
|
1210
1351
|
|
|
1352
|
+
// Apply STATIC nested aria.X / data.X values at mount. Function values
|
|
1353
|
+
// are owned by the reactive aria/data effects in registerEffects — those
|
|
1354
|
+
// re-run on state changes. Without this loop, aria/data static values
|
|
1355
|
+
// declared as nested objects (`aria: { label: 'Search' }`) silently
|
|
1356
|
+
// dropped — `applyStaticMixins` already promoted top-level data.X to
|
|
1357
|
+
// dataset and the parallel aria branch had no equivalent.
|
|
1358
|
+
if (element.aria && typeof element.aria === 'object') {
|
|
1359
|
+
for (const ariaKey in element.aria) {
|
|
1360
|
+
const v = element.aria[ariaKey]
|
|
1361
|
+
if (typeof v === 'function' || typeof v === 'object') continue
|
|
1362
|
+
if (v == null || v === false) continue
|
|
1363
|
+
const attrName = 'aria-' + ariaKey.replace(/[A-Z]/g, m => '-' + m.toLowerCase())
|
|
1364
|
+
node.setAttribute(attrName, v === true ? '' : v)
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1211
1368
|
// Apply attribute transforms (src, href, poster, etc. with file/template resolution)
|
|
1212
1369
|
const transformed = applyAttrTransforms(element)
|
|
1213
1370
|
for (const attr in transformed) {
|
|
@@ -1264,8 +1421,10 @@ function registerEvents(element, options) {
|
|
|
1264
1421
|
|
|
1265
1422
|
let handler = element[key]
|
|
1266
1423
|
|
|
1267
|
-
// String shortcut: onClick: 'fnName' → el.call('fnName', ev)
|
|
1268
|
-
|
|
1424
|
+
// String shortcut: onClick: 'fnName' → el.call('fnName', ev).
|
|
1425
|
+
// Only valid JS identifiers — arbitrary strings like 'not a function' fall
|
|
1426
|
+
// through to the typeof-function check below and are skipped.
|
|
1427
|
+
if (typeof handler === 'string' && /^[A-Za-z_$][\w$]*$/.test(handler)) {
|
|
1269
1428
|
const fnName = handler
|
|
1270
1429
|
handler = function stringHandlerShortcut (ev) {
|
|
1271
1430
|
return element.call(fnName, ev)
|
package/src/methods.js
CHANGED
|
@@ -195,7 +195,11 @@ const elementProto = {
|
|
|
195
195
|
setNodeStyles(styles) {
|
|
196
196
|
if (!this.node?.style) return
|
|
197
197
|
for (const prop in styles) {
|
|
198
|
-
|
|
198
|
+
const val = styles[prop]
|
|
199
|
+
if (val == null) continue
|
|
200
|
+
const cssProp = prop.replace(/[A-Z]/g, m => '-' + m.toLowerCase())
|
|
201
|
+
const str = String(val).replace(/\s*!important\s*$/, '')
|
|
202
|
+
this.node.style.setProperty(cssProp, str, 'important')
|
|
199
203
|
}
|
|
200
204
|
},
|
|
201
205
|
|
package/src/update.js
CHANGED
|
@@ -47,11 +47,19 @@ export function update(params = {}, opts = {}) {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
// Update style
|
|
50
|
+
// Update style — mirrors applyStyles in create.js: write via setProperty
|
|
51
|
+
// with `important` priority so cascade resolves to the inline value
|
|
52
|
+
// even when the element carries atomic CSS classes for sibling static
|
|
53
|
+
// props. Plain `node.style[prop] = val` writes don't invalidate the
|
|
54
|
+
// cached cascade when the first-paint resolved to a different value.
|
|
51
55
|
if (params.style && element.node?.style) {
|
|
52
56
|
if (!ref.__exec.style) {
|
|
53
57
|
for (const prop in params.style) {
|
|
54
|
-
|
|
58
|
+
const val = params.style[prop]
|
|
59
|
+
if (val == null) continue
|
|
60
|
+
const cssProp = prop.replace(/[A-Z]/g, m => '-' + m.toLowerCase())
|
|
61
|
+
const str = String(val).replace(/\s*!important\s*$/, '')
|
|
62
|
+
element.node.style.setProperty(cssProp, str, 'important')
|
|
55
63
|
}
|
|
56
64
|
}
|
|
57
65
|
if (!element.style) element.style = {}
|