@domql/element 3.4.5 → 3.4.6

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.
Files changed (47) hide show
  1. package/event/__tests__/applyAnimationFrame.test.js +114 -0
  2. package/event/__tests__/applyEvent.test.js +159 -0
  3. package/event/__tests__/applyEventUpdate.test.js +198 -0
  4. package/event/__tests__/applyEventsOnNode.test.js +216 -0
  5. package/event/__tests__/canRenderTag.test.js +50 -0
  6. package/event/__tests__/index.test.js +39 -0
  7. package/event/__tests__/initAnimationFrame.test.js +156 -0
  8. package/event/__tests__/registerFrameListener.test.js +97 -0
  9. package/event/__tests__/store.test.js +93 -0
  10. package/event/__tests__/triggerEventOn.test.js +195 -0
  11. package/event/__tests__/triggerEventOnUpdate.test.js +207 -0
  12. package/event/animationFrame.js +92 -0
  13. package/event/can.js +8 -0
  14. package/event/index.js +5 -0
  15. package/event/on.js +71 -0
  16. package/event/store.js +6 -0
  17. package/methods/set.js +73 -0
  18. package/methods/v2.js +83 -0
  19. package/mixins/attr.js +32 -0
  20. package/mixins/classList.js +62 -0
  21. package/mixins/content.js +65 -0
  22. package/mixins/data.js +26 -0
  23. package/mixins/html.js +19 -0
  24. package/mixins/index.js +23 -0
  25. package/mixins/registry.js +46 -0
  26. package/mixins/scope.js +23 -0
  27. package/mixins/state.js +18 -0
  28. package/mixins/style.js +25 -0
  29. package/mixins/text.js +31 -0
  30. package/package.json +13 -8
  31. package/render/__tests__/appendNode.test.js +53 -0
  32. package/render/__tests__/assignNode.test.js +151 -0
  33. package/render/__tests__/cacheNode.test.js +168 -0
  34. package/render/__tests__/createHTMLNode.test.js +118 -0
  35. package/render/__tests__/createNode.test.js +9 -0
  36. package/render/__tests__/detectTag.test.js +99 -0
  37. package/render/__tests__/index.test.js +56 -0
  38. package/render/__tests__/insertNodeAfter.test.js +111 -0
  39. package/render/__tests__/insertNodeBefore.test.js +65 -0
  40. package/render/append.js +61 -0
  41. package/render/cache.js +68 -0
  42. package/render/create.js +3 -0
  43. package/render/index.js +5 -0
  44. package/utils/applyParam.js +33 -0
  45. package/utils/extendUtils.js +135 -0
  46. package/utils/index.js +4 -0
  47. package/utils/propEvents.js +36 -0
