ref: 5d0deebfe6f6b3a12358cea6ac40dbf1f96dbd0f
dir: /domino/domino-lib/Element.js/
"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; }; }