Changeset View
Changeset View
Standalone View
Standalone View
externals/javelin/src/lib/DOM.js
- This file was added.
| /** | |||||
| * @requires javelin-magical-init | |||||
| * javelin-install | |||||
| * javelin-util | |||||
| * javelin-vector | |||||
| * javelin-stratcom | |||||
| * @provides javelin-dom | |||||
| * | |||||
| * @javelin-installs JX.$ | |||||
| * @javelin-installs JX.$N | |||||
| * @javelin-installs JX.$H | |||||
| * | |||||
| * @javelin | |||||
| */ | |||||
| /** | |||||
| * Select an element by its "id" attribute, like ##document.getElementById()##. | |||||
| * For example: | |||||
| * | |||||
| * var node = JX.$('some_id'); | |||||
| * | |||||
| * This will select the node with the specified "id" attribute: | |||||
| * | |||||
| * LANG=HTML | |||||
| * <div id="some_id">...</div> | |||||
| * | |||||
| * If the specified node does not exist, @{JX.$()} will throw an exception. | |||||
| * | |||||
| * For other ways to select nodes from the document, see @{JX.DOM.scry()} and | |||||
| * @{JX.DOM.find()}. | |||||
| * | |||||
| * @param string "id" attribute to select from the document. | |||||
| * @return Node Node with the specified "id" attribute. | |||||
| * | |||||
| * @group dom | |||||
| */ | |||||
| JX.$ = function(id) { | |||||
| if (__DEV__) { | |||||
| if (!id) { | |||||
| JX.$E('Empty ID passed to JX.$()!'); | |||||
| } | |||||
| } | |||||
| var node = document.getElementById(id); | |||||
| if (!node || (node.id != id)) { | |||||
| if (__DEV__) { | |||||
| if (node && (node.id != id)) { | |||||
| JX.$E( | |||||
| 'JX.$("'+id+'"): '+ | |||||
| 'document.getElementById() returned an element without the '+ | |||||
| 'correct ID. This usually means that the element you are trying '+ | |||||
| 'to select is being masked by a form with the same value in its '+ | |||||
| '"name" attribute.'); | |||||
| } | |||||
| } | |||||
| JX.$E("JX.$('" + id + "') call matched no nodes."); | |||||
| } | |||||
| return node; | |||||
| }; | |||||
| /** | |||||
| * Upcast a string into an HTML object so it is treated as markup instead of | |||||
| * plain text. See @{JX.$N} for discussion of Javelin's security model. Every | |||||
| * time you call this function you potentially open up a security hole. Avoid | |||||
| * its use wherever possible. | |||||
| * | |||||
| * This class intentionally supports only a subset of HTML because many browsers | |||||
| * named "Internet Explorer" have awkward restrictions around what they'll | |||||
| * accept for conversion to document fragments. Alter your datasource to emit | |||||
| * valid HTML within this subset if you run into an unsupported edge case. All | |||||
| * the edge cases are crazy and you should always be reasonably able to emit | |||||
| * a cohesive tag instead of an unappendable fragment. | |||||
| * | |||||
| * You may use @{JX.$H} as a shortcut for creating new JX.HTML instances: | |||||
| * | |||||
| * JX.$N('div', {}, some_html_blob); // Treat as string (safe) | |||||
| * JX.$N('div', {}, JX.$H(some_html_blob)); // Treat as HTML (unsafe!) | |||||
| * | |||||
| * @task build String into HTML | |||||
| * @task nodes HTML into Nodes | |||||
| * | |||||
| * @group dom | |||||
| */ | |||||
| JX.install('HTML', { | |||||
| construct : function(str) { | |||||
| if (__DEV__) { | |||||
| var tags = ['legend', 'thead', 'tbody', 'tfoot', 'column', 'colgroup', | |||||
| 'caption', 'tr', 'th', 'td', 'option']; | |||||
| var evil_stuff = new RegExp('^\\s*<(' + tags.join('|') + ')\\b', 'i'); | |||||
| var match = null; | |||||
| if (match = str.match(evil_stuff)) { | |||||
| JX.$E( | |||||
| 'new JX.HTML("<' + match[1] + '>..."): ' + | |||||
| 'call initializes an HTML object with an invalid partial fragment ' + | |||||
| 'and can not be converted into DOM nodes. The enclosing tag of an ' + | |||||
| 'HTML content string must be appendable to a document fragment. ' + | |||||
| 'For example, <table> is allowed but <tr> or <tfoot> are not.'); | |||||
| } | |||||
| var really_evil = /<script\b/; | |||||
| if (str.match(really_evil)) { | |||||
| JX.$E( | |||||
| 'new JX.HTML("...<script>..."): ' + | |||||
| 'call initializes an HTML object with an embedded script tag! ' + | |||||
| 'Are you crazy?! Do NOT do this!!!'); | |||||
| } | |||||
| var wont_work = /<object\b/; | |||||
| if (str.match(wont_work)) { | |||||
| JX.$E( | |||||
| 'new JX.HTML("...<object>..."): ' + | |||||
| 'call initializes an HTML object with an embedded <object> tag. IE ' + | |||||
| 'will not do the right thing with this.'); | |||||
| } | |||||
| // TODO(epriestley): May need to deny <option> more broadly, see | |||||
| // http://support.microsoft.com/kb/829907 and the whole mess in the | |||||
| // heavy stack. But I seem to have gotten away without cloning into the | |||||
| // documentFragment below, so this may be a nonissue. | |||||
| } | |||||
| this._content = str; | |||||
| }, | |||||
| members : { | |||||
| _content : null, | |||||
| /** | |||||
| * Convert the raw HTML string into a DOM node tree. | |||||
| * | |||||
| * @task nodes | |||||
| * @return DocumentFragment A document fragment which contains the nodes | |||||
| * corresponding to the HTML string you provided. | |||||
| */ | |||||
| getFragment : function() { | |||||
| var wrapper = JX.$N('div'); | |||||
| wrapper.innerHTML = this._content; | |||||
| var fragment = document.createDocumentFragment(); | |||||
| while (wrapper.firstChild) { | |||||
| // TODO(epriestley): Do we need to do a bunch of cloning junk here? | |||||
| // See heavy stack. I'm disconnecting the nodes instead; this seems | |||||
| // to work but maybe my test case just isn't extensive enough. | |||||
| fragment.appendChild(wrapper.removeChild(wrapper.firstChild)); | |||||
| } | |||||
| return fragment; | |||||
| } | |||||
| } | |||||
| }); | |||||
| /** | |||||
| * Build a new HTML object from a trustworthy string. JX.$H is a shortcut for | |||||
| * creating new JX.HTML instances. | |||||
| * | |||||
| * @task build | |||||
| * @param string A string which you want to be treated as HTML, because you | |||||
| * know it is from a trusted source and any data in it has been | |||||
| * properly escaped. | |||||
| * @return JX.HTML HTML object, suitable for use with @{JX.$N}. | |||||
| * | |||||
| * @group dom | |||||
| */ | |||||
| JX.$H = function(str) { | |||||
| return new JX.HTML(str); | |||||
| }; | |||||
| /** | |||||
| * Create a new DOM node with attributes and content. | |||||
| * | |||||
| * var link = JX.$N('a'); | |||||
| * | |||||
| * This creates a new, empty anchor tag without any attributes. The equivalent | |||||
| * markup would be: | |||||
| * | |||||
| * LANG=HTML | |||||
| * <a /> | |||||
| * | |||||
| * You can also specify attributes by passing a dictionary: | |||||
| * | |||||
| * JX.$N('a', {name: 'anchor'}); | |||||
| * | |||||
| * This is equivalent to: | |||||
| * | |||||
| * LANG=HTML | |||||
| * <a name="anchor" /> | |||||
| * | |||||
| * Additionally, you can specify content: | |||||
| * | |||||
| * JX.$N( | |||||
| * 'a', | |||||
| * {href: 'http://www.javelinjs.com'}, | |||||
| * 'Visit the Javelin Homepage'); | |||||
| * | |||||
| * This is equivalent to: | |||||
| * | |||||
| * LANG=HTML | |||||
| * <a href="http://www.javelinjs.com">Visit the Javelin Homepage</a> | |||||
| * | |||||
| * If you only want to specify content, you can omit the attribute parameter. | |||||
| * That is, these calls are equivalent: | |||||
| * | |||||
| * JX.$N('div', {}, 'Lorem ipsum...'); // No attributes. | |||||
| * JX.$N('div', 'Lorem ipsum...') // Same as above. | |||||
| * | |||||
| * Both are equivalent to: | |||||
| * | |||||
| * LANG=HTML | |||||
| * <div>Lorem ipsum...</div> | |||||
| * | |||||
| * Note that the content is treated as plain text, not HTML. This means it is | |||||
| * safe to use untrusted strings: | |||||
| * | |||||
| * JX.$N('div', '<script src="evil.com" />'); | |||||
| * | |||||
| * This is equivalent to: | |||||
| * | |||||
| * LANG=HTML | |||||
| * <div><script src="evil.com" /></div> | |||||
| * | |||||
| * That is, the content will be properly escaped and will not create a | |||||
| * vulnerability. If you want to set HTML content, you can use @{JX.HTML}: | |||||
| * | |||||
| * JX.$N('div', JX.$H(some_html)); | |||||
| * | |||||
| * **This is potentially unsafe**, so make sure you understand what you're | |||||
| * doing. You should usually avoid passing HTML around in string form. See | |||||
| * @{JX.HTML} for discussion. | |||||
| * | |||||
| * You can create new nodes with a Javelin sigil (and, optionally, metadata) by | |||||
| * providing "sigil" and "meta" keys in the attribute dictionary. | |||||
| * | |||||
| * @param string Tag name, like 'a' or 'div'. | |||||
| * @param dict|string|@{JX.HTML}? Property dictionary, or content if you don't | |||||
| * want to specify any properties. | |||||
| * @param string|@{JX.HTML}? Content string (interpreted as plain text) | |||||
| * or @{JX.HTML} object (interpreted as HTML, | |||||
| * which may be dangerous). | |||||
| * @return Node New node with whatever attributes and | |||||
| * content were specified. | |||||
| * | |||||
| * @group dom | |||||
| */ | |||||
| JX.$N = function(tag, attr, content) { | |||||
| if (typeof content == 'undefined' && | |||||
| (typeof attr != 'object' || attr instanceof JX.HTML)) { | |||||
| content = attr; | |||||
| attr = {}; | |||||
| } | |||||
| if (__DEV__) { | |||||
| if (tag.toLowerCase() != tag) { | |||||
| JX.$E( | |||||
| '$N("'+tag+'", ...): '+ | |||||
| 'tag name must be in lower case; '+ | |||||
| 'use "'+tag.toLowerCase()+'", not "'+tag+'".'); | |||||
| } | |||||
| } | |||||
| var node = document.createElement(tag); | |||||
| if (attr.style) { | |||||
| JX.copy(node.style, attr.style); | |||||
| delete attr.style; | |||||
| } | |||||
| if (attr.sigil) { | |||||
| JX.Stratcom.addSigil(node, attr.sigil); | |||||
| delete attr.sigil; | |||||
| } | |||||
| if (attr.meta) { | |||||
| JX.Stratcom.addData(node, attr.meta); | |||||
| delete attr.meta; | |||||
| } | |||||
| if (__DEV__) { | |||||
| if (('metadata' in attr) || ('data' in attr)) { | |||||
| JX.$E( | |||||
| '$N(' + tag + ', ...): ' + | |||||
| 'use the key "meta" to specify metadata, not "data" or "metadata".'); | |||||
| } | |||||
| } | |||||
| JX.copy(node, attr); | |||||
| if (content) { | |||||
| JX.DOM.setContent(node, content); | |||||
| } | |||||
| return node; | |||||
| }; | |||||
| /** | |||||
| * Query and update the DOM. Everything here is static, this is essentially | |||||
| * a collection of common utility functions. | |||||
| * | |||||
| * @task stratcom Attaching Event Listeners | |||||
| * @task content Changing DOM Content | |||||
| * @task nodes Updating Nodes | |||||
| * @task serialize Serializing Forms | |||||
| * @task test Testing DOM Properties | |||||
| * @task convenience Convenience Methods | |||||
| * @task query Finding Nodes in the DOM | |||||
| * @task view Changing View State | |||||
| * | |||||
| * @group dom | |||||
| */ | |||||
| JX.install('DOM', { | |||||
| statics : { | |||||
| _autoid : 0, | |||||
| _uniqid : 0, | |||||
| _metrics : {}, | |||||
| /* -( Changing DOM Content )----------------------------------------------- */ | |||||
| /** | |||||
| * Set the content of some node. This uses the same content semantics as | |||||
| * other Javelin content methods, see @{function:JX.$N} for a detailed | |||||
| * explanation. Previous content will be replaced: you can also | |||||
| * @{method:prependContent} or @{method:appendContent}. | |||||
| * | |||||
| * @param Node Node to set content of. | |||||
| * @param mixed Content to set. | |||||
| * @return void | |||||
| * @task content | |||||
| */ | |||||
| setContent : function(node, content) { | |||||
| if (__DEV__) { | |||||
| if (!JX.DOM.isNode(node)) { | |||||
| JX.$E( | |||||
| 'JX.DOM.setContent(<yuck>, ...): '+ | |||||
| 'first argument must be a DOM node.'); | |||||
| } | |||||
| } | |||||
| while (node.firstChild) { | |||||
| JX.DOM.remove(node.firstChild); | |||||
| } | |||||
| JX.DOM.appendContent(node, content); | |||||
| }, | |||||
| /** | |||||
| * Prepend content to some node. This method uses the same content semantics | |||||
| * as other Javelin methods, see @{function:JX.$N} for an explanation. You | |||||
| * can also @{method:setContent} or @{method:appendContent}. | |||||
| * | |||||
| * @param Node Node to prepend content to. | |||||
| * @param mixed Content to prepend. | |||||
| * @return void | |||||
| * @task content | |||||
| */ | |||||
| prependContent : function(node, content) { | |||||
| if (__DEV__) { | |||||
| if (!JX.DOM.isNode(node)) { | |||||
| JX.$E( | |||||
| 'JX.DOM.prependContent(<junk>, ...): '+ | |||||
| 'first argument must be a DOM node.'); | |||||
| } | |||||
| } | |||||
| this._insertContent(node, content, this._mechanismPrepend, true); | |||||
| }, | |||||
| /** | |||||
| * Append content to some node. This method uses the same content semantics | |||||
| * as other Javelin methods, see @{function:JX.$N} for an explanation. You | |||||
| * can also @{method:setContent} or @{method:prependContent}. | |||||
| * | |||||
| * @param Node Node to append the content of. | |||||
| * @param mixed Content to append. | |||||
| * @return void | |||||
| * @task content | |||||
| */ | |||||
| appendContent : function(node, content) { | |||||
| if (__DEV__) { | |||||
| if (!JX.DOM.isNode(node)) { | |||||
| JX.$E( | |||||
| 'JX.DOM.appendContent(<bleh>, ...): '+ | |||||
| 'first argument must be a DOM node.'); | |||||
| } | |||||
| } | |||||
| this._insertContent(node, content, this._mechanismAppend); | |||||
| }, | |||||
| /** | |||||
| * Internal, add content to a node by prepending. | |||||
| * | |||||
| * @param Node Node to prepend content to. | |||||
| * @param Node Node to prepend. | |||||
| * @return void | |||||
| * @task content | |||||
| */ | |||||
| _mechanismPrepend : function(node, content) { | |||||
| node.insertBefore(content, node.firstChild); | |||||
| }, | |||||
| /** | |||||
| * Internal, add content to a node by appending. | |||||
| * | |||||
| * @param Node Node to append content to. | |||||
| * @param Node Node to append. | |||||
| * @task content | |||||
| */ | |||||
| _mechanismAppend : function(node, content) { | |||||
| node.appendChild(content); | |||||
| }, | |||||
| /** | |||||
| * Internal, add content to a node using some specified mechanism. | |||||
| * | |||||
| * @param Node Node to add content to. | |||||
| * @param mixed Content to add. | |||||
| * @param function Callback for actually adding the nodes. | |||||
| * @param bool True if array elements should be passed to the mechanism | |||||
| * in reverse order, i.e. the mechanism prepends nodes. | |||||
| * @return void | |||||
| * @task content | |||||
| */ | |||||
| _insertContent : function(parent, content, mechanism, reverse) { | |||||
| if (JX.isArray(content)) { | |||||
| if (reverse) { | |||||
| content = [].concat(content).reverse(); | |||||
| } | |||||
| for (var ii = 0; ii < content.length; ii++) { | |||||
| JX.DOM._insertContent(parent, content[ii], mechanism, reverse); | |||||
| } | |||||
| } else { | |||||
| var type = typeof content; | |||||
| if (content instanceof JX.HTML) { | |||||
| content = content.getFragment(); | |||||
| } else if (type == 'string' || type == 'number') { | |||||
| content = document.createTextNode(content); | |||||
| } | |||||
| if (__DEV__) { | |||||
| if (content && !content.nodeType) { | |||||
| JX.$E( | |||||
| 'JX.DOM._insertContent(<node>, ...): '+ | |||||
| 'second argument must be a string, a number, ' + | |||||
| 'a DOM node or a JX.HTML instance'); | |||||
| } | |||||
| } | |||||
| content && mechanism(parent, content); | |||||
| } | |||||
| }, | |||||
| /* -( Updating Nodes )----------------------------------------------------- */ | |||||
| /** | |||||
| * Remove a node from its parent, so it is no longer a child of any other | |||||
| * node. | |||||
| * | |||||
| * @param Node Node to remove. | |||||
| * @return Node The node. | |||||
| * @task nodes | |||||
| */ | |||||
| remove : function(node) { | |||||
| node.parentNode && JX.DOM.replace(node, null); | |||||
| return node; | |||||
| }, | |||||
| /** | |||||
| * Replace a node with some other piece of content. This method obeys | |||||
| * Javelin content semantics, see @{function:JX.$N} for an explanation. | |||||
| * You can also @{method:setContent}, @{method:prependContent}, or | |||||
| * @{method:appendContent}. | |||||
| * | |||||
| * @param Node Node to replace. | |||||
| * @param mixed Content to replace it with. | |||||
| * @return Node the original node. | |||||
| * @task nodes | |||||
| */ | |||||
| replace : function(node, replacement) { | |||||
| if (__DEV__) { | |||||
| if (!node.parentNode) { | |||||
| JX.$E( | |||||
| 'JX.DOM.replace(<node>, ...): '+ | |||||
| 'node has no parent node, so it can not be replaced.'); | |||||
| } | |||||
| } | |||||
| var mechanism; | |||||
| if (node.nextSibling) { | |||||
| mechanism = JX.bind(node.nextSibling, function(parent, content) { | |||||
| parent.insertBefore(content, this); | |||||
| }); | |||||
| } else { | |||||
| mechanism = this._mechanismAppend; | |||||
| } | |||||
| var parent = node.parentNode; | |||||
| parent.removeChild(node); | |||||
| this._insertContent(parent, replacement, mechanism); | |||||
| return node; | |||||
| }, | |||||
| /* -( Serializing Froms )-------------------------------------------------- */ | |||||
| /** | |||||
| * Converts a form into a list of <name, value> pairs. | |||||
| * | |||||
| * Note: This function explicity does not match for submit inputs as there | |||||
| * could be multiple in a form. It's the caller's obligation to add the | |||||
| * submit input value if desired. | |||||
| * | |||||
| * @param Node The form element to convert into a list of pairs. | |||||
| * @return List A list of <name, value> pairs. | |||||
| * @task serialize | |||||
| */ | |||||
| convertFormToListOfPairs : function(form) { | |||||
| var elements = form.getElementsByTagName('*'); | |||||
| var data = []; | |||||
| for (var ii = 0; ii < elements.length; ++ii) { | |||||
| if (!elements[ii].name) { | |||||
| continue; | |||||
| } | |||||
| if (elements[ii].disabled) { | |||||
| continue; | |||||
| } | |||||
| var type = elements[ii].type; | |||||
| var tag = elements[ii].tagName; | |||||
| if ((type in {radio: 1, checkbox: 1} && elements[ii].checked) || | |||||
| type in {text: 1, hidden: 1, password: 1, email: 1, tel: 1, | |||||
| number: 1} || | |||||
| tag in {TEXTAREA: 1, SELECT: 1}) { | |||||
| data.push([elements[ii].name, elements[ii].value]); | |||||
| } | |||||
| } | |||||
| return data; | |||||
| }, | |||||
| /** | |||||
| * Converts a form into a dictionary mapping input names to values. This | |||||
| * will overwrite duplicate inputs in an undefined way. | |||||
| * | |||||
| * @param Node The form element to convert into a dictionary. | |||||
| * @return Dict A dictionary of form values. | |||||
| * @task serialize | |||||
| */ | |||||
| convertFormToDictionary : function(form) { | |||||
| var data = {}; | |||||
| var pairs = JX.DOM.convertFormToListOfPairs(form); | |||||
| for (var ii = 0; ii < pairs.length; ii++) { | |||||
| data[pairs[ii][0]] = pairs[ii][1]; | |||||
| } | |||||
| return data; | |||||
| }, | |||||
| /* -( Testing DOM Properties )--------------------------------------------- */ | |||||
| /** | |||||
| * Test if an object is a valid Node. | |||||
| * | |||||
| * @param wild Something which might be a Node. | |||||
| * @return bool True if the parameter is a DOM node. | |||||
| * @task test | |||||
| */ | |||||
| isNode : function(node) { | |||||
| return !!(node && node.nodeName && (node !== window)); | |||||
| }, | |||||
| /** | |||||
| * Test if an object is a node of some specific (or one of several) types. | |||||
| * For example, this tests if the node is an ##<input />##, ##<select />##, | |||||
| * or ##<textarea />##. | |||||
| * | |||||
| * JX.DOM.isType(node, ['input', 'select', 'textarea']); | |||||
| * | |||||
| * @param wild Something which might be a Node. | |||||
| * @param string|list One or more tags which you want to test for. | |||||
| * @return bool True if the object is a node, and it's a node of one | |||||
| * of the provided types. | |||||
| * @task test | |||||
| */ | |||||
| isType : function(node, of_type) { | |||||
| node = ('' + (node.nodeName || '')).toUpperCase(); | |||||
| of_type = JX.$AX(of_type); | |||||
| for (var ii = 0; ii < of_type.length; ++ii) { | |||||
| if (of_type[ii].toUpperCase() == node) { | |||||
| return true; | |||||
| } | |||||
| } | |||||
| return false; | |||||
| }, | |||||
| /** | |||||
| * Listen for events occuring beneath a specific node in the DOM. This is | |||||
| * similar to @{JX.Stratcom.listen()}, but allows you to specify some node | |||||
| * which serves as a scope instead of the default scope (the whole document) | |||||
| * which you get if you install using @{JX.Stratcom.listen()} directly. For | |||||
| * example, to listen for clicks on nodes with the sigil 'menu-item' below | |||||
| * the root menu node: | |||||
| * | |||||
| * var the_menu = getReferenceToTheMenuNodeSomehow(); | |||||
| * JX.DOM.listen(the_menu, 'click', 'menu-item', function(e) { ... }); | |||||
| * | |||||
| * @task stratcom | |||||
| * @param Node The node to listen for events underneath. | |||||
| * @param string|list One or more event types to listen for. | |||||
| * @param list? A path to listen on, or a list of paths. | |||||
| * @param function Callback to invoke when a matching event occurs. | |||||
| * @return object A reference to the installed listener. You can later | |||||
| * remove the listener by calling this object's remove() | |||||
| * method. | |||||
| */ | |||||
| listen : function(node, type, path, callback) { | |||||
| var auto_id = ['autoid:' + JX.DOM._getAutoID(node)]; | |||||
| path = JX.$AX(path || []); | |||||
| if (!path.length) { | |||||
| path = auto_id; | |||||
| } else { | |||||
| for (var ii = 0; ii < path.length; ii++) { | |||||
| path[ii] = auto_id.concat(JX.$AX(path[ii])); | |||||
| } | |||||
| } | |||||
| return JX.Stratcom.listen(type, path, callback); | |||||
| }, | |||||
| /** | |||||
| * Invoke a custom event on a node. This method is a companion to | |||||
| * @{method:JX.DOM.listen} and parallels @{method:JX.Stratcom.invoke} in | |||||
| * the same way that method parallels @{method:JX.Stratcom.listen}. | |||||
| * | |||||
| * This method can not be used to invoke native events (like 'click'). | |||||
| * | |||||
| * @param Node The node to invoke an event on. | |||||
| * @param string Custom event type. | |||||
| * @param dict Event data. | |||||
| * @return JX.Event The event object which was dispatched to listeners. | |||||
| * The main use of this is to test whether any | |||||
| * listeners prevented the event. | |||||
| */ | |||||
| invoke : function(node, type, data) { | |||||
| if (__DEV__) { | |||||
| if (type in JX.__allowedEvents) { | |||||
| throw new Error( | |||||
| 'JX.DOM.invoke(..., "' + type + '", ...): ' + | |||||
| 'you cannot invoke with the same type as a native event.'); | |||||
| } | |||||
| } | |||||
| return JX.Stratcom.dispatch({ | |||||
| target: node, | |||||
| type: type, | |||||
| customData: data | |||||
| }); | |||||
| }, | |||||
| uniqID : function(node) { | |||||
| if (!node.getAttribute('id')) { | |||||
| node.setAttribute('id', 'uniqid_'+(++JX.DOM._uniqid)); | |||||
| } | |||||
| return node.getAttribute('id'); | |||||
| }, | |||||
| alterClass : function(node, className, add) { | |||||
| if (__DEV__) { | |||||
| if (add !== false && add !== true) { | |||||
| JX.$E( | |||||
| 'JX.DOM.alterClass(...): ' + | |||||
| 'expects the third parameter to be Boolean: ' + | |||||
| add + ' was provided'); | |||||
| } | |||||
| } | |||||
| var has = ((' '+node.className+' ').indexOf(' '+className+' ') > -1); | |||||
| if (add && !has) { | |||||
| node.className += ' '+className; | |||||
| } else if (has && !add) { | |||||
| node.className = node.className.replace( | |||||
| new RegExp('(^|\\s)' + className + '(?:\\s|$)', 'g'), ' '); | |||||
| } | |||||
| }, | |||||
| htmlize : function(str) { | |||||
| return (''+str) | |||||
| .replace(/&/g, '&') | |||||
| .replace(/"/g, '"') | |||||
| .replace(/</g, '<') | |||||
| .replace(/>/g, '>'); | |||||
| }, | |||||
| /** | |||||
| * Show one or more elements, by removing their "display" style. This | |||||
| * assumes you have hidden them with @{method:hide}, or explicitly set | |||||
| * the style to `display: none;`. | |||||
| * | |||||
| * @task convenience | |||||
| * @param ... One or more nodes to remove "display" styles from. | |||||
| * @return void | |||||
| */ | |||||
| show : function() { | |||||
| if (__DEV__) { | |||||
| for (var ii = 0; ii < arguments.length; ++ii) { | |||||
| if (!arguments[ii]) { | |||||
| JX.$E( | |||||
| 'JX.DOM.show(...): ' + | |||||
| 'one or more arguments were null or empty.'); | |||||
| } | |||||
| } | |||||
| } | |||||
| for (var ii = 0; ii < arguments.length; ++ii) { | |||||
| arguments[ii].style.display = ''; | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Hide one or more elements, by setting `display: none;` on them. This is | |||||
| * a convenience method. See also @{method:show}. | |||||
| * | |||||
| * @task convenience | |||||
| * @param ... One or more nodes to set "display: none" on. | |||||
| * @return void | |||||
| */ | |||||
| hide : function() { | |||||
| if (__DEV__) { | |||||
| for (var ii = 0; ii < arguments.length; ++ii) { | |||||
| if (!arguments[ii]) { | |||||
| JX.$E( | |||||
| 'JX.DOM.hide(...): ' + | |||||
| 'one or more arguments were null or empty.'); | |||||
| } | |||||
| } | |||||
| } | |||||
| for (var ii = 0; ii < arguments.length; ++ii) { | |||||
| arguments[ii].style.display = 'none'; | |||||
| } | |||||
| }, | |||||
| textMetrics : function(node, pseudoclass, x) { | |||||
| if (!this._metrics[pseudoclass]) { | |||||
| var n = JX.$N( | |||||
| 'var', | |||||
| {className: pseudoclass}); | |||||
| this._metrics[pseudoclass] = n; | |||||
| } | |||||
| var proxy = this._metrics[pseudoclass]; | |||||
| document.body.appendChild(proxy); | |||||
| proxy.style.width = x ? (x+'px') : ''; | |||||
| JX.DOM.setContent( | |||||
| proxy, | |||||
| JX.$H(JX.DOM.htmlize(node.value).replace(/\n/g, '<br />'))); | |||||
| var metrics = JX.Vector.getDim(proxy); | |||||
| document.body.removeChild(proxy); | |||||
| return metrics; | |||||
| }, | |||||
| /** | |||||
| * Search the document for DOM nodes by providing a root node to look | |||||
| * beneath, a tag name, and (optionally) a sigil. Nodes which match all | |||||
| * specified conditions are returned. | |||||
| * | |||||
| * @task query | |||||
| * | |||||
| * @param Node Root node to search beneath. | |||||
| * @param string Tag name, like 'a' or 'textarea'. | |||||
| * @param string Optionally, a sigil which nodes are required to have. | |||||
| * | |||||
| * @return list List of matching nodes, which may be empty. | |||||
| */ | |||||
| scry : function(root, tagname, sigil) { | |||||
| if (__DEV__) { | |||||
| if (!JX.DOM.isNode(root)) { | |||||
| JX.$E( | |||||
| 'JX.DOM.scry(<yuck>, ...): '+ | |||||
| 'first argument must be a DOM node.'); | |||||
| } | |||||
| } | |||||
| var nodes = root.getElementsByTagName(tagname); | |||||
| if (!sigil) { | |||||
| return JX.$A(nodes); | |||||
| } | |||||
| var result = []; | |||||
| for (var ii = 0; ii < nodes.length; ii++) { | |||||
| if (JX.Stratcom.hasSigil(nodes[ii], sigil)) { | |||||
| result.push(nodes[ii]); | |||||
| } | |||||
| } | |||||
| return result; | |||||
| }, | |||||
| /** | |||||
| * Select a node uniquely identified by a root, tagname and sigil. This | |||||
| * is similar to JX.DOM.scry() but expects exactly one result. | |||||
| * | |||||
| * @task query | |||||
| * | |||||
| * @param Node Root node to search beneath. | |||||
| * @param string Tag name, like 'a' or 'textarea'. | |||||
| * @param string Optionally, sigil which selected node must have. | |||||
| * | |||||
| * @return Node Node uniquely identified by the criteria. | |||||
| */ | |||||
| find : function(root, tagname, sigil) { | |||||
| if (__DEV__) { | |||||
| if (!JX.DOM.isNode(root)) { | |||||
| JX.$E( | |||||
| 'JX.DOM.find(<glop>, "'+tagname+'", "'+sigil+'"): '+ | |||||
| 'first argument must be a DOM node.'); | |||||
| } | |||||
| } | |||||
| var result = JX.DOM.scry(root, tagname, sigil); | |||||
| if (__DEV__) { | |||||
| if (result.length > 1) { | |||||
| JX.$E( | |||||
| 'JX.DOM.find(<node>, "'+tagname+'", "'+sigil+'"): '+ | |||||
| 'matched more than one node.'); | |||||
| } | |||||
| } | |||||
| if (!result.length) { | |||||
| JX.$E('JX.DOM.find(<node>, "' + | |||||
| tagname + '", "' + sigil + '"): '+ 'matched no nodes.'); | |||||
| } | |||||
| return result[0]; | |||||
| }, | |||||
| /** | |||||
| * Focus a node safely. This is just a convenience wrapper that allows you | |||||
| * to avoid IE's habit of throwing when nearly any focus operation is | |||||
| * invoked. | |||||
| * | |||||
| * @task convenience | |||||
| * @param Node Node to move cursor focus to, if possible. | |||||
| * @return void | |||||
| */ | |||||
| focus : function(node) { | |||||
| try { node.focus(); } catch (lol_ie) {} | |||||
| }, | |||||
| /** | |||||
| * Scroll to the position of an element in the document. | |||||
| * @task view | |||||
| * @param Node Node to move document scroll position to, if possible. | |||||
| * @return void | |||||
| */ | |||||
| scrollTo : function(node) { | |||||
| window.scrollTo(0, JX.$V(node).y); | |||||
| }, | |||||
| _getAutoID : function(node) { | |||||
| if (!node.getAttribute('data-autoid')) { | |||||
| node.setAttribute('data-autoid', 'autoid_'+(++JX.DOM._autoid)); | |||||
| } | |||||
| return node.getAttribute('data-autoid'); | |||||
| } | |||||
| } | |||||
| }); | |||||