@pfern/elements 0.1.10 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +11 -11
- package/README.md +54 -237
- package/elements.js +4 -1970
- package/env.d.ts +24 -0
- package/mathml.js +1 -0
- package/package.json +19 -23
- package/src/core/elements.js +513 -0
- package/src/core/events.js +143 -0
- package/src/core/props.js +177 -0
- package/src/core/tags.js +225 -0
- package/src/core/tick.js +116 -0
- package/src/core/types.js +696 -0
- package/src/helpers.js +73 -0
- package/src/html.js +996 -0
- package/src/mathml.js +340 -0
- package/src/router.js +51 -0
- package/src/ssr.js +175 -0
- package/src/svg.js +407 -0
- package/types/elements.d.ts +4 -1403
- package/types/mathml.d.ts +1 -0
- package/types/src/core/elements.d.ts +29 -0
- package/types/src/core/events.d.ts +15 -0
- package/types/src/core/props.d.ts +9 -0
- package/types/src/core/tags.d.ts +4 -0
- package/types/src/core/tick.d.ts +5 -0
- package/types/src/core/types.d.ts +507 -0
- package/types/src/helpers.d.ts +5 -0
- package/types/src/html.d.ts +802 -0
- package/types/src/mathml.d.ts +264 -0
- package/types/src/router.d.ts +4 -0
- package/types/src/ssr.d.ts +3 -0
- package/types/src/svg.d.ts +348 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative DOM event wrappers.
|
|
3
|
+
*
|
|
4
|
+
* Elements.js treats DOM events as a place to *declare* the next UI tree:
|
|
5
|
+
* if an event handler returns a vnode, the closest component boundary is
|
|
6
|
+
* replaced with the rendered result.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const isEventProp = (key, value) =>
|
|
10
|
+
key.startsWith('on') && typeof value === 'function'
|
|
11
|
+
|
|
12
|
+
export const isFormEventProp = key => /^(oninput|onsubmit|onchange)$/.test(key)
|
|
13
|
+
export const isSubmitEventProp = key => key === 'onsubmit'
|
|
14
|
+
export const isValueEventProp = key => /^(oninput|onchange)$/.test(key)
|
|
15
|
+
|
|
16
|
+
export const getNearestRoot = (el, isRoot) =>
|
|
17
|
+
!el || isRoot(el) ? el : getNearestRoot(el.parentNode, isRoot)
|
|
18
|
+
|
|
19
|
+
const isThenable = x =>
|
|
20
|
+
!!x
|
|
21
|
+
&& (typeof x === 'object' || typeof x === 'function')
|
|
22
|
+
&& typeof x.then === 'function'
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
const getHref = el =>
|
|
26
|
+
typeof el?.getAttribute === 'function'
|
|
27
|
+
? el.getAttribute('href')
|
|
28
|
+
: el?.attributes?.href ?? el?.href
|
|
29
|
+
|
|
30
|
+
const isPlainLeftClick = event =>
|
|
31
|
+
event?.button == null
|
|
32
|
+
? false
|
|
33
|
+
: event.button === 0
|
|
34
|
+
&& !event.defaultPrevented
|
|
35
|
+
&& !event.metaKey
|
|
36
|
+
&& !event.ctrlKey
|
|
37
|
+
&& !event.shiftKey
|
|
38
|
+
&& !event.altKey
|
|
39
|
+
|
|
40
|
+
const shouldPreventDefaultOnUpdate = (env, event) =>
|
|
41
|
+
env.key === 'onclick'
|
|
42
|
+
&& env.el?.tagName?.toLowerCase?.() === 'a'
|
|
43
|
+
&& !!getHref(env.el)
|
|
44
|
+
&& event?.cancelable !== false
|
|
45
|
+
&& typeof event?.preventDefault === 'function'
|
|
46
|
+
&& isPlainLeftClick(event)
|
|
47
|
+
|
|
48
|
+
const describeListener = ({ el, key }) =>
|
|
49
|
+
`Listener '${key}' on <${el.tagName.toLowerCase()}>`
|
|
50
|
+
|
|
51
|
+
const withEventRoot = (env, eventRoot, fn) => {
|
|
52
|
+
const prevEventRoot = env.getCurrentEventRoot()
|
|
53
|
+
const restoreEventRoot = () => env.setCurrentEventRoot(prevEventRoot)
|
|
54
|
+
env.setCurrentEventRoot(eventRoot)
|
|
55
|
+
|
|
56
|
+
let result
|
|
57
|
+
try { result = fn() }
|
|
58
|
+
catch (err) { restoreEventRoot(); throw err }
|
|
59
|
+
|
|
60
|
+
return !isThenable(result)
|
|
61
|
+
? (restoreEventRoot(), result)
|
|
62
|
+
: result.then(
|
|
63
|
+
value => (restoreEventRoot(), value),
|
|
64
|
+
err => { restoreEventRoot(); throw err }
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const warnPassiveReturn = (env, resolved) =>
|
|
69
|
+
resolved === undefined
|
|
70
|
+
? console.warn(
|
|
71
|
+
`${describeListener(env)} returned nothing.\n`
|
|
72
|
+
+ 'If you intended a UI update, return a vnode array like: '
|
|
73
|
+
+ 'div({}, ...)'
|
|
74
|
+
)
|
|
75
|
+
: !Array.isArray(resolved)
|
|
76
|
+
? console.warn(
|
|
77
|
+
`${describeListener(env)} returned "${resolved}".\n`
|
|
78
|
+
+ 'If you intended a UI update, return a vnode array like: '
|
|
79
|
+
+ 'div({}, ...).\n'
|
|
80
|
+
+ 'Otherwise, return undefined (or nothing) for native event '
|
|
81
|
+
+ 'listener behavior.'
|
|
82
|
+
)
|
|
83
|
+
: undefined
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Wrap an event handler so it can return a vnode to trigger an update.
|
|
87
|
+
*
|
|
88
|
+
* @param {{
|
|
89
|
+
* el: any,
|
|
90
|
+
* key: string,
|
|
91
|
+
* handler: Function,
|
|
92
|
+
* isRoot: (el: any) => boolean,
|
|
93
|
+
* renderTree:
|
|
94
|
+
* (node: any, isRoot?: boolean, namespaceURI?: string | null) => any,
|
|
95
|
+
* getCurrentEventRoot: () => any,
|
|
96
|
+
* setCurrentEventRoot: (el: any) => void,
|
|
97
|
+
* debug: boolean
|
|
98
|
+
* }} env
|
|
99
|
+
*/
|
|
100
|
+
export const createDeclarativeEventHandler = env =>
|
|
101
|
+
(...args) => {
|
|
102
|
+
const eventRoot = getNearestRoot(env.el, env.isRoot)
|
|
103
|
+
if (!eventRoot) return
|
|
104
|
+
|
|
105
|
+
return withEventRoot(env, eventRoot, () => {
|
|
106
|
+
const event = args[0]
|
|
107
|
+
const isFormEvent = isFormEventProp(env.key)
|
|
108
|
+
const isSubmitEvent = isSubmitEventProp(env.key)
|
|
109
|
+
const isValueEvent = isValueEventProp(env.key)
|
|
110
|
+
|
|
111
|
+
const arg =
|
|
112
|
+
isSubmitEvent
|
|
113
|
+
? event?.target?.elements || null
|
|
114
|
+
: isValueEvent
|
|
115
|
+
? event?.target?.value
|
|
116
|
+
: null
|
|
117
|
+
|
|
118
|
+
const result = isFormEvent
|
|
119
|
+
? env.handler.call(env.el, arg, event)
|
|
120
|
+
: env.handler.call(env.el, event)
|
|
121
|
+
|
|
122
|
+
const handleResult = resolved => {
|
|
123
|
+
isSubmitEvent && resolved !== undefined && event.preventDefault()
|
|
124
|
+
|
|
125
|
+
env.debug && warnPassiveReturn(env, resolved)
|
|
126
|
+
|
|
127
|
+
Array.isArray(resolved)
|
|
128
|
+
&& shouldPreventDefaultOnUpdate(env, event)
|
|
129
|
+
&& event.preventDefault()
|
|
130
|
+
|
|
131
|
+
if (!Array.isArray(resolved)) return resolved
|
|
132
|
+
|
|
133
|
+
const parent = eventRoot.parentNode
|
|
134
|
+
if (!parent) return resolved
|
|
135
|
+
|
|
136
|
+
const replacement = env.renderTree(resolved, true)
|
|
137
|
+
parent.replaceChild(replacement, eventRoot)
|
|
138
|
+
return resolved
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return isThenable(result) ? result.then(handleResult) : handleResult(result)
|
|
142
|
+
})
|
|
143
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Element property assignment.
|
|
3
|
+
*
|
|
4
|
+
* The props object is treated as:
|
|
5
|
+
* - DOM events: `on*` functions (wrapped to support vnode returns)
|
|
6
|
+
* - Hooks: non-DOM behavior such as `ontick`
|
|
7
|
+
* - Styling: `style` objects
|
|
8
|
+
* - Everything else: attributes (including SVG namespace handling)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createDeclarativeEventHandler, isEventProp } from './events.js'
|
|
12
|
+
import { startTickLoop, stopTickLoop } from './tick.js'
|
|
13
|
+
|
|
14
|
+
const isObject = x =>
|
|
15
|
+
typeof x === 'object'
|
|
16
|
+
&& x !== null
|
|
17
|
+
|
|
18
|
+
const deleteKey = (obj, key) =>
|
|
19
|
+
(delete obj[String(key)], undefined)
|
|
20
|
+
|
|
21
|
+
const isX3DOMReadyFor = el =>
|
|
22
|
+
(x3d => !x3d || !!x3d.runtime)(el?.closest?.('x3d'))
|
|
23
|
+
|
|
24
|
+
const propertyExceptions = {
|
|
25
|
+
value: 'value',
|
|
26
|
+
checked: 'checked',
|
|
27
|
+
selected: 'selected',
|
|
28
|
+
disabled: 'disabled',
|
|
29
|
+
multiple: 'multiple',
|
|
30
|
+
muted: 'muted',
|
|
31
|
+
volume: 'volume',
|
|
32
|
+
currentTime: 'currentTime',
|
|
33
|
+
playbackRate: 'playbackRate',
|
|
34
|
+
open: 'open',
|
|
35
|
+
indeterminate: 'indeterminate'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const propertyExceptionDefaults = {
|
|
39
|
+
value: '',
|
|
40
|
+
checked: false,
|
|
41
|
+
selected: false,
|
|
42
|
+
disabled: false,
|
|
43
|
+
multiple: false,
|
|
44
|
+
muted: false,
|
|
45
|
+
volume: 1,
|
|
46
|
+
currentTime: 0,
|
|
47
|
+
playbackRate: 1,
|
|
48
|
+
open: false,
|
|
49
|
+
indeterminate: false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const removeAttribute = (el, key) =>
|
|
53
|
+
typeof el.removeAttribute === 'function'
|
|
54
|
+
? el.removeAttribute(key)
|
|
55
|
+
: el.attributes ? deleteKey(el.attributes, key) : undefined
|
|
56
|
+
|
|
57
|
+
const clearStyle = el =>
|
|
58
|
+
(style =>
|
|
59
|
+
!style
|
|
60
|
+
? undefined
|
|
61
|
+
: 'cssText' in style
|
|
62
|
+
? (style.cssText = '', undefined)
|
|
63
|
+
: (Object.keys(style).forEach(k => delete style[k]), undefined)
|
|
64
|
+
)(el?.style)
|
|
65
|
+
|
|
66
|
+
const clearInnerHTML = el =>
|
|
67
|
+
el.innerHTML = ''
|
|
68
|
+
|
|
69
|
+
const clearEventProp = (el, key) =>
|
|
70
|
+
el[key] = null
|
|
71
|
+
|
|
72
|
+
const clearPropertyException = (el, key) =>
|
|
73
|
+
key in propertyExceptions && key in el
|
|
74
|
+
? (el[propertyExceptions[key]] = propertyExceptionDefaults[key], undefined)
|
|
75
|
+
: undefined
|
|
76
|
+
|
|
77
|
+
const clearTick = el =>
|
|
78
|
+
(el.ontick = null, stopTickLoop(el))
|
|
79
|
+
|
|
80
|
+
const clearProp = (el, key) =>
|
|
81
|
+
key === 'ontick' ? clearTick(el)
|
|
82
|
+
: key === 'style' ? clearStyle(el)
|
|
83
|
+
: key === 'innerHTML' ? clearInnerHTML(el)
|
|
84
|
+
: key in propertyExceptions ? clearPropertyException(el, key)
|
|
85
|
+
: key.startsWith('on') ? clearEventProp(el, key)
|
|
86
|
+
: removeAttribute(el, key)
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Remove props that existed previously but are absent in the next vnode.
|
|
90
|
+
*
|
|
91
|
+
* This keeps updates symmetric: setting a prop then omitting it later clears it
|
|
92
|
+
* from the DOM element.
|
|
93
|
+
*
|
|
94
|
+
* @param {any} el
|
|
95
|
+
* @param {Record<string, any>} prevProps
|
|
96
|
+
* @param {Record<string, any>} nextProps
|
|
97
|
+
*/
|
|
98
|
+
export const removeMissingProps = (el, prevProps, nextProps) => {
|
|
99
|
+
const keys = Object.keys(prevProps)
|
|
100
|
+
for (let i = 0; i < keys.length; i++) {
|
|
101
|
+
const key = keys[i]
|
|
102
|
+
!(key in nextProps) && clearProp(el, key)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Assign props to a DOM element.
|
|
108
|
+
*
|
|
109
|
+
* @param {any} el
|
|
110
|
+
* @param {Record<string, any>} props
|
|
111
|
+
* @param {{
|
|
112
|
+
* svgNS: string,
|
|
113
|
+
* debug: boolean,
|
|
114
|
+
* isRoot: (el: any) => boolean,
|
|
115
|
+
* renderTree: (node: any, isRoot?: boolean, namespaceURI?: string | null) =>
|
|
116
|
+
* any,
|
|
117
|
+
* getCurrentEventRoot: () => any,
|
|
118
|
+
* setCurrentEventRoot: (el: any) => void
|
|
119
|
+
* }} env
|
|
120
|
+
*/
|
|
121
|
+
export const assignProperties = (el, props, env) => {
|
|
122
|
+
const isSvg = el.namespaceURI === env.svgNS
|
|
123
|
+
const keys = Object.keys(props)
|
|
124
|
+
for (let i = 0; i < keys.length; i++) {
|
|
125
|
+
const key = keys[i]
|
|
126
|
+
const value = props[key]
|
|
127
|
+
|
|
128
|
+
if (key === 'ontick' && typeof value === 'function') {
|
|
129
|
+
el.ontick = value
|
|
130
|
+
startTickLoop(el, value, { ready: isX3DOMReadyFor })
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (key in propertyExceptions && key in el) {
|
|
135
|
+
el[propertyExceptions[key]] = value
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (isEventProp(key, value)) {
|
|
140
|
+
el[key] = createDeclarativeEventHandler({
|
|
141
|
+
el,
|
|
142
|
+
key,
|
|
143
|
+
handler: value,
|
|
144
|
+
isRoot: env.isRoot,
|
|
145
|
+
renderTree: env.renderTree,
|
|
146
|
+
getCurrentEventRoot: env.getCurrentEventRoot,
|
|
147
|
+
setCurrentEventRoot: env.setCurrentEventRoot,
|
|
148
|
+
debug: env.debug
|
|
149
|
+
})
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (key === 'style' && isObject(value)) {
|
|
154
|
+
Object.assign(el.style, value)
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (key === 'innerHTML') {
|
|
159
|
+
el.innerHTML = value
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (typeof value === 'boolean') {
|
|
164
|
+
if (key.startsWith('aria-') || key.startsWith('data-')) {
|
|
165
|
+
const str = value ? 'true' : 'false'
|
|
166
|
+
isSvg ? el.setAttributeNS(null, key, str) : el.setAttribute(key, str)
|
|
167
|
+
} else if (value) {
|
|
168
|
+
isSvg ? el.setAttributeNS(null, key, '') : el.setAttribute(key, '')
|
|
169
|
+
} else {
|
|
170
|
+
removeAttribute(el, key)
|
|
171
|
+
}
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
isSvg ? el.setAttributeNS(null, key, value) : el.setAttribute(key, value)
|
|
176
|
+
}
|
|
177
|
+
}
|
package/src/core/tags.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/** @internal */
|
|
2
|
+
export const htmlTagNames = /** @type {const} */ ([
|
|
3
|
+
// Document metadata
|
|
4
|
+
'html',
|
|
5
|
+
'head',
|
|
6
|
+
'base',
|
|
7
|
+
'link',
|
|
8
|
+
'meta',
|
|
9
|
+
'title',
|
|
10
|
+
|
|
11
|
+
// Sections
|
|
12
|
+
'body',
|
|
13
|
+
'header',
|
|
14
|
+
'hgroup',
|
|
15
|
+
'nav',
|
|
16
|
+
'main',
|
|
17
|
+
'section',
|
|
18
|
+
'article',
|
|
19
|
+
'aside',
|
|
20
|
+
'footer',
|
|
21
|
+
'address',
|
|
22
|
+
|
|
23
|
+
// Text content
|
|
24
|
+
'h1',
|
|
25
|
+
'h2',
|
|
26
|
+
'h3',
|
|
27
|
+
'h4',
|
|
28
|
+
'h5',
|
|
29
|
+
'h6',
|
|
30
|
+
'p',
|
|
31
|
+
'hr',
|
|
32
|
+
'menu',
|
|
33
|
+
'pre',
|
|
34
|
+
'blockquote',
|
|
35
|
+
'ol',
|
|
36
|
+
'ul',
|
|
37
|
+
'li',
|
|
38
|
+
'dl',
|
|
39
|
+
'dt',
|
|
40
|
+
'dd',
|
|
41
|
+
'figure',
|
|
42
|
+
'figcaption',
|
|
43
|
+
'div',
|
|
44
|
+
|
|
45
|
+
// Inline text semantics
|
|
46
|
+
'a',
|
|
47
|
+
'abbr',
|
|
48
|
+
'b',
|
|
49
|
+
'bdi',
|
|
50
|
+
'bdo',
|
|
51
|
+
'br',
|
|
52
|
+
'cite',
|
|
53
|
+
'code',
|
|
54
|
+
'data',
|
|
55
|
+
'dfn',
|
|
56
|
+
'em',
|
|
57
|
+
'i',
|
|
58
|
+
'kbd',
|
|
59
|
+
'mark',
|
|
60
|
+
'q',
|
|
61
|
+
'rb',
|
|
62
|
+
'rp',
|
|
63
|
+
'rt',
|
|
64
|
+
'rtc',
|
|
65
|
+
'ruby',
|
|
66
|
+
's',
|
|
67
|
+
'samp',
|
|
68
|
+
'small',
|
|
69
|
+
'span',
|
|
70
|
+
'strong',
|
|
71
|
+
'sub',
|
|
72
|
+
'sup',
|
|
73
|
+
'time',
|
|
74
|
+
'u',
|
|
75
|
+
'var',
|
|
76
|
+
'wbr',
|
|
77
|
+
|
|
78
|
+
// Edits
|
|
79
|
+
'ins',
|
|
80
|
+
'del',
|
|
81
|
+
|
|
82
|
+
// Embedded content
|
|
83
|
+
'img',
|
|
84
|
+
'iframe',
|
|
85
|
+
'embed',
|
|
86
|
+
'object',
|
|
87
|
+
'param',
|
|
88
|
+
'video',
|
|
89
|
+
'audio',
|
|
90
|
+
'source',
|
|
91
|
+
'track',
|
|
92
|
+
'picture',
|
|
93
|
+
|
|
94
|
+
// Table content
|
|
95
|
+
'table',
|
|
96
|
+
'caption',
|
|
97
|
+
'thead',
|
|
98
|
+
'tbody',
|
|
99
|
+
'tfoot',
|
|
100
|
+
'tr',
|
|
101
|
+
'th',
|
|
102
|
+
'td',
|
|
103
|
+
'colgroup',
|
|
104
|
+
'col',
|
|
105
|
+
|
|
106
|
+
// Forms
|
|
107
|
+
'form',
|
|
108
|
+
'fieldset',
|
|
109
|
+
'legend',
|
|
110
|
+
'label',
|
|
111
|
+
'input',
|
|
112
|
+
'button',
|
|
113
|
+
'select',
|
|
114
|
+
'datalist',
|
|
115
|
+
'optgroup',
|
|
116
|
+
'option',
|
|
117
|
+
'textarea',
|
|
118
|
+
'output',
|
|
119
|
+
'progress',
|
|
120
|
+
'meter',
|
|
121
|
+
|
|
122
|
+
// Interactive elements
|
|
123
|
+
'details',
|
|
124
|
+
'search',
|
|
125
|
+
'summary',
|
|
126
|
+
'dialog',
|
|
127
|
+
'slot',
|
|
128
|
+
'template',
|
|
129
|
+
|
|
130
|
+
// Scripting and style
|
|
131
|
+
'script',
|
|
132
|
+
'noscript',
|
|
133
|
+
'style',
|
|
134
|
+
|
|
135
|
+
// Web components and others
|
|
136
|
+
'canvas',
|
|
137
|
+
'picture',
|
|
138
|
+
'map',
|
|
139
|
+
'area',
|
|
140
|
+
'slot'
|
|
141
|
+
])
|
|
142
|
+
|
|
143
|
+
/** @internal */
|
|
144
|
+
export const svgTagNames = /** @type {const} */ ([
|
|
145
|
+
// Animation elements
|
|
146
|
+
'animate',
|
|
147
|
+
'animateMotion',
|
|
148
|
+
'animateTransform',
|
|
149
|
+
'mpath',
|
|
150
|
+
'set',
|
|
151
|
+
|
|
152
|
+
// Basic shapes
|
|
153
|
+
'circle',
|
|
154
|
+
'ellipse',
|
|
155
|
+
'line',
|
|
156
|
+
'path',
|
|
157
|
+
'polygon',
|
|
158
|
+
'polyline',
|
|
159
|
+
'rect',
|
|
160
|
+
|
|
161
|
+
// Container / structural
|
|
162
|
+
'defs',
|
|
163
|
+
'g',
|
|
164
|
+
'marker',
|
|
165
|
+
'mask',
|
|
166
|
+
'pattern',
|
|
167
|
+
'svg',
|
|
168
|
+
'switch',
|
|
169
|
+
'symbol',
|
|
170
|
+
'use',
|
|
171
|
+
|
|
172
|
+
// Descriptive
|
|
173
|
+
'desc',
|
|
174
|
+
'metadata',
|
|
175
|
+
'title',
|
|
176
|
+
|
|
177
|
+
// Filter primitives
|
|
178
|
+
'filter',
|
|
179
|
+
'feBlend',
|
|
180
|
+
'feColorMatrix',
|
|
181
|
+
'feComponentTransfer',
|
|
182
|
+
'feComposite',
|
|
183
|
+
'feConvolveMatrix',
|
|
184
|
+
'feDiffuseLighting',
|
|
185
|
+
'feDisplacementMap',
|
|
186
|
+
'feDistantLight',
|
|
187
|
+
'feDropShadow',
|
|
188
|
+
'feFlood',
|
|
189
|
+
'feFuncA',
|
|
190
|
+
'feFuncB',
|
|
191
|
+
'feFuncG',
|
|
192
|
+
'feFuncR',
|
|
193
|
+
'feGaussianBlur',
|
|
194
|
+
'feImage',
|
|
195
|
+
'feMerge',
|
|
196
|
+
'feMergeNode',
|
|
197
|
+
'feMorphology',
|
|
198
|
+
'feOffset',
|
|
199
|
+
'fePointLight',
|
|
200
|
+
'feSpecularLighting',
|
|
201
|
+
'feSpotLight',
|
|
202
|
+
'feTile',
|
|
203
|
+
'feTurbulence',
|
|
204
|
+
|
|
205
|
+
// Gradient / paint servers
|
|
206
|
+
'linearGradient',
|
|
207
|
+
'radialGradient',
|
|
208
|
+
'stop',
|
|
209
|
+
|
|
210
|
+
// Graphics elements
|
|
211
|
+
'image',
|
|
212
|
+
'foreignObject', // included in graphics section as non‑standard children
|
|
213
|
+
|
|
214
|
+
// Text and text-path
|
|
215
|
+
'text',
|
|
216
|
+
'textPath',
|
|
217
|
+
'tspan',
|
|
218
|
+
|
|
219
|
+
// Scripting/style
|
|
220
|
+
'script',
|
|
221
|
+
'style',
|
|
222
|
+
|
|
223
|
+
// View
|
|
224
|
+
'view'
|
|
225
|
+
])
|
package/src/core/tick.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tick engine for `ontick`.
|
|
3
|
+
*
|
|
4
|
+
* `ontick` is a hook (not a DOM event) that runs a handler once per frame.
|
|
5
|
+
* It is intentionally minimal: it schedules via `requestAnimationFrame`,
|
|
6
|
+
* threads optional context, and provides a `dt` in milliseconds.
|
|
7
|
+
*
|
|
8
|
+
* The engine is generic and can be used with any element; callers may provide
|
|
9
|
+
* a `ready(el)` predicate to gate ticking on external readiness (e.g. X3DOM
|
|
10
|
+
* runtime availability).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const tickStateMap = new WeakMap()
|
|
14
|
+
|
|
15
|
+
export const isConnected = el =>
|
|
16
|
+
typeof el?.isConnected === 'boolean' ? el.isConnected : !!el?.parentNode
|
|
17
|
+
|
|
18
|
+
const isThenable = x =>
|
|
19
|
+
!!x
|
|
20
|
+
&& (typeof x === 'object' || typeof x === 'function')
|
|
21
|
+
&& typeof x.then === 'function'
|
|
22
|
+
|
|
23
|
+
export const stopTickLoop = el =>
|
|
24
|
+
(state =>
|
|
25
|
+
!state ? undefined
|
|
26
|
+
: (state.running = false,
|
|
27
|
+
state.rafId != null
|
|
28
|
+
&& typeof window?.cancelAnimationFrame === 'function'
|
|
29
|
+
&& window.cancelAnimationFrame(state.rafId),
|
|
30
|
+
tickStateMap.delete(el),
|
|
31
|
+
undefined)
|
|
32
|
+
)(tickStateMap.get(el))
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Start (or restart) a tick loop for an element.
|
|
36
|
+
*
|
|
37
|
+
* The handler signature is:
|
|
38
|
+
*
|
|
39
|
+
* ```js
|
|
40
|
+
* (el, ctx, dtMs) => nextCtx | undefined
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* Returning `undefined` preserves the previous `ctx`. Returning any other value
|
|
44
|
+
* replaces `ctx` for the next tick.
|
|
45
|
+
*
|
|
46
|
+
* If the handler throws (or returns a Promise), ticking stops.
|
|
47
|
+
*
|
|
48
|
+
* @template Ctx
|
|
49
|
+
* @param {Element} el
|
|
50
|
+
* @param {(el: Element, ctx: any, dtMs: number) => (any | void)} handler
|
|
51
|
+
* @param {{ ready?: (el: Element) => boolean }} [options]
|
|
52
|
+
*/
|
|
53
|
+
export const startTickLoop = (el, handler, { ready = () => true } = {}) => {
|
|
54
|
+
const canTick =
|
|
55
|
+
typeof window !== 'undefined'
|
|
56
|
+
&& typeof window.requestAnimationFrame === 'function'
|
|
57
|
+
|
|
58
|
+
if (!canTick) return
|
|
59
|
+
|
|
60
|
+
const existing = tickStateMap.get(el)
|
|
61
|
+
|
|
62
|
+
if (existing?.handler === handler
|
|
63
|
+
&& existing?.ready === ready
|
|
64
|
+
&& existing?.running) return
|
|
65
|
+
|
|
66
|
+
existing && stopTickLoop(el)
|
|
67
|
+
|
|
68
|
+
const state = {
|
|
69
|
+
handler,
|
|
70
|
+
ready,
|
|
71
|
+
ctx: undefined,
|
|
72
|
+
lastTime: null,
|
|
73
|
+
rafId: null,
|
|
74
|
+
wasConnected: false,
|
|
75
|
+
running: true
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
tickStateMap.set(el, state)
|
|
79
|
+
|
|
80
|
+
const step = t => {
|
|
81
|
+
const connected = isConnected(el)
|
|
82
|
+
if (!state.running || (!connected && state.wasConnected))
|
|
83
|
+
return stopTickLoop(el)
|
|
84
|
+
|
|
85
|
+
if (!connected) {
|
|
86
|
+
state.lastTime = null
|
|
87
|
+
state.rafId = window.requestAnimationFrame(step)
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
state.wasConnected = true
|
|
92
|
+
|
|
93
|
+
if (!ready(el)) {
|
|
94
|
+
state.lastTime = null
|
|
95
|
+
state.rafId = window.requestAnimationFrame(step)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const dt = state.lastTime == null ? 0 : t - state.lastTime
|
|
100
|
+
state.lastTime = t
|
|
101
|
+
|
|
102
|
+
let next
|
|
103
|
+
try { next = handler.call(el, el, state.ctx, dt) }
|
|
104
|
+
catch (err) { stopTickLoop(el); throw err }
|
|
105
|
+
|
|
106
|
+
if (isThenable(next)) {
|
|
107
|
+
stopTickLoop(el)
|
|
108
|
+
throw new TypeError('ontick must be synchronous (no Promises).')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
next !== undefined && (state.ctx = next)
|
|
112
|
+
state.running && (state.rafId = window.requestAnimationFrame(step))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
state.rafId = window.requestAnimationFrame(step)
|
|
116
|
+
}
|