SGN::Test::WWW::Mechanize should use the SGN_TEST_SERVER env var
[sgn.git] / js / MochiKit / Selector.js
blobc385df71e1d18eb2b11a88f9d1089b3dc5fb8eef
1 /***
3 MochiKit.Selector 1.4
5 See <http://mochikit.com/> for documentation, downloads, license, etc.
7 (c) 2005 Bob Ippolito and others.  All rights Reserved.
9 ***/
11 if (typeof(dojo) != 'undefined') {
12     dojo.provide('MochiKit.Selector');
13     dojo.require('MochiKit.Base');
14     dojo.require('MochiKit.DOM');
15     dojo.require('MochiKit.Iter');
18 if (typeof(JSAN) != 'undefined') {
19     JSAN.use("MochiKit.Base", []);
20     JSAN.use("MochiKit.DOM", []);
21     JSAN.use("MochiKit.Iter", []);
24 try {
25     if (typeof(MochiKit.Base) === 'undefined' ||
26         typeof(MochiKit.DOM) === 'undefined' ||
27         typeof(MochiKit.Iter) === 'undefined') {
28         throw "";
29     }
30 } catch (e) {
31     throw "MochiKit.Selector depends on MochiKit.Base, MochiKit.DOM and MochiKit.Iter!";
34 if (typeof(MochiKit.Selector) == 'undefined') {
35     MochiKit.Selector = {};
38 MochiKit.Selector.NAME = "MochiKit.Selector";
40 MochiKit.Selector.VERSION = "1.4";
42 MochiKit.Selector.__repr__ = function () {
43     return "[" + this.NAME + " " + this.VERSION + "]";
46 MochiKit.Selector.toString = function () {
47     return this.__repr__();
50 MochiKit.Selector.EXPORT = [
51     "Selector",
52     "findChildElements",
53     "findDocElements",
54     "$$"
57 MochiKit.Selector.EXPORT_OK = [
60 MochiKit.Selector.Selector = function (expression) {
61     this.params = {classNames: [], pseudoClassNames: []};
62     this.expression = expression.toString().replace(/(^\s+|\s+$)/g, '');
63     this.parseExpression();
64     this.compileMatcher();
67 MochiKit.Selector.Selector.prototype = {
68     /***
70     Selector class: convenient object to make CSS selections.
72     ***/
73     __class__: MochiKit.Selector.Selector,
75     /** @id MochiKit.Selector.Selector.prototype.parseExpression */
76     parseExpression: function () {
77         function abort(message) {
78             throw 'Parse error in selector: ' + message;
79         }
81         if (this.expression == '')  {
82             abort('empty expression');
83         }
85         var repr = MochiKit.Base.repr;
86         var params = this.params;
87         var expr = this.expression;
88         var match, modifier, clause, rest;
89         while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!^$*]?=)(?:"([^"]*)"|([^\]\s]*)))?\]$/i)) {
90             params.attributes = params.attributes || [];
91             params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''});
92             expr = match[1];
93         }
95         if (expr == '*') {
96             return this.params.wildcard = true;
97         }
99         while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+(?:\([^)]*\))?)(.*)/i)) {
100             modifier = match[1];
101             clause = match[2];
102             rest = match[3];
103             switch (modifier) {
104                 case '#':
105                     params.id = clause;
106                     break;
107                 case '.':
108                     params.classNames.push(clause);
109                     break;
110                 case ':':
111                     params.pseudoClassNames.push(clause);
112                     break;
113                 case '':
114                 case undefined:
115                     params.tagName = clause.toUpperCase();
116                     break;
117                 default:
118                     abort(repr(expr));
119             }
120             expr = rest;
121         }
123         if (expr.length > 0) {
124             abort(repr(expr));
125         }
126     },
128     /** @id MochiKit.Selector.Selector.prototype.buildMatchExpression */
129     buildMatchExpression: function () {
130         var repr = MochiKit.Base.repr;
131         var params = this.params;
132         var conditions = [];
133         var clause, i;
135         function childElements(element) {
136             return "MochiKit.Base.filter(function (node) { return node.nodeType == 1; }, " + element + ".childNodes)";
137         }
139         if (params.wildcard) {
140             conditions.push('true');
141         }
142         if (clause = params.id) {
143             conditions.push('element.id == ' + repr(clause));
144         }
145         if (clause = params.tagName) {
146             conditions.push('element.tagName.toUpperCase() == ' + repr(clause));
147         }
148         if ((clause = params.classNames).length > 0) {
149             for (i = 0; i < clause.length; i++) {
150                 conditions.push('MochiKit.DOM.hasElementClass(element, ' + repr(clause[i]) + ')');
151             }
152         }
153         if ((clause = params.pseudoClassNames).length > 0) {
154             for (i = 0; i < clause.length; i++) {
155                 var match = clause[i].match(/^([^(]+)(?:\((.*)\))?$/);
156                 var pseudoClass = match[1];
157                 var pseudoClassArgument = match[2];
158                 switch (pseudoClass) {
159                     case 'root':
160                         conditions.push('element.nodeType == 9 || element === element.ownerDocument.documentElement'); break;
161                     case 'nth-child':
162                     case 'nth-last-child':
163                     case 'nth-of-type':
164                     case 'nth-last-of-type':
165                         match = pseudoClassArgument.match(/^((?:(\d+)n\+)?(\d+)|odd|even)$/);
166                         if (!match) {
167                             throw "Invalid argument to pseudo element nth-child: " + pseudoClassArgument;
168                         }
169                         var a, b;
170                         if (match[0] == 'odd') {
171                             a = 2;
172                             b = 1;
173                         } else if (match[0] == 'even') {
174                             a = 2;
175                             b = 0;
176                         } else {
177                             a = match[2] && parseInt(match) || null;
178                             b = parseInt(match[3]);
179                         }
180                         conditions.push('this.nthChild(element,' + a + ',' + b
181                                         + ',' + !!pseudoClass.match('^nth-last')    // Reverse
182                                         + ',' + !!pseudoClass.match('of-type$')     // Restrict to same tagName
183                                         + ')');
184                         break;
185                     case 'first-child':
186                         conditions.push('this.nthChild(element, null, 1)');
187                         break;
188                     case 'last-child':
189                         conditions.push('this.nthChild(element, null, 1, true)');
190                         break;
191                     case 'first-of-type':
192                         conditions.push('this.nthChild(element, null, 1, false, true)');
193                         break;
194                     case 'last-of-type':
195                         conditions.push('this.nthChild(element, null, 1, true, true)');
196                         break;
197                     case 'only-child':
198                         conditions.push(childElements('element.parentNode') + '.length == 1');
199                         break;
200                     case 'only-of-type':
201                         conditions.push('MochiKit.Base.filter(function (node) { return node.tagName == element.tagName; }, ' + childElements('element.parentNode') + ').length == 1');
202                         break;
203                     case 'empty':
204                         conditions.push('element.childNodes.length == 0');
205                         break;
206                     case 'enabled':
207                         conditions.push('(this.isUIElement(element) && element.disabled === false)');
208                         break;
209                     case 'disabled':
210                         conditions.push('(this.isUIElement(element) && element.disabled === true)');
211                         break;
212                     case 'checked':
213                         conditions.push('(this.isUIElement(element) && element.checked === true)');
214                         break;
215                     case 'not':
216                         var subselector = new MochiKit.Selector.Selector(pseudoClassArgument);
217                         conditions.push('!( ' + subselector.buildMatchExpression() + ')')
218                         break;
219                 }
220             }
221         }
222         if (clause = params.attributes) {
223             MochiKit.Base.map(function (attribute) {
224                 var value = 'MochiKit.DOM.getNodeAttribute(element, ' + repr(attribute.name) + ')';
225                 var splitValueBy = function (delimiter) {
226                     return value + ' && ' + value + '.split(' + repr(delimiter) + ')';
227                 }
229                 switch (attribute.operator) {
230                     case '=':
231                         conditions.push(value + ' == ' + repr(attribute.value));
232                         break;
233                     case '~=':
234                         conditions.push('MochiKit.Base.findValue(' + splitValueBy(' ') + ', ' + repr(attribute.value) + ') > -1');
235                         break;
236                     case '^=':
237                         conditions.push(value + '.substring(0, ' + attribute.value.length + ') == ' + repr(attribute.value));
238                         break;
239                     case '$=':
240                         conditions.push(value + '.substring(' + value + '.length - ' + attribute.value.length + ') == ' + repr(attribute.value));
241                         break;
242                     case '*=':
243                         conditions.push(value + '.match(' + repr(attribute.value) + ')');
244                         break;
245                     case '|=':
246                         conditions.push(
247                             splitValueBy('-') + '[0].toUpperCase() == ' + repr(attribute.value.toUpperCase())
248                         );
249                         break;
250                     case '!=':
251                         conditions.push(value + ' != ' + repr(attribute.value));
252                         break;
253                     case '':
254                     case undefined:
255                         conditions.push(value + ' != null');
256                         break;
257                     default:
258                         throw 'Unknown operator ' + attribute.operator + ' in selector';
259                 }
260             }, clause);
261         }
263         return conditions.join(' && ');
264     },
266     /** @id MochiKit.Selector.Selector.prototype.compileMatcher */
267     compileMatcher: function () {
268         this.match = new Function('element', 'if (!element.tagName) return false; \
269                 return ' + this.buildMatchExpression());
270     },
272     /** @id MochiKit.Selector.Selector.prototype.nthChild */
273     nthChild: function (element, a, b, reverse, sametag){
274         var siblings = MochiKit.Base.filter(function (node) {
275             return node.nodeType == 1;
276         }, element.parentNode.childNodes);
277         if (sametag) {
278             siblings = MochiKit.Base.filter(function (node) {
279                 return node.tagName == element.tagName;
280             }, siblings);
281         }
282         if (reverse) {
283             siblings = MochiKit.Iter.reversed(siblings);
284         }
285         if (a) {
286             var actualIndex = MochiKit.Base.findIdentical(siblings, element);
287             return ((actualIndex + 1 - b) / a) % 1 == 0;
288         } else {
289             return b == MochiKit.Base.findIdentical(siblings, element) + 1;
290         }
291     },
293     /** @id MochiKit.Selector.Selector.prototype.isUIElement */
294     isUIElement: function (element) {
295         return findValue(['input', 'button', 'select', 'option', 'textarea', 'object'],
296                 element.tagName.toLowerCase()) > -1;
297     },
299     /** @id MochiKit.Selector.Selector.prototype.findElements */
300     findElements: function (scope, axis) {
301         var element;
303         if (axis == undefined) {
304             axis = "";
305         }
307         function inScope(element, scope) {
308             if (axis == "") {
309                 return MochiKit.DOM.isChildNode(element, scope);
310             } else if (axis == ">") {
311                 return element.parentNode == scope;
312             } else if (axis == "+") {
313                 return element == nextSiblingElement(scope);
314             } else if (axis == "~") {
315                 var sibling = scope;
316                 while (sibling = nextSiblingElement(sibling)) {
317                     if (element == sibling) {
318                         return true;
319                     }
320                 }
321                 return false;
322             } else {
323                 throw "Invalid axis: " + axis;
324             }
325         }
327         if (element = MochiKit.DOM.getElement(this.params.id)) {
328             if (this.match(element)) {
329                 if (!scope || inScope(element, scope)) {
330                     return [element];
331                 }
332             }
333         }
335         function nextSiblingElement(node) {
336             node = node.nextSibling;
337             while (node && node.nodeType != 1) {
338                 node = node.nextSibling;
339             }
340             return node;
341         }
343         if (axis == "") {
344             scope = (scope || currentDocument()).getElementsByTagName(this.params.tagName || '*');
345         } else if (axis == ">") {
346             if (!scope) {
347                 throw "> combinator not allowed without preceeding expression";
348             }
349             scope = MochiKit.Base.filter(function (node) {
350                 return node.nodeType == 1;
351             }, scope.childNodes);
352         } else if (axis == "+") {
353             if (!scope) {
354                 throw "+ combinator not allowed without preceeding expression";
355             }
356             scope = nextSiblingElement(scope) && [nextSiblingElement(scope)];
357         } else if (axis == "~") {
358             if (!scope) {
359                 throw "~ combinator not allowed without preceeding expression";
360             }
361             var newscope = [];
362             while (nextSiblingElement(scope)) {
363                 scope = nextSiblingElement(scope);
364                 newscope.push(scope);
365             }
366             scope = newscope;
367         }
369         if (!scope) {
370             return [];
371         }
373         var results = MochiKit.Base.filter(MochiKit.Base.bind(function (scopeElt) {
374             return this.match(scopeElt);
375         }, this), scope);
377         return results;
378     },
380     /** @id MochiKit.Selector.Selector.prototype.repr */
381     repr: function () {
382         return 'Selector(' + this.expression + ')';
383     },
385     toString: MochiKit.Base.forwardCall("repr")
388 MochiKit.Base.update(MochiKit.Selector, {
390     /** @id MochiKit.Selector.findChildElements */
391     findChildElements: function (element, expressions) {
392         return MochiKit.Base.flattenArray(MochiKit.Base.map(function (expression) {
393             var nextScope = "";
394             return MochiKit.Iter.reduce(function (results, expr) {
395                 if (match = expr.match(/^[>+~]$/)) {
396                     nextScope = match[0];
397                     return results;
398                 } else {
399                     var selector = new MochiKit.Selector.Selector(expr);
400                     var elements = MochiKit.Iter.reduce(function (elements, result) {
401                         return MochiKit.Base.extend(elements, selector.findElements(result || element, nextScope));
402                     }, results, []);
403                     nextScope = "";
404                     return elements;
405                 }
406             }, expression.replace(/(^\s+|\s+$)/g, '').split(/\s+/), [null]);
407         }, expressions));
408     },
410     findDocElements: function () {
411         return MochiKit.Selector.findChildElements(MochiKit.DOM.currentDocument(), arguments);
412     },
414     __new__: function () {
415         var m = MochiKit.Base;
417         this.$$ = this.findDocElements;
419         this.EXPORT_TAGS = {
420             ":common": this.EXPORT,
421             ":all": m.concat(this.EXPORT, this.EXPORT_OK)
422         };
424         m.nameFunctions(this);
425     }
428 MochiKit.Selector.__new__();
430 MochiKit.Base._exportSymbols(this, MochiKit.Selector);