mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-30 02:32:42 +01:00
fe66d52a22
Summary: Fixes T8919. In Safari, `node.href = null;` has no effect, but in Chrome it is like `node.href = "null";`. Instead, just use semantics similar to `phutil_tag()`: don't assign attributes with `null` values. Test Plan: No more `/null` href in Chrome in Owners typehaead. Typeahead still works in Chrome/Safari. Reviewers: chad Reviewed By: chad Maniphest Tasks: T8919 Differential Revision: https://secure.phabricator.com/D14021
1021 lines
30 KiB
JavaScript
1021 lines
30 KiB
JavaScript
/**
|
|
* @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.
|
|
*/
|
|
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
|
|
*/
|
|
JX.install('HTML', {
|
|
|
|
construct : function(str) {
|
|
if (str instanceof JX.HTML) {
|
|
this._content = str._content;
|
|
return;
|
|
}
|
|
|
|
if (__DEV__) {
|
|
if ((typeof str !== 'string') && (!str || !str.match)) {
|
|
JX.$E(
|
|
'new JX.HTML(<empty?>): ' +
|
|
'call initializes an HTML object with an empty value.');
|
|
}
|
|
|
|
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 = str.match(evil_stuff);
|
|
if (match) {
|
|
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;
|
|
},
|
|
|
|
/**
|
|
* Convert the raw HTML string into a single DOM node. This only works
|
|
* if the element has a single top-level element. Otherwise, use
|
|
* @{method:getFragment} to get a document fragment instead.
|
|
*
|
|
* @return Node Single node represented by the object.
|
|
* @task nodes
|
|
*/
|
|
getNode : function() {
|
|
var fragment = this.getFragment();
|
|
if (__DEV__) {
|
|
if (fragment.childNodes.length < 1) {
|
|
JX.$E('JX.HTML.getNode(): Markup has no root node!');
|
|
}
|
|
if (fragment.childNodes.length > 1) {
|
|
JX.$E('JX.HTML.getNode(): Markup has more than one root node!');
|
|
}
|
|
}
|
|
return fragment.firstChild;
|
|
}
|
|
|
|
}
|
|
});
|
|
|
|
|
|
/**
|
|
* 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}.
|
|
*/
|
|
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.
|
|
*/
|
|
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".');
|
|
}
|
|
}
|
|
|
|
for (var k in attr) {
|
|
if (attr[k] === null) {
|
|
continue;
|
|
}
|
|
node[k] = attr[k];
|
|
}
|
|
|
|
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
|
|
*/
|
|
JX.install('DOM', {
|
|
statics : {
|
|
_autoid : 0,
|
|
_uniqid : 0,
|
|
_metrics : {},
|
|
_frameNode: null,
|
|
_contentNode: null,
|
|
|
|
|
|
/* -( 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 Forms )-------------------------------------------------- */
|
|
|
|
|
|
/**
|
|
* 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'), ' ').trim();
|
|
}
|
|
},
|
|
|
|
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() {
|
|
var ii;
|
|
|
|
if (__DEV__) {
|
|
for (ii = 0; ii < arguments.length; ++ii) {
|
|
if (!arguments[ii]) {
|
|
JX.$E(
|
|
'JX.DOM.show(...): ' +
|
|
'one or more arguments were null or empty.');
|
|
}
|
|
}
|
|
}
|
|
|
|
for (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() {
|
|
var ii;
|
|
|
|
if (__DEV__) {
|
|
for (ii = 0; ii < arguments.length; ++ii) {
|
|
if (!arguments[ii]) {
|
|
JX.$E(
|
|
'JX.DOM.hide(...): ' +
|
|
'one or more arguments were null or empty.');
|
|
}
|
|
}
|
|
}
|
|
|
|
for (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];
|
|
},
|
|
|
|
|
|
/**
|
|
* Select a node uniquely identified by an anchor, tagname, and sigil. This
|
|
* is similar to JX.DOM.find() but walks up the DOM tree instead of down
|
|
* it.
|
|
*
|
|
* @param Node Node to look above.
|
|
* @param string Tag name, like 'a' or 'textarea'.
|
|
* @param string Optionally, sigil which selected node must have.
|
|
* @return Node Matching node.
|
|
*
|
|
* @task query
|
|
*/
|
|
findAbove : function(anchor, tagname, sigil) {
|
|
if (__DEV__) {
|
|
if (!JX.DOM.isNode(anchor)) {
|
|
JX.$E(
|
|
'JX.DOM.findAbove(<glop>, "' + tagname + '", "' + sigil + '"): ' +
|
|
'first argument must be a DOM node.');
|
|
}
|
|
}
|
|
|
|
var result = anchor.parentNode;
|
|
while (true) {
|
|
if (!result) {
|
|
break;
|
|
}
|
|
if (JX.DOM.isType(result, tagname)) {
|
|
if (!sigil || JX.Stratcom.hasSigil(result, sigil)) {
|
|
break;
|
|
}
|
|
}
|
|
result = result.parentNode;
|
|
}
|
|
|
|
if (!result) {
|
|
JX.$E(
|
|
'JX.DOM.findAbove(<node>, "' + tagname + '", "' + sigil + '"): ' +
|
|
'no matching node.');
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
|
|
/**
|
|
* 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) {}
|
|
},
|
|
|
|
|
|
/**
|
|
* Set specific nodes as content and frame nodes for the document.
|
|
*
|
|
* This will cause @{method:scrollTo} and @{method:scrollToPosition} to
|
|
* affect the given frame node instead of the window. This is useful if the
|
|
* page content is broken into multiple panels which scroll independently.
|
|
*
|
|
* Normally, both nodes are the document body.
|
|
*
|
|
* @task view
|
|
* @param Node Node to set as the scroll frame.
|
|
* @param Node Node to set as the content frame.
|
|
* @return void
|
|
*/
|
|
setContentFrame: function(frame_node, content_node) {
|
|
JX.DOM._frameNode = frame_node;
|
|
JX.DOM._contentNode = content_node;
|
|
},
|
|
|
|
|
|
/**
|
|
* Get the current content frame, or `document.body` if one has not been
|
|
* set.
|
|
*
|
|
* @task view
|
|
* @return Node The node which frames the main page content.
|
|
* @return void
|
|
*/
|
|
getContentFrame: function() {
|
|
return JX.DOM._contentNode || document.body;
|
|
},
|
|
|
|
/**
|
|
* Scroll to the position of an element in the document.
|
|
*
|
|
* If @{method:setContentFrame} has been used to set a frame, that node is
|
|
* scrolled.
|
|
*
|
|
* @task view
|
|
* @param Node Node to move document scroll position to, if possible.
|
|
* @return void
|
|
*/
|
|
scrollTo : function(node) {
|
|
var pos = JX.Vector.getPosWithScroll(node);
|
|
JX.DOM.scrollToPosition(0, pos.y);
|
|
},
|
|
|
|
/**
|
|
* Scroll to a specific position in the document.
|
|
*
|
|
* If @{method:setContentFrame} has been used to set a frame, that node is
|
|
* scrolled.
|
|
*
|
|
* @task view
|
|
* @param int X position, in pixels.
|
|
* @param int Y position, in pixels.
|
|
* @return void
|
|
*/
|
|
scrollToPosition: function(x, y) {
|
|
var self = JX.DOM;
|
|
if (self._frameNode) {
|
|
self._frameNode.scrollLeft = x;
|
|
self._frameNode.scrollTop = y;
|
|
} else {
|
|
window.scrollTo(x, y);
|
|
}
|
|
},
|
|
|
|
_getAutoID : function(node) {
|
|
if (!node.getAttribute('data-autoid')) {
|
|
node.setAttribute('data-autoid', 'autoid_'+(++JX.DOM._autoid));
|
|
}
|
|
return node.getAttribute('data-autoid');
|
|
}
|
|
}
|
|
});
|