michael@0: /*** michael@0: michael@0: MochiKit.Selector 1.4.2 michael@0: michael@0: See for documentation, downloads, license, etc. michael@0: michael@0: (c) 2005 Bob Ippolito and others. All rights Reserved. michael@0: michael@0: ***/ michael@0: michael@0: MochiKit.Base._deps('Selector', ['Base', 'DOM', 'Iter']); michael@0: michael@0: MochiKit.Selector.NAME = "MochiKit.Selector"; michael@0: MochiKit.Selector.VERSION = "1.4.2"; michael@0: michael@0: MochiKit.Selector.__repr__ = function () { michael@0: return "[" + this.NAME + " " + this.VERSION + "]"; michael@0: }; michael@0: michael@0: MochiKit.Selector.toString = function () { michael@0: return this.__repr__(); michael@0: }; michael@0: michael@0: MochiKit.Selector.EXPORT = [ michael@0: "Selector", michael@0: "findChildElements", michael@0: "findDocElements", michael@0: "$$" michael@0: ]; michael@0: michael@0: MochiKit.Selector.EXPORT_OK = [ michael@0: ]; michael@0: michael@0: MochiKit.Selector.Selector = function (expression) { michael@0: this.params = {classNames: [], pseudoClassNames: []}; michael@0: this.expression = expression.toString().replace(/(^\s+|\s+$)/g, ''); michael@0: this.parseExpression(); michael@0: this.compileMatcher(); michael@0: }; michael@0: michael@0: MochiKit.Selector.Selector.prototype = { michael@0: /*** michael@0: michael@0: Selector class: convenient object to make CSS selections. michael@0: michael@0: ***/ michael@0: __class__: MochiKit.Selector.Selector, michael@0: michael@0: /** @id MochiKit.Selector.Selector.prototype.parseExpression */ michael@0: parseExpression: function () { michael@0: function abort(message) { michael@0: throw 'Parse error in selector: ' + message; michael@0: } michael@0: michael@0: if (this.expression == '') { michael@0: abort('empty expression'); michael@0: } michael@0: michael@0: var repr = MochiKit.Base.repr; michael@0: var params = this.params; michael@0: var expr = this.expression; michael@0: var match, modifier, clause, rest; michael@0: while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!^$*]?=)(?:"([^"]*)"|([^\]\s]*)))?\]$/i)) { michael@0: params.attributes = params.attributes || []; michael@0: params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''}); michael@0: expr = match[1]; michael@0: } michael@0: michael@0: if (expr == '*') { michael@0: return this.params.wildcard = true; michael@0: } michael@0: michael@0: while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+(?:\([^)]*\))?)(.*)/i)) { michael@0: modifier = match[1]; michael@0: clause = match[2]; michael@0: rest = match[3]; michael@0: switch (modifier) { michael@0: case '#': michael@0: params.id = clause; michael@0: break; michael@0: case '.': michael@0: params.classNames.push(clause); michael@0: break; michael@0: case ':': michael@0: params.pseudoClassNames.push(clause); michael@0: break; michael@0: case '': michael@0: case undefined: michael@0: params.tagName = clause.toUpperCase(); michael@0: break; michael@0: default: michael@0: abort(repr(expr)); michael@0: } michael@0: expr = rest; michael@0: } michael@0: michael@0: if (expr.length > 0) { michael@0: abort(repr(expr)); michael@0: } michael@0: }, michael@0: michael@0: /** @id MochiKit.Selector.Selector.prototype.buildMatchExpression */ michael@0: buildMatchExpression: function () { michael@0: var repr = MochiKit.Base.repr; michael@0: var params = this.params; michael@0: var conditions = []; michael@0: var clause, i; michael@0: michael@0: function childElements(element) { michael@0: return "MochiKit.Base.filter(function (node) { return node.nodeType == 1; }, " + element + ".childNodes)"; michael@0: } michael@0: michael@0: if (params.wildcard) { michael@0: conditions.push('true'); michael@0: } michael@0: if (clause = params.id) { michael@0: conditions.push('element.id == ' + repr(clause)); michael@0: } michael@0: if (clause = params.tagName) { michael@0: conditions.push('element.tagName.toUpperCase() == ' + repr(clause)); michael@0: } michael@0: if ((clause = params.classNames).length > 0) { michael@0: for (i = 0; i < clause.length; i++) { michael@0: conditions.push('MochiKit.DOM.hasElementClass(element, ' + repr(clause[i]) + ')'); michael@0: } michael@0: } michael@0: if ((clause = params.pseudoClassNames).length > 0) { michael@0: for (i = 0; i < clause.length; i++) { michael@0: var match = clause[i].match(/^([^(]+)(?:\((.*)\))?$/); michael@0: var pseudoClass = match[1]; michael@0: var pseudoClassArgument = match[2]; michael@0: switch (pseudoClass) { michael@0: case 'root': michael@0: conditions.push('element.nodeType == 9 || element === element.ownerDocument.documentElement'); break; michael@0: case 'nth-child': michael@0: case 'nth-last-child': michael@0: case 'nth-of-type': michael@0: case 'nth-last-of-type': michael@0: match = pseudoClassArgument.match(/^((?:(\d+)n\+)?(\d+)|odd|even)$/); michael@0: if (!match) { michael@0: throw "Invalid argument to pseudo element nth-child: " + pseudoClassArgument; michael@0: } michael@0: var a, b; michael@0: if (match[0] == 'odd') { michael@0: a = 2; michael@0: b = 1; michael@0: } else if (match[0] == 'even') { michael@0: a = 2; michael@0: b = 0; michael@0: } else { michael@0: a = match[2] && parseInt(match) || null; michael@0: b = parseInt(match[3]); michael@0: } michael@0: conditions.push('this.nthChild(element,' + a + ',' + b michael@0: + ',' + !!pseudoClass.match('^nth-last') // Reverse michael@0: + ',' + !!pseudoClass.match('of-type$') // Restrict to same tagName michael@0: + ')'); michael@0: break; michael@0: case 'first-child': michael@0: conditions.push('this.nthChild(element, null, 1)'); michael@0: break; michael@0: case 'last-child': michael@0: conditions.push('this.nthChild(element, null, 1, true)'); michael@0: break; michael@0: case 'first-of-type': michael@0: conditions.push('this.nthChild(element, null, 1, false, true)'); michael@0: break; michael@0: case 'last-of-type': michael@0: conditions.push('this.nthChild(element, null, 1, true, true)'); michael@0: break; michael@0: case 'only-child': michael@0: conditions.push(childElements('element.parentNode') + '.length == 1'); michael@0: break; michael@0: case 'only-of-type': michael@0: conditions.push('MochiKit.Base.filter(function (node) { return node.tagName == element.tagName; }, ' + childElements('element.parentNode') + ').length == 1'); michael@0: break; michael@0: case 'empty': michael@0: conditions.push('element.childNodes.length == 0'); michael@0: break; michael@0: case 'enabled': michael@0: conditions.push('(this.isUIElement(element) && element.disabled === false)'); michael@0: break; michael@0: case 'disabled': michael@0: conditions.push('(this.isUIElement(element) && element.disabled === true)'); michael@0: break; michael@0: case 'checked': michael@0: conditions.push('(this.isUIElement(element) && element.checked === true)'); michael@0: break; michael@0: case 'not': michael@0: var subselector = new MochiKit.Selector.Selector(pseudoClassArgument); michael@0: conditions.push('!( ' + subselector.buildMatchExpression() + ')') michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: if (clause = params.attributes) { michael@0: MochiKit.Base.map(function (attribute) { michael@0: var value = 'MochiKit.DOM.getNodeAttribute(element, ' + repr(attribute.name) + ')'; michael@0: var splitValueBy = function (delimiter) { michael@0: return value + '.split(' + repr(delimiter) + ')'; michael@0: } michael@0: conditions.push(value + ' != null'); michael@0: switch (attribute.operator) { michael@0: case '=': michael@0: conditions.push(value + ' == ' + repr(attribute.value)); michael@0: break; michael@0: case '~=': michael@0: conditions.push('MochiKit.Base.findValue(' + splitValueBy(' ') + ', ' + repr(attribute.value) + ') > -1'); michael@0: break; michael@0: case '^=': michael@0: conditions.push(value + '.substring(0, ' + attribute.value.length + ') == ' + repr(attribute.value)); michael@0: break; michael@0: case '$=': michael@0: conditions.push(value + '.substring(' + value + '.length - ' + attribute.value.length + ') == ' + repr(attribute.value)); michael@0: break; michael@0: case '*=': michael@0: conditions.push(value + '.match(' + repr(attribute.value) + ')'); michael@0: break; michael@0: case '|=': michael@0: conditions.push(splitValueBy('-') + '[0].toUpperCase() == ' + repr(attribute.value.toUpperCase())); michael@0: break; michael@0: case '!=': michael@0: conditions.push(value + ' != ' + repr(attribute.value)); michael@0: break; michael@0: case '': michael@0: case undefined: michael@0: // Condition already added above michael@0: break; michael@0: default: michael@0: throw 'Unknown operator ' + attribute.operator + ' in selector'; michael@0: } michael@0: }, clause); michael@0: } michael@0: michael@0: return conditions.join(' && '); michael@0: }, michael@0: michael@0: /** @id MochiKit.Selector.Selector.prototype.compileMatcher */ michael@0: compileMatcher: function () { michael@0: var code = 'return (!element.tagName) ? false : ' + michael@0: this.buildMatchExpression() + ';'; michael@0: this.match = new Function('element', code); michael@0: }, michael@0: michael@0: /** @id MochiKit.Selector.Selector.prototype.nthChild */ michael@0: nthChild: function (element, a, b, reverse, sametag){ michael@0: var siblings = MochiKit.Base.filter(function (node) { michael@0: return node.nodeType == 1; michael@0: }, element.parentNode.childNodes); michael@0: if (sametag) { michael@0: siblings = MochiKit.Base.filter(function (node) { michael@0: return node.tagName == element.tagName; michael@0: }, siblings); michael@0: } michael@0: if (reverse) { michael@0: siblings = MochiKit.Iter.reversed(siblings); michael@0: } michael@0: if (a) { michael@0: var actualIndex = MochiKit.Base.findIdentical(siblings, element); michael@0: return ((actualIndex + 1 - b) / a) % 1 == 0; michael@0: } else { michael@0: return b == MochiKit.Base.findIdentical(siblings, element) + 1; michael@0: } michael@0: }, michael@0: michael@0: /** @id MochiKit.Selector.Selector.prototype.isUIElement */ michael@0: isUIElement: function (element) { michael@0: return MochiKit.Base.findValue(['input', 'button', 'select', 'option', 'textarea', 'object'], michael@0: element.tagName.toLowerCase()) > -1; michael@0: }, michael@0: michael@0: /** @id MochiKit.Selector.Selector.prototype.findElements */ michael@0: findElements: function (scope, axis) { michael@0: var element; michael@0: michael@0: if (axis == undefined) { michael@0: axis = ""; michael@0: } michael@0: michael@0: function inScope(element, scope) { michael@0: if (axis == "") { michael@0: return MochiKit.DOM.isChildNode(element, scope); michael@0: } else if (axis == ">") { michael@0: return element.parentNode === scope; michael@0: } else if (axis == "+") { michael@0: return element === nextSiblingElement(scope); michael@0: } else if (axis == "~") { michael@0: var sibling = scope; michael@0: while (sibling = nextSiblingElement(sibling)) { michael@0: if (element === sibling) { michael@0: return true; michael@0: } michael@0: } michael@0: return false; michael@0: } else { michael@0: throw "Invalid axis: " + axis; michael@0: } michael@0: } michael@0: michael@0: if (element = MochiKit.DOM.getElement(this.params.id)) { michael@0: if (this.match(element)) { michael@0: if (!scope || inScope(element, scope)) { michael@0: return [element]; michael@0: } michael@0: } michael@0: } michael@0: michael@0: function nextSiblingElement(node) { michael@0: node = node.nextSibling; michael@0: while (node && node.nodeType != 1) { michael@0: node = node.nextSibling; michael@0: } michael@0: return node; michael@0: } michael@0: michael@0: if (axis == "") { michael@0: scope = (scope || MochiKit.DOM.currentDocument()).getElementsByTagName(this.params.tagName || '*'); michael@0: } else if (axis == ">") { michael@0: if (!scope) { michael@0: throw "> combinator not allowed without preceding expression"; michael@0: } michael@0: scope = MochiKit.Base.filter(function (node) { michael@0: return node.nodeType == 1; michael@0: }, scope.childNodes); michael@0: } else if (axis == "+") { michael@0: if (!scope) { michael@0: throw "+ combinator not allowed without preceding expression"; michael@0: } michael@0: scope = nextSiblingElement(scope) && [nextSiblingElement(scope)]; michael@0: } else if (axis == "~") { michael@0: if (!scope) { michael@0: throw "~ combinator not allowed without preceding expression"; michael@0: } michael@0: var newscope = []; michael@0: while (nextSiblingElement(scope)) { michael@0: scope = nextSiblingElement(scope); michael@0: newscope.push(scope); michael@0: } michael@0: scope = newscope; michael@0: } michael@0: michael@0: if (!scope) { michael@0: return []; michael@0: } michael@0: michael@0: var results = MochiKit.Base.filter(MochiKit.Base.bind(function (scopeElt) { michael@0: return this.match(scopeElt); michael@0: }, this), scope); michael@0: michael@0: return results; michael@0: }, michael@0: michael@0: /** @id MochiKit.Selector.Selector.prototype.repr */ michael@0: repr: function () { michael@0: return 'Selector(' + this.expression + ')'; michael@0: }, michael@0: michael@0: toString: MochiKit.Base.forwardCall("repr") michael@0: }; michael@0: michael@0: MochiKit.Base.update(MochiKit.Selector, { michael@0: michael@0: /** @id MochiKit.Selector.findChildElements */ michael@0: findChildElements: function (element, expressions) { michael@0: var uniq = function(arr) { michael@0: var res = []; michael@0: for (var i = 0; i < arr.length; i++) { michael@0: if (MochiKit.Base.findIdentical(res, arr[i]) < 0) { michael@0: res.push(arr[i]); michael@0: } michael@0: } michael@0: return res; michael@0: }; michael@0: return MochiKit.Base.flattenArray(MochiKit.Base.map(function (expression) { michael@0: var nextScope = ""; michael@0: var reducer = function (results, expr) { michael@0: if (match = expr.match(/^[>+~]$/)) { michael@0: nextScope = match[0]; michael@0: return results; michael@0: } else { michael@0: var selector = new MochiKit.Selector.Selector(expr); michael@0: var elements = MochiKit.Iter.reduce(function (elements, result) { michael@0: return MochiKit.Base.extend(elements, selector.findElements(result || element, nextScope)); michael@0: }, results, []); michael@0: nextScope = ""; michael@0: return elements; michael@0: } michael@0: }; michael@0: var exprs = expression.replace(/(^\s+|\s+$)/g, '').split(/\s+/); michael@0: return uniq(MochiKit.Iter.reduce(reducer, exprs, [null])); michael@0: }, expressions)); michael@0: }, michael@0: michael@0: findDocElements: function () { michael@0: return MochiKit.Selector.findChildElements(MochiKit.DOM.currentDocument(), arguments); michael@0: }, michael@0: michael@0: __new__: function () { michael@0: var m = MochiKit.Base; michael@0: michael@0: this.$$ = this.findDocElements; michael@0: michael@0: this.EXPORT_TAGS = { michael@0: ":common": this.EXPORT, michael@0: ":all": m.concat(this.EXPORT, this.EXPORT_OK) michael@0: }; michael@0: michael@0: m.nameFunctions(this); michael@0: } michael@0: }); michael@0: michael@0: MochiKit.Selector.__new__(); michael@0: michael@0: MochiKit.Base._exportSymbols(this, MochiKit.Selector); michael@0: