root/mochikit/trunk/MochiKit/Selector.js

Revision 1562, 14.6 KB (checked in by cederberg@…, 3 weeks ago)

Fixed lots of missing semi-colons (#361). Thanks to Niek Kouwenberg for the patch.

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