/** * The Node Utility provides a DOM-like interface for interacting with DOM nodes. * @module node * @main node * @submodule node-core */ /** * The Node class provides a wrapper for manipulating DOM Nodes. * Node properties can be accessed via the set/get methods. * Use `Y.one()` to retrieve Node instances. * * <strong>NOTE:</strong> Node properties are accessed using * the <code>set</code> and <code>get</code> methods. * * @class Node * @constructor * @param {HTMLElement} node the DOM node to be mapped to the Node instance. * @uses EventTarget */ // "globals" var DOT = '.', NODE_NAME = 'nodeName', NODE_TYPE = 'nodeType', OWNER_DOCUMENT = 'ownerDocument', TAG_NAME = 'tagName', UID = '_yuid', EMPTY_OBJ = {}, _slice = Array.prototype.slice, Y_DOM = Y.DOM, Y_Node = function(node) { if (!this.getDOMNode) { // support optional "new" return new Y_Node(node); } if (typeof node == 'string') { node = Y_Node._fromString(node); if (!node) { return null; // NOTE: return } } var uid = (node.nodeType !== 9) ? node.uniqueID : node[UID]; if (uid && Y_Node._instances[uid] && Y_Node._instances[uid]._node !== node) { node[UID] = null; // unset existing uid to prevent collision (via clone or hack) } uid = uid || Y.stamp(node); if (!uid) { // stamp failed; likely IE non-HTMLElement uid = Y.guid(); } this[UID] = uid; /** * The underlying DOM node bound to the Y.Node instance * @property _node * @type HTMLElement * @private */ this._node = node; this._stateProxy = node; // when augmented with Attribute if (this._initPlugins) { // when augmented with Plugin.Host this._initPlugins(); } }, // used with previous/next/ancestor tests _wrapFn = function(fn) { var ret = null; if (fn) { ret = (typeof fn == 'string') ? function(n) { return Y.Selector.test(n, fn); } : function(n) { return fn(Y.one(n)); }; } return ret; }; // end "globals" Y_Node.ATTRS = {}; Y_Node.DOM_EVENTS = {}; Y_Node._fromString = function(node) { if (node) { if (node.indexOf('doc') === 0) { // doc OR document node = Y.config.doc; } else if (node.indexOf('win') === 0) { // win OR window node = Y.config.win; } else { node = Y.Selector.query(node, null, true); } } return node || null; }; /** * The name of the component * @static * @type String * @property NAME */ Y_Node.NAME = 'node'; /* * The pattern used to identify ARIA attributes */ Y_Node.re_aria = /^(?:role$|aria-)/; Y_Node.SHOW_TRANSITION = 'fadeIn'; Y_Node.HIDE_TRANSITION = 'fadeOut'; /** * A list of Node instances that have been created * @private * @type Object * @property _instances * @static * */ Y_Node._instances = {}; /** * Retrieves the DOM node bound to a Node instance * @method getDOMNode * @static * * @param {Node|HTMLElement} node The Node instance or an HTMLElement * @return {HTMLElement} The DOM node bound to the Node instance. If a DOM node is passed * as the node argument, it is simply returned. */ Y_Node.getDOMNode = function(node) { if (node) { return (node.nodeType) ? node : node._node || null; } return null; }; /** * Checks Node return values and wraps DOM Nodes as Y.Node instances * and DOM Collections / Arrays as Y.NodeList instances. * Other return values just pass thru. If undefined is returned (e.g. no return) * then the Node instance is returned for chainability. * @method scrubVal * @static * * @param {HTMLElement|HTMLElement[]|Node} node The Node instance or an HTMLElement * @return {Node | NodeList | Any} Depends on what is returned from the DOM node. */ Y_Node.scrubVal = function(val, node) { if (val) { // only truthy values are risky if (typeof val == 'object' || typeof val == 'function') { // safari nodeList === function if (NODE_TYPE in val || Y_DOM.isWindow(val)) {// node || window val = Y.one(val); } else if ((val.item && !val._nodes) || // dom collection or Node instance (val[0] && val[0][NODE_TYPE])) { // array of DOM Nodes val = Y.all(val); } } } else if (typeof val === 'undefined') { val = node; // for chaining } else if (val === null) { val = null; // IE: DOM null not the same as null } return val; }; /** * Adds methods to the Y.Node prototype, routing through scrubVal. * @method addMethod * @static * * @param {String} name The name of the method to add * @param {Function} fn The function that becomes the method * @param {Object} context An optional context to call the method with * (defaults to the Node instance) * @return {any} Depends on what is returned from the DOM node. */ Y_Node.addMethod = function(name, fn, context) { if (name && fn && typeof fn == 'function') { Y_Node.prototype[name] = function() { var args = _slice.call(arguments), node = this, ret; if (args[0] && args[0]._node) { args[0] = args[0]._node; } if (args[1] && args[1]._node) { args[1] = args[1]._node; } args.unshift(node._node); ret = fn.apply(context || node, args); if (ret) { // scrub truthy ret = Y_Node.scrubVal(ret, node); } (typeof ret != 'undefined') || (ret = node); return ret; }; } else { Y.log('unable to add method: ' + name, 'warn', 'Node'); } }; /** * Imports utility methods to be added as Y.Node methods. * @method importMethod * @static * * @param {Object} host The object that contains the method to import. * @param {String} name The name of the method to import * @param {String} altName An optional name to use in place of the host name * @param {Object} context An optional context to call the method with */ Y_Node.importMethod = function(host, name, altName) { if (typeof name == 'string') { altName = altName || name; Y_Node.addMethod(altName, host[name], host); } else { Y.Array.each(name, function(n) { Y_Node.importMethod(host, n); }); } }; /** * Retrieves a NodeList based on the given CSS selector. * @method all * * @param {string} selector The CSS selector to test against. * @return {NodeList} A NodeList instance for the matching HTMLCollection/Array. * @for YUI */ /** * Returns a single Node instance bound to the node or the * first element matching the given selector. Returns null if no match found. * <strong>Note:</strong> For chaining purposes you may want to * use <code>Y.all</code>, which returns a NodeList when no match is found. * @method one * @param {String | HTMLElement} node a node or Selector * @return {Node | null} a Node instance or null if no match found. * @for YUI */ /** * Returns a single Node instance bound to the node or the * first element matching the given selector. Returns null if no match found. * <strong>Note:</strong> For chaining purposes you may want to * use <code>Y.all</code>, which returns a NodeList when no match is found. * @method one * @static * @param {String | HTMLElement} node a node or Selector * @return {Node | null} a Node instance or null if no match found. * @for Node */ Y_Node.one = function(node) { var instance = null, cachedNode, uid; if (node) { if (typeof node == 'string') { node = Y_Node._fromString(node); if (!node) { return null; // NOTE: return } } else if (node.getDOMNode) { return node; // NOTE: return } if (node.nodeType || Y.DOM.isWindow(node)) { // avoid bad input (numbers, boolean, etc) uid = (node.uniqueID && node.nodeType !== 9) ? node.uniqueID : node._yuid; instance = Y_Node._instances[uid]; // reuse exising instances cachedNode = instance ? instance._node : null; if (!instance || (cachedNode && node !== cachedNode)) { // new Node when nodes don't match instance = new Y_Node(node); if (node.nodeType != 11) { // dont cache document fragment Y_Node._instances[instance[UID]] = instance; // cache node } } } } return instance; }; /** * The default setter for DOM properties * Called with instance context (this === the Node instance) * @method DEFAULT_SETTER * @static * @param {String} name The attribute/property being set * @param {any} val The value to be set * @return {any} The value */ Y_Node.DEFAULT_SETTER = function(name, val) { var node = this._stateProxy, strPath; if (name.indexOf(DOT) > -1) { strPath = name; name = name.split(DOT); // only allow when defined on node Y.Object.setValue(node, name, val); } else if (typeof node[name] != 'undefined') { // pass thru DOM properties node[name] = val; } return val; }; /** * The default getter for DOM properties * Called with instance context (this === the Node instance) * @method DEFAULT_GETTER * @static * @param {String} name The attribute/property to look up * @return {any} The current value */ Y_Node.DEFAULT_GETTER = function(name) { var node = this._stateProxy, val; if (name.indexOf && name.indexOf(DOT) > -1) { val = Y.Object.getValue(node, name.split(DOT)); } else if (typeof node[name] != 'undefined') { // pass thru from DOM val = node[name]; } return val; }; Y.mix(Y_Node.prototype, { DATA_PREFIX: 'data-', /** * The method called when outputting Node instances as strings * @method toString * @return {String} A string representation of the Node instance */ toString: function() { var str = this[UID] + ': not bound to a node', node = this._node, attrs, id, className; if (node) { attrs = node.attributes; id = (attrs && attrs.id) ? node.getAttribute('id') : null; className = (attrs && attrs.className) ? node.getAttribute('className') : null; str = node[NODE_NAME]; if (id) { str += '#' + id; } if (className) { str += '.' + className.replace(' ', '.'); } // TODO: add yuid? str += ' ' + this[UID]; } return str; }, /** * Returns an attribute value on the Node instance. * Unless pre-configured (via `Node.ATTRS`), get hands * off to the underlying DOM node. Only valid * attributes/properties for the node will be queried. * @method get * @param {String} attr The attribute * @return {any} The current value of the attribute */ get: function(attr) { var val; if (this._getAttr) { // use Attribute imple val = this._getAttr(attr); } else { val = this._get(attr); } if (val) { val = Y_Node.scrubVal(val, this); } else if (val === null) { val = null; // IE: DOM null is not true null (even though they ===) } return val; }, /** * Helper method for get. * @method _get * @private * @param {String} attr The attribute * @return {any} The current value of the attribute */ _get: function(attr) { var attrConfig = Y_Node.ATTRS[attr], val; if (attrConfig && attrConfig.getter) { val = attrConfig.getter.call(this); } else if (Y_Node.re_aria.test(attr)) { val = this._node.getAttribute(attr, 2); } else { val = Y_Node.DEFAULT_GETTER.apply(this, arguments); } return val; }, /** * Sets an attribute on the Node instance. * Unless pre-configured (via Node.ATTRS), set hands * off to the underlying DOM node. Only valid * attributes/properties for the node will be set. * To set custom attributes use setAttribute. * @method set * @param {String} attr The attribute to be set. * @param {any} val The value to set the attribute to. * @chainable */ set: function(attr, val) { var attrConfig = Y_Node.ATTRS[attr]; if (this._setAttr) { // use Attribute imple this._setAttr.apply(this, arguments); } else { // use setters inline if (attrConfig && attrConfig.setter) { attrConfig.setter.call(this, val, attr); } else if (Y_Node.re_aria.test(attr)) { // special case Aria this._node.setAttribute(attr, val); } else { Y_Node.DEFAULT_SETTER.apply(this, arguments); } } return this; }, /** * Sets multiple attributes. * @method setAttrs * @param {Object} attrMap an object of name/value pairs to set * @chainable */ setAttrs: function(attrMap) { if (this._setAttrs) { // use Attribute imple this._setAttrs(attrMap); } else { // use setters inline Y.Object.each(attrMap, function(v, n) { this.set(n, v); }, this); } return this; }, /** * Returns an object containing the values for the requested attributes. * @method getAttrs * @param {Array} attrs an array of attributes to get values * @return {Object} An object with attribute name/value pairs. */ getAttrs: function(attrs) { var ret = {}; if (this._getAttrs) { // use Attribute imple this._getAttrs(attrs); } else { // use setters inline Y.Array.each(attrs, function(v, n) { ret[v] = this.get(v); }, this); } return ret; }, /** * Compares nodes to determine if they match. * Node instances can be compared to each other and/or HTMLElements. * @method compareTo * @param {HTMLElement | Node} refNode The reference node to compare to the node. * @return {Boolean} True if the nodes match, false if they do not. */ compareTo: function(refNode) { var node = this._node; if (refNode && refNode._node) { refNode = refNode._node; } return node === refNode; }, /** * Determines whether the node is appended to the document. * @method inDoc * @param {Node|HTMLElement} doc optional An optional document to check against. * Defaults to current document. * @return {Boolean} Whether or not this node is appended to the document. */ inDoc: function(doc) { var node = this._node; if (node) { doc = (doc) ? doc._node || doc : node[OWNER_DOCUMENT]; if (doc.documentElement) { return Y_DOM.contains(doc.documentElement, node); } } return false; }, getById: function(id) { var node = this._node, ret = Y_DOM.byId(id, node[OWNER_DOCUMENT]); if (ret && Y_DOM.contains(node, ret)) { ret = Y.one(ret); } else { ret = null; } return ret; }, /** * Returns the nearest ancestor that passes the test applied by supplied boolean method. * @method ancestor * @param {String | Function} fn A selector string or boolean method for testing elements. * If a function is used, it receives the current node being tested as the only argument. * If fn is not passed as an argument, the parent node will be returned. * @param {Boolean} testSelf optional Whether or not to include the element in the scan * @param {String | Function} stopFn optional A selector string or boolean * method to indicate when the search should stop. The search bails when the function * returns true or the selector matches. * If a function is used, it receives the current node being tested as the only argument. * @return {Node} The matching Node instance or null if not found */ ancestor: function(fn, testSelf, stopFn) { // testSelf is optional, check for stopFn as 2nd arg if (arguments.length === 2 && (typeof testSelf == 'string' || typeof testSelf == 'function')) { stopFn = testSelf; } return Y.one(Y_DOM.ancestor(this._node, _wrapFn(fn), testSelf, _wrapFn(stopFn))); }, /** * Returns the ancestors that pass the test applied by supplied boolean method. * @method ancestors * @param {String | Function} fn A selector string or boolean method for testing elements. * @param {Boolean} testSelf optional Whether or not to include the element in the scan * If a function is used, it receives the current node being tested as the only argument. * @return {NodeList} A NodeList instance containing the matching elements */ ancestors: function(fn, testSelf, stopFn) { if (arguments.length === 2 && (typeof testSelf == 'string' || typeof testSelf == 'function')) { stopFn = testSelf; } return Y.all(Y_DOM.ancestors(this._node, _wrapFn(fn), testSelf, _wrapFn(stopFn))); }, /** * Returns the previous matching sibling. * Returns the nearest element node sibling if no method provided. * @method previous * @param {String | Function} fn A selector or boolean method for testing elements. * If a function is used, it receives the current node being tested as the only argument. * @param {Boolean} [all] Whether text nodes as well as element nodes should be returned, or * just element nodes will be returned(default) * @return {Node} Node instance or null if not found */ previous: function(fn, all) { return Y.one(Y_DOM.elementByAxis(this._node, 'previousSibling', _wrapFn(fn), all)); }, /** * Returns the next matching sibling. * Returns the nearest element node sibling if no method provided. * @method next * @param {String | Function} fn A selector or boolean method for testing elements. * If a function is used, it receives the current node being tested as the only argument. * @param {Boolean} [all] Whether text nodes as well as element nodes should be returned, or * just element nodes will be returned(default) * @return {Node} Node instance or null if not found */ next: function(fn, all) { return Y.one(Y_DOM.elementByAxis(this._node, 'nextSibling', _wrapFn(fn), all)); }, /** * Returns all matching siblings. * Returns all siblings if no method provided. * @method siblings * @param {String | Function} fn A selector or boolean method for testing elements. * If a function is used, it receives the current node being tested as the only argument. * @return {NodeList} NodeList instance bound to found siblings */ siblings: function(fn) { return Y.all(Y_DOM.siblings(this._node, _wrapFn(fn))); }, /** * Retrieves a single Node instance, the first element matching the given * CSS selector. * Returns null if no match found. * @method one * * @param {string} selector The CSS selector to test against. * @return {Node | null} A Node instance for the matching HTMLElement or null * if no match found. */ one: function(selector) { return Y.one(Y.Selector.query(selector, this._node, true)); }, /** * Retrieves a NodeList based on the given CSS selector. * @method all * * @param {string} selector The CSS selector to test against. * @return {NodeList} A NodeList instance for the matching HTMLCollection/Array. */ all: function(selector) { var nodelist; if (this._node) { nodelist = Y.all(Y.Selector.query(selector, this._node)); nodelist._query = selector; nodelist._queryRoot = this._node; } return nodelist || Y.all([]); }, // TODO: allow fn test /** * Test if the supplied node matches the supplied selector. * @method test * * @param {string} selector The CSS selector to test against. * @return {boolean} Whether or not the node matches the selector. */ test: function(selector) { return Y.Selector.test(this._node, selector); }, /** * Removes the node from its parent. * Shortcut for myNode.get('parentNode').removeChild(myNode); * @method remove * @param {Boolean} destroy whether or not to call destroy() on the node * after removal. * @chainable * */ remove: function(destroy) { var node = this._node; if (node && node.parentNode) { node.parentNode.removeChild(node); } if (destroy) { this.destroy(); } return this; }, /** * Replace the node with the other node. This is a DOM update only * and does not change the node bound to the Node instance. * Shortcut for myNode.get('parentNode').replaceChild(newNode, myNode); * @method replace * @param {Node | HTMLElement} newNode Node to be inserted * @chainable * */ replace: function(newNode) { var node = this._node; if (typeof newNode == 'string') { newNode = Y_Node.create(newNode); } node.parentNode.replaceChild(Y_Node.getDOMNode(newNode), node); return this; }, /** * @method replaceChild * @for Node * @param {String | HTMLElement | Node} node Node to be inserted * @param {HTMLElement | Node} refNode Node to be replaced * @return {Node} The replaced node */ replaceChild: function(node, refNode) { if (typeof node == 'string') { node = Y_DOM.create(node); } return Y.one(this._node.replaceChild(Y_Node.getDOMNode(node), Y_Node.getDOMNode(refNode))); }, /** * Nulls internal node references, removes any plugins and event listeners. * Note that destroy() will not remove the node from its parent or from the DOM. For that * functionality, call remove(true). * @method destroy * @param {Boolean} recursivePurge (optional) Whether or not to remove listeners from the * node's subtree (default is false) * */ destroy: function(recursive) { var UID = Y.config.doc.uniqueID ? 'uniqueID' : '_yuid', instance; this.purge(); // TODO: only remove events add via this Node if (this.unplug) { // may not be a PluginHost this.unplug(); } this.clearData(); if (recursive) { Y.NodeList.each(this.all('*'), function(node) { instance = Y_Node._instances[node[UID]]; if (instance) { instance.destroy(); } else { // purge in case added by other means Y.Event.purgeElement(node); } }); } this._node = null; this._stateProxy = null; delete Y_Node._instances[this._yuid]; }, /** * Invokes a method on the Node instance * @method invoke * @param {String} method The name of the method to invoke * @param {any} [args*] Arguments to invoke the method with. * @return {any} Whatever the underly method returns. * DOM Nodes and Collections return values * are converted to Node/NodeList instances. * */ invoke: function(method, a, b, c, d, e) { var node = this._node, ret; if (a && a._node) { a = a._node; } if (b && b._node) { b = b._node; } ret = node[method](a, b, c, d, e); return Y_Node.scrubVal(ret, this); }, /** * @method swap * @description Swap DOM locations with the given node. * This does not change which DOM node each Node instance refers to. * @param {Node} otherNode The node to swap with * @chainable */ swap: Y.config.doc.documentElement.swapNode ? function(otherNode) { this._node.swapNode(Y_Node.getDOMNode(otherNode)); } : function(otherNode) { otherNode = Y_Node.getDOMNode(otherNode); var node = this._node, parent = otherNode.parentNode, nextSibling = otherNode.nextSibling; if (nextSibling === node) { parent.insertBefore(node, otherNode); } else if (otherNode === node.nextSibling) { parent.insertBefore(otherNode, node); } else { node.parentNode.replaceChild(otherNode, node); Y_DOM.addHTML(parent, node, nextSibling); } return this; }, hasMethod: function(method) { var node = this._node; return !!(node && method in node && typeof node[method] != 'unknown' && (typeof node[method] == 'function' || String(node[method]).indexOf('function') === 1)); // IE reports as object, prepends space }, isFragment: function() { return (this.get('nodeType') === 11); }, /** * Removes and destroys all of the nodes within the node. * @method empty * @chainable */ empty: function() { this.get('childNodes').remove().destroy(true); return this; }, /** * Returns the DOM node bound to the Node instance * @method getDOMNode * @return {HTMLElement} */ getDOMNode: function() { return this._node; } }, true); Y.Node = Y_Node; Y.one = Y_Node.one;