pseudo-dom 0.8.0 → 0.10.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.
Files changed (41) hide show
  1. package/README.md +971 -3
  2. package/browser/pseudo-dom.js +1141 -470
  3. package/browser/pseudo-dom.min.js +1 -1
  4. package/dist/classes/PseudoHTMLDocument.d.ts +41 -41
  5. package/dist/classes/PseudoHTMLDocument.js +74 -74
  6. package/dist/factories/createDataset.d.ts +10 -0
  7. package/dist/factories/createDataset.js +78 -0
  8. package/dist/factories/createDataset.min.js +1 -0
  9. package/dist/factories/createStyleDeclaration.d.ts +30 -0
  10. package/dist/factories/createStyleDeclaration.js +64 -0
  11. package/dist/factories/createStyleDeclaration.min.js +1 -0
  12. package/dist/functions/getElementById.d.ts +13 -13
  13. package/dist/functions/getElementById.js +33 -33
  14. package/dist/interfaces/PseudoCSSStyleDeclaration.d.ts +14 -0
  15. package/dist/interfaces/PseudoCSSStyleDeclaration.js +5 -0
  16. package/dist/interfaces/PseudoCSSStyleDeclaration.min.js +1 -0
  17. package/dist/interfaces/PseudoDocument.d.ts +35 -35
  18. package/dist/interfaces/PseudoDocumentFragment.d.ts +10 -10
  19. package/dist/interfaces/PseudoElement.d.ts +765 -702
  20. package/dist/interfaces/PseudoHTMLElement.d.ts +12 -1
  21. package/dist/interfaces/PseudoShadowRoot.d.ts +12 -3
  22. package/dist/services/CSSStyleDeclarationService.d.ts +73 -0
  23. package/dist/services/CSSStyleDeclarationService.js +156 -0
  24. package/dist/services/CSSStyleDeclarationService.min.js +1 -0
  25. package/dist/services/DocumentFragmentService.d.ts +21 -21
  26. package/dist/services/DocumentFragmentService.js +42 -42
  27. package/dist/services/DocumentService.d.ts +46 -46
  28. package/dist/services/DocumentService.js +95 -95
  29. package/dist/services/ElementService.d.ts +361 -191
  30. package/dist/services/ElementService.js +717 -426
  31. package/dist/services/ElementService.min.js +1 -1
  32. package/dist/services/HTMLElementService.d.ts +31 -1
  33. package/dist/services/HTMLElementService.js +43 -4
  34. package/dist/services/HTMLElementService.min.js +1 -1
  35. package/dist/services/NodeService.d.ts +305 -297
  36. package/dist/services/NodeService.js +732 -721
  37. package/dist/services/NodeService.min.js +1 -1
  38. package/dist/services/ShadowRootService.d.ts +16 -4
  39. package/dist/services/ShadowRootService.js +24 -9
  40. package/dist/services/ShadowRootService.min.js +1 -1
  41. package/package.json +1 -1
