switch to YUI compressor for making packed JS -- 49k -> 42k
[dygraphs.git] / mochikit_v14 / MochiKit / Selector.js
1 /***
2
3 MochiKit.Selector 1.4
4
5 See <http://mochikit.com/> for documentation, downloads, license, etc.
6
7 (c) 2005 Bob Ippolito and others. All rights Reserved.
8
9 ***/
10
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');
16 }
17
18 if (typeof(JSAN) != 'undefined') {
19 JSAN.use("MochiKit.Base", []);
20 JSAN.use("MochiKit.DOM", []);
21 JSAN.use("MochiKit.Iter", []);
22 }
23
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!";
32 }
33
34 if (typeof(MochiKit.Selector) == 'undefined') {
35 MochiKit.Selector = {};
36 }
37
38 MochiKit.Selector.NAME = "MochiKit.Selector";
39
40 MochiKit.Selector.VERSION = "1.4";
41
42 MochiKit.Selector.__repr__ = function () {
43 return "[" + this.NAME + " " + this.VERSION + "]";
44 };
45
46 MochiKit.Selector.toString = function () {
47 return this.__repr__();
48 };
49
50 MochiKit.Selector.EXPORT = [
51 "Selector",
52 "findChildElements",
53 "findDocElements",
54 "$$"
55 ];
56
57 MochiKit.Selector.EXPORT_OK = [
58 ];
59
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();
65 };
66
67 MochiKit.Selector.Selector.prototype = {
68 /***
69
70 Selector class: convenient object to make CSS selections.
71
72 ***/
73 __class__: MochiKit.Selector.Selector,
74
75 /** @id MochiKit.Selector.Selector.prototype.parseExpression */
76 parseExpression: function () {
77 function abort(message) {
78 throw 'Parse error in selector: ' + message;
79 }
80
81 if (this.expression == '') {
82 abort('empty expression');
83 }
84
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 }
94
95 if (expr == '*') {
96 return this.params.wildcard = true;
97 }
98
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 }
122
123 if (expr.length > 0) {
124 abort(repr(expr));
125 }
126 },
127
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;
134
135 function childElements(element) {
136 return "MochiKit.Base.filter(function (node) { return node.nodeType == 1; }, " + element + ".childNodes)";
137 }
138
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 + '.split(' + repr(delimiter) + ')';
227 }
228
229 switch (attribute.operator) {
230 case '=':
231 conditions.push(value + ' == ' + repr(attribute.value));
232 break;
233 case '~=':
234 conditions.push(value + ' && 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 value + ' && ' + 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 }
262
263 return conditions.join(' && ');
264 },
265
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 },
271
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 },
292
293 /** @id MochiKit.Selector.Selector.prototype.isUIElement */
294 isUIElement: function (element) {
295 return MochiKit.Base.findValue(['input', 'button', 'select', 'option', 'textarea', 'object'],
296 element.tagName.toLowerCase()) > -1;
297 },
298
299 /** @id MochiKit.Selector.Selector.prototype.findElements */
300 findElements: function (scope, axis) {
301 var element;
302
303 if (axis == undefined) {
304 axis = "";
305 }
306
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 }
326
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 }
334
335 function nextSiblingElement(node) {
336 node = node.nextSibling;
337 while (node && node.nodeType != 1) {
338 node = node.nextSibling;
339 }
340 return node;
341 }
342
343 if (axis == "") {
344 scope = (scope || MochiKit.DOM.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 }
368
369 if (!scope) {
370 return [];
371 }
372
373 var results = MochiKit.Base.filter(MochiKit.Base.bind(function (scopeElt) {
374 return this.match(scopeElt);
375 }, this), scope);
376
377 return results;
378 },
379
380 /** @id MochiKit.Selector.Selector.prototype.repr */
381 repr: function () {
382 return 'Selector(' + this.expression + ')';
383 },
384
385 toString: MochiKit.Base.forwardCall("repr")
386 };
387
388 MochiKit.Base.update(MochiKit.Selector, {
389
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 },
409
410 findDocElements: function () {
411 return MochiKit.Selector.findChildElements(MochiKit.DOM.currentDocument(), arguments);
412 },
413
414 __new__: function () {
415 var m = MochiKit.Base;
416
417 this.$$ = this.findDocElements;
418
419 this.EXPORT_TAGS = {
420 ":common": this.EXPORT,
421 ":all": m.concat(this.EXPORT, this.EXPORT_OK)
422 };
423
424 m.nameFunctions(this);
425 }
426 });
427
428 MochiKit.Selector.__new__();
429
430 MochiKit.Base._exportSymbols(this, MochiKit.Selector);
431