@@ -0,0 +1,207 @@
1
+ import { jest } from '@jest/globals'
2
+ import { triggerEventOnUpdate } from '../on'
3
+
4
+ describe('triggerEventOnUpdate', () => {
5
+ let mockElement
6
+ let mockEventHandler
7
+ let mockUpdatedObj
8
+ let mockOptions
9
+
10
+ beforeEach(() => {
11
+ mockEventHandler = jest.fn(() => 'eventResult')
12
+ mockUpdatedObj = { updated: 'value' }
13
+ mockOptions = { option1: 'value1' }
14
+
15
+ mockElement = {
16
+ state: { elementState: 'test' },
17
+ context: { elementContext: 'test' },
18
+ on: {},
19
+ props: {}
20
+ }
21
+ })
22
+
23
+ describe('event resolution and execution', () => {
24
+ test('should trigger event from on property', () => {
25
+ // Arrange
26
+ mockElement.on.click = mockEventHandler
27
+
28
+ // Act
29
+ const result = triggerEventOnUpdate(
30
+ 'click',
31
+ mockUpdatedObj,
32
+ mockElement,
33
+ mockOptions
34
+ )
35
+
36
+ // Assert
37
+ expect(result).toBe('eventResult')
38
+ expect(mockEventHandler).toHaveBeenCalledWith(
39
+ mockUpdatedObj,
40
+ mockElement,
41
+ mockElement.state,
42
+ mockElement.context,
43
+ mockOptions
44
+ )
45
+ })
46
+
47
+ test('should trigger event from props using camelCase', () => {
48
+ // Arrange
49
+ mockElement.props.onClick = mockEventHandler
50
+
51
+ // Act
52
+ const result = triggerEventOnUpdate(
53
+ 'click',
54
+ mockUpdatedObj,
55
+ mockElement,
56
+ mockOptions
57
+ )
58
+
59
+ // Assert
60
+ expect(result).toBe('eventResult')
61
+ expect(mockEventHandler).toHaveBeenCalledWith(
62
+ mockUpdatedObj,
63
+ mockElement,
64
+ mockElement.state,
65
+ mockElement.context,
66
+ mockOptions
67
+ )
68
+ })
69
+
70
+ test('should prioritize on property over props', () => {
71
+ // Arrange
72
+ const onEventHandler = jest.fn(() => 'onResult')
73
+ const propsEventHandler = jest.fn(() => 'propsResult')
74
+
75
+ mockElement.on.click = onEventHandler
76
+ mockElement.props.onClick = propsEventHandler
77
+
78
+ // Act
79
+ const result = triggerEventOnUpdate(
80
+ 'click',
81
+ mockUpdatedObj,
82
+ mockElement,
83
+ mockOptions
84
+ )
85
+
86
+ // Assert
87
+ expect(result).toBe('onResult')
88
+ expect(onEventHandler).toHaveBeenCalled()
89
+ expect(propsEventHandler).not.toHaveBeenCalled()
90
+ })
91
+ })
92
+
93
+ describe('parameter handling', () => {
94
+ test('should handle undefined state and context', () => {
95
+ // Arrange
96
+ mockElement = {
97
+ on: { click: mockEventHandler }
98
+ }
99
+
100
+ // Act
101
+ triggerEventOnUpdate('click', mockUpdatedObj, mockElement, mockOptions)
102
+
103
+ // Assert
104
+ expect(mockEventHandler).toHaveBeenCalledWith(
105
+ mockUpdatedObj,
106
+ mockElement,
107
+ undefined,
108
+ undefined,
109
+ mockOptions
110
+ )
111
+ })
112
+
113
+ test('should handle missing options', () => {
114
+ // Arrange
115
+ mockElement.on.click = mockEventHandler
116
+
117
+ // Act
118
+ triggerEventOnUpdate('click', mockUpdatedObj, mockElement)
119
+
120
+ // Assert
121
+ expect(mockEventHandler).toHaveBeenCalledWith(
122
+ mockUpdatedObj,
123
+ mockElement,
124
+ mockElement.state,
125
+ mockElement.context,
126
+ undefined
127
+ )
128
+ })
129
+
130
+ test('should handle empty updatedObj', () => {
131
+ // Arrange
132
+ mockElement.on.click = mockEventHandler
133
+
134
+ // Act
135
+ triggerEventOnUpdate('click', {}, mockElement, mockOptions)
136
+
137
+ // Assert
138
+ expect(mockEventHandler).toHaveBeenCalledWith(
139
+ {},
140
+ mockElement,
141
+ mockElement.state,
142
+ mockElement.context,
143
+ mockOptions
144
+ )
145
+ })
146
+ })
147
+
148
+ describe('event naming', () => {
149
+ test('should handle various event names correctly', () => {
150
+ const eventTests = [
151
+ { input: 'mouseenter', prop: 'onMouseenter' },
152
+ { input: 'mouseleave', prop: 'onMouseleave' },
153
+ { input: 'submit', prop: 'onSubmit' },
154
+ { input: 'change', prop: 'onChange' }
155
+ ]
156
+
157
+ for (const { input, prop } of eventTests) {
158
+ // Arrange
159
+ mockElement.props[prop] = mockEventHandler
160
+
161
+ // Act
162
+ triggerEventOnUpdate(input, mockUpdatedObj, mockElement, mockOptions)
163
+
164
+ // Assert
165
+ expect(mockEventHandler).toHaveBeenCalledWith(
166
+ mockUpdatedObj,
167
+ mockElement,
168
+ mockElement.state,
169
+ mockElement.context,
170
+ mockOptions
171
+ )
172
+
173
+ // Reset for next test
174
+ mockElement.props[prop] = undefined
175
+ mockEventHandler.mockClear()
176
+ }
177
+ })
178
+ })
179
+
180
+ describe('error handling', () => {
181
+ test('should return undefined when no event handler exists', () => {
182
+ // Act
183
+ const result = triggerEventOnUpdate(
184
+ 'click',
185
+ mockUpdatedObj,
186
+ mockElement,
187
+ mockOptions
188
+ )
189
+
190
+ // Assert
191
+ expect(result).toBeUndefined()
192
+ })
193
+
194
+ test('should handle thrown error in event handler', () => {
195
+ // Arrange
196
+ const error = new Error('Event handler error')
197
+ mockElement.on.click = jest.fn(() => {
198
+ throw error
199
+ })
200
+
201
+ // Act & Assert
202
+ expect(() =>
203
+ triggerEventOnUpdate('click', mockUpdatedObj, mockElement, mockOptions)
204
+ ).toThrow('Event handler error')
205
+ })
206
+ })
207
+ })
@@ -0,0 +1,92 @@
1
+ 'use strict'
2
+
3
+ export const registerFrameListener = el => {
4
+ if (!el || !el.__ref) {
5
+ throw new Error('Element reference is invalid')
6
+ }
7
+
8
+ const { __ref: ref } = el
9
+
10
+ if (!ref.root) {
11
+ throw new Error('Root reference is invalid')
12
+ }
13
+
14
+ if (!ref.root.data) {
15
+ throw new Error('Data are undefined')
16
+ }
17
+
18
+ const { frameListeners } = ref.root.data
19
+
20
+ // Check if frameListeners exists and the element is not already in the Set
21
+ if (frameListeners && !frameListeners.has(el)) {
22
+ frameListeners.add(el) // Add the element to the Set
23
+ }
24
+ }
25
+
26
+ const processFrameListeners = (frameListeners) => {
27
+ for (const element of frameListeners) {
28
+ // Cache the handler on first use to avoid repeated property lookups per frame
29
+ if (!element.__ref.__frameHandler) {
30
+ const handler = element.on?.frame || element.onFrame || element.props?.onFrame
31
+ if (handler) element.__ref.__frameHandler = handler
32
+ else {
33
+ frameListeners.delete(element)
34
+ continue
35
+ }
36
+ }
37
+
38
+ if (!element.node?.parentNode) {
39
+ frameListeners.delete(element)
40
+ delete element.__ref.__frameHandler
41
+ } else {
42
+ try {
43
+ element.__ref.__frameHandler(element, element.state, element.context)
44
+ } catch (e) {
45
+ console.warn(e)
46
+ frameListeners.delete(element)
47
+ delete element.__ref.__frameHandler
48
+ }
49
+ }
50
+ }
51
+ }
52
+
53
+ const startFrameLoop = (frameListeners) => {
54
+ if (_frameRunning) return
55
+ _frameRunning = true
56
+
57
+ function requestFrame () {
58
+ if (frameListeners.size === 0) {
59
+ _frameRunning = false
60
+ return
61
+ }
62
+ processFrameListeners(frameListeners)
63
+ window.requestAnimationFrame(requestFrame)
64
+ }
65
+
66
+ window.requestAnimationFrame(requestFrame)
67
+ }
68
+
69
+ export const applyAnimationFrame = element => {
70
+ if (!element) {
71
+ throw new Error('Element is invalid')
72
+ }
73
+ const { on, props, __ref: ref } = element
74
+ if (!ref.root || !ref.root.data) return
75
+ const { frameListeners } = ref.root.data
76
+
77
+ // Register if any of the frame handlers exists
78
+ if (frameListeners && (on?.frame || element.onFrame || props?.onFrame)) {
79
+ registerFrameListener(element)
80
+ startFrameLoop(frameListeners)
81
+ }
82
+ }
83
+
84
+ let _frameRunning = false
85
+
86
+ export const initAnimationFrame = () => {
87
+ const frameListeners = new Set()
88
+
89
+ startFrameLoop(frameListeners)
90
+
91
+ return frameListeners
92
+ }
package/event/can.js ADDED
@@ -0,0 +1,8 @@
1
+ 'use strict'
2
+
3
+ import { report } from '@domql/report'
4
+ import { isValidHtmlTag } from '@domql/utils'
5
+
6
+ export const canRenderTag = (tag) => {
7
+ return isValidHtmlTag(tag || 'div') || report('HTMLInvalidTag')
8
+ }
package/event/index.js ADDED
@@ -0,0 +1,5 @@
1
+ 'use strict'
2
+
3
+ export * from './on.js'
4
+ export * from './can.js'
5
+ export * from './animationFrame.js'
package/event/on.js ADDED
@@ -0,0 +1,71 @@
1
+ 'use strict'
2
+
3
+ import { DOMQL_EVENTS, isFunction } from '@domql/utils'
4
+
5
+ // Re-export event trigger functions from @domql/utils (moved there to break circular dep)
6
+ export { applyEvent, triggerEventOn, applyEventUpdate, triggerEventOnUpdate } from '@domql/utils'
7
+
8
+ const getOnOrPropsEvent = (param, element) => {
9
+ const onEvent = element.on?.[param]
10
+ if (onEvent) return onEvent
11
+ const props = element.props
12
+ if (!props) return
13
+ const propKey = 'on' + param.charAt(0).toUpperCase() + param.slice(1)
14
+ return props[propKey]
15
+ }
16
+
17
+ const registerNodeEvent = (param, element, node, options) => {
18
+ const appliedFunction = getOnOrPropsEvent(param, element)
19
+ if (isFunction(appliedFunction)) {
20
+ const { __ref: ref } = element
21
+ if (!ref.__eventListeners) ref.__eventListeners = {}
22
+
23
+ // Remove previous listener for this event to avoid duplicates
24
+ if (ref.__eventListeners[param]) {
25
+ node.removeEventListener(param, ref.__eventListeners[param])
26
+ }
27
+
28
+ const handler = event => {
29
+ const { state, context } = element
30
+ const result = appliedFunction.call(
31
+ element,
32
+ event,
33
+ element,
34
+ state,
35
+ context,
36
+ options
37
+ )
38
+ if (result && typeof result.then === 'function') {
39
+ result.catch(() => {})
40
+ }
41
+ }
42
+
43
+ ref.__eventListeners[param] = handler
44
+ node.addEventListener(param, handler)
45
+ }
46
+ }
47
+
48
+ export const applyEventsOnNode = (element, options) => {
49
+ const { node, on, props } = element
50
+ const handled = new Set()
51
+
52
+ // Register events from on: { click: ..., input: ... }
53
+ for (const param in on) {
54
+ if (DOMQL_EVENTS.has(param)) continue
55
+ handled.add(param)
56
+ registerNodeEvent(param, element, node, options)
57
+ }
58
+
59
+ // Also pick up props.onClick, props.onInput, etc.
60
+ if (props) {
61
+ for (const key in props) {
62
+ if (key.length > 2 && key.charCodeAt(0) === 111 && key.charCodeAt(1) === 110 && isFunction(props[key])) {
63
+ const thirdChar = key[2]
64
+ if (thirdChar !== thirdChar.toUpperCase()) continue
65
+ const eventName = thirdChar.toLowerCase() + key.slice(3)
66
+ if (handled.has(eventName) || DOMQL_EVENTS.has(eventName)) continue
67
+ registerNodeEvent(eventName, element, node, options)
68
+ }
69
+ }
70
+ }
71
+ }
package/event/store.js ADDED
@@ -0,0 +1,6 @@
1
+ 'use strict'
2
+
3
+ export default {
4
+ click: [],
5
+ render: []
6
+ }
package/methods/set.js ADDED
@@ -0,0 +1,73 @@
1
+ 'use strict'
2
+
3
+ import { merge, overwrite } from '@domql/utils'
4
+
5
+ import { set, reset, updateContent, removeContent } from '../set.js'
6
+ import { update } from '../update.js'
7
+
8
+ import {
9
+ call,
10
+ error,
11
+ getContext,
12
+ getPath,
13
+ getRef,
14
+ getRoot,
15
+ getRootContext,
16
+ getRootData,
17
+ getRootState,
18
+ keys,
19
+ log,
20
+ lookdown,
21
+ lookdownAll,
22
+ lookup,
23
+ nextElement,
24
+ parse,
25
+ parseDeep,
26
+ previousElement,
27
+ remove,
28
+ setNodeStyles,
29
+ setProps,
30
+ spotByPath,
31
+ variables,
32
+ verbose,
33
+ warn
34
+ } from '@domql/utils/methods'
35
+
36
+ export const addMethods = (element, parent, options = {}) => {
37
+ const proto = {
38
+ set,
39
+ reset,
40
+ update,
41
+ variables,
42
+ remove,
43
+ updateContent,
44
+ removeContent,
45
+ setProps,
46
+ lookup,
47
+ lookdown,
48
+ lookdownAll,
49
+ getRef,
50
+ getPath,
51
+ getRootState,
52
+ getRoot,
53
+ getRootData,
54
+ getRootContext,
55
+ getContext,
56
+ setNodeStyles,
57
+ spotByPath,
58
+ parse,
59
+ parseDeep,
60
+ keys,
61
+ nextElement,
62
+ previousElement,
63
+ log,
64
+ verbose,
65
+ warn,
66
+ error,
67
+ call
68
+ }
69
+ if (element.context.methods) {
70
+ ;(options.strict ? merge : overwrite)(proto, element.context.methods)
71
+ }
72
+ Object.setPrototypeOf(element, proto)
73
+ }
package/methods/v2.js ADDED
@@ -0,0 +1,83 @@
1
+ 'use strict'
2
+
3
+ import { isDefined, isFunction, isObjectLike } from '@domql/utils'
4
+
5
+ export const defineSetter = (element, key, get, set) =>
6
+ Object.defineProperty(element, key, { get, set })
7
+
8
+ export const keys = function () {
9
+ const element = this
10
+ const keys = []
11
+ for (const param in element) {
12
+ // if (REGISTRY[param] && !parseFilters.elementKeys.includes(param)) { continue }
13
+ keys.push(param)
14
+ }
15
+ return keys
16
+ }
17
+
18
+ export const parse = function (excl = []) {
19
+ const element = this
20
+ const obj = {}
21
+ const keyList = keys.call(element)
22
+ keyList.forEach(v => {
23
+ if (excl.includes(v)) return
24
+ let val = element[v]
25
+ if (v === 'state') {
26
+ if (element.__ref && element.__ref.__hasRootState) return
27
+ if (isFunction(val && val.parse)) val = val.parse()
28
+ } else if (v === 'props') {
29
+ const { __element, update, ...props } = element[v]
30
+ obj[v] = props
31
+ } else if (isDefined(val)) obj[v] = val
32
+ })
33
+ return obj
34
+ }
35
+
36
+ export const parseDeep = function (excl = []) {
37
+ const element = this
38
+ const obj = parse.call(element, excl)
39
+ for (const v in obj) {
40
+ if (excl.includes(v)) return
41
+ if (isObjectLike(obj[v])) {
42
+ obj[v] = parseDeep.call(obj[v], excl)
43
+ }
44
+ }
45
+ return obj
46
+ }
47
+
48
+ export const log = function (...args) {
49
+ const element = this
50
+ const { __ref } = element
51
+ console.group(element.key)
52
+ if (args.length) {
53
+ args.forEach(v => console.log(`%c${v}:\n`, 'font-weight: bold', element[v]))
54
+ } else {
55
+ console.log(__ref?.path)
56
+ const keys = element.keys()
57
+ keys.forEach(v => console.log(`%c${v}:\n`, 'font-weight: bold', element[v]))
58
+ }
59
+ console.groupEnd(element.key)
60
+ return element
61
+ }
62
+
63
+ export const nextElement = function () {
64
+ const element = this
65
+ const { key, parent } = element
66
+ const { __children } = parent.__ref
67
+
68
+ const currentIndex = __children.indexOf(key)
69
+ const nextChild = __children[currentIndex + 1]
70
+
71
+ return parent[nextChild]
72
+ }
73
+
74
+ export const previousElement = function (el) {
75
+ const element = el || this
76
+ const { key, parent } = element
77
+ const { __children } = parent.__ref
78
+
79
+ if (!__children) return
80
+
81
+ const currentIndex = __children.indexOf(key)
82
+ return parent[__children[currentIndex - 1]]
83
+ }
package/mixins/attr.js ADDED
@@ -0,0 +1,32 @@
1
+ 'use strict'
2
+
3
+ import { deepMerge, exec, isNot } from '@domql/utils'
4
+ import { report } from '@domql/report'
5
+
6
+ /**
7
+ * Recursively add attributes to a DOM node
8
+ */
9
+ export function attr (params, element, node) {
10
+ const { __ref: ref, props } = element
11
+ const { __attr } = ref
12
+ if (isNot(params)('object')) report('HTMLInvalidAttr', params)
13
+ if (params) {
14
+ const attrs = exec(params, element)
15
+ if (props.attr) deepMerge(attrs, props.attr)
16
+ for (const attr in attrs) {
17
+ const val = exec(attrs[attr], element)
18
+ if (val === __attr[attr]) continue
19
+ if (
20
+ val !== false &&
21
+ val !== undefined &&
22
+ val !== null &&
23
+ node.setAttribute
24
+ ) {
25
+ node.setAttribute(attr, val)
26
+ } else if (node.removeAttribute) node.removeAttribute(attr)
27
+ __attr[attr] = val
28
+ }
29
+ }
30
+ }
31
+
32
+ export default attr
@@ -0,0 +1,62 @@
1
+ 'use strict'
2
+
3
+ import { exec, isObject, isString } from '@domql/utils'
4
+
5
+ export const assignKeyAsClassname = element => {
6
+ const { key } = element
7
+ if (element.classlist === true) element.classlist = key
8
+ else if (
9
+ !element.classlist &&
10
+ typeof key === 'string' &&
11
+ key.charAt(0) === '_' &&
12
+ key.charAt(1) !== '_'
13
+ ) {
14
+ element.classlist = key.slice(1)
15
+ }
16
+ }
17
+
18
+ // stringifies class object
19
+ export const classify = (obj, element) => {
20
+ let className = ''
21
+ for (const item in obj) {
22
+ const param = obj[item]
23
+ if (typeof param === 'boolean' && param) className += ` ${item}`
24
+ else if (typeof param === 'string') className += ` ${param}`
25
+ else if (typeof param === 'function') {
26
+ className += ` ${exec(param, element)}`
27
+ }
28
+ }
29
+ return className
30
+ }
31
+
32
+ export const classList = (params, element) => {
33
+ if (!params) return
34
+ const { key } = element
35
+ if (params === true) params = element.classlist = { key }
36
+ if (isString(params)) params = element.classlist = { default: params }
37
+ if (isObject(params)) params = classify(params, element)
38
+ // TODO: fails on string
39
+ const className = params.replace(/\s+/g, ' ').trim()
40
+ return className
41
+ }
42
+
43
+ // LEGACY (still needed in old domql)
44
+ export const applyClassListOnNode = (params, element, node) => {
45
+ const className = classList(params, element)
46
+ const { __ref } = element
47
+ if (className === __ref.__className) return className
48
+ __ref.__className = className
49
+ // Use setAttribute for universal SVG/HTML compatibility
50
+ if (node && typeof node.setAttribute === 'function') {
51
+ node.setAttribute('class', className)
52
+ } else if (node) {
53
+ try { node.className = className } catch (e) { /* SVG element */ }
54
+ }
55
+ return className
56
+ }
57
+
58
+ export function applyClasslist (params, element, node) {
59
+ applyClassListOnNode(params, element, node)
60
+ }
61
+
62
+ export default applyClasslist