Commit | Line | Data |
---|---|---|
6a1aa64f DV |
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 |