urltomarkdown/node_modules/domino/lib/Element.js

1203 lines
38 KiB
JavaScript
Executable File

"use strict";
module.exports = Element;
var xml = require('./xmlnames');
var utils = require('./utils');
var NAMESPACE = utils.NAMESPACE;
var attributes = require('./attributes');
var Node = require('./Node');
var NodeList = require('./NodeList');
var NodeUtils = require('./NodeUtils');
var FilteredElementList = require('./FilteredElementList');
var DOMException = require('./DOMException');
var DOMTokenList = require('./DOMTokenList');
var select = require('./select');
var ContainerNode = require('./ContainerNode');
var ChildNode = require('./ChildNode');
var NonDocumentTypeChildNode = require('./NonDocumentTypeChildNode');
var NamedNodeMap = require('./NamedNodeMap');
var uppercaseCache = Object.create(null);
function Element(doc, localName, namespaceURI, prefix) {
ContainerNode.call(this);
this.nodeType = Node.ELEMENT_NODE;
this.ownerDocument = doc;
this.localName = localName;
this.namespaceURI = namespaceURI;
this.prefix = prefix;
this._tagName = undefined;
// These properties maintain the set of attributes
this._attrsByQName = Object.create(null); // The qname->Attr map
this._attrsByLName = Object.create(null); // The ns|lname->Attr map
this._attrKeys = []; // attr index -> ns|lname
}
function recursiveGetText(node, a) {
if (node.nodeType === Node.TEXT_NODE) {
a.push(node._data);
}
else {
for(var i = 0, n = node.childNodes.length; i < n; i++)
recursiveGetText(node.childNodes[i], a);
}
}
Element.prototype = Object.create(ContainerNode.prototype, {
isHTML: { get: function isHTML() {
return this.namespaceURI === NAMESPACE.HTML && this.ownerDocument.isHTML;
}},
tagName: { get: function tagName() {
if (this._tagName === undefined) {
var tn;
if (this.prefix === null) {
tn = this.localName;
} else {
tn = this.prefix + ':' + this.localName;
}
if (this.isHTML) {
var up = uppercaseCache[tn];
if (!up) {
// Converting to uppercase can be slow, so cache the conversion.
uppercaseCache[tn] = up = utils.toASCIIUpperCase(tn);
}
tn = up;
}
this._tagName = tn;
}
return this._tagName;
}},
nodeName: { get: function() { return this.tagName; }},
nodeValue: {
get: function() {
return null;
},
set: function() {}
},
textContent: {
get: function() {
var strings = [];
recursiveGetText(this, strings);
return strings.join('');
},
set: function(newtext) {
this.removeChildren();
if (newtext !== null && newtext !== undefined && newtext !== '') {
this._appendChild(this.ownerDocument.createTextNode(newtext));
}
}
},
innerHTML: {
get: function() {
return this.serialize();
},
set: utils.nyi
},
outerHTML: {
get: function() {
// "the attribute must return the result of running the HTML fragment
// serialization algorithm on a fictional node whose only child is
// the context object"
//
// The serialization logic is intentionally implemented in a separate
// `NodeUtils` helper instead of the more obvious choice of a private
// `_serializeOne()` method on the `Node.prototype` in order to avoid
// the megamorphic `this._serializeOne` property access, which reduces
// performance unnecessarily. If you need specialized behavior for a
// certain subclass, you'll need to implement that in `NodeUtils`.
// See https://github.com/fgnass/domino/pull/142 for more information.
return NodeUtils.serializeOne(this, { nodeType: 0 });
},
set: function(v) {
var document = this.ownerDocument;
var parent = this.parentNode;
if (parent === null) { return; }
if (parent.nodeType === Node.DOCUMENT_NODE) {
utils.NoModificationAllowedError();
}
if (parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
parent = parent.ownerDocument.createElement("body");
}
var parser = document.implementation.mozHTMLParser(
document._address,
parent
);
parser.parse(v===null?'':String(v), true);
this.replaceWith(parser._asDocumentFragment());
},
},
_insertAdjacent: { value: function _insertAdjacent(position, node) {
var first = false;
switch(position) {
case 'beforebegin':
first = true;
/* falls through */
case 'afterend':
var parent = this.parentNode;
if (parent === null) { return null; }
return parent.insertBefore(node, first ? this : this.nextSibling);
case 'afterbegin':
first = true;
/* falls through */
case 'beforeend':
return this.insertBefore(node, first ? this.firstChild : null);
default:
return utils.SyntaxError();
}
}},
insertAdjacentElement: { value: function insertAdjacentElement(position, element) {
if (element.nodeType !== Node.ELEMENT_NODE) {
throw new TypeError('not an element');
}
position = utils.toASCIILowerCase(String(position));
return this._insertAdjacent(position, element);
}},
insertAdjacentText: { value: function insertAdjacentText(position, data) {
var textNode = this.ownerDocument.createTextNode(data);
position = utils.toASCIILowerCase(String(position));
this._insertAdjacent(position, textNode);
// "This method returns nothing because it existed before we had a chance
// to design it."
}},
insertAdjacentHTML: { value: function insertAdjacentHTML(position, text) {
position = utils.toASCIILowerCase(String(position));
text = String(text);
var context;
switch(position) {
case 'beforebegin':
case 'afterend':
context = this.parentNode;
if (context === null || context.nodeType === Node.DOCUMENT_NODE) {
utils.NoModificationAllowedError();
}
break;
case 'afterbegin':
case 'beforeend':
context = this;
break;
default:
utils.SyntaxError();
}
if ( (!(context instanceof Element)) || (
context.ownerDocument.isHTML &&
context.localName === 'html' &&
context.namespaceURI === NAMESPACE.HTML
) ) {
context = context.ownerDocument.createElementNS(NAMESPACE.HTML, 'body');
}
var parser = this.ownerDocument.implementation.mozHTMLParser(
this.ownerDocument._address, context
);
parser.parse(text, true);
this._insertAdjacent(position, parser._asDocumentFragment());
}},
children: { get: function() {
if (!this._children) {
this._children = new ChildrenCollection(this);
}
return this._children;
}},
attributes: { get: function() {
if (!this._attributes) {
this._attributes = new AttributesArray(this);
}
return this._attributes;
}},
firstElementChild: { get: function() {
for (var kid = this.firstChild; kid !== null; kid = kid.nextSibling) {
if (kid.nodeType === Node.ELEMENT_NODE) return kid;
}
return null;
}},
lastElementChild: { get: function() {
for (var kid = this.lastChild; kid !== null; kid = kid.previousSibling) {
if (kid.nodeType === Node.ELEMENT_NODE) return kid;
}
return null;
}},
childElementCount: { get: function() {
return this.children.length;
}},
// Return the next element, in source order, after this one or
// null if there are no more. If root element is specified,
// then don't traverse beyond its subtree.
//
// This is not a DOM method, but is convenient for
// lazy traversals of the tree.
nextElement: { value: function(root) {
if (!root) root = this.ownerDocument.documentElement;
var next = this.firstElementChild;
if (!next) {
// don't use sibling if we're at root
if (this===root) return null;
next = this.nextElementSibling;
}
if (next) return next;
// If we can't go down or across, then we have to go up
// and across to the parent sibling or another ancestor's
// sibling. Be careful, though: if we reach the root
// element, or if we reach the documentElement, then
// the traversal ends.
for(var parent = this.parentElement;
parent && parent !== root;
parent = parent.parentElement) {
next = parent.nextElementSibling;
if (next) return next;
}
return null;
}},
// XXX:
// Tests are currently failing for this function.
// Awaiting resolution of:
// http://lists.w3.org/Archives/Public/www-dom/2011JulSep/0016.html
getElementsByTagName: { value: function getElementsByTagName(lname) {
var filter;
if (!lname) return new NodeList();
if (lname === '*')
filter = function() { return true; };
else if (this.isHTML)
filter = htmlLocalNameElementFilter(lname);
else
filter = localNameElementFilter(lname);
return new FilteredElementList(this, filter);
}},
getElementsByTagNameNS: { value: function getElementsByTagNameNS(ns, lname){
var filter;
if (ns === '*' && lname === '*')
filter = function() { return true; };
else if (ns === '*')
filter = localNameElementFilter(lname);
else if (lname === '*')
filter = namespaceElementFilter(ns);
else
filter = namespaceLocalNameElementFilter(ns, lname);
return new FilteredElementList(this, filter);
}},
getElementsByClassName: { value: function getElementsByClassName(names){
names = String(names).trim();
if (names === '') {
var result = new NodeList(); // Empty node list
return result;
}
names = names.split(/[ \t\r\n\f]+/); // Split on ASCII whitespace
return new FilteredElementList(this, classNamesElementFilter(names));
}},
getElementsByName: { value: function getElementsByName(name) {
return new FilteredElementList(this, elementNameFilter(String(name)));
}},
// Utility methods used by the public API methods above
clone: { value: function clone() {
var e;
// XXX:
// Modify this to use the constructor directly or
// avoid error checking in some other way. In case we try
// to clone an invalid node that the parser inserted.
//
if (this.namespaceURI !== NAMESPACE.HTML || this.prefix || !this.ownerDocument.isHTML) {
e = this.ownerDocument.createElementNS(
this.namespaceURI, (this.prefix !== null) ?
(this.prefix + ':' + this.localName) : this.localName
);
} else {
e = this.ownerDocument.createElement(this.localName);
}
for(var i = 0, n = this._attrKeys.length; i < n; i++) {
var lname = this._attrKeys[i];
var a = this._attrsByLName[lname];
var b = a.cloneNode();
b._setOwnerElement(e);
e._attrsByLName[lname] = b;
e._addQName(b);
}
e._attrKeys = this._attrKeys.concat();
return e;
}},
isEqual: { value: function isEqual(that) {
if (this.localName !== that.localName ||
this.namespaceURI !== that.namespaceURI ||
this.prefix !== that.prefix ||
this._numattrs !== that._numattrs)
return false;
// Compare the sets of attributes, ignoring order
// and ignoring attribute prefixes.
for(var i = 0, n = this._numattrs; i < n; i++) {
var a = this._attr(i);
if (!that.hasAttributeNS(a.namespaceURI, a.localName))
return false;
if (that.getAttributeNS(a.namespaceURI,a.localName) !== a.value)
return false;
}
return true;
}},
// This is the 'locate a namespace prefix' algorithm from the
// DOM specification. It is used by Node.lookupPrefix()
// (Be sure to compare DOM3 and DOM4 versions of spec.)
_lookupNamespacePrefix: { value: function _lookupNamespacePrefix(ns, originalElement) {
if (
this.namespaceURI &&
this.namespaceURI === ns &&
this.prefix !== null &&
originalElement.lookupNamespaceURI(this.prefix) === ns
) {
return this.prefix;
}
for(var i = 0, n = this._numattrs; i < n; i++) {
var a = this._attr(i);
if (
a.prefix === 'xmlns' &&
a.value === ns &&
originalElement.lookupNamespaceURI(a.localName) === ns
) {
return a.localName;
}
}
var parent = this.parentElement;
return parent ? parent._lookupNamespacePrefix(ns, originalElement) : null;
}},
// This is the 'locate a namespace' algorithm for Element nodes
// from the DOM Core spec. It is used by Node#lookupNamespaceURI()
lookupNamespaceURI: { value: function lookupNamespaceURI(prefix) {
if (prefix === '' || prefix === undefined) { prefix = null; }
if (this.namespaceURI !== null && this.prefix === prefix)
return this.namespaceURI;
for(var i = 0, n = this._numattrs; i < n; i++) {
var a = this._attr(i);
if (a.namespaceURI === NAMESPACE.XMLNS) {
if (
(a.prefix === 'xmlns' && a.localName === prefix) ||
(prefix === null && a.prefix === null && a.localName === 'xmlns')
) {
return a.value || null;
}
}
}
var parent = this.parentElement;
return parent ? parent.lookupNamespaceURI(prefix) : null;
}},
//
// Attribute handling methods and utilities
//
/*
* Attributes in the DOM are tricky:
*
* - there are the 8 basic get/set/has/removeAttribute{NS} methods
*
* - but many HTML attributes are also 'reflected' through IDL
* attributes which means that they can be queried and set through
* regular properties of the element. There is just one attribute
* value, but two ways to get and set it.
*
* - Different HTML element types have different sets of reflected
attributes.
*
* - attributes can also be queried and set through the .attributes
* property of an element. This property behaves like an array of
* Attr objects. The value property of each Attr is writeable, so
* this is a third way to read and write attributes.
*
* - for efficiency, we really want to store attributes in some kind
* of name->attr map. But the attributes[] array is an array, not a
* map, which is kind of unnatural.
*
* - When using namespaces and prefixes, and mixing the NS methods
* with the non-NS methods, it is apparently actually possible for
* an attributes[] array to have more than one attribute with the
* same qualified name. And certain methods must operate on only
* the first attribute with such a name. So for these methods, an
* inefficient array-like data structure would be easier to
* implement.
*
* - The attributes[] array is live, not a snapshot, so changes to the
* attributes must be immediately visible through existing arrays.
*
* - When attributes are queried and set through IDL properties
* (instead of the get/setAttributes() method or the attributes[]
* array) they may be subject to type conversions, URL
* normalization, etc., so some extra processing is required in that
* case.
*
* - But access through IDL properties is probably the most common
* case, so we'd like that to be as fast as possible.
*
* - We can't just store attribute values in their parsed idl form,
* because setAttribute() has to return whatever string is passed to
* getAttribute even if it is not a legal, parseable value. So
* attribute values must be stored in unparsed string form.
*
* - We need to be able to send change notifications or mutation
* events of some sort to the renderer whenever an attribute value
* changes, regardless of the way in which it changes.
*
* - Some attributes, such as id and class affect other parts of the
* DOM API, like getElementById and getElementsByClassName and so
* for efficiency, we need to specially track changes to these
* special attributes.
*
* - Some attributes like class have different names (className) when
* reflected.
*
* - Attributes whose names begin with the string 'data-' are treated
specially.
*
* - Reflected attributes that have a boolean type in IDL have special
* behavior: setting them to false (in IDL) is the same as removing
* them with removeAttribute()
*
* - numeric attributes (like HTMLElement.tabIndex) can have default
* values that must be returned by the idl getter even if the
* content attribute does not exist. (The default tabIndex value
* actually varies based on the type of the element, so that is a
* tricky one).
*
* See
* http://www.whatwg.org/specs/web-apps/current-work/multipage/urls.html#reflect
* for rules on how attributes are reflected.
*
*/
getAttribute: { value: function getAttribute(qname) {
var attr = this.getAttributeNode(qname);
return attr ? attr.value : null;
}},
getAttributeNS: { value: function getAttributeNS(ns, lname) {
var attr = this.getAttributeNodeNS(ns, lname);
return attr ? attr.value : null;
}},
getAttributeNode: { value: function getAttributeNode(qname) {
qname = String(qname);
if (/[A-Z]/.test(qname) && this.isHTML)
qname = utils.toASCIILowerCase(qname);
var attr = this._attrsByQName[qname];
if (!attr) return null;
if (Array.isArray(attr)) // If there is more than one
attr = attr[0]; // use the first
return attr;
}},
getAttributeNodeNS: { value: function getAttributeNodeNS(ns, lname) {
ns = (ns === undefined || ns === null) ? '' : String(ns);
lname = String(lname);
var attr = this._attrsByLName[ns + '|' + lname];
return attr ? attr : null;
}},
hasAttribute: { value: function hasAttribute(qname) {
qname = String(qname);
if (/[A-Z]/.test(qname) && this.isHTML)
qname = utils.toASCIILowerCase(qname);
return this._attrsByQName[qname] !== undefined;
}},
hasAttributeNS: { value: function hasAttributeNS(ns, lname) {
ns = (ns === undefined || ns === null) ? '' : String(ns);
lname = String(lname);
var key = ns + '|' + lname;
return this._attrsByLName[key] !== undefined;
}},
hasAttributes: { value: function hasAttributes() {
return this._numattrs > 0;
}},
toggleAttribute: { value: function toggleAttribute(qname, force) {
qname = String(qname);
if (!xml.isValidName(qname)) utils.InvalidCharacterError();
if (/[A-Z]/.test(qname) && this.isHTML)
qname = utils.toASCIILowerCase(qname);
var a = this._attrsByQName[qname];
if (a === undefined) {
if (force === undefined || force === true) {
this._setAttribute(qname, '');
return true;
}
return false;
} else {
if (force === undefined || force === false) {
this.removeAttribute(qname);
return false;
}
return true;
}
}},
// Set the attribute without error checking. The parser uses this.
_setAttribute: { value: function _setAttribute(qname, value) {
// XXX: the spec says that this next search should be done
// on the local name, but I think that is an error.
// email pending on www-dom about it.
var attr = this._attrsByQName[qname];
var isnew;
if (!attr) {
attr = this._newattr(qname);
isnew = true;
}
else {
if (Array.isArray(attr)) attr = attr[0];
}
// Now set the attribute value on the new or existing Attr object.
// The Attr.value setter method handles mutation events, etc.
attr.value = value;
if (this._attributes) this._attributes[qname] = attr;
if (isnew && this._newattrhook) this._newattrhook(qname, value);
}},
// Check for errors, and then set the attribute
setAttribute: { value: function setAttribute(qname, value) {
qname = String(qname);
if (!xml.isValidName(qname)) utils.InvalidCharacterError();
if (/[A-Z]/.test(qname) && this.isHTML)
qname = utils.toASCIILowerCase(qname);
this._setAttribute(qname, String(value));
}},
// The version with no error checking used by the parser
_setAttributeNS: { value: function _setAttributeNS(ns, qname, value) {
var pos = qname.indexOf(':'), prefix, lname;
if (pos < 0) {
prefix = null;
lname = qname;
}
else {
prefix = qname.substring(0, pos);
lname = qname.substring(pos+1);
}
if (ns === '' || ns === undefined) ns = null;
var key = (ns === null ? '' : ns) + '|' + lname;
var attr = this._attrsByLName[key];
var isnew;
if (!attr) {
attr = new Attr(this, lname, prefix, ns);
isnew = true;
this._attrsByLName[key] = attr;
if (this._attributes) {
this._attributes[this._attrKeys.length] = attr;
}
this._attrKeys.push(key);
// We also have to make the attr searchable by qname.
// But we have to be careful because there may already
// be an attr with this qname.
this._addQName(attr);
}
else if (false /* changed in DOM 4 */) {
// Calling setAttributeNS() can change the prefix of an
// existing attribute in DOM 2/3.
if (attr.prefix !== prefix) {
// Unbind the old qname
this._removeQName(attr);
// Update the prefix
attr.prefix = prefix;
// Bind the new qname
this._addQName(attr);
}
}
attr.value = value; // Automatically sends mutation event
if (isnew && this._newattrhook) this._newattrhook(qname, value);
}},
// Do error checking then call _setAttributeNS
setAttributeNS: { value: function setAttributeNS(ns, qname, value) {
// Convert parameter types according to WebIDL
ns = (ns === null || ns === undefined || ns === '') ? null : String(ns);
qname = String(qname);
if (!xml.isValidQName(qname)) utils.InvalidCharacterError();
var pos = qname.indexOf(':');
var prefix = (pos < 0) ? null : qname.substring(0, pos);
if ((prefix !== null && ns === null) ||
(prefix === 'xml' && ns !== NAMESPACE.XML) ||
((qname === 'xmlns' || prefix === 'xmlns') &&
(ns !== NAMESPACE.XMLNS)) ||
(ns === NAMESPACE.XMLNS &&
!(qname === 'xmlns' || prefix === 'xmlns')))
utils.NamespaceError();
this._setAttributeNS(ns, qname, String(value));
}},
setAttributeNode: { value: function setAttributeNode(attr) {
if (attr.ownerElement !== null && attr.ownerElement !== this) {
throw new DOMException(DOMException.INUSE_ATTRIBUTE_ERR);
}
var result = null;
var oldAttrs = this._attrsByQName[attr.name];
if (oldAttrs) {
if (!Array.isArray(oldAttrs)) { oldAttrs = [ oldAttrs ]; }
if (oldAttrs.some(function(a) { return a===attr; })) {
return attr;
} else if (attr.ownerElement !== null) {
throw new DOMException(DOMException.INUSE_ATTRIBUTE_ERR);
}
oldAttrs.forEach(function(a) { this.removeAttributeNode(a); }, this);
result = oldAttrs[0];
}
this.setAttributeNodeNS(attr);
return result;
}},
setAttributeNodeNS: { value: function setAttributeNodeNS(attr) {
if (attr.ownerElement !== null) {
throw new DOMException(DOMException.INUSE_ATTRIBUTE_ERR);
}
var ns = attr.namespaceURI;
var key = (ns === null ? '' : ns) + '|' + attr.localName;
var oldAttr = this._attrsByLName[key];
if (oldAttr) { this.removeAttributeNode(oldAttr); }
attr._setOwnerElement(this);
this._attrsByLName[key] = attr;
if (this._attributes) {
this._attributes[this._attrKeys.length] = attr;
}
this._attrKeys.push(key);
this._addQName(attr);
if (this._newattrhook) this._newattrhook(attr.name, attr.value);
return oldAttr || null;
}},
removeAttribute: { value: function removeAttribute(qname) {
qname = String(qname);
if (/[A-Z]/.test(qname) && this.isHTML)
qname = utils.toASCIILowerCase(qname);
var attr = this._attrsByQName[qname];
if (!attr) return;
// If there is more than one match for this qname
// so don't delete the qname mapping, just remove the first
// element from it.
if (Array.isArray(attr)) {
if (attr.length > 2) {
attr = attr.shift(); // remove it from the array
}
else {
this._attrsByQName[qname] = attr[1];
attr = attr[0];
}
}
else {
// only a single match, so remove the qname mapping
this._attrsByQName[qname] = undefined;
}
var ns = attr.namespaceURI;
// Now attr is the removed attribute. Figure out its
// ns+lname key and remove it from the other mapping as well.
var key = (ns === null ? '' : ns) + '|' + attr.localName;
this._attrsByLName[key] = undefined;
var i = this._attrKeys.indexOf(key);
if (this._attributes) {
Array.prototype.splice.call(this._attributes, i, 1);
this._attributes[qname] = undefined;
}
this._attrKeys.splice(i, 1);
// Onchange handler for the attribute
var onchange = attr.onchange;
attr._setOwnerElement(null);
if (onchange) {
onchange.call(attr, this, attr.localName, attr.value, null);
}
// Mutation event
if (this.rooted) this.ownerDocument.mutateRemoveAttr(attr);
}},
removeAttributeNS: { value: function removeAttributeNS(ns, lname) {
ns = (ns === undefined || ns === null) ? '' : String(ns);
lname = String(lname);
var key = ns + '|' + lname;
var attr = this._attrsByLName[key];
if (!attr) return;
this._attrsByLName[key] = undefined;
var i = this._attrKeys.indexOf(key);
if (this._attributes) {
Array.prototype.splice.call(this._attributes, i, 1);
}
this._attrKeys.splice(i, 1);
// Now find the same Attr object in the qname mapping and remove it
// But be careful because there may be more than one match.
this._removeQName(attr);
// Onchange handler for the attribute
var onchange = attr.onchange;
attr._setOwnerElement(null);
if (onchange) {
onchange.call(attr, this, attr.localName, attr.value, null);
}
// Mutation event
if (this.rooted) this.ownerDocument.mutateRemoveAttr(attr);
}},
removeAttributeNode: { value: function removeAttributeNode(attr) {
var ns = attr.namespaceURI;
var key = (ns === null ? '' : ns) + '|' + attr.localName;
if (this._attrsByLName[key] !== attr) {
utils.NotFoundError();
}
this.removeAttributeNS(ns, attr.localName);
return attr;
}},
getAttributeNames: { value: function getAttributeNames() {
var elt = this;
return this._attrKeys.map(function(key) {
return elt._attrsByLName[key].name;
});
}},
// This 'raw' version of getAttribute is used by the getter functions
// of reflected attributes. It skips some error checking and
// namespace steps
_getattr: { value: function _getattr(qname) {
// Assume that qname is already lowercased, so don't do it here.
// Also don't check whether attr is an array: a qname with no
// prefix will never have two matching Attr objects (because
// setAttributeNS doesn't allow a non-null namespace with a
// null prefix.
var attr = this._attrsByQName[qname];
return attr ? attr.value : null;
}},
// The raw version of setAttribute for reflected idl attributes.
_setattr: { value: function _setattr(qname, value) {
var attr = this._attrsByQName[qname];
var isnew;
if (!attr) {
attr = this._newattr(qname);
isnew = true;
}
attr.value = String(value);
if (this._attributes) this._attributes[qname] = attr;
if (isnew && this._newattrhook) this._newattrhook(qname, value);
}},
// Create a new Attr object, insert it, and return it.
// Used by setAttribute() and by set()
_newattr: { value: function _newattr(qname) {
var attr = new Attr(this, qname, null, null);
var key = '|' + qname;
this._attrsByQName[qname] = attr;
this._attrsByLName[key] = attr;
if (this._attributes) {
this._attributes[this._attrKeys.length] = attr;
}
this._attrKeys.push(key);
return attr;
}},
// Add a qname->Attr mapping to the _attrsByQName object, taking into
// account that there may be more than one attr object with the
// same qname
_addQName: { value: function(attr) {
var qname = attr.name;
var existing = this._attrsByQName[qname];
if (!existing) {
this._attrsByQName[qname] = attr;
}
else if (Array.isArray(existing)) {
existing.push(attr);
}
else {
this._attrsByQName[qname] = [existing, attr];
}
if (this._attributes) this._attributes[qname] = attr;
}},
// Remove a qname->Attr mapping to the _attrsByQName object, taking into
// account that there may be more than one attr object with the
// same qname
_removeQName: { value: function(attr) {
var qname = attr.name;
var target = this._attrsByQName[qname];
if (Array.isArray(target)) {
var idx = target.indexOf(attr);
utils.assert(idx !== -1); // It must be here somewhere
if (target.length === 2) {
this._attrsByQName[qname] = target[1-idx];
if (this._attributes) {
this._attributes[qname] = this._attrsByQName[qname];
}
} else {
target.splice(idx, 1);
if (this._attributes && this._attributes[qname] === attr) {
this._attributes[qname] = target[0];
}
}
}
else {
utils.assert(target === attr); // If only one, it must match
this._attrsByQName[qname] = undefined;
if (this._attributes) {
this._attributes[qname] = undefined;
}
}
}},
// Return the number of attributes
_numattrs: { get: function() { return this._attrKeys.length; }},
// Return the nth Attr object
_attr: { value: function(n) {
return this._attrsByLName[this._attrKeys[n]];
}},
// Define getters and setters for an 'id' property that reflects
// the content attribute 'id'.
id: attributes.property({name: 'id'}),
// Define getters and setters for a 'className' property that reflects
// the content attribute 'class'.
className: attributes.property({name: 'class'}),
classList: { get: function() {
var self = this;
if (this._classList) {
return this._classList;
}
var dtlist = new DOMTokenList(
function() {
return self.className || "";
},
function(v) {
self.className = v;
}
);
this._classList = dtlist;
return dtlist;
}, set: function(v) { this.className = v; }},
matches: { value: function(selector) {
return select.matches(this, selector);
}},
closest: { value: function(selector) {
var el = this;
do {
if (el.matches && el.matches(selector)) { return el; }
el = el.parentElement || el.parentNode;
} while (el !== null && el.nodeType === Node.ELEMENT_NODE);
return null;
}},
querySelector: { value: function(selector) {
return select(selector, this)[0];
}},
querySelectorAll: { value: function(selector) {
var nodes = select(selector, this);
return nodes.item ? nodes : new NodeList(nodes);
}}
});
Object.defineProperties(Element.prototype, ChildNode);
Object.defineProperties(Element.prototype, NonDocumentTypeChildNode);
// Register special handling for the id attribute
attributes.registerChangeHandler(Element, 'id',
function(element, lname, oldval, newval) {
if (element.rooted) {
if (oldval) {
element.ownerDocument.delId(oldval, element);
}
if (newval) {
element.ownerDocument.addId(newval, element);
}
}
}
);
attributes.registerChangeHandler(Element, 'class',
function(element, lname, oldval, newval) {
if (element._classList) { element._classList._update(); }
}
);
// The Attr class represents a single attribute. The values in
// _attrsByQName and _attrsByLName are instances of this class.
function Attr(elt, lname, prefix, namespace, value) {
// localName and namespace are constant for any attr object.
// But value may change. And so can prefix, and so, therefore can name.
this.localName = lname;
this.prefix = (prefix===null || prefix==='') ? null : ('' + prefix);
this.namespaceURI = (namespace===null || namespace==='') ? null : ('' + namespace);
this.data = value;
// Set ownerElement last to ensure it is hooked up to onchange handler
this._setOwnerElement(elt);
}
// In DOM 3 Attr was supposed to extend Node; in DOM 4 that was abandoned.
Attr.prototype = Object.create(Object.prototype, {
ownerElement: {
get: function() { return this._ownerElement; },
},
_setOwnerElement: { value: function _setOwnerElement(elt) {
this._ownerElement = elt;
if (this.prefix === null && this.namespaceURI === null && elt) {
this.onchange = elt._attributeChangeHandlers[this.localName];
} else {
this.onchange = null;
}
}},
name: { get: function() {
return this.prefix ? this.prefix + ':' + this.localName : this.localName;
}},
specified: { get: function() {
// Deprecated
return true;
}},
value: {
get: function() {
return this.data;
},
set: function(value) {
var oldval = this.data;
value = (value === undefined) ? '' : value + '';
if (value === oldval) return;
this.data = value;
// Run the onchange hook for the attribute
// if there is one.
if (this.ownerElement) {
if (this.onchange)
this.onchange(this.ownerElement,this.localName, oldval, value);
// Generate a mutation event if the element is rooted
if (this.ownerElement.rooted)
this.ownerElement.ownerDocument.mutateAttr(this, oldval);
}
},
},
cloneNode: { value: function cloneNode(deep) {
// Both this method and Document#createAttribute*() create unowned Attrs
return new Attr(
null, this.localName, this.prefix, this.namespaceURI, this.data
);
}},
// Legacy aliases (see gh#70 and https://dom.spec.whatwg.org/#interface-attr)
nodeType: { get: function() { return Node.ATTRIBUTE_NODE; } },
nodeName: { get: function() { return this.name; } },
nodeValue: {
get: function() { return this.value; },
set: function(v) { this.value = v; },
},
textContent: {
get: function() { return this.value; },
set: function(v) {
if (v === null || v === undefined) { v = ''; }
this.value = v;
},
},
});
// Sneakily export this class for use by Document.createAttribute()
Element._Attr = Attr;
// The attributes property of an Element will be an instance of this class.
// This class is really just a dummy, though. It only defines a length
// property and an item() method. The AttrArrayProxy that
// defines the public API just uses the Element object itself.
function AttributesArray(elt) {
NamedNodeMap.call(this, elt);
for (var name in elt._attrsByQName) {
this[name] = elt._attrsByQName[name];
}
for (var i = 0; i < elt._attrKeys.length; i++) {
this[i] = elt._attrsByLName[elt._attrKeys[i]];
}
}
AttributesArray.prototype = Object.create(NamedNodeMap.prototype, {
length: { get: function() {
return this.element._attrKeys.length;
}, set: function() { /* ignore */ } },
item: { value: function(n) {
/* jshint bitwise: false */
n = n >>> 0;
if (n >= this.length) { return null; }
return this.element._attrsByLName[this.element._attrKeys[n]];
/* jshint bitwise: true */
} },
});
// We can't make direct array access work (without Proxies, node >=6)
// but we can make `Array.from(node.attributes)` and for-of loops work.
if (global.Symbol && global.Symbol.iterator) {
AttributesArray.prototype[global.Symbol.iterator] = function() {
var i=0, n=this.length, self=this;
return {
next: function() {
if (i<n) return { value: self.item(i++) };
return { done: true };
}
};
};
}
// The children property of an Element will be an instance of this class.
// It defines length, item() and namedItem() and will be wrapped by an
// HTMLCollection when exposed through the DOM.
function ChildrenCollection(e) {
this.element = e;
this.updateCache();
}
ChildrenCollection.prototype = Object.create(Object.prototype, {
length: { get: function() {
this.updateCache();
return this.childrenByNumber.length;
} },
item: { value: function item(n) {
this.updateCache();
return this.childrenByNumber[n] || null;
} },
namedItem: { value: function namedItem(name) {
this.updateCache();
return this.childrenByName[name] || null;
} },
// This attribute returns the entire name->element map.
// It is not part of the HTMLCollection API, but we need it in
// src/HTMLCollectionProxy
namedItems: { get: function() {
this.updateCache();
return this.childrenByName;
} },
updateCache: { value: function updateCache() {
var namedElts = /^(a|applet|area|embed|form|frame|frameset|iframe|img|object)$/;
if (this.lastModTime !== this.element.lastModTime) {
this.lastModTime = this.element.lastModTime;
var n = this.childrenByNumber && this.childrenByNumber.length || 0;
for(var i = 0; i < n; i++) {
this[i] = undefined;
}
this.childrenByNumber = [];
this.childrenByName = Object.create(null);
for (var c = this.element.firstChild; c !== null; c = c.nextSibling) {
if (c.nodeType === Node.ELEMENT_NODE) {
this[this.childrenByNumber.length] = c;
this.childrenByNumber.push(c);
// XXX Are there any requirements about the namespace
// of the id property?
var id = c.getAttribute('id');
// If there is an id that is not already in use...
if (id && !this.childrenByName[id])
this.childrenByName[id] = c;
// For certain HTML elements we check the name attribute
var name = c.getAttribute('name');
if (name &&
this.element.namespaceURI === NAMESPACE.HTML &&
namedElts.test(this.element.localName) &&
!this.childrenByName[name])
this.childrenByName[id] = c;
}
}
}
} },
});
// These functions return predicates for filtering elements.
// They're used by the Document and Element classes for methods like
// getElementsByTagName and getElementsByClassName
function localNameElementFilter(lname) {
return function(e) { return e.localName === lname; };
}
function htmlLocalNameElementFilter(lname) {
var lclname = utils.toASCIILowerCase(lname);
if (lclname === lname)
return localNameElementFilter(lname);
return function(e) {
return e.isHTML ? e.localName === lclname : e.localName === lname;
};
}
function namespaceElementFilter(ns) {
return function(e) { return e.namespaceURI === ns; };
}
function namespaceLocalNameElementFilter(ns, lname) {
return function(e) {
return e.namespaceURI === ns && e.localName === lname;
};
}
function classNamesElementFilter(names) {
return function(e) {
return names.every(function(n) { return e.classList.contains(n); });
};
}
function elementNameFilter(name) {
return function(e) {
// All the *HTML elements* in the document with the given name attribute
if (e.namespaceURI !== NAMESPACE.HTML) { return false; }
return e.getAttribute('name') === name;
};
}