/* Copyright 2008-2013 Clipperz Srl This file is part of Clipperz, the online password manager. For further information about its features and functionalities please refer to http://www.clipperz.com. * Clipperz is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * Clipperz is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. * You should have received a copy of the GNU Affero General Public License along with Clipperz. If not, see http://www.gnu.org/licenses/. */ /*** MochiKit.Selector 1.5 See for documentation, downloads, license, etc. (c) 2005 Bob Ippolito and others. All rights Reserved. ***/ MochiKit.Base.module(MochiKit, 'Selector', '1.5', ['Base', 'DOM', 'Iter']); MochiKit.Selector.Selector = function (expression) { this.params = {classNames: [], pseudoClassNames: []}; this.expression = expression.toString().replace(/(^\s+|\s+$)/g, ''); this.parseExpression(); this.compileMatcher(); }; MochiKit.Selector.Selector.prototype = { /*** Selector class: convenient object to make CSS selections. ***/ __class__: MochiKit.Selector.Selector, /** @id MochiKit.Selector.Selector.prototype.parseExpression */ parseExpression: function () { function abort(message) { throw 'Parse error in selector: ' + message; } if (this.expression == '') { abort('empty expression'); } var repr = MochiKit.Base.repr; var params = this.params; var expr = this.expression; var match, modifier, clause, rest; while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!^$*]?=)(?:"([^"]*)"|([^\]\s]*)))?\]$/i)) { params.attributes = params.attributes || []; params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''}); expr = match[1]; } if (expr == '*') { return this.params.wildcard = true; } while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+(?:\([^)]*\))?)(.*)/i)) { modifier = match[1]; clause = match[2]; rest = match[3]; switch (modifier) { case '#': params.id = clause; break; case '.': params.classNames.push(clause); break; case ':': params.pseudoClassNames.push(clause); break; case '': case undefined: params.tagName = clause.toUpperCase(); break; default: abort(repr(expr)); } expr = rest; } if (expr.length > 0) { abort(repr(expr)); } }, /** @id MochiKit.Selector.Selector.prototype.buildMatchExpression */ buildMatchExpression: function () { var repr = MochiKit.Base.repr; var params = this.params; var conditions = []; var clause, i; function childElements(element) { return "MochiKit.Base.filter(function (node) { return node.nodeType == 1; }, " + element + ".childNodes)"; } if (params.wildcard) { conditions.push('true'); } if (clause = params.id) { conditions.push('element.id == ' + repr(clause)); } if (clause = params.tagName) { conditions.push('element.tagName.toUpperCase() == ' + repr(clause)); } if ((clause = params.classNames).length > 0) { for (i = 0; i < clause.length; i++) { conditions.push('MochiKit.DOM.hasElementClass(element, ' + repr(clause[i]) + ')'); } } if ((clause = params.pseudoClassNames).length > 0) { for (i = 0; i < clause.length; i++) { var match = clause[i].match(/^([^(]+)(?:\((.*)\))?$/); var pseudoClass = match[1]; var pseudoClassArgument = match[2]; switch (pseudoClass) { case 'root': conditions.push('element.nodeType == 9 || element === element.ownerDocument.documentElement'); break; case 'nth-child': case 'nth-last-child': case 'nth-of-type': case 'nth-last-of-type': match = pseudoClassArgument.match(/^((?:(\d+)n\+)?(\d+)|odd|even)$/); if (!match) { throw "Invalid argument to pseudo element nth-child: " + pseudoClassArgument; } var a, b; if (match[0] == 'odd') { a = 2; b = 1; } else if (match[0] == 'even') { a = 2; b = 0; } else { a = match[2] && parseInt(match, 10) || null; b = parseInt(match[3], 10); } conditions.push('this.nthChild(element,' + a + ',' + b + ',' + !!pseudoClass.match('^nth-last') // Reverse + ',' + !!pseudoClass.match('of-type$') // Restrict to same tagName + ')'); break; case 'first-child': conditions.push('this.nthChild(element, null, 1)'); break; case 'last-child': conditions.push('this.nthChild(element, null, 1, true)'); break; case 'first-of-type': conditions.push('this.nthChild(element, null, 1, false, true)'); break; case 'last-of-type': conditions.push('this.nthChild(element, null, 1, true, true)'); break; case 'only-child': conditions.push(childElements('element.parentNode') + '.length == 1'); break; case 'only-of-type': conditions.push('MochiKit.Base.filter(function (node) { return node.tagName == element.tagName; }, ' + childElements('element.parentNode') + ').length == 1'); break; case 'empty': conditions.push('element.childNodes.length == 0'); break; case 'enabled': conditions.push('(this.isUIElement(element) && element.disabled === false)'); break; case 'disabled': conditions.push('(this.isUIElement(element) && element.disabled === true)'); break; case 'checked': conditions.push('(this.isUIElement(element) && element.checked === true)'); break; case 'not': var subselector = new MochiKit.Selector.Selector(pseudoClassArgument); conditions.push('!( ' + subselector.buildMatchExpression() + ')'); break; } } } if (clause = params.attributes) { MochiKit.Base.map(function (attribute) { var value = 'MochiKit.DOM.getNodeAttribute(element, ' + repr(attribute.name) + ')'; var splitValueBy = function (delimiter) { return value + '.split(' + repr(delimiter) + ')'; }; conditions.push(value + ' != null'); switch (attribute.operator) { case '=': conditions.push(value + ' == ' + repr(attribute.value)); break; case '~=': conditions.push('MochiKit.Base.findValue(' + splitValueBy(' ') + ', ' + repr(attribute.value) + ') > -1'); break; case '^=': conditions.push(value + '.substring(0, ' + attribute.value.length + ') == ' + repr(attribute.value)); break; case '$=': conditions.push(value + '.substring(' + value + '.length - ' + attribute.value.length + ') == ' + repr(attribute.value)); break; case '*=': conditions.push(value + '.match(' + repr(attribute.value) + ')'); break; case '|=': conditions.push(splitValueBy('-') + '[0].toUpperCase() == ' + repr(attribute.value.toUpperCase())); break; case '!=': conditions.push(value + ' != ' + repr(attribute.value)); break; case '': case undefined: // Condition already added above break; default: throw 'Unknown operator ' + attribute.operator + ' in selector'; } }, clause); } return conditions.join(' && '); }, /** @id MochiKit.Selector.Selector.prototype.compileMatcher */ compileMatcher: function () { var code = 'return (!element.tagName) ? false : ' + this.buildMatchExpression() + ';'; this.match = new Function('element', code); }, /** @id MochiKit.Selector.Selector.prototype.nthChild */ nthChild: function (element, a, b, reverse, sametag){ var siblings = MochiKit.Base.filter(function (node) { return node.nodeType == 1; }, element.parentNode.childNodes); if (sametag) { siblings = MochiKit.Base.filter(function (node) { return node.tagName == element.tagName; }, siblings); } if (reverse) { siblings = MochiKit.Iter.reversed(siblings); } if (a) { var actualIndex = MochiKit.Base.findIdentical(siblings, element); return ((actualIndex + 1 - b) / a) % 1 == 0; } else { return b == MochiKit.Base.findIdentical(siblings, element) + 1; } }, /** @id MochiKit.Selector.Selector.prototype.isUIElement */ isUIElement: function (element) { return MochiKit.Base.findValue(['input', 'button', 'select', 'option', 'textarea', 'object'], element.tagName.toLowerCase()) > -1; }, /** @id MochiKit.Selector.Selector.prototype.findElements */ findElements: function (scope, axis) { var element; if (axis == undefined) { axis = ""; } function inScope(element, scope) { if (axis == "") { return MochiKit.DOM.isChildNode(element, scope); } else if (axis == ">") { return element.parentNode === scope; } else if (axis == "+") { return element === nextSiblingElement(scope); } else if (axis == "~") { var sibling = scope; while (sibling = nextSiblingElement(sibling)) { if (element === sibling) { return true; } } return false; } else { throw "Invalid axis: " + axis; } } if (element = MochiKit.DOM.getElement(this.params.id)) { if (this.match(element)) { if (!scope || inScope(element, scope)) { return [element]; } } } function nextSiblingElement(node) { node = node.nextSibling; while (node && node.nodeType != 1) { node = node.nextSibling; } return node; } if (axis == "") { scope = (scope || MochiKit.DOM.currentDocument()).getElementsByTagName(this.params.tagName || '*'); } else if (axis == ">") { if (!scope) { throw "> combinator not allowed without preceeding expression"; } scope = MochiKit.Base.filter(function (node) { return node.nodeType == 1; }, scope.childNodes); } else if (axis == "+") { if (!scope) { throw "+ combinator not allowed without preceeding expression"; } scope = nextSiblingElement(scope) && [nextSiblingElement(scope)]; } else if (axis == "~") { if (!scope) { throw "~ combinator not allowed without preceeding expression"; } var newscope = []; while (nextSiblingElement(scope)) { scope = nextSiblingElement(scope); newscope.push(scope); } scope = newscope; } if (!scope) { return []; } var results = MochiKit.Base.filter(MochiKit.Base.bind(function (scopeElt) { return this.match(scopeElt); }, this), scope); return results; }, /** @id MochiKit.Selector.Selector.prototype.repr */ repr: function () { return 'Selector(' + this.expression + ')'; }, toString: MochiKit.Base.forwardCall("repr") }; MochiKit.Base.update(MochiKit.Selector, { /** @id MochiKit.Selector.findChildElements */ findChildElements: function (element, expressions) { element = MochiKit.DOM.getElement(element); var uniq = function(arr) { var res = []; for (var i = 0; i < arr.length; i++) { if (MochiKit.Base.findIdentical(res, arr[i]) < 0) { res.push(arr[i]); } } return res; }; return MochiKit.Base.flattenArray(MochiKit.Base.map(function (expression) { try { var res = element.querySelectorAll(expression); return Array.prototype.slice.call(res, 0); } catch (ignore) { // No querySelectorAll or extended expression syntax used } var nextScope = ""; var reducer = function (results, expr) { var match = expr.match(/^[>+~]$/); if (match) { nextScope = match[0]; return results; } else { var selector = new MochiKit.Selector.Selector(expr); var elements = MochiKit.Iter.reduce(function (elements, result) { return MochiKit.Base.extend(elements, selector.findElements(result || element, nextScope)); }, results, []); nextScope = ""; return elements; } }; var exprs = expression.replace(/(^\s+|\s+$)/g, '').split(/\s+/); return uniq(MochiKit.Iter.reduce(reducer, exprs, [null])); }, expressions)); }, findDocElements: function () { return MochiKit.Selector.findChildElements(MochiKit.DOM.currentDocument(), arguments); }, __new__: function () { this.$$ = this.findDocElements; MochiKit.Base.nameFunctions(this); } }); MochiKit.Selector.__new__(); MochiKit.Base._exportSymbols(this, MochiKit.Selector);