@symbo.ls/element 3.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +1 -0
- package/package.json +21 -0
- package/src/cache.js +124 -0
- package/src/create.js +1705 -0
- package/src/delegate.js +163 -0
- package/src/dispose.js +76 -0
- package/src/extends.js +105 -0
- package/src/index.js +8 -0
- package/src/methods.js +257 -0
- package/src/registry.js +79 -0
- package/src/update.js +124 -0
package/src/delegate.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
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
|
+
export function applyExtends(element, definition, options) {
|
|
9
|
+
let ext = definition.extends || definition.extend
|
|
10
|
+
if (!ext) return
|
|
11
|
+
|
|
12
|
+
const context = element.context || options?.context || {}
|
|
13
|
+
const components = context.components || {}
|
|
14
|
+
|
|
15
|
+
// Normalize to array
|
|
16
|
+
if (!Array.isArray(ext)) ext = [ext]
|
|
17
|
+
|
|
18
|
+
// Resolve extends chain
|
|
19
|
+
for (let i = 0; i < ext.length; i++) {
|
|
20
|
+
const base = resolveExtend(ext[i], components)
|
|
21
|
+
// Skip self-references (component extends itself by same name)
|
|
22
|
+
if (base && base !== definition) {
|
|
23
|
+
mergeBase(element, base, options, new Set([definition]))
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveExtend(ext, components) {
|
|
29
|
+
if (typeof ext === 'string') {
|
|
30
|
+
// Support nested child access: 'Component > Child > SubChild'
|
|
31
|
+
if (ext.includes('>')) {
|
|
32
|
+
const parts = ext.split('>').map(s => s.trim())
|
|
33
|
+
let resolved = components[parts[0]] || components[`smbls.${parts[0]}`]
|
|
34
|
+
for (let i = 1; i < parts.length && resolved; i++) {
|
|
35
|
+
resolved = resolved[parts[i]]
|
|
36
|
+
}
|
|
37
|
+
return resolved || null
|
|
38
|
+
}
|
|
39
|
+
return components[ext] || components[`smbls.${ext}`] || null
|
|
40
|
+
}
|
|
41
|
+
if (typeof ext === 'object' && ext !== null) {
|
|
42
|
+
return ext
|
|
43
|
+
}
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function mergeBase(element, base, options, visited = new Set()) {
|
|
48
|
+
if (visited.has(base)) return
|
|
49
|
+
visited.add(base)
|
|
50
|
+
|
|
51
|
+
// Recursively resolve base's own extends first (support both 'extends' and 'extend')
|
|
52
|
+
const baseExtends = base.extends || base.extend
|
|
53
|
+
if (baseExtends) {
|
|
54
|
+
const context = element.context || options?.context || {}
|
|
55
|
+
const components = context.components || {}
|
|
56
|
+
let baseExt = baseExtends
|
|
57
|
+
if (!Array.isArray(baseExt)) baseExt = [baseExt]
|
|
58
|
+
for (let i = 0; i < baseExt.length; i++) {
|
|
59
|
+
const ancestor = resolveExtend(baseExt[i], components)
|
|
60
|
+
if (ancestor) mergeBase(element, ancestor, options, visited)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Merge properties from base onto element (later extends override earlier)
|
|
65
|
+
for (const key in base) {
|
|
66
|
+
if (key === 'extends' || key === 'extend') continue
|
|
67
|
+
|
|
68
|
+
const val = base[key]
|
|
69
|
+
if (element[key] === undefined) {
|
|
70
|
+
if (val && typeof val === 'object' && !Array.isArray(val) && typeof val !== 'function') {
|
|
71
|
+
element[key] = { ...val }
|
|
72
|
+
} else {
|
|
73
|
+
element[key] = val
|
|
74
|
+
}
|
|
75
|
+
} else if (typeof element[key] === 'object' && typeof val === 'object' &&
|
|
76
|
+
!Array.isArray(element[key]) && !Array.isArray(val) &&
|
|
77
|
+
element[key] !== null && val !== null) {
|
|
78
|
+
// Deep merge objects — element (child) values override base values
|
|
79
|
+
// element[key] is already set from a previous extends; val is the newer/more-specific definition
|
|
80
|
+
// The newer definition (val) should override the older ancestor (element[key])
|
|
81
|
+
element[key] = deepMergeChild(element[key], val)
|
|
82
|
+
} else {
|
|
83
|
+
// Later extends override earlier for primitives/functions
|
|
84
|
+
element[key] = val
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Deep merge where override wins over base at every level
|
|
90
|
+
function deepMergeChild(base, override) {
|
|
91
|
+
const result = { ...base }
|
|
92
|
+
for (const key in override) {
|
|
93
|
+
const baseVal = result[key]
|
|
94
|
+
const overVal = override[key]
|
|
95
|
+
if (baseVal && overVal &&
|
|
96
|
+
typeof baseVal === 'object' && typeof overVal === 'object' &&
|
|
97
|
+
!Array.isArray(baseVal) && !Array.isArray(overVal) &&
|
|
98
|
+
typeof baseVal !== 'function' && typeof overVal !== 'function') {
|
|
99
|
+
result[key] = deepMergeChild(baseVal, overVal)
|
|
100
|
+
} else {
|
|
101
|
+
result[key] = overVal
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return result
|
|
105
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
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
|
+
for (const key in this) {
|
|
156
|
+
if (key === 'parent' || key === 'node' || key === '__ref') continue
|
|
157
|
+
if (typeof this[key] === 'function') continue
|
|
158
|
+
if (excludeSet?.has(key)) continue
|
|
159
|
+
result[key] = this[key]
|
|
160
|
+
}
|
|
161
|
+
return result
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
parseDeep(exclude) {
|
|
165
|
+
const result = this.parse(exclude)
|
|
166
|
+
const ref = this.__ref
|
|
167
|
+
if (ref?.__children) {
|
|
168
|
+
for (const key of ref.__children) {
|
|
169
|
+
if (this[key]?.parseDeep) {
|
|
170
|
+
result[key] = this[key].parseDeep(exclude)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return result
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
keys() {
|
|
178
|
+
return Object.keys(this)
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
log(...keys) {
|
|
182
|
+
if (keys.length) {
|
|
183
|
+
const data = {}
|
|
184
|
+
for (const k of keys) data[k] = this[k]
|
|
185
|
+
console.log(data)
|
|
186
|
+
} else {
|
|
187
|
+
console.log(this)
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
setProps(props, options) {
|
|
192
|
+
return this.update(props, options)
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
setNodeStyles(styles) {
|
|
196
|
+
if (!this.node?.style) return
|
|
197
|
+
for (const prop in styles) {
|
|
198
|
+
this.node.style[prop] = styles[prop]
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
call(methodName, ...args) {
|
|
203
|
+
const ctx = this.context
|
|
204
|
+
const method =
|
|
205
|
+
ctx?.methods?.[methodName] ||
|
|
206
|
+
ctx?.functions?.[methodName] ||
|
|
207
|
+
ctx?.utils?.[methodName] ||
|
|
208
|
+
this[methodName]
|
|
209
|
+
if (typeof method === 'function') {
|
|
210
|
+
return method.call(this, ...args)
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
removeContent() {
|
|
215
|
+
const ref = this.__ref
|
|
216
|
+
if (!ref?.__children) return
|
|
217
|
+
const contentChild = this.content
|
|
218
|
+
if (contentChild?.remove) {
|
|
219
|
+
contentChild.remove()
|
|
220
|
+
delete this.content
|
|
221
|
+
const idx = ref.__children.indexOf('content')
|
|
222
|
+
if (idx !== -1) ref.__children.splice(idx, 1)
|
|
223
|
+
}
|
|
224
|
+
if (contentChild?.node?.parentNode) {
|
|
225
|
+
contentChild.node.parentNode.removeChild(contentChild.node)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Cache per-context prototypes so we don't recreate them for every element
|
|
231
|
+
const contextProtoCache = new WeakMap()
|
|
232
|
+
|
|
233
|
+
function getContextProto(ctx) {
|
|
234
|
+
if (!ctx) return elementProto
|
|
235
|
+
let proto = contextProtoCache.get(ctx)
|
|
236
|
+
if (proto) return proto
|
|
237
|
+
|
|
238
|
+
// Create a context-specific prototype that chains from elementProto
|
|
239
|
+
proto = Object.create(elementProto)
|
|
240
|
+
const methods = ctx.methods
|
|
241
|
+
if (methods) {
|
|
242
|
+
for (const k in methods) {
|
|
243
|
+
// Functions become prototype methods; non-function values (e.g. an SDK
|
|
244
|
+
// instance) are exposed as shared properties so `el.sdk.x` keeps working.
|
|
245
|
+
proto[k] = methods[k]
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
contextProtoCache.set(ctx, proto)
|
|
249
|
+
return proto
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function addMethods(element) {
|
|
253
|
+
const proto = getContextProto(element.context)
|
|
254
|
+
Object.setPrototypeOf(element, proto)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export { elementProto }
|
package/src/registry.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// REGISTRY defines all framework-reserved property keys.
|
|
4
|
+
// Any key NOT in this set that starts with uppercase is treated as a child element.
|
|
5
|
+
|
|
6
|
+
export const REGISTRY = new Set([
|
|
7
|
+
// Extends/Inheritance
|
|
8
|
+
'extends',
|
|
9
|
+
// Children/Content
|
|
10
|
+
'children', 'content',
|
|
11
|
+
'childExtends',
|
|
12
|
+
'childExtendsRecursive',
|
|
13
|
+
'childProps', 'inheritParentProps',
|
|
14
|
+
// State
|
|
15
|
+
'state', 'if',
|
|
16
|
+
// Define
|
|
17
|
+
'define', 'deps',
|
|
18
|
+
// DOM
|
|
19
|
+
'tag', 'attr', 'style', 'text', 'html', 'value',
|
|
20
|
+
'data', 'classlist', 'class',
|
|
21
|
+
// Scope/Context
|
|
22
|
+
'scope', 'root', 'context',
|
|
23
|
+
// Routing/Fetching
|
|
24
|
+
'$router', 'routes', 'fetch',
|
|
25
|
+
// Meta
|
|
26
|
+
'variables', 'query', 'key',
|
|
27
|
+
'component', 'preventContentUpdate',
|
|
28
|
+
'preventContentRecreate',
|
|
29
|
+
// Internal
|
|
30
|
+
'__name', '__ref', '__hash', '__text',
|
|
31
|
+
'parent', 'node'
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
// Mixin handlers - these get special processing during create/update
|
|
35
|
+
export const MIXINS = {
|
|
36
|
+
text: 'text',
|
|
37
|
+
html: 'html',
|
|
38
|
+
style: 'style',
|
|
39
|
+
attr: 'attr',
|
|
40
|
+
data: 'data',
|
|
41
|
+
classlist: 'classlist',
|
|
42
|
+
state: 'state',
|
|
43
|
+
scope: 'scope'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Events that are lifecycle events (not DOM events)
|
|
47
|
+
export const LIFECYCLE_EVENTS = new Set([
|
|
48
|
+
'start', 'init', 'complete', 'attachNode',
|
|
49
|
+
'stateInit', 'stateCreated',
|
|
50
|
+
'beforeUpdate', 'update', 'afterUpdate',
|
|
51
|
+
'beforeClassAssign', 'lazyLoad',
|
|
52
|
+
'render', 'renderRouter', 'done', 'create', 'frame',
|
|
53
|
+
'startUpdate', 'stateUpdate', 'beforeStateUpdate',
|
|
54
|
+
'initInspect', 'initSync', 'error',
|
|
55
|
+
'beforeRemove', 'remove'
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
// DOM events that block scroll performance unless registered `{ passive: true }`.
|
|
59
|
+
// Chrome logs a "Added non-passive event listener to a scroll-blocking" warning
|
|
60
|
+
// for each non-passive listener on these.
|
|
61
|
+
export const PASSIVE_EVENTS = new Set([
|
|
62
|
+
'wheel', 'mousewheel',
|
|
63
|
+
'touchstart', 'touchmove', 'touchend', 'touchcancel'
|
|
64
|
+
])
|
|
65
|
+
|
|
66
|
+
// DOM events that should be delegated
|
|
67
|
+
export const DELEGATED_EVENTS = new Set([
|
|
68
|
+
'click', 'dblclick', 'mousedown', 'mouseup', 'mousemove',
|
|
69
|
+
'mouseenter', 'mouseleave', 'mouseover', 'mouseout',
|
|
70
|
+
'pointerdown', 'pointerup', 'pointermove',
|
|
71
|
+
'pointerenter', 'pointerleave', 'pointerover', 'pointerout',
|
|
72
|
+
'touchstart', 'touchend', 'touchmove',
|
|
73
|
+
'keydown', 'keyup', 'keypress',
|
|
74
|
+
'input', 'change', 'submit', 'reset',
|
|
75
|
+
'focus', 'blur', 'focusin', 'focusout',
|
|
76
|
+
'scroll', 'wheel',
|
|
77
|
+
'contextmenu', 'drag', 'dragstart', 'dragend',
|
|
78
|
+
'dragenter', 'dragleave', 'dragover', 'drop'
|
|
79
|
+
])
|