mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-23 07:12:41 +01:00
eaa883cf37
Summary: Fixes T7069. When jumping to a comment anchor, we get the scroll positions wrong. Partly this is fixing some calcaulations; partly, the "show older comments" and "scroll anchor" stuff were fighting over the scroll position. Since the anchor can take care of things on its own, just let it handle stuff. Test Plan: - Clicked comment anchors. - Loaded pages with anchors in the URI. - Loaded pages with anchors hidden behind "show older comments". In all cases, got the right scroll position. Reviewers: btrahan, chad Reviewed By: chad Subscribers: epriestley Maniphest Tasks: T7069 Differential Revision: https://secure.phabricator.com/D11540
1015 lines
30 KiB
JavaScript
1015 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".');
|
|
}
|
|
}
|
|
|
|
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
|
|
*/
|
|
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'), ' ');
|
|
}
|
|
},
|
|
|
|
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');
|
|
}
|
|
}
|
|
});
|