este-library-oldschool
Version:
Library for github.com/steida/este.git
664 lines (642 loc) • 19.8 kB
JavaScript
// Generated by github.com/steida/coffee2closure 900.1.18
/**
@fileoverview DOM utils.
@namespace este.dom
*/
goog.provide('este.dom');
goog.require('este.object');
goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.dom.classlist');
goog.require('goog.dom.forms');
goog.require('goog.string');
goog.require('goog.Uri');
/**
Check if node matches given simple selector. Only tag and class are
supported now.
Examples:
este.dom.match el, 'button'
este.dom.match el, '.box'
este.dom.match el, '*'
@param {Node} node
@param {string} simpleSelector
@return {boolean} Whether the given element matches the selector.
*/
este.dom.match = function(node, simpleSelector) {
var className, i, j, len, len1, part, queryParts, ref;
if (simpleSelector === '*') {
return true;
}
if (!goog.dom.isElement(node)) {
return false;
}
queryParts = este.dom.getQueryParts(simpleSelector);
for (i = 0, len = queryParts.length; i < len; i++) {
part = queryParts[i];
if (part.tag && part.tag !== '*' && node.tagName.toLowerCase() !== part.tag.toLowerCase()) {
return false;
}
if (part.id && node.id !== part.id) {
return false;
}
ref = part.classes;
for (j = 0, len1 = ref.length; j < len1; j++) {
className = ref[j];
node = /** @type {Element} */(node);
if (!goog.dom.classlist.contains(node, className)) {
return false;
}
}
}
return true;
};
/**
Get node ancestors.
@param {Node} node
@param {boolean=} includeNode
@param {boolean=} stopOnBody
@return {Array.<Element>}
*/
este.dom.getAncestors = function(node, includeNode, stopOnBody) {
var elements;
elements = [];
if (!includeNode) {
node = node.parentNode;
}
while (node) {
if (stopOnBody && node.tagName === 'BODY') {
break;
}
if (node.nodeType === goog.dom.NodeType.DOCUMENT) {
break;
}
elements.push(node);
node = node.parentNode;
}
return elements;
};
/**
@param {Array.<Element>} elements
*/
este.dom.getDomPath = function(elements) {
var className, element, i, j, len, len1, path, ref;
path = [];
for (i = 0, len = elements.length; i < len; i++) {
element = elements[i];
path.push(element.tagName.toUpperCase());
if (element.id) {
path.push('#', element.id);
}
ref = goog.dom.classlist.get(element);
for (j = 0, len1 = ref.length; j < len1; j++) {
className = ref[j];
path.push('.', className);
}
path.push(' ');
}
path.pop();
return path.join('');
};
/**
@param {Element} newNode
@param {Element} refNode
@param {string} where Before, after, prepend, append.
*/
este.dom.insert = function(newNode, refNode, where) {
switch (where) {
case 'before':
goog.dom.insertSiblingBefore(newNode, refNode);
break;
case 'after':
goog.dom.insertSiblingAfter(newNode, refNode);
break;
case 'prepend':
goog.dom.insertChildAt(refNode, newNode, 0);
break;
case 'append':
goog.dom.appendChild(refNode, newNode);
}
};
/**
@param {goog.events.BrowserEvent} e
@param {Object.<string, Function>} object Key is className, value is
callback.
@return {boolean}
*/
este.dom.onTargetWithClass = function(e, object) {
var callback, className, node;
node = e.target;
while (node && node.nodeType === 1) {
for (className in object) {
callback = object[className];
node = /** @type {Element} */(node);
if (goog.dom.classlist.contains(node, className)) {
callback(node);
return true;
}
}
node = node.parentNode;
}
return false;
};
/**
Alias for goog.dom.forms.getFormDataMap(form).toObject().
getFormDataMap method always returns array which is not handy. That's why
this method normalize ['foo'] into 'foo'.
@param {Element} form
@return {Object}
*/
este.dom.serializeForm = function(form) {
var object;
form = /** @type {HTMLFormElement} */(form);
object = goog.dom.forms.getFormDataMap(form).toObject();
return este.object.normalizeOneItemArrayValues(object);
};
/**
Returns a single value of a form element.
If there are more elements with given name, returns value of the first one.
If none is found, returns null.
@param {Element} form
@param {string} name
@return {?string}
*/
este.dom.getSingleFormValueByName = function(form, name) {
var value;
form = /** @type {HTMLFormElement} */(form);
value = goog.dom.forms.getValueByName(form, name);
if (!goog.isArray(value)) {
return value;
}
if (value.length === 0) {
return null;
}
return value[0];
};
/**
@param {Element} element
@return {Array.<number>}
*/
este.dom.getDomPathIndexes = function(element) {
var index, indexes, parent;
indexes = [];
parent = null;
while (true) {
parent = /** @type {Element} */(element.parentNode);
if (!parent || parent.nodeType !== 1) {
break;
}
index = goog.array.indexOf(parent.childNodes, element);
indexes.push(index);
element = parent;
}
indexes.reverse();
return indexes;
};
/**
@param {Array.<number>} path
@param {Document=} doc
@return {Node}
*/
este.dom.getElementByDomPathIndex = function(path, doc) {
var index, node;
if (doc == null) {
doc = document;
}
node = doc.documentElement;
while (path.length) {
index = path.shift();
node = node.childNodes[index];
}
return node;
};
/**
@param {Element} el
*/
este.dom.focus = function(el) {
var e, error1;
try {
return el.focus();
} catch (error1) {
e = error1;
}
};
/**
@param {Element} el
*/
este.dom.focusAsync = function(el) {
return setTimeout(function() {
return este.dom.focus(el);
}, 0);
};
/**
Hack to force blur.
*/
este.dom.forceBlur = function() {
return setTimeout(function() {
var input;
input = goog.dom.createDom('input', {
style: 'position: fixed; left: 0; top: 0'
});
document.body.appendChild(input);
input.focus();
return document.body.removeChild(input);
}, 0);
};
/**
Check if click is useable for client side routing.
@param {goog.events.BrowserEvent} e
@return {boolean}
*/
este.dom.isRoutingClick = function(e) {
if (!este.dom.isRoutingEvent(e)) {
return false;
}
if (!este.dom.isRoutingAnchor(/** @type {Element} */(e.target))) {
return false;
}
return true;
};
/**
@param {goog.events.BrowserEvent} e
@return {boolean}
*/
este.dom.isRoutingEvent = function(e) {
if (!e.isMouseActionButton()) {
return false;
}
if (e.ctrlKey || e.altKey || e.shiftKey || e.metaKey) {
return false;
}
if (e.getBrowserEvent().defaultPrevented) {
return false;
}
return true;
};
/**
@param {Element} anchor
@return {boolean}
*/
este.dom.isRoutingAnchor = function(anchor) {
var anchorUri, locationUri;
if (anchor.target) {
return false;
}
locationUri = new goog.Uri(window.location);
anchorUri = new goog.Uri(anchor.href);
if (!locationUri.hasSameDomainAs(anchorUri)) {
return false;
}
return true;
};
/**
@param {Element} form
@param {boolean} enable
*/
este.dom.setFormEnabled = function(form, enable) {
var field, i, len, ref, results;
ref = form.elements;
results = [];
for (i = 0, len = ref.length; i < len; i++) {
field = ref[i];
results.push(field.disabled = !enable);
}
return results;
};
/**
@param {Element|Node} form
@param {Object} errors
*/
este.dom.showErrorsOnForm = function(form, errors) {
var error, field, invalidField, name, ref;
ref = form.elements;
for (name in ref) {
field = ref[name];
goog.dom.classlist.remove(field, 'e-dom-field-error');
}
if (!errors) {
return;
}
error = errors[0];
alert(error.getMsg());
invalidField = form.elements[error.key];
goog.dom.classlist.add(invalidField, 'e-dom-field-error');
return este.dom.focus(invalidField);
};
/**
For mousehover (mouseenter, mouseleave) detection.
@param {goog.events.BrowserEvent} e
@param {Node} target
@return {boolean}
*/
este.dom.isMouseHoverEventWithinElement = function(e, target) {
var ref;
return ((ref = e.type) === 'mouseover' || ref === 'mouseout') && !!e.relatedTarget && goog.dom.contains(target, e.relatedTarget);
};
/**
Extracted and modified goog.dom.query getQueryParts method.
Returns [
query: null, // the full text of the part's rule
pseudos: [], // CSS supports multiple pseudo-class matches in a single
// rule
attrs: [], // CSS supports multi-attribute match, so we need an array
classes: [], // class matches may be additive,
// e.g.: .thinger.blah.howdy
tag: null, // only one tag...
oper: null, // ...or operator per component. Note that these wind up
// being exclusive.
id: null // the id component of a rule
, ..
]
@private
*/
este.dom.getQueryPartsCache_ = {};
este.dom.getQueryParts = function(query) {
var queryKey = query;
var cached = este.dom.getQueryPartsCache_[queryKey];
if (cached) {
return cached;
}
// summary:
// state machine for query tokenization
// description:
// instead of using a brittle and slow regex-based CSS parser,
// dojo.query implements an AST-style query representation. This
// representation is only generated once per query. For example,
// the same query run multiple times or under different root nodes
// does not re-parse the selector expression but instead uses the
// cached data structure. The state machine implemented here
// terminates on the last " " (space) character and returns an
// ordered array of query component structures (or "parts"). Each
// part represents an operator or a simple CSS filtering
// expression. The structure for parts is documented in the code
// below.
// NOTE:
// this code is designed to run fast and compress well. Sacrifices
// to readability and maintainability have been made.
if ('>~+'.indexOf(query.slice(-1)) >= 0) {
// If we end with a ">", "+", or "~", that means we're implicitly
// searching all children, so make it explicit.
query += ' * '
} else {
// if you have not provided a terminator, one will be provided for
// you...
query += ' ';
}
var ts = function(/*Integer*/ s, /*Integer*/ e) {
// trim and slice.
// take an index to start a string slice from and an end position
// and return a trimmed copy of that sub-string
return goog.string.trim(query.slice(s, e));
};
// The overall data graph of the full query, as represented by queryPart
// objects.
var queryParts = [];
// state keeping vars
var inBrackets = -1,
inParens = -1,
inMatchFor = -1,
inPseudo = -1,
inClass = -1,
inId = -1,
inTag = -1,
lc = '',
cc = '',
pStart;
// iteration vars
var x = 0, // index in the query
ql = query.length,
currentPart = null, // data structure representing the entire clause
cp = null; // the current pseudo or attr matcher
// several temporary variables are assigned to this structure during a
// potential sub-expression match:
// attr:
// a string representing the current full attribute match in a
// bracket expression
// type:
// if there's an operator in a bracket expression, this is
// used to keep track of it
// value:
// the internals of parenthetical expression for a pseudo. for
// :nth-child(2n+1), value might be '2n+1'
var endTag = function() {
// called when the tokenizer hits the end of a particular tag name.
// Re-sets state variables for tag matching and sets up the matcher
// to handle the next type of token (tag or operator).
if (inTag >= 0) {
var tv = (inTag == x) ? null : ts(inTag, x);
if ('>~+'.indexOf(tv) < 0) {
currentPart.tag = tv;
} else {
currentPart.oper = tv;
}
inTag = -1;
}
};
var endId = function() {
// Called when the tokenizer might be at the end of an ID portion of a
// match.
if (inId >= 0) {
currentPart.id = ts(inId, x).replace(/\\/g, '');
inId = -1;
}
};
var endClass = function() {
// Called when the tokenizer might be at the end of a class name
// match. CSS allows for multiple classes, so we augment the
// current item with another class in its list.
if (inClass >= 0) {
currentPart.classes.push(ts(inClass + 1, x).replace(/\\/g, ''));
inClass = -1;
}
};
var endAll = function() {
// at the end of a simple fragment, so wall off the matches
endId(); endTag(); endClass();
};
var endPart = function() {
endAll();
if (inPseudo >= 0) {
currentPart.pseudos.push({ name: ts(inPseudo + 1, x) });
}
// Hint to the selector engine to tell it whether or not it
// needs to do any iteration. Many simple selectors don't, and
// we can avoid significant construction-time work by advising
// the system to skip them.
currentPart.loops = currentPart.pseudos.length ||
currentPart.attrs.length ||
currentPart.classes.length;
// save the full expression as a string
currentPart.oquery = currentPart.query = ts(pStart, x);
// otag/tag are hints to suggest to the system whether or not
// it's an operator or a tag. We save a copy of otag since the
// tag name is cast to upper-case in regular HTML matches. The
// system has a global switch to figure out if the current
// expression needs to be case sensitive or not and it will use
// otag or tag accordingly
currentPart.otag = currentPart.tag = (currentPart.oper) ?
null :
(currentPart.tag || '*');
if (currentPart.tag) {
// if we're in a case-insensitive HTML doc, we likely want
// the toUpperCase when matching on element.tagName. If we
// do it here, we can skip the string op per node
// comparison
currentPart.tag = currentPart.tag.toUpperCase();
}
// add the part to the list
if (queryParts.length && (queryParts[queryParts.length - 1].oper)) {
// operators are always infix, so we remove them from the
// list and attach them to the next match. The evaluator is
// responsible for sorting out how to handle them.
currentPart.infixOper = queryParts.pop();
currentPart.query = currentPart.infixOper.query + ' ' +
currentPart.query;
}
queryParts.push(currentPart);
currentPart = null;
}
// iterate over the query, character by character, building up a
// list of query part objects
for (; lc = cc, cc = query.charAt(x), x < ql; x++) {
// cc: the current character in the match
// lc: the last character (if any)
// someone is trying to escape something, so don't try to match any
// fragments. We assume we're inside a literal.
if (lc == '\\') {
continue;
}
if (!currentPart) { // a part was just ended or none has yet been created
// NOTE: I hate all this alloc, but it's shorter than writing tons of
// if's
pStart = x;
// rules describe full CSS sub-expressions, like:
// #someId
// .className:first-child
// but not:
// thinger > div.howdy[type=thinger]
// the individual components of the previous query would be
// split into 3 parts that would be represented a structure
// like:
// [
// {
// query: 'thinger',
// tag: 'thinger',
// },
// {
// query: 'div.howdy[type=thinger]',
// classes: ['howdy'],
// infixOper: {
// query: '>',
// oper: '>',
// }
// },
// ]
currentPart = {
query: null, // the full text of the part's rule
pseudos: [], // CSS supports multiple pseudo-class matches in a single
// rule
attrs: [], // CSS supports multi-attribute match, so we need an array
classes: [], // class matches may be additive,
// e.g.: .thinger.blah.howdy
tag: null, // only one tag...
oper: null, // ...or operator per component. Note that these wind up
// being exclusive.
id: null // the id component of a rule
};
// if we don't have a part, we assume we're going to start at
// the beginning of a match, which should be a tag name. This
// might fault a little later on, but we detect that and this
// iteration will still be fine.
inTag = x;
}
if (inBrackets >= 0) {
// look for a the close first
if (cc == ']') { // if we're in a [...] clause and we end, do assignment
if (!cp.attr) {
// no attribute match was previously begun, so we
// assume this is an attribute existence match in the
// form of [someAttributeName]
cp.attr = ts(inBrackets + 1, x);
} else {
// we had an attribute already, so we know that we're
// matching some sort of value, as in [attrName=howdy]
cp.matchFor = ts((inMatchFor || inBrackets + 1), x);
}
var cmf = cp.matchFor;
if (cmf) {
// try to strip quotes from the matchFor value. We want
// [attrName=howdy] to match the same
// as [attrName = 'howdy' ]
if ((cmf.charAt(0) == '"') || (cmf.charAt(0) == "'")) {
cp.matchFor = cmf.slice(1, -1);
}
}
// end the attribute by adding it to the list of attributes.
currentPart.attrs.push(cp);
cp = null; // necessary?
inBrackets = inMatchFor = -1;
} else if (cc == '=') {
// if the last char was an operator prefix, make sure we
// record it along with the '=' operator.
var addToCc = ('|~^$*'.indexOf(lc) >= 0) ? lc : '';
cp.type = addToCc + cc;
cp.attr = ts(inBrackets + 1, x - addToCc.length);
inMatchFor = x + 1;
}
// now look for other clause parts
} else if (inParens >= 0) {
// if we're in a parenthetical expression, we need to figure
// out if it's attached to a pseudo-selector rule like
// :nth-child(1)
if (cc == ')') {
if (inPseudo >= 0) {
cp.value = ts(inParens + 1, x);
}
inPseudo = inParens = -1;
}
} else if (cc == '#') {
// start of an ID match
endAll();
inId = x + 1;
} else if (cc == '.') {
// start of a class match
endAll();
inClass = x;
} else if (cc == ':') {
// start of a pseudo-selector match
endAll();
inPseudo = x;
} else if (cc == '[') {
// start of an attribute match.
endAll();
inBrackets = x;
// provide a new structure for the attribute match to fill-in
cp = {
/*=====
attr: null, type: null, matchFor: null
=====*/
};
} else if (cc == '(') {
// we really only care if we've entered a parenthetical
// expression if we're already inside a pseudo-selector match
if (inPseudo >= 0) {
// provide a new structure for the pseudo match to fill-in
cp = {
name: ts(inPseudo + 1, x),
value: null
}
currentPart.pseudos.push(cp);
}
inParens = x;
} else if (
(cc == ' ') &&
// if it's a space char and the last char is too, consume the
// current one without doing more work
(lc != cc)
) {
endPart();
}
}
return este.dom.getQueryPartsCache_[queryKey] = queryParts;
};
;