Skip to content
Permalink
Newer
Older
100644 546 lines (448 sloc) 14.9 KB
May 14, 2018
2
const { addNwsapi } = require("../helpers/selectors");
3
const { HTML_NS } = require("../helpers/namespaces");
4
const { mixin, memoizeQuery } = require("../../utils");
5
const idlUtils = require("../generated/utils");
6
const NodeImpl = require("./Node-impl").implementation;
7
const ParentNodeImpl = require("./ParentNode-impl").implementation;
8
const ChildNodeImpl = require("./ChildNode-impl").implementation;
9
const attributes = require("../attributes");
10
const namedPropertiesWindow = require("../named-properties-window");
11
const NODE_TYPE = require("../node-type");
12
const { parseFragment } = require("../../browser/parser");
13
const { fragmentSerialization } = require("../domparsing/serialization");
14
const { domSymbolTree } = require("../helpers/internal-constants");
15
const DOMException = require("domexception");
16
const DOMTokenList = require("../generated/DOMTokenList");
17
const attrGenerated = require("../generated/Attr");
18
const NamedNodeMap = require("../generated/NamedNodeMap");
19
const validateNames = require("../helpers/validate-names");
20
const { asciiLowercase, asciiUppercase } = require("../helpers/strings");
21
const { listOfElementsWithQualifiedName, listOfElementsWithNamespaceAndLocalName,
22
listOfElementsWithClassNames } = require("../node");
23
const SlotableMixinImpl = require("./Slotable-impl").implementation;
24
const NonDocumentTypeChildNode = require("./NonDocumentTypeChildNode-impl").implementation;
25
const ShadowRoot = require("../generated/ShadowRoot");
26
const Text = require("../generated/Text");
27
const { isValidHostElementName } = require("../helpers/shadow-dom");
28
const { isValidCustomElementName } = require("../helpers/custom-elements");
30
function attachId(id, elm, doc) {
31
if (id && elm && doc) {
32
if (!doc._ids[id]) {
33
doc._ids[id] = [];
34
}
35
doc._ids[id].push(elm);
36
}
37
}
38
39
function detachId(id, elm, doc) {
40
if (id && elm && doc) {
41
if (doc._ids && doc._ids[id]) {
42
const elms = doc._ids[id];
43
for (let i = 0; i < elms.length; i++) {
44
if (elms[i] === elm) {
45
elms.splice(i, 1);
46
--i;
47
}
48
}
49
if (elms.length === 0) {
50
delete doc._ids[id];
51
}
52
}
53
}
54
}
55
56
class ElementImpl extends NodeImpl {
57
constructor(args, privateData) {
58
super(args, privateData);
59
60
this._initSlotableMixin();
61
62
this.nodeType = NODE_TYPE.ELEMENT_NODE;
63
this.scrollTop = 0;
64
this.scrollLeft = 0;
65
66
this._namespaceURI = privateData.namespace || null;
67
this._prefix = null;
68
this._localName = privateData.localName;
70
this._shadowRoot = null;
71
72
this._attributeList = [];
73
// Used for caching.
74
this._attributesByNameMap = new Map();
75
this._attributes = NamedNodeMap.createImpl([], {
76
element: this
77
});
80
_attach() {
81
namedPropertiesWindow.nodeAttachedToDocument(this);
82
83
const id = this.getAttributeNS(null, "id");
84
if (id) {
85
attachId(id, this, this._ownerDocument);
86
}
87
88
super._attach();
89
}
90
91
_detach() {
92
super._detach();
93
94
namedPropertiesWindow.nodeDetachedFromDocument(this);
95
96
const id = this.getAttributeNS(null, "id");
97
if (id) {
98
detachId(id, this, this._ownerDocument);
99
}
100
}
101
102
_attrModified(name, value, oldValue) {
103
this._modified();
104
namedPropertiesWindow.elementAttributeModified(this, name, value, oldValue);
105
106
if (name === "id" && this._attached) {
107
const doc = this._ownerDocument;
108
detachId(oldValue, this, doc);
109
attachId(value, this, doc);
110
}
111
112
// update classList
113
if (name === "class" && this._classList !== undefined) {
114
this._classList.attrModified();
116
117
this._attrModifiedSlotableMixin(name, value, oldValue);
120
get namespaceURI() {
121
return this._namespaceURI;
122
}
123
get prefix() {
124
return this._prefix;
125
}
126
get localName() {
127
return this._localName;
128
}
129
get _qualifiedName() {
130
return this._prefix !== null ? this._prefix + ":" + this._localName : this._localName;
131
}
132
get tagName() {
133
let qualifiedName = this._qualifiedName;
134
if (this.namespaceURI === HTML_NS && this._ownerDocument._parsingMode === "html") {
135
qualifiedName = asciiUppercase(qualifiedName);
136
}
137
return qualifiedName;
138
}
139
140
get attributes() {
141
return this._attributes;
142
}
143
144
// https://w3c.github.io/DOM-Parsing/#dom-element-outerhtml
145
get outerHTML() {
146
// TODO: maybe parse5 can give us a hook where it serializes the node itself too:
147
// https://github.com/inikulin/parse5/issues/230
148
// Alternatively, if we can create a virtual node in domSymbolTree, that'd also work.
149
// It's currently prevented by the fact that a node can't be duplicated in the same tree.
150
// Then we could get rid of all the code for childNodesForSerializing.
151
return fragmentSerialization({ childNodesForSerializing: [this], _ownerDocument: this._ownerDocument }, {
152
requireWellFormed: true
153
});
155
set outerHTML(markup) {
156
let parent = domSymbolTree.parent(this);
157
const document = this._ownerDocument;
158
159
if (!parent) {
160
return;
161
}
162
163
if (parent.nodeType === NODE_TYPE.DOCUMENT_NODE) {
164
throw new DOMException("Modifications are not allowed for this document", "NoModificationAllowedError");
167
if (parent.nodeType === NODE_TYPE.DOCUMENT_FRAGMENT_NODE) {
168
parent = document.createElementNS(HTML_NS, "body");
171
const fragment = parseFragment(markup, parent);
172
173
const contextObjectParent = domSymbolTree.parent(this);
174
contextObjectParent._replace(fragment, this);
177
// https://w3c.github.io/DOM-Parsing/#dfn-innerhtml
178
get innerHTML() {
179
return fragmentSerialization(this, { requireWellFormed: true });
181
set innerHTML(markup) {
182
const fragment = parseFragment(markup, this);
184
let contextObject = this;
185
if (this.localName === "template" && this.namespaceURI === HTML_NS) {
186
contextObject = contextObject._templateContents;
189
contextObject._replaceAll(fragment);
190
}
191
192
get classList() {
193
if (this._classList === undefined) {
194
this._classList = DOMTokenList.createImpl([], {
195
element: this,
196
attributeLocalName: "class"
197
});
198
}
199
return this._classList;
200
}
201
202
hasAttributes() {
203
return attributes.hasAttributes(this);
204
}
205
206
getAttributeNames() {
207
return attributes.attributeNames(this);
208
}
209
210
getAttribute(name) {
211
const attr = attributes.getAttributeByName(this, name);
212
if (!attr) {
213
return null;
214
}
215
return attr._value;
216
}
217
218
getAttributeNS(namespace, localName) {
219
const attr = attributes.getAttributeByNameNS(this, namespace, localName);
220
if (!attr) {
221
return null;
222
}
223
return attr._value;
224
}
225
226
setAttribute(name, value) {
227
validateNames.name(name);
228
229
if (this._namespaceURI === HTML_NS && this._ownerDocument._parsingMode === "html") {
230
name = asciiLowercase(name);
231
}
232
233
const attribute = attributes.getAttributeByName(this, name);
234
235
if (attribute === null) {
236
const newAttr = attrGenerated.createImpl([], { localName: name, value });
237
attributes.appendAttribute(this, newAttr);
238
return;
239
}
240
241
attributes.changeAttribute(this, attribute, value);
242
}
243
244
setAttributeNS(namespace, name, value) {
245
const extracted = validateNames.validateAndExtract(namespace, name);
246
247
// Because of widespread use of this method internally, e.g. to manually implement attribute/content reflection, we
248
// centralize the conversion to a string here, so that all call sites don't have to do it.
249
value = `${value}`;
250
251
attributes.setAttributeValue(this, extracted.localName, value, extracted.prefix, extracted.namespace);
252
}
253
254
removeAttribute(name) {
255
attributes.removeAttributeByName(this, name);
256
}
257
258
removeAttributeNS(namespace, localName) {
259
attributes.removeAttributeByNameNS(this, namespace, localName);
260
}
261
262
toggleAttribute(qualifiedName, force) {
263
validateNames.name(qualifiedName);
264
265
if (this._namespaceURI === HTML_NS && this._ownerDocument._parsingMode === "html") {
266
qualifiedName = asciiLowercase(qualifiedName);
267
}
268
269
const attribute = attributes.getAttributeByName(this, qualifiedName);
270
271
if (attribute === null) {
272
if (force === undefined || force === true) {
273
const newAttr = attrGenerated.createImpl([], { localName: qualifiedName, value: "" });
274
attributes.appendAttribute(this, newAttr);
275
return true;
276
}
277
return false;
278
}
279
280
if (force === undefined || force === false) {
281
attributes.removeAttributeByName(this, qualifiedName);
282
return false;
283
}
284
285
return true;
286
}
287
288
hasAttribute(name) {
289
if (this._namespaceURI === HTML_NS && this._ownerDocument._parsingMode === "html") {
290
name = asciiLowercase(name);
291
}
292
293
return attributes.hasAttributeByName(this, name);
294
}
295
296
hasAttributeNS(namespace, localName) {
297
if (namespace === "") {
298
namespace = null;
299
}
300
301
return attributes.hasAttributeByNameNS(this, namespace, localName);
302
}
303
304
getAttributeNode(name) {
305
return attributes.getAttributeByName(this, name);
306
}
307
308
getAttributeNodeNS(namespace, localName) {
309
return attributes.getAttributeByNameNS(this, namespace, localName);
310
}
311
312
setAttributeNode(attr) {
313
// eslint-disable-next-line no-restricted-properties
314
return attributes.setAttribute(this, attr);
315
}
316
317
setAttributeNodeNS(attr) {
318
// eslint-disable-next-line no-restricted-properties
319
return attributes.setAttribute(this, attr);
320
}
321
322
removeAttributeNode(attr) {
323
// eslint-disable-next-line no-restricted-properties
324
if (!attributes.hasAttribute(this, attr)) {
325
throw new DOMException("Tried to remove an attribute that was not present", "NotFoundError");
328
// eslint-disable-next-line no-restricted-properties
329
attributes.removeAttribute(this, attr);
330
331
return attr;
332
}
333
334
getBoundingClientRect() {
335
return {
336
bottom: 0,
337
height: 0,
338
left: 0,
339
right: 0,
340
top: 0,
341
width: 0
342
};
343
}
344
345
getClientRects() {
346
return [];
347
}
348
349
get scrollWidth() {
350
return 0;
351
}
352
353
get scrollHeight() {
354
return 0;
355
}
356
357
get clientTop() {
358
return 0;
359
}
360
361
get clientLeft() {
362
return 0;
363
}
364
365
get clientWidth() {
366
return 0;
367
}
368
369
get clientHeight() {
370
return 0;
373
// https://dom.spec.whatwg.org/#dom-element-attachshadow
374
attachShadow(init) {
375
if (this.namespaceURI !== HTML_NS) {
376
throw new DOMException(
377
"This element does not support attachShadow. This element is not part of the HTML namespace.",
378
"NotSupportedError"
379
);
380
}
381
382
if (!isValidHostElementName(this.localName) && !isValidCustomElementName(this.localName)) {
383
const message = "This element does not support attachShadow. This element is not a custom element nor " +
384
"a standard element supporting a shadow root.";
385
throw new DOMException(message, "NotSupportedError");
386
}
387
388
if (this._shadowRoot !== null) {
389
throw new DOMException(
390
"Shadow root cannot be created on a host which already hosts a shadow tree.",
391
"InvalidStateError"
392
);
393
}
394
395
const shadow = ShadowRoot.createImpl([], {
396
ownerDocument: this.ownerDocument,
397
mode: init.mode,
398
host: this
399
});
400
401
this._shadowRoot = shadow;
402
403
return shadow;
404
}
405
406
// https://dom.spec.whatwg.org/#dom-element-shadowroot
407
get shadowRoot() {
408
const shadow = this._shadowRoot;
409
410
if (shadow === null || shadow.mode === "closed") {
411
return null;
412
}
413
414
return shadow;
415
}
416
417
// https://dom.spec.whatwg.org/#insert-adjacent
418
_insertAdjacent(element, where, node) {
419
where = asciiLowercase(where);
420
421
if (where === "beforebegin") {
422
if (element.parentNode === null) {
423
return null;
424
}
425
return element.parentNode._preInsert(node, element);
426
}
427
if (where === "afterbegin") {
428
return element._preInsert(node, element.firstChild);
429
}
430
if (where === "beforeend") {
431
return element._preInsert(node, null);
432
}
433
if (where === "afterend") {
434
if (element.parentNode === null) {
435
return null;
436
}
437
return element.parentNode._preInsert(node, element.nextSibling);
438
}
439
440
throw new DOMException('Must provide one of "beforebegin", "afterbegin", ' +
441
'"beforeend", or "afterend".', "SyntaxError");
442
}
443
444
insertAdjacentElement(where, element) {
445
return this._insertAdjacent(this, where, element);
446
}
447
448
insertAdjacentText(where, data) {
449
const text = Text.createImpl([], { data, ownerDocument: this._ownerDocument });
450
451
this._insertAdjacent(this, where, text);
452
}
453
454
// https://w3c.github.io/DOM-Parsing/#dom-element-insertadjacenthtml
455
insertAdjacentHTML(position, text) {
456
position = asciiLowercase(position);
458
let context;
459
switch (position) {
460
case "beforebegin":
461
case "afterend": {
462
context = this.parentNode;
463
if (context === null || context.nodeType === NODE_TYPE.DOCUMENT_NODE) {
464
throw new DOMException("Cannot insert HTML adjacent to " +
465
"parent-less nodes or children of document nodes.", "NoModificationAllowedError");
466
}
467
break;
468
}
469
case "afterbegin":
470
case "beforeend": {
471
context = this;
472
break;
473
}
474
default: {
475
throw new DOMException('Must provide one of "beforebegin", "afterbegin", ' +
476
'"beforeend", or "afterend".', "SyntaxError");
480
if (
481
context.nodeType !== NODE_TYPE.ELEMENT_NODE ||
482
(
483
context._ownerDocument._parsingMode === "html" &&
484
context._localName === "html" &&
485
context._namespaceURI === HTML_NS
486
)
487
) {
488
context = context._ownerDocument.createElement("body");
489
}
490
491
const fragment = parseFragment(text, context);
492
493
switch (position) {
495
this.parentNode._insert(fragment, this);
497
}
498
case "afterbegin": {
499
this._insert(fragment, this.firstChild);
501
}
502
case "beforeend": {
503
this._append(fragment);
505
}
506
case "afterend": {
507
this.parentNode._insert(fragment, this.nextSibling);
512
513
closest(selectors) {
514
const matcher = addNwsapi(this);
515
return matcher.closest(selectors, idlUtils.wrapperForImpl(this));
516
}
519
mixin(ElementImpl.prototype, NonDocumentTypeChildNode.prototype);
520
mixin(ElementImpl.prototype, ParentNodeImpl.prototype);
521
mixin(ElementImpl.prototype, ChildNodeImpl.prototype);
522
mixin(ElementImpl.prototype, SlotableMixinImpl.prototype);
524
ElementImpl.prototype.getElementsByTagName = memoizeQuery(function (qualifiedName) {
525
return listOfElementsWithQualifiedName(qualifiedName, this);
528
ElementImpl.prototype.getElementsByTagNameNS = memoizeQuery(function (namespace, localName) {
529
return listOfElementsWithNamespaceAndLocalName(namespace, localName, this);
532
ElementImpl.prototype.getElementsByClassName = memoizeQuery(function (classNames) {
533
return listOfElementsWithClassNames(classNames, this);
534
});
535
536
ElementImpl.prototype.matches = function (selectors) {
May 14, 2018
537
const matcher = addNwsapi(this);
May 14, 2018
539
return matcher.match(selectors, idlUtils.wrapperForImpl(this));
541
542
ElementImpl.prototype.webkitMatchesSelector = ElementImpl.prototype.matches;
543
544
module.exports = {
545
implementation: ElementImpl
546
};