@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/src/delegate.js DELETED
@@ -1,163 +0,0 @@
1
- 'use strict'
2
-
3
- // Event Delegation - single root listener per event type
4
- // Uses WeakMap to associate DOM nodes with their handlers
5
-
6
- import { DELEGATED_EVENTS, LIFECYCLE_EVENTS, PASSIVE_EVENTS } from './registry.js'
7
- import { pushConfig, popConfig } from '@symbo.ls/scratch'
8
-
9
- const delegateRoots = new WeakMap()
10
-
11
- // Reused listener-option objects — browsers treat `{ passive: true }` as
12
- // read-only metadata, so a single shared instance per shape is safe and
13
- // avoids a per-listener allocation.
14
- const PASSIVE_BUBBLE = { passive: true }
15
- const PASSIVE_CAPTURE = { passive: true, capture: true }
16
-
17
- const listenerOptions = (eventName, useCapture) => {
18
- if (PASSIVE_EVENTS.has(eventName)) {
19
- return useCapture ? PASSIVE_CAPTURE : PASSIVE_BUBBLE
20
- }
21
- return useCapture || false
22
- }
23
-
24
- function getDelegateRoot(rootNode) {
25
- let root = delegateRoots.get(rootNode)
26
- if (!root) {
27
- root = { handlers: new Map(), installed: new Set() }
28
- delegateRoots.set(rootNode, root)
29
- }
30
- return root
31
- }
32
-
33
- function installDelegatedEvent(eventName, rootNode) {
34
- const root = getDelegateRoot(rootNode)
35
- if (root.installed.has(eventName)) return
36
-
37
- root.installed.add(eventName)
38
-
39
- // Use capture for non-bubbling events (focus/blur, mouseenter/mouseleave)
40
- const useCapture = eventName === 'focus' || eventName === 'blur' ||
41
- eventName === 'focusin' || eventName === 'focusout' ||
42
- eventName === 'mouseenter' || eventName === 'mouseleave'
43
-
44
- const isEnterLeave = eventName === 'mouseenter' || eventName === 'mouseleave'
45
-
46
- rootNode.addEventListener(eventName, (event) => {
47
- let target = event.target
48
-
49
- while (target && target !== rootNode) {
50
- const handlerMap = root.handlers.get(eventName)
51
- if (handlerMap) {
52
- const handler = handlerMap.get(target)
53
- if (handler) {
54
- // For mouseenter/mouseleave: only fire if relatedTarget is outside this node
55
- // (prevents firing when moving between children inside the element)
56
- if (isEnterLeave && event.relatedTarget && target.contains(event.relatedTarget)) {
57
- target = target.parentNode
58
- continue
59
- }
60
- handler(event)
61
- if (event.cancelBubble) return
62
- }
63
- }
64
- target = target.parentNode
65
- }
66
- }, listenerOptions(eventName, useCapture))
67
- }
68
-
69
- // Register a delegated event handler for a DOM node
70
- export function addDelegatedEvent(eventName, node, handler, rootNode) {
71
- if (!rootNode) rootNode = node.ownerDocument?.documentElement || node.getRootNode()
72
-
73
- const root = getDelegateRoot(rootNode)
74
-
75
- // Ensure the root listener is installed
76
- installDelegatedEvent(eventName, rootNode)
77
-
78
- // Get or create the handler map for this event
79
- if (!root.handlers.has(eventName)) {
80
- root.handlers.set(eventName, new WeakMap())
81
- }
82
-
83
- root.handlers.get(eventName).set(node, handler)
84
- }
85
-
86
- // Remove a delegated event handler
87
- export function removeDelegatedEvent(eventName, node, rootNode) {
88
- if (!rootNode) rootNode = node.ownerDocument?.documentElement || node.getRootNode()
89
-
90
- const root = delegateRoots.get(rootNode)
91
- if (!root) return
92
-
93
- const handlerMap = root.handlers.get(eventName)
94
- if (handlerMap) {
95
- handlerMap.delete(node)
96
- }
97
- }
98
-
99
- // Check if an event name should be delegated
100
- export function isDelegatable(eventName) {
101
- return DELEGATED_EVENTS.has(eventName) && !LIFECYCLE_EVENTS.has(eventName)
102
- }
103
-
104
- // Register event from element's `on` object or props
105
- export function registerEvent(eventName, element, handler) {
106
- const { node, context } = element
107
- if (!node) return
108
-
109
- // Capture config reference at registration time so it remains
110
- // stable across async boundaries. The document is stored on the
111
- // config, so getActiveDocument() resolves it via the config stack.
112
- const ds = context?.designSystem
113
-
114
- const wrappedHandler = (event) => {
115
- // Push the element's design system config so runtime CSS injections
116
- // and token resolution use the correct app scope. The document is
117
- // stored on the config, so getActiveDocument() resolves it too.
118
- if (ds) pushConfig(ds)
119
- try {
120
- const result = handler.call(element, event, element, element.state, context)
121
- if (result && typeof result.then === 'function') {
122
- // For async handlers, re-scope config around the promise
123
- // resolution so continuations see the right config.
124
- result
125
- .catch(err => {
126
- element.error = err
127
- console.error('[DOMQL] Async event error:', err)
128
- })
129
- .finally(() => {
130
- if (ds) popConfig()
131
- })
132
- // Don't pop in the outer finally — the promise chain owns it
133
- return
134
- }
135
- } catch (err) {
136
- element.error = err
137
- console.error('[DOMQL] Event error:', err)
138
- if (context?.strictMode) throw err
139
- }
140
- // Sync path — pop immediately
141
- if (ds) popConfig()
142
- }
143
-
144
- if (isDelegatable(eventName)) {
145
- // Find root node for delegation
146
- const rootNode = context?.rootNode || node.ownerDocument?.documentElement
147
- addDelegatedEvent(eventName, node, wrappedHandler, rootNode)
148
- } else {
149
- // Non-delegatable events (e.g., transitionend, animationend, resize)
150
- node.addEventListener(eventName, wrappedHandler, listenerOptions(eventName, false))
151
- }
152
-
153
- // Track for cleanup
154
- if (!element.__ref.__eventCleanup) element.__ref.__eventCleanup = []
155
- element.__ref.__eventCleanup.push(() => {
156
- if (isDelegatable(eventName)) {
157
- const rootNode = context?.rootNode || node.ownerDocument?.documentElement
158
- removeDelegatedEvent(eventName, node, rootNode)
159
- } else {
160
- node.removeEventListener(eventName, wrappedHandler, listenerOptions(eventName, false))
161
- }
162
- })
163
- }
package/src/dispose.js DELETED
@@ -1,76 +0,0 @@
1
- 'use strict'
2
-
3
- // Element disposal - cleanup effects, events, and children
4
-
5
- export function dispose(element) {
6
- if (!element) return
7
-
8
- const ref = element.__ref
9
- if (!ref) return
10
-
11
- // User-defined cleanup hooks fire BEFORE we tear refs/state/effects down,
12
- // so handlers can still read state and call methods on the element.
13
- // `onBeforeRemove` is for last-chance cleanup (close sockets, cancel
14
- // in-flight requests); `onRemove` is the canonical post-unmount hook.
15
- try { if (typeof element.onBeforeRemove === 'function') element.onBeforeRemove(element, element.state, element.context) } catch (e) { console.error('[smbls] onBeforeRemove error:', e) }
16
-
17
- // Dispose all reactive effects
18
- if (ref.__effects) {
19
- for (let i = 0; i < ref.__effects.length; i++) {
20
- ref.__effects[i].dispose()
21
- }
22
- ref.__effects.length = 0
23
- }
24
-
25
- // Cleanup event listeners
26
- if (ref.__eventCleanup) {
27
- for (let i = 0; i < ref.__eventCleanup.length; i++) {
28
- ref.__eventCleanup[i]()
29
- }
30
- ref.__eventCleanup.length = 0
31
- }
32
-
33
- // Recursively dispose children
34
- if (ref.__children) {
35
- for (let i = 0; i < ref.__children.length; i++) {
36
- const childKey = ref.__children[i]
37
- const child = element[childKey]
38
- if (child && child.__ref) {
39
- dispose(child)
40
- }
41
- }
42
- ref.__children.length = 0
43
- }
44
-
45
- // Remove DOM node
46
- if (element.node?.parentNode) {
47
- element.node.remove()
48
- }
49
-
50
- // Destroy state if owned
51
- if (element.state?.destroy && element.state.__element === element) {
52
- element.state.destroy()
53
- }
54
-
55
- // Remove from parent's tracking
56
- if (element.parent?.__ref?.__children) {
57
- const idx = element.parent.__ref.__children.indexOf(element.key)
58
- if (idx !== -1) {
59
- element.parent.__ref.__children.splice(idx, 1)
60
- }
61
- delete element.parent[element.key]
62
- }
63
-
64
- // Fire onRemove AFTER DOM detach + state.destroy but BEFORE clearing refs,
65
- // so the hook can still read element.key / element.parent for logging.
66
- try { if (typeof element.onRemove === 'function') element.onRemove(element) } catch (e) { console.error('[smbls] onRemove error:', e) }
67
-
68
- // Clear references
69
- element.node = null
70
- element.parent = null
71
- element.__ref = null
72
- }
73
-
74
- export function remove() {
75
- dispose(this)
76
- }
package/src/extends.js DELETED
@@ -1,123 +0,0 @@
1
- 'use strict'
2
-
3
- // Extends resolution - merges component inheritance chains
4
- // Uses structural sharing where possible (no deepClone)
5
-
6
- const extendsCache = new WeakMap()
7
-
8
- // FT-FRAMEWORK-EXTEND-CUSTOM: collect entries from BOTH `extends:`
9
- // (plural) and `extend:` (singular). Previously the resolver did
10
- // `definition.extends || definition.extend`, silently dropping one when
11
- // the other was set. childsafetynet's blog-detail pages used both
12
- // (`extends: 'Page'` for route-mode + `extend: 'BlogArticle'` for the
13
- // article shell) and only Page resolved — BlogArticle's child keys
14
- // never reached the render tree.
15
- function collectExtendsList (definition) {
16
- const list = []
17
- if (definition.extends) {
18
- if (Array.isArray(definition.extends)) list.push(...definition.extends)
19
- else list.push(definition.extends)
20
- }
21
- if (definition.extend) {
22
- if (Array.isArray(definition.extend)) list.push(...definition.extend)
23
- else list.push(definition.extend)
24
- }
25
- return list
26
- }
27
-
28
- export function applyExtends(element, definition, options) {
29
- const ext = collectExtendsList(definition)
30
- if (!ext.length) return
31
-
32
- const context = element.context || options?.context || {}
33
- const components = context.components || {}
34
-
35
- // Resolve extends chain
36
- for (let i = 0; i < ext.length; i++) {
37
- const base = resolveExtend(ext[i], components)
38
- // Skip self-references (component extends itself by same name)
39
- if (base && base !== definition) {
40
- mergeBase(element, base, options, new Set([definition]))
41
- }
42
- }
43
- }
44
-
45
- function resolveExtend(ext, components) {
46
- if (typeof ext === 'string') {
47
- // Support nested child access: 'Component > Child > SubChild'
48
- if (ext.includes('>')) {
49
- const parts = ext.split('>').map(s => s.trim())
50
- let resolved = components[parts[0]] || components[`smbls.${parts[0]}`]
51
- for (let i = 1; i < parts.length && resolved; i++) {
52
- resolved = resolved[parts[i]]
53
- }
54
- return resolved || null
55
- }
56
- return components[ext] || components[`smbls.${ext}`] || null
57
- }
58
- if (typeof ext === 'object' && ext !== null) {
59
- return ext
60
- }
61
- return null
62
- }
63
-
64
- function mergeBase(element, base, options, visited = new Set()) {
65
- if (visited.has(base)) return
66
- visited.add(base)
67
-
68
- // Recursively resolve base's own extends first — combining both
69
- // `extends:` (plural) and `extend:` (singular). Mirrors the entry-point
70
- // collectExtendsList logic so chains like Page → BlogArticle → Flex
71
- // resolve correctly when intermediate components mix the two forms.
72
- const baseExt = collectExtendsList(base)
73
- if (baseExt.length) {
74
- const context = element.context || options?.context || {}
75
- const components = context.components || {}
76
- for (let i = 0; i < baseExt.length; i++) {
77
- const ancestor = resolveExtend(baseExt[i], components)
78
- if (ancestor) mergeBase(element, ancestor, options, visited)
79
- }
80
- }
81
-
82
- // Merge properties from base onto element (later extends override earlier)
83
- for (const key in base) {
84
- if (key === 'extends' || key === 'extend') continue
85
-
86
- const val = base[key]
87
- if (element[key] === undefined) {
88
- if (val && typeof val === 'object' && !Array.isArray(val) && typeof val !== 'function') {
89
- element[key] = { ...val }
90
- } else {
91
- element[key] = val
92
- }
93
- } else if (typeof element[key] === 'object' && typeof val === 'object' &&
94
- !Array.isArray(element[key]) && !Array.isArray(val) &&
95
- element[key] !== null && val !== null) {
96
- // Deep merge objects — element (child) values override base values
97
- // element[key] is already set from a previous extends; val is the newer/more-specific definition
98
- // The newer definition (val) should override the older ancestor (element[key])
99
- element[key] = deepMergeChild(element[key], val)
100
- } else {
101
- // Later extends override earlier for primitives/functions
102
- element[key] = val
103
- }
104
- }
105
- }
106
-
107
- // Deep merge where override wins over base at every level
108
- function deepMergeChild(base, override) {
109
- const result = { ...base }
110
- for (const key in override) {
111
- const baseVal = result[key]
112
- const overVal = override[key]
113
- if (baseVal && overVal &&
114
- typeof baseVal === 'object' && typeof overVal === 'object' &&
115
- !Array.isArray(baseVal) && !Array.isArray(overVal) &&
116
- typeof baseVal !== 'function' && typeof overVal !== 'function') {
117
- result[key] = deepMergeChild(baseVal, overVal)
118
- } else {
119
- result[key] = overVal
120
- }
121
- }
122
- return result
123
- }
package/src/index.js DELETED
@@ -1,8 +0,0 @@
1
- export { create, default } from './create.js'
2
- export { update } from './update.js'
3
- export { dispose, remove } from './dispose.js'
4
- export { REGISTRY, LIFECYCLE_EVENTS, DELEGATED_EVENTS, MIXINS } from './registry.js'
5
- export { addDelegatedEvent, removeDelegatedEvent, registerEvent } from './delegate.js'
6
- export { cacheNode, detectTag, createHTMLNode, isValidTag, isSvgTag, SVG_TAGS } from './cache.js'
7
- export { applyExtends } from './extends.js'
8
- export { addMethods } from './methods.js'
package/src/methods.js DELETED
@@ -1,271 +0,0 @@
1
- 'use strict'
2
-
3
- import { update } from './update.js'
4
- import { remove } from './dispose.js'
5
- import { CSS_PROPS_REGISTRY } from 'css-in-props/src/props'
6
- import {
7
- getDB,
8
- getQuery,
9
- getRootData,
10
- getRootState as _getRootState,
11
- getRoot as _getRoot,
12
- getRootContext as _getRootContext,
13
- getContext as _getContext,
14
- getChildren,
15
- getPath,
16
- nextElement,
17
- previousElement,
18
- verbose as _verbose,
19
- warn as _warn,
20
- error as _error,
21
- variables as _variables,
22
- spotByPath
23
- } from '@symbo.ls/utils'
24
-
25
- // Forward warnings + errors to the analyze plugin (when present) so they
26
- // appear in the audit log alongside lifecycle events. Falls through to the
27
- // original env-gated console.warn / throw.
28
- function emitToAnalyze(element, level, params) {
29
- const a = element?.context?.analyze
30
- if (!a || !a.emit) return
31
- if (level === 'warn' && !a.shouldCapture('warnings')) return
32
- if (level === 'error' && !a.shouldCapture('errors')) return
33
- const msg = params.map(p => p?.message || (typeof p === 'string' ? p : null)).filter(Boolean).join(' ')
34
- a.emit({
35
- id: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`,
36
- ts: Date.now(),
37
- type: level === 'error' ? 'error' : 'warning',
38
- level,
39
- hook: level === 'error' ? 'el.error' : 'el.warn',
40
- element: {
41
- key: element.key != null ? String(element.key) : null,
42
- name: element.__name || null,
43
- tag: element.tag || null
44
- },
45
- message: msg || null,
46
- args: params
47
- })
48
- }
49
-
50
- function elWarn(...params) {
51
- emitToAnalyze(this, 'warn', params)
52
- return _warn.apply(this, params)
53
- }
54
-
55
- function elError(...params) {
56
- emitToAnalyze(this, 'error', params)
57
- return _error.apply(this, params)
58
- }
59
-
60
- // Shared prototype for all elements — methods are defined once, shared by all instances
61
- const elementProto = {
62
- update,
63
- remove,
64
- getDB,
65
- getQuery,
66
- getRootData,
67
- getChildren,
68
- getPath,
69
- nextElement,
70
- previousElement,
71
- verbose: _verbose,
72
- warn: elWarn,
73
- error: elError,
74
- variables: _variables,
75
- spotByPath,
76
-
77
- set(content, options) {
78
- this.removeContent()
79
- if (content != null) {
80
- const createFn = this.context?.__create
81
- if (createFn) createFn(content, this, 'content', options)
82
- }
83
- },
84
-
85
- reset(options) {
86
- const createFn = this.context?.__create
87
- if (!createFn) return this
88
- const parent = this.parent
89
- const key = this.key
90
- const def = this.parse()
91
- this.remove()
92
- return createFn(def, parent, key, options)
93
- },
94
-
95
- lookup(predicate) {
96
- let current = this.parent
97
- while (current) {
98
- if (typeof predicate === 'function') {
99
- if (predicate(current)) return current
100
- } else if (typeof predicate === 'string') {
101
- if (current.key === predicate) return current
102
- if (current[predicate] && current[predicate].__ref)
103
- return current[predicate]
104
- }
105
- current = current.parent
106
- }
107
- return null
108
- },
109
-
110
- lookdown(predicate) {
111
- const ref = this.__ref
112
- if (!ref?.__children) return null
113
- for (const key of ref.__children) {
114
- const child = this[key]
115
- if (!child) continue
116
- if (typeof predicate === 'function') {
117
- if (predicate(child)) return child
118
- } else if (typeof predicate === 'string') {
119
- if (child.key === predicate) return child
120
- }
121
- const found = child.lookdown?.(predicate)
122
- if (found) return found
123
- }
124
- return null
125
- },
126
-
127
- lookdownAll(predicate, results = []) {
128
- const ref = this.__ref
129
- if (!ref?.__children) return results
130
- for (const key of ref.__children) {
131
- const child = this[key]
132
- if (!child) continue
133
- if (typeof predicate === 'function') {
134
- if (predicate(child)) results.push(child)
135
- } else if (typeof predicate === 'string') {
136
- if (child.key === predicate) results.push(child)
137
- }
138
- child.lookdownAll?.(predicate, results)
139
- }
140
- return results
141
- },
142
-
143
- getRef() {
144
- return this.__ref
145
- },
146
-
147
- getRoot: _getRoot,
148
- getRootState: _getRootState,
149
- getRootContext: _getRootContext,
150
- getContext: _getContext,
151
-
152
- parse(exclude) {
153
- const result = {}
154
- const excludeSet = exclude ? new Set(exclude) : null
155
- const exec = this.__ref?.__exec
156
- for (const key in this) {
157
- if (key === 'parent' || key === 'node' || key === '__ref') continue
158
- if (excludeSet?.has(key)) continue
159
- // Reactive props (text/html/value/style/attr/css/custom-prop fns) are
160
- // wrapped at create time; the live value is the evaluated result, but
161
- // ref.__exec[key] holds the original function (or template). Restore it
162
- // so reset() / parseDeep() reproduce the reactive definition rather
163
- // than baking in a snapshot value.
164
- if (exec && exec[key] !== undefined) {
165
- result[key] = exec[key]
166
- continue
167
- }
168
- if (typeof this[key] === 'function') continue
169
- result[key] = this[key]
170
- }
171
- return result
172
- },
173
-
174
- parseDeep(exclude) {
175
- const result = this.parse(exclude)
176
- const ref = this.__ref
177
- if (ref?.__children) {
178
- for (const key of ref.__children) {
179
- if (this[key]?.parseDeep) {
180
- result[key] = this[key].parseDeep(exclude)
181
- }
182
- }
183
- }
184
- return result
185
- },
186
-
187
- keys() {
188
- return Object.keys(this)
189
- },
190
-
191
- log(...keys) {
192
- if (keys.length) {
193
- const data = {}
194
- for (const k of keys) data[k] = this[k]
195
- console.log(data)
196
- } else {
197
- console.log(this)
198
- }
199
- },
200
-
201
- setProps(props, options) {
202
- return this.update(props, options)
203
- },
204
-
205
- setNodeStyles(styles) {
206
- if (!this.node?.style) return
207
- for (const prop in styles) {
208
- const val = styles[prop]
209
- if (val == null) continue
210
- const cssProp = prop.replace(/[A-Z]/g, m => '-' + m.toLowerCase())
211
- const str = String(val).replace(/\s*!important\s*$/, '')
212
- this.node.style.setProperty(cssProp, str, 'important')
213
- }
214
- },
215
-
216
- call(methodName, ...args) {
217
- const ctx = this.context
218
- const method =
219
- ctx?.methods?.[methodName] ||
220
- ctx?.functions?.[methodName] ||
221
- ctx?.utils?.[methodName] ||
222
- this[methodName]
223
- if (typeof method === 'function') {
224
- return method.call(this, ...args)
225
- }
226
- },
227
-
228
- removeContent() {
229
- const ref = this.__ref
230
- if (!ref?.__children) return
231
- const contentChild = this.content
232
- if (contentChild?.remove) {
233
- contentChild.remove()
234
- delete this.content
235
- const idx = ref.__children.indexOf('content')
236
- if (idx !== -1) ref.__children.splice(idx, 1)
237
- }
238
- if (contentChild?.node?.parentNode) {
239
- contentChild.node.parentNode.removeChild(contentChild.node)
240
- }
241
- }
242
- }
243
-
244
- // Cache per-context prototypes so we don't recreate them for every element
245
- const contextProtoCache = new WeakMap()
246
-
247
- function getContextProto(ctx) {
248
- if (!ctx) return elementProto
249
- let proto = contextProtoCache.get(ctx)
250
- if (proto) return proto
251
-
252
- // Create a context-specific prototype that chains from elementProto
253
- proto = Object.create(elementProto)
254
- const methods = ctx.methods
255
- if (methods) {
256
- for (const k in methods) {
257
- // Functions become prototype methods; non-function values (e.g. an SDK
258
- // instance) are exposed as shared properties so `el.sdk.x` keeps working.
259
- proto[k] = methods[k]
260
- }
261
- }
262
- contextProtoCache.set(ctx, proto)
263
- return proto
264
- }
265
-
266
- export function addMethods(element) {
267
- const proto = getContextProto(element.context)
268
- Object.setPrototypeOf(element, proto)
269
- }
270
-
271
- export { elementProto }