@@ -1,426 +1,717 @@
1
- 'use strict'
2
-
3
- require('core-js/modules/esnext.iterator.constructor.js')
4
- require('core-js/modules/esnext.iterator.every.js')
5
- require('core-js/modules/esnext.iterator.find.js')
6
- require('core-js/modules/esnext.iterator.for-each.js')
7
- require('core-js/modules/esnext.iterator.map.js')
8
- require('core-js/modules/esnext.iterator.some.js')
9
- const __importDefault = void 0 && (void 0).__importDefault || function (mod) {
10
- return mod && mod.__esModule
11
- ? mod
12
- : {
13
- default: mod
14
- }
15
- }
16
- Object.defineProperty(exports, '__esModule', {
17
- value: true
18
- })
19
- exports.ElementService = void 0
20
- const createEvent_1 = __importDefault(require('../factories/createEvent'))
21
- const NodeService_1 = require('./NodeService')
22
- const AttrService_1 = require('./AttrService')
23
- const DOMTokenListService_1 = require('./DOMTokenListService')
24
- const NamedNodeMapService_1 = require('./NamedNodeMapService')
25
- const getParentNodesFromAttribute_1 = __importDefault(require('../functions/getParentNodesFromAttribute'))
26
- const HTMLCollectionService_1 = require('./HTMLCollectionService')
27
- const cloneObject_1 = __importDefault(require('si-funciona/dist/helpers/objects/cloneObject'))
28
- const isEqual_1 = __importDefault(require('si-funciona/dist/helpers/objects/isEqual'))
29
- const query_1 = require('../factories/query')
30
- /**
31
- * Simulate the behaviour of the Element Class when there is no DOM available.
32
- * @author Joshua Heagle <joshuaheagle@gmail.com>
33
- * @class
34
- * @augments PseudoNode
35
- * @property {string} tagName
36
- * @property {string} className
37
- * @property {string} id
38
- * @property {string} innerHtml
39
- * @property {Array} attributes
40
- * @property {function} hasAttribute
41
- * @property {function} setAttribute
42
- * @property {function} getAttribute
43
- * @property {function} removeAttribute
44
- */
45
- class ElementService extends NodeService_1.NodeService {
46
- /**
47
- * @param {Object} [settings={}]
48
- * @param {string} [settings.tagName=''] The name of the tag this element represents
49
- * @param {Array<{name: string, value: *}>} [settings.attributes=[]] The attributes (also assigned as properties) to start with
50
- * @param {PseudoNode|null} [settings.parent=null] The node to add this element to as its last child
51
- * @param {Array<PseudoNode>} [settings.children=[]] The nodes to start as children
52
- * @constructor
53
- */
54
- constructor ({
55
- tagName = '',
56
- attributes = [],
57
- parent = null,
58
- children = []
59
- } = {}) {
60
- super()
61
- this.defaultEventApplied = false
62
- this.tokenList = new DOMTokenListService_1.DOMTokenListService()
63
- this.tag = tagName
64
- this.attributeList = attributes.concat([{
65
- name: 'className',
66
- value: ''
67
- }, {
68
- name: 'id',
69
- value: ''
70
- }, {
71
- name: 'innerHTML',
72
- value: ''
73
- }])
74
- this.propertyAttributes = this.attributeList.map(({
75
- name
76
- }) => name)
77
- this.attributeList.forEach(({
78
- name,
79
- value
80
- }) => {
81
- this[name] = value
82
- })
83
- children.forEach(child => {
84
- if (!child || typeof child.nodeType !== 'number') {
85
- throw new TypeError('The children of an element must be nodes.')
86
- }
87
- this.appendChild(child)
88
- })
89
- if (parent) {
90
- parent.appendChild(this)
91
- }
92
- }
93
-
94
- /**
95
- * The attributes with the values they have now: the ones which are also properties (className, id, style, ...) can
96
- * have been changed through the property, which does not change the stored list.
97
- * @returns {Array<{name: string, value: *}>}
98
- */
99
- currentAttributes () {
100
- return this.attributeList.map(({
101
- name,
102
- value
103
- }) => ({
104
- name,
105
- value: this.propertyAttributes.indexOf(name) >= 0 ? this[name] : value
106
- }))
107
- }
108
-
109
- get nodeName () {
110
- return this.tag
111
- }
112
-
113
- /**
114
- * A copy of this element without its children: the same tag and attributes (the values which are objects, such as
115
- * style, are copied too rather than shared), but not its parent or listeners.
116
- * @returns {ElementService}
117
- */
118
- cloneShallow () {
119
- const copy = new this.constructor({
120
- tagName: this.tag
121
- })
122
- copy.attributeList.length = 0
123
- this.currentAttributes().forEach(({
124
- name,
125
- value
126
- }) => {
127
- const copied = typeof value === 'object' && value !== null ? (0, cloneObject_1.default)(value) : value
128
- copy.attributeList.push({
129
- name,
130
- value: copied
131
- })
132
- if (copy.propertyAttributes.indexOf(name) >= 0) {
133
- copy[name] = copied
134
- }
135
- })
136
- copy.ownerDocumentStore = this.ownerDocumentStore
137
- return copy
138
- }
139
-
140
- /**
141
- * Elements are equal when they have the same tag and the same attributes (in any order), which is what isEqualNode
142
- * checks before it compares the children.
143
- * @param {NodeService} other The element to compare with
144
- * @returns {boolean}
145
- */
146
- equalsShallow (other) {
147
- const mine = this.currentAttributes()
148
- const theirs = other.currentAttributes()
149
- return this.nodeName === other.nodeName && mine.length === theirs.length && mine.every(({
150
- name,
151
- value
152
- }) => {
153
- const match = theirs.find(attributeOfOther => attributeOfOther.name === name)
154
- return typeof match !== 'undefined' && (0, isEqual_1.default)(value, match.value)
155
- })
156
- }
157
-
158
- get tagName () {
159
- return this.tag
160
- }
161
-
162
- get nodeType () {
163
- return NodeService_1.NodeService.ELEMENT_NODE
164
- }
165
-
166
- get attributes () {
167
- return new NamedNodeMapService_1.NamedNodeMapService(this.currentAttributes().map(({
168
- name,
169
- value
170
- }) => new AttrService_1.AttrService(name, String(value), this)))
171
- }
172
-
173
- get classList () {
174
- return this.tokenList
175
- }
176
-
177
- get className () {
178
- return this.tokenList.value
179
- }
180
-
181
- set className (className) {
182
- this.tokenList.value = className
183
- }
184
-
185
- /**
186
- * Some elements have default behaviour, this registers it when the element is added.
187
- * @returns {Function}
188
- */
189
- /**
190
- * A live view of this element's element children (text, comments and the like are not included).
191
- * @returns {PseudoHTMLCollection}
192
- */
193
- get children () {
194
- return new HTMLCollectionService_1.HTMLCollectionService(this)
195
- }
196
-
197
- /**
198
- * How many element children this element has.
199
- * @returns {number}
200
- */
201
- get childElementCount () {
202
- return this.children.length
203
- }
204
-
205
- /**
206
- * The first child of this element which is an element, or null when there is none.
207
- * @returns {PseudoElement|null}
208
- */
209
- get firstElementChild () {
210
- return this.children.item(0)
211
- }
212
-
213
- /**
214
- * The last child of this element which is an element, or null when there is none.
215
- * @returns {PseudoElement|null}
216
- */
217
- get lastElementChild () {
218
- const elementChildren = this.children
219
- return elementChildren.item(elementChildren.length - 1)
220
- }
221
-
222
- /**
223
- * The sibling after this one which is an element, or null when there is none.
224
- * @returns {PseudoElement|null}
225
- */
226
- get nextElementSibling () {
227
- let sibling = this.nextSibling
228
- while (sibling && sibling.nodeType !== NodeService_1.NodeService.ELEMENT_NODE) {
229
- sibling = sibling.nextSibling
230
- }
231
- return sibling
232
- }
233
-
234
- /**
235
- * The sibling before this one which is an element, or null when there is none.
236
- * @returns {PseudoElement|null}
237
- */
238
- get previousElementSibling () {
239
- let sibling = this.previousSibling
240
- while (sibling && sibling.nodeType !== NodeService_1.NodeService.ELEMENT_NODE) {
241
- sibling = sibling.previousSibling
242
- }
243
- return sibling
244
- }
245
-
246
- /**
247
- * Put an element at a position relative to this one: beforebegin (before this element, as its previous sibling),
248
- * afterbegin (as this element's first child), beforeend (as this element's last child) or afterend (after this
249
- * element, as its next sibling).
250
- * @param {string} position beforebegin, afterbegin, beforeend or afterend
251
- * @param {ElementService} element The element to insert
252
- * @returns {ElementService|null} The inserted element, or null when the position needed a parent this element does not have
253
- * @throws {Error} When the position is not one of the four above
254
- */
255
- insertAdjacentElement (position, element) {
256
- return this.insertAdjacent(position, element)
257
- }
258
-
259
- /**
260
- * Put text at a position relative to this element, the same as insertAdjacentElement but the text becomes a text node.
261
- * @param {string} position beforebegin, afterbegin, beforeend or afterend
262
- * @param {string} text The text to insert
263
- * @throws {Error} When the position is not one of the four above
264
- */
265
- insertAdjacentText (position, text) {
266
- this.insertAdjacent(position, text)
267
- }
268
-
269
- /**
270
- * Shared implementation for insertAdjacentElement / insertAdjacentText.
271
- * @param {string} position beforebegin, afterbegin, beforeend or afterend
272
- * @param {PseudoNode|string} node The node (or text) to insert
273
- * @returns {PseudoNode|null} The inserted node, or null when the position needed a parent this element does not have
274
- * @throws {Error} When the position is not one of the four above
275
- */
276
- insertAdjacent (position, node) {
277
- switch (position) {
278
- case 'beforebegin':
279
- if (!this.parentNode) {
280
- return null
281
- }
282
- this.before(node)
283
- return node
284
- case 'afterbegin':
285
- this.prepend(node)
286
- return node
287
- case 'beforeend':
288
- this.append(node)
289
- return node
290
- case 'afterend':
291
- if (!this.parentNode) {
292
- return null
293
- }
294
- this.after(node)
295
- return node
296
- default:
297
- throw new Error(`"${position}" is not one of beforebegin, afterbegin, beforeend or afterend.`)
298
- }
299
- }
300
-
301
- /**
302
- * Not implemented yet (HTML parsing is out of scope for now).
303
- * @param {string} position beforebegin, afterbegin, beforeend or afterend
304
- * @param {string} text The markup which would be parsed
305
- * @throws {Error}
306
- */
307
- insertAdjacentHTML (position, text) {
308
- throw new Error('ElementService.insertAdjacentHTML() is not implemented yet.')
309
- }
310
-
311
- /**
312
- * Whether this element itself (not its descendants) matches the given CSS selector.
313
- * @param {string} selectors A CSS selector
314
- * @returns {boolean}
315
- */
316
- matches (selectors) {
317
- return (0, query_1.matches)(this, selectors)
318
- }
319
-
320
- /**
321
- * The nearest ancestor of this element (starting with this element itself) which matches the CSS selector, or
322
- * null when none of them do.
323
- * @param {string} selectors A CSS selector
324
- * @returns {PseudoElement|null}
325
- */
326
- closest (selectors) {
327
- return (0, query_1.closest)(this, selectors)
328
- }
329
-
330
- applyDefaultEvent () {
331
- let callback = event => undefined
332
- if (this.defaultEventApplied) {
333
- return callback
334
- }
335
- switch (this.tagName) {
336
- case 'button':
337
- case 'input':
338
- // Clicking a submit button submits the form it is in: the form gets a submit event, which can be cancelled
339
- callback = event => {
340
- const type = String(this.getAttribute('type') || this.type || '').toLowerCase()
341
- const submits = this.tagName === 'button' ? type !== 'button' && type !== 'reset' : /^(submit|image)$/.test(type)
342
- const forms = (0, getParentNodesFromAttribute_1.default)('tagName', 'form', this)
343
- if (submits && forms.length && !this.hasAttribute('disabled')) {
344
- forms[forms.length - 1].dispatchEvent((0, createEvent_1.default)('submit', {}, {
345
- browser: true,
346
- trusted: event.isTrusted
347
- }))
348
- }
349
- }
350
- super.setDefaultEvent('click', callback)
351
- this.defaultEventApplied = true
352
- }
353
- return callback
354
- }
355
-
356
- /**
357
- * An element which is added as a child gets its default events (for example a submit button submits its form).
358
- * @param {NodeService} child The node which was inserted
359
- */
360
- childInserted (child) {
361
- if (typeof child.applyDefaultEvent === 'function') {
362
- child.applyDefaultEvent()
363
- }
364
- }
365
-
366
- /**
367
- * Check whether the element has an attribute by that name.
368
- * @param {string} attributeName
369
- * @returns {boolean}
370
- */
371
- hasAttribute (attributeName) {
372
- return this.attributeList.some(({
373
- name
374
- }) => name === attributeName)
375
- }
376
-
377
- /**
378
- * Set the value of an attribute, adding the attribute if it did not exist.
379
- * @param {string} attributeName
380
- * @param {string} attributeValue
381
- * @returns {undefined}
382
- */
383
- setAttribute (attributeName, attributeValue) {
384
- const existing = this.attributeList.find(({
385
- name
386
- }) => name === attributeName)
387
- if (existing) {
388
- existing.value = attributeValue
389
- } else {
390
- this.attributeList.push({
391
- name: attributeName,
392
- value: attributeValue
393
- })
394
- }
395
- if (this.propertyAttributes.indexOf(attributeName) >= 0) {
396
- this[attributeName] = attributeValue
397
- }
398
- }
399
-
400
- /**
401
- * Retrieve the value of an attribute.
402
- * @param {string} attributeName
403
- * @returns {string|null} The value, or null when there is no such attribute
404
- */
405
- getAttribute (attributeName) {
406
- const found = this.currentAttributes().find(({
407
- name
408
- }) => name === attributeName)
409
- return found ? found.value : null
410
- }
411
-
412
- /**
413
- * Remove an attribute from the element.
414
- * @param {string} attributeName
415
- * @returns {undefined}
416
- */
417
- removeAttribute (attributeName) {
418
- const index = this.attributeList.findIndex(({
419
- name
420
- }) => name === attributeName)
421
- if (index >= 0) {
422
- this.attributeList.splice(index, 1)
423
- }
424
- }
425
- }
426
- exports.ElementService = ElementService
1
+ 'use strict'
2
+
3
+ require('core-js/modules/esnext.iterator.constructor.js')
4
+ require('core-js/modules/esnext.iterator.every.js')
5
+ require('core-js/modules/esnext.iterator.find.js')
6
+ require('core-js/modules/esnext.iterator.for-each.js')
7
+ require('core-js/modules/esnext.iterator.map.js')
8
+ require('core-js/modules/esnext.iterator.some.js')
9
+ require('core-js/modules/esnext.set.add-all.js')
10
+ require('core-js/modules/esnext.set.delete-all.js')
11
+ require('core-js/modules/esnext.set.difference.js')
12
+ require('core-js/modules/esnext.set.every.js')
13
+ require('core-js/modules/esnext.set.filter.js')
14
+ require('core-js/modules/esnext.set.find.js')
15
+ require('core-js/modules/esnext.set.intersection.js')
16
+ require('core-js/modules/esnext.set.is-disjoint-from.js')
17
+ require('core-js/modules/esnext.set.is-subset-of.js')
18
+ require('core-js/modules/esnext.set.is-superset-of.js')
19
+ require('core-js/modules/esnext.set.join.js')
20
+ require('core-js/modules/esnext.set.map.js')
21
+ require('core-js/modules/esnext.set.reduce.js')
22
+ require('core-js/modules/esnext.set.some.js')
23
+ require('core-js/modules/esnext.set.symmetric-difference.js')
24
+ require('core-js/modules/esnext.set.union.js')
25
+ const __importDefault = void 0 && (void 0).__importDefault || function (mod) {
26
+ return mod && mod.__esModule
27
+ ? mod
28
+ : {
29
+ default: mod
30
+ }
31
+ }
32
+ Object.defineProperty(exports, '__esModule', {
33
+ value: true
34
+ })
35
+ exports.ElementService = void 0
36
+ const createEvent_1 = __importDefault(require('../factories/createEvent'))
37
+ const NodeService_1 = require('./NodeService')
38
+ const AttrService_1 = require('./AttrService')
39
+ const DOMTokenListService_1 = require('./DOMTokenListService')
40
+ const NamedNodeMapService_1 = require('./NamedNodeMapService')
41
+ const getParentNodesFromAttribute_1 = __importDefault(require('../functions/getParentNodesFromAttribute'))
42
+ const HTMLCollectionService_1 = require('./HTMLCollectionService')
43
+ const cloneObject_1 = __importDefault(require('si-funciona/dist/helpers/objects/cloneObject'))
44
+ const isEqual_1 = __importDefault(require('si-funciona/dist/helpers/objects/isEqual'))
45
+ const query_1 = require('../factories/query')
46
+ const ShadowRootService_1 = require('./ShadowRootService')
47
+ const zeroRect = () => ({
48
+ x: 0,
49
+ y: 0,
50
+ width: 0,
51
+ height: 0,
52
+ top: 0,
53
+ right: 0,
54
+ bottom: 0,
55
+ left: 0
56
+ })
57
+ /**
58
+ * Simulate the behaviour of the Element Class when there is no DOM available.
59
+ * @author Joshua Heagle <joshuaheagle@gmail.com>
60
+ * @class
61
+ * @augments PseudoNode
62
+ * @property {string} tagName
63
+ * @property {string} className
64
+ * @property {string} id
65
+ * @property {string} innerHtml
66
+ * @property {Array} attributes
67
+ * @property {function} hasAttribute
68
+ * @property {function} setAttribute
69
+ * @property {function} getAttribute
70
+ * @property {function} removeAttribute
71
+ */
72
+ class ElementService extends NodeService_1.NodeService {
73
+ /**
74
+ * @param {Object} [settings={}]
75
+ * @param {string} [settings.tagName=''] The name of the tag this element represents
76
+ * @param {Array<{name: string, value: *}>} [settings.attributes=[]] The attributes (also assigned as properties) to start with
77
+ * @param {PseudoNode|null} [settings.parent=null] The node to add this element to as its last child
78
+ * @param {Array<PseudoNode>} [settings.children=[]] The nodes to start as children
79
+ * @constructor
80
+ */
81
+ constructor ({
82
+ tagName = '',
83
+ attributes = [],
84
+ parent = null,
85
+ children = []
86
+ } = {}) {
87
+ super()
88
+ /** What getBoundingClientRect() returns - not really computed (there is no layout engine), set this directly. */
89
+ this.boundingClientRect = zeroRect()
90
+ /** What getClientRects() returns - set this directly. */
91
+ this.clientRects = []
92
+ /** What getAnimations() returns - set this directly. */
93
+ this.animations = []
94
+ /** What checkVisibility() returns - set this directly. */
95
+ this.isVisible = true
96
+ this.capturedPointers = new Set()
97
+ this.shadowRootInstance = null
98
+ this.defaultEventApplied = false
99
+ this.tokenList = new DOMTokenListService_1.DOMTokenListService()
100
+ this.tag = tagName
101
+ this.attributeList = attributes.concat([{
102
+ name: 'className',
103
+ value: ''
104
+ }, {
105
+ name: 'id',
106
+ value: ''
107
+ }, {
108
+ name: 'innerHTML',
109
+ value: ''
110
+ },
111
+ // Layout is not really computed (there is no rendering engine here): these start at 0, like an unrendered
112
+ // element's would, but can be set directly to whatever a test needs code under test to see.
113
+ {
114
+ name: 'clientHeight',
115
+ value: 0
116
+ }, {
117
+ name: 'clientLeft',
118
+ value: 0
119
+ }, {
120
+ name: 'clientTop',
121
+ value: 0
122
+ }, {
123
+ name: 'clientWidth',
124
+ value: 0
125
+ }, {
126
+ name: 'scrollHeight',
127
+ value: 0
128
+ }, {
129
+ name: 'scrollLeft',
130
+ value: 0
131
+ }, {
132
+ name: 'scrollTop',
133
+ value: 0
134
+ }, {
135
+ name: 'scrollWidth',
136
+ value: 0
137
+ }])
138
+ this.propertyAttributes = this.attributeList.map(({
139
+ name
140
+ }) => name)
141
+ this.attributeList.forEach(({
142
+ name,
143
+ value
144
+ }) => {
145
+ this[name] = value
146
+ })
147
+ children.forEach(child => {
148
+ if (!child || typeof child.nodeType !== 'number') {
149
+ throw new TypeError('The children of an element must be nodes.')
150
+ }
151
+ this.appendChild(child)
152
+ })
153
+ if (parent) {
154
+ parent.appendChild(this)
155
+ }
156
+ }
157
+
158
+ /**
159
+ * The attributes with the values they have now: the ones which are also properties (className, id, style, ...) can
160
+ * have been changed through the property, which does not change the stored list.
161
+ * @returns {Array<{name: string, value: *}>}
162
+ */
163
+ currentAttributes () {
164
+ return this.attributeList.map(({
165
+ name,
166
+ value
167
+ }) => ({
168
+ name,
169
+ value: this.propertyAttributes.indexOf(name) >= 0 ? this[name] : value
170
+ }))
171
+ }
172
+
173
+ get nodeName () {
174
+ return this.tag
175
+ }
176
+
177
+ /**
178
+ * A copy of this element without its children: the same tag and attributes (the values which are objects, such as
179
+ * style, are copied too rather than shared), but not its parent or listeners.
180
+ * @returns {ElementService}
181
+ */
182
+ cloneShallow () {
183
+ const copy = new this.constructor({
184
+ tagName: this.tag
185
+ })
186
+ copy.attributeList.length = 0
187
+ this.currentAttributes().forEach(({
188
+ name,
189
+ value
190
+ }) => {
191
+ const copied = typeof value === 'object' && value !== null ? (0, cloneObject_1.default)(value) : value
192
+ copy.attributeList.push({
193
+ name,
194
+ value: copied
195
+ })
196
+ if (copy.propertyAttributes.indexOf(name) >= 0) {
197
+ copy[name] = copied
198
+ }
199
+ })
200
+ copy.ownerDocumentStore = this.ownerDocumentStore
201
+ return copy
202
+ }
203
+
204
+ /**
205
+ * Elements are equal when they have the same tag and the same attributes (in any order), which is what isEqualNode
206
+ * checks before it compares the children.
207
+ * @param {NodeService} other The element to compare with
208
+ * @returns {boolean}
209
+ */
210
+ equalsShallow (other) {
211
+ const mine = this.currentAttributes()
212
+ const theirs = other.currentAttributes()
213
+ return this.nodeName === other.nodeName && mine.length === theirs.length && mine.every(({
214
+ name,
215
+ value
216
+ }) => {
217
+ const match = theirs.find(attributeOfOther => attributeOfOther.name === name)
218
+ return typeof match !== 'undefined' && (0, isEqual_1.default)(value, match.value)
219
+ })
220
+ }
221
+
222
+ get tagName () {
223
+ return this.tag
224
+ }
225
+
226
+ /**
227
+ * The local part of the element's qualified name. There is no real namespace parsing here, so this is always the
228
+ * same as tagName.
229
+ * @returns {string}
230
+ */
231
+ get localName () {
232
+ return this.tag
233
+ }
234
+
235
+ /**
236
+ * The element's namespace prefix, or null when it has none. There is no real namespace parsing here, so this is
237
+ * always null.
238
+ * @returns {string|null}
239
+ */
240
+ get prefix () {
241
+ return null
242
+ }
243
+
244
+ get nodeType () {
245
+ return NodeService_1.NodeService.ELEMENT_NODE
246
+ }
247
+
248
+ get attributes () {
249
+ return new NamedNodeMapService_1.NamedNodeMapService(this.currentAttributes().map(({
250
+ name,
251
+ value
252
+ }) => new AttrService_1.AttrService(name, String(value), this)))
253
+ }
254
+
255
+ get classList () {
256
+ return this.tokenList
257
+ }
258
+
259
+ get className () {
260
+ return this.tokenList.value
261
+ }
262
+
263
+ set className (className) {
264
+ this.tokenList.value = className
265
+ }
266
+
267
+ /**
268
+ * Some elements have default behaviour, this registers it when the element is added.
269
+ * @returns {Function}
270
+ */
271
+ /**
272
+ * A live view of this element's element children (text, comments and the like are not included).
273
+ * @returns {PseudoHTMLCollection}
274
+ */
275
+ get children () {
276
+ return new HTMLCollectionService_1.HTMLCollectionService(this)
277
+ }
278
+
279
+ /**
280
+ * How many element children this element has.
281
+ * @returns {number}
282
+ */
283
+ get childElementCount () {
284
+ return this.children.length
285
+ }
286
+
287
+ /**
288
+ * The first child of this element which is an element, or null when there is none.
289
+ * @returns {PseudoElement|null}
290
+ */
291
+ get firstElementChild () {
292
+ return this.children.item(0)
293
+ }
294
+
295
+ /**
296
+ * The last child of this element which is an element, or null when there is none.
297
+ * @returns {PseudoElement|null}
298
+ */
299
+ get lastElementChild () {
300
+ const elementChildren = this.children
301
+ return elementChildren.item(elementChildren.length - 1)
302
+ }
303
+
304
+ /**
305
+ * The sibling after this one which is an element, or null when there is none.
306
+ * @returns {PseudoElement|null}
307
+ */
308
+ get nextElementSibling () {
309
+ let sibling = this.nextSibling
310
+ while (sibling && sibling.nodeType !== NodeService_1.NodeService.ELEMENT_NODE) {
311
+ sibling = sibling.nextSibling
312
+ }
313
+ return sibling
314
+ }
315
+
316
+ /**
317
+ * The sibling before this one which is an element, or null when there is none.
318
+ * @returns {PseudoElement|null}
319
+ */
320
+ get previousElementSibling () {
321
+ let sibling = this.previousSibling
322
+ while (sibling && sibling.nodeType !== NodeService_1.NodeService.ELEMENT_NODE) {
323
+ sibling = sibling.previousSibling
324
+ }
325
+ return sibling
326
+ }
327
+
328
+ /**
329
+ * Put an element at a position relative to this one: beforebegin (before this element, as its previous sibling),
330
+ * afterbegin (as this element's first child), beforeend (as this element's last child) or afterend (after this
331
+ * element, as its next sibling).
332
+ * @param {string} position beforebegin, afterbegin, beforeend or afterend
333
+ * @param {ElementService} element The element to insert
334
+ * @returns {ElementService|null} The inserted element, or null when the position needed a parent this element does not have
335
+ * @throws {Error} When the position is not one of the four above
336
+ */
337
+ insertAdjacentElement (position, element) {
338
+ return this.insertAdjacent(position, element)
339
+ }
340
+
341
+ /**
342
+ * Put text at a position relative to this element, the same as insertAdjacentElement but the text becomes a text node.
343
+ * @param {string} position beforebegin, afterbegin, beforeend or afterend
344
+ * @param {string} text The text to insert
345
+ * @throws {Error} When the position is not one of the four above
346
+ */
347
+ insertAdjacentText (position, text) {
348
+ this.insertAdjacent(position, text)
349
+ }
350
+
351
+ /**
352
+ * Shared implementation for insertAdjacentElement / insertAdjacentText.
353
+ * @param {string} position beforebegin, afterbegin, beforeend or afterend
354
+ * @param {PseudoNode|string} node The node (or text) to insert
355
+ * @returns {PseudoNode|null} The inserted node, or null when the position needed a parent this element does not have
356
+ * @throws {Error} When the position is not one of the four above
357
+ */
358
+ insertAdjacent (position, node) {
359
+ switch (position) {
360
+ case 'beforebegin':
361
+ if (!this.parentNode) {
362
+ return null
363
+ }
364
+ this.before(node)
365
+ return node
366
+ case 'afterbegin':
367
+ this.prepend(node)
368
+ return node
369
+ case 'beforeend':
370
+ this.append(node)
371
+ return node
372
+ case 'afterend':
373
+ if (!this.parentNode) {
374
+ return null
375
+ }
376
+ this.after(node)
377
+ return node
378
+ default:
379
+ throw new Error(`"${position}" is not one of beforebegin, afterbegin, beforeend or afterend.`)
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Not implemented yet (HTML parsing is out of scope for now).
385
+ * @param {string} position beforebegin, afterbegin, beforeend or afterend
386
+ * @param {string} text The markup which would be parsed
387
+ * @throws {Error}
388
+ */
389
+ insertAdjacentHTML (position, text) {
390
+ throw new Error('ElementService.insertAdjacentHTML() is not implemented yet.')
391
+ }
392
+
393
+ /**
394
+ * Whether this element itself (not its descendants) matches the given CSS selector.
395
+ * @param {string} selectors A CSS selector
396
+ * @returns {boolean}
397
+ */
398
+ matches (selectors) {
399
+ return (0, query_1.matches)(this, selectors)
400
+ }
401
+
402
+ /**
403
+ * The nearest ancestor of this element (starting with this element itself) which matches the CSS selector, or
404
+ * null when none of them do.
405
+ * @param {string} selectors A CSS selector
406
+ * @returns {PseudoElement|null}
407
+ */
408
+ closest (selectors) {
409
+ return (0, query_1.closest)(this, selectors)
410
+ }
411
+
412
+ applyDefaultEvent () {
413
+ let callback = event => undefined
414
+ if (this.defaultEventApplied) {
415
+ return callback
416
+ }
417
+ switch (this.tagName) {
418
+ case 'button':
419
+ case 'input':
420
+ // Clicking a submit button submits the form it is in: the form gets a submit event, which can be cancelled
421
+ callback = event => {
422
+ const type = String(this.getAttribute('type') || this.type || '').toLowerCase()
423
+ const submits = this.tagName === 'button' ? type !== 'button' && type !== 'reset' : /^(submit|image)$/.test(type)
424
+ const forms = (0, getParentNodesFromAttribute_1.default)('tagName', 'form', this)
425
+ if (submits && forms.length && !this.hasAttribute('disabled')) {
426
+ forms[forms.length - 1].dispatchEvent((0, createEvent_1.default)('submit', {}, {
427
+ browser: true,
428
+ trusted: event.isTrusted
429
+ }))
430
+ }
431
+ }
432
+ super.setDefaultEvent('click', callback)
433
+ this.defaultEventApplied = true
434
+ }
435
+ return callback
436
+ }
437
+
438
+ /**
439
+ * An element which is added as a child gets its default events (for example a submit button submits its form).
440
+ * @param {NodeService} child The node which was inserted
441
+ */
442
+ childInserted (child) {
443
+ if (typeof child.applyDefaultEvent === 'function') {
444
+ child.applyDefaultEvent()
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Check whether the element has an attribute by that name.
450
+ * @param {string} attributeName
451
+ * @returns {boolean}
452
+ */
453
+ hasAttribute (attributeName) {
454
+ return this.attributeList.some(({
455
+ name
456
+ }) => name === attributeName)
457
+ }
458
+
459
+ /**
460
+ * Set the value of an attribute, adding the attribute if it did not exist.
461
+ * @param {string} attributeName
462
+ * @param {string} attributeValue
463
+ * @returns {undefined}
464
+ */
465
+ setAttribute (attributeName, attributeValue) {
466
+ const existing = this.attributeList.find(({
467
+ name
468
+ }) => name === attributeName)
469
+ if (existing) {
470
+ existing.value = attributeValue
471
+ } else {
472
+ this.attributeList.push({
473
+ name: attributeName,
474
+ value: attributeValue
475
+ })
476
+ }
477
+ if (this.propertyAttributes.indexOf(attributeName) >= 0) {
478
+ this[attributeName] = attributeValue
479
+ }
480
+ }
481
+
482
+ /**
483
+ * Retrieve the value of an attribute.
484
+ * @param {string} attributeName
485
+ * @returns {string|null} The value, or null when there is no such attribute
486
+ */
487
+ getAttribute (attributeName) {
488
+ const found = this.currentAttributes().find(({
489
+ name
490
+ }) => name === attributeName)
491
+ return found ? found.value : null
492
+ }
493
+
494
+ /**
495
+ * Remove an attribute from the element.
496
+ * @param {string} attributeName
497
+ * @returns {undefined}
498
+ */
499
+ removeAttribute (attributeName) {
500
+ const index = this.attributeList.findIndex(({
501
+ name
502
+ }) => name === attributeName)
503
+ if (index >= 0) {
504
+ this.attributeList.splice(index, 1)
505
+ }
506
+ }
507
+
508
+ /**
509
+ * The name of every attribute on the element, in the order they were set.
510
+ * @returns {Array<string>}
511
+ */
512
+ getAttributeNames () {
513
+ return this.currentAttributes().map(({
514
+ name
515
+ }) => name)
516
+ }
517
+
518
+ /**
519
+ * Whether the element has any attributes at all.
520
+ * @returns {boolean}
521
+ */
522
+ hasAttributes () {
523
+ return this.attributeList.length > 0
524
+ }
525
+
526
+ /**
527
+ * Add the attribute (with an empty value) when it is not present, or remove it when it is - unless force says
528
+ * which of those to do instead. Returns whether the attribute is present after the call.
529
+ * @param {string} attributeName
530
+ * @param {boolean} [force]
531
+ * @returns {boolean}
532
+ */
533
+ toggleAttribute (attributeName, force) {
534
+ const present = this.hasAttribute(attributeName)
535
+ const shouldHave = typeof force === 'boolean' ? force : !present
536
+ if (shouldHave && !present) {
537
+ this.setAttribute(attributeName, '')
538
+ } else if (!shouldHave && present) {
539
+ this.removeAttribute(attributeName)
540
+ }
541
+ return shouldHave
542
+ }
543
+
544
+ /**
545
+ * The size of the element and its position, settable directly - there is no layout engine here to compute it.
546
+ * @returns {DOMRect}
547
+ */
548
+ getBoundingClientRect () {
549
+ return this.boundingClientRect
550
+ }
551
+
552
+ /**
553
+ * The bounding rectangles for each line of text in the element, settable directly - there is no layout engine
554
+ * here to compute it.
555
+ * @returns {Array<DOMRect>}
556
+ */
557
+ getClientRects () {
558
+ return this.clientRects
559
+ }
560
+
561
+ /**
562
+ * The Animation objects currently active on the element, settable directly - there is no animation engine here.
563
+ * @returns {Array<*>}
564
+ */
565
+ getAnimations () {
566
+ return this.animations
567
+ }
568
+
569
+ /**
570
+ * Whether the element is expected to be visible, settable directly - there is no rendering here to check it.
571
+ * @returns {boolean}
572
+ */
573
+ checkVisibility () {
574
+ return this.isVisible
575
+ }
576
+
577
+ /**
578
+ * A read-only view of the element's own inline style declarations (there is no CSS cascade here, so this is not a
579
+ * real computed style - just what the element's own style object holds).
580
+ * @returns {{get: function(string): (string|undefined)}}
581
+ */
582
+ computedStyleMap () {
583
+ const style = this.style
584
+ return {
585
+ get: property => {
586
+ const value = style ? style.getPropertyValue(property) : ''
587
+ return value === '' ? undefined : value
588
+ }
589
+ }
590
+ }
591
+
592
+ /**
593
+ * Whether this element currently has capture of the given pointer.
594
+ * @param {number} pointerId
595
+ * @returns {boolean}
596
+ */
597
+ hasPointerCapture (pointerId) {
598
+ return this.capturedPointers.has(pointerId)
599
+ }
600
+
601
+ /**
602
+ * Give this element capture of the given pointer.
603
+ * @param {number} pointerId
604
+ * @returns {undefined}
605
+ */
606
+ setPointerCapture (pointerId) {
607
+ this.capturedPointers.add(pointerId)
608
+ }
609
+
610
+ /**
611
+ * Release this element's capture of the given pointer, if it had it.
612
+ * @param {number} pointerId
613
+ * @returns {undefined}
614
+ */
615
+ releasePointerCapture (pointerId) {
616
+ this.capturedPointers.delete(pointerId)
617
+ }
618
+
619
+ /**
620
+ * Scroll to the given position (or, given an options object, the position(s) it has). There is no real scrollable
621
+ * viewport here: this just sets scrollLeft / scrollTop.
622
+ * @param {number|{left: number, top: number}} [x=0]
623
+ * @param {number} [y=0]
624
+ * @returns {undefined}
625
+ */
626
+ scroll (x = 0, y = 0) {
627
+ if (typeof x === 'number') {
628
+ this.scrollLeft = x
629
+ this.scrollTop = y
630
+ return
631
+ }
632
+ if (typeof x.left === 'number') {
633
+ this.scrollLeft = x.left
634
+ }
635
+ if (typeof x.top === 'number') {
636
+ this.scrollTop = x.top
637
+ }
638
+ }
639
+
640
+ /**
641
+ * Scroll to the given position. An alias for scroll.
642
+ * @param {number|{left: number, top: number}} [x=0]
643
+ * @param {number} [y=0]
644
+ * @returns {undefined}
645
+ */
646
+ scrollTo (x = 0, y = 0) {
647
+ this.scroll(x, y)
648
+ }
649
+
650
+ /**
651
+ * Scroll by the given amount, relative to the current position.
652
+ * @param {number|{left: number, top: number}} [x=0]
653
+ * @param {number} [y=0]
654
+ * @returns {undefined}
655
+ */
656
+ scrollBy (x = 0, y = 0) {
657
+ if (typeof x === 'number') {
658
+ this.scroll(this.scrollLeft + x, this.scrollTop + y)
659
+ return
660
+ }
661
+ this.scroll({
662
+ left: this.scrollLeft + (x.left || 0),
663
+ top: this.scrollTop + (x.top || 0)
664
+ })
665
+ }
666
+
667
+ /**
668
+ * Scroll an ancestor until this element is in view. There is no real viewport here for that to mean anything, so
669
+ * this does nothing (override it on an instance in a test which needs to observe the call).
670
+ * @returns {undefined}
671
+ */
672
+ scrollIntoView () {}
673
+ /**
674
+ * Asynchronously ask for the element to be shown fullscreen. There is no real fullscreen here, so this just
675
+ * resolves, like a browser granting the request would.
676
+ * @returns {Promise<void>}
677
+ */
678
+ requestFullscreen () {
679
+ return Promise.resolve()
680
+ }
681
+
682
+ /**
683
+ * Asynchronously ask for the pointer to be locked to this element. There is no real pointer lock here, so this
684
+ * just resolves, like a browser granting the request would.
685
+ * @returns {Promise<void>}
686
+ */
687
+ requestPointerLock () {
688
+ return Promise.resolve()
689
+ }
690
+
691
+ /**
692
+ * Attach a shadow tree to this element and return its ShadowRoot. Throws when it already hosts one.
693
+ * @param {{mode: string}} options
694
+ * @returns {PseudoShadowRoot}
695
+ * @throws {Error}
696
+ */
697
+ attachShadow (options) {
698
+ if (this.shadowRootInstance) {
699
+ throw new Error('Shadow root cannot be created on a host which already hosts a shadow tree.')
700
+ }
701
+ const root = new ShadowRootService_1.ShadowRootService()
702
+ root.host = this
703
+ root.mode = options.mode
704
+ this.shadowRootInstance = root
705
+ return root
706
+ }
707
+
708
+ /**
709
+ * This element's shadow root, when it has one attached in 'open' mode, or null (including when the mode is
710
+ * 'closed' - it still exists, but is not reachable this way, like the DOM's).
711
+ * @returns {PseudoShadowRoot|null}
712
+ */
713
+ get shadowRoot () {
714
+ return this.shadowRootInstance && this.shadowRootInstance.mode === 'open' ? this.shadowRootInstance : null
715
+ }
716
+ }
717
+ exports.ElementService = ElementService