pseudo-dom 0.7.0 → 0.8.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/README.md +262 -236
- package/browser/pseudo-dom.js +628 -548
- package/browser/pseudo-dom.min.js +1 -1
- package/dist/classes/PseudoHTMLDocument.d.ts +41 -67
- package/dist/classes/PseudoHTMLDocument.js +74 -144
- package/dist/classes/PseudoHTMLDocument.min.js +1 -1
- package/dist/factories/cssSelectAdapter.d.ts +23 -23
- package/dist/factories/cssSelectAdapter.js +87 -87
- package/dist/factories/query.d.ts +33 -33
- package/dist/factories/query.js +66 -66
- package/dist/functions/getElementById.d.ts +13 -0
- package/dist/functions/getElementById.js +33 -0
- package/dist/functions/getElementById.min.js +1 -0
- package/dist/interfaces/PseudoDocument.d.ts +25 -0
- package/dist/interfaces/PseudoDocumentFragment.d.ts +10 -3
- package/dist/interfaces/PseudoElement.d.ts +702 -702
- package/dist/services/DocumentFragmentService.d.ts +21 -13
- package/dist/services/DocumentFragmentService.js +42 -24
- package/dist/services/DocumentFragmentService.min.js +1 -1
- package/dist/services/DocumentService.d.ts +28 -1
- package/dist/services/DocumentService.js +64 -15
- package/dist/services/DocumentService.min.js +1 -1
- package/dist/services/ElementService.d.ts +191 -191
- package/dist/services/ElementService.js +426 -426
- package/dist/services/HTMLCollectionService.d.ts +54 -54
- package/dist/services/HTMLCollectionService.js +92 -92
- package/dist/services/NodeService.d.ts +297 -297
- package/dist/services/NodeService.js +721 -721
- package/package.json +2 -2
- package/dist/classes.d.ts +0 -20
- package/dist/classes.js +0 -31
- package/dist/classes.min.js +0 -1
- package/dist/factories/generateNode.d.ts +0 -32
- package/dist/factories/generateNode.js +0 -85
- package/dist/factories/generateNode.min.js +0 -1
- package/dist/factories.d.ts +0 -11
- package/dist/factories.js +0 -27
- package/dist/factories.min.js +0 -1
- package/dist/functions.d.ts +0 -5
- package/dist/functions.js +0 -23
- package/dist/functions.min.js +0 -1
|
@@ -1,426 +1,426 @@
|
|
|
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
|
+
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
|