5 See <http://mochikit.com/> for documentation, downloads, license, etc.
7 (c) 2005 Bob Ippolito and others. All rights Reserved.
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", []);
25 if (typeof(MochiKit
.Base
) === 'undefined' ||
26 typeof(MochiKit
.DOM
) === 'undefined' ||
27 typeof(MochiKit
.Iter
) === 'undefined') {
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
= [
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 = {
70 Selector class: convenient object to make CSS selections.
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
;
81 if (this.expression
== '') {
82 abort('empty expression');
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] || ''});
96 return this.params
.wildcard
= true;
99 while (match
= expr
.match(/^([^a-z0-9_-])?([a-z0-9_-]+(?:\([^)]*\))?)(.*)/i)) {
108 params
.classNames
.push(clause
);
111 params
.pseudoClassNames
.push(clause
);
115 params
.tagName
= clause
.toUpperCase();
123 if (expr
.length
> 0) {
128 /** @id MochiKit.Selector.Selector.prototype.buildMatchExpression */
129 buildMatchExpression: function () {
130 var repr
= MochiKit
.Base
.repr
;
131 var params
= this.params
;
135 function childElements(element
) {
136 return "MochiKit.Base.filter(function (node) { return node.nodeType == 1; }, " + element
+ ".childNodes)";
139 if (params
.wildcard
) {
140 conditions
.push('true');
142 if (clause
= params
.id
) {
143 conditions
.push('element.id == ' + repr(clause
));
145 if (clause
= params
.tagName
) {
146 conditions
.push('element.tagName.toUpperCase() == ' + repr(clause
));
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
]) + ')');
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
) {
160 conditions
.push('element.nodeType == 9 || element === element.ownerDocument.documentElement'); break;
162 case 'nth-last-child':
164 case 'nth-last-of-type':
165 match
= pseudoClassArgument
.match(/^((?:(\d+)n\+)?(\d+)|odd|even)$/);
167 throw "Invalid argument to pseudo element nth-child: " + pseudoClassArgument
;
170 if (match
[0] == 'odd') {
173 } else if (match
[0] == 'even') {
177 a
= match
[2] && parseInt(match
) || null;
178 b
= parseInt(match
[3]);
180 conditions
.push('this.nthChild(element,' + a
+ ',' + b
181 + ',' + !!pseudoClass
.match('^nth-last') // Reverse
182 + ',' + !!pseudoClass
.match('of-type$') // Restrict to same tagName
186 conditions
.push('this.nthChild(element, null, 1)');
189 conditions
.push('this.nthChild(element, null, 1, true)');
191 case 'first-of-type':
192 conditions
.push('this.nthChild(element, null, 1, false, true)');
195 conditions
.push('this.nthChild(element, null, 1, true, true)');
198 conditions
.push(childElements('element.parentNode') + '.length == 1');
201 conditions
.push('MochiKit.Base.filter(function (node) { return node.tagName == element.tagName; }, ' + childElements('element.parentNode') + ').length == 1');
204 conditions
.push('element.childNodes.length == 0');
207 conditions
.push('(this.isUIElement(element) && element.disabled === false)');
210 conditions
.push('(this.isUIElement(element) && element.disabled === true)');
213 conditions
.push('(this.isUIElement(element) && element.checked === true)');
216 var subselector
= new MochiKit
.Selector
.Selector(pseudoClassArgument
);
217 conditions
.push('!( ' + subselector
.buildMatchExpression() + ')')
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
) + ')';
229 switch (attribute
.operator
) {
231 conditions
.push(value
+ ' == ' + repr(attribute
.value
));
234 conditions
.push('MochiKit.Base.findValue(' + splitValueBy(' ') + ', ' + repr(attribute
.value
) + ') > -1');
237 conditions
.push(value
+ '.substring(0, ' + attribute
.value
.length
+ ') == ' + repr(attribute
.value
));
240 conditions
.push(value
+ '.substring(' + value
+ '.length - ' + attribute
.value
.length
+ ') == ' + repr(attribute
.value
));
243 conditions
.push(value
+ '.match(' + repr(attribute
.value
) + ')');
247 splitValueBy('-') + '[0].toUpperCase() == ' + repr(attribute
.value
.toUpperCase())
251 conditions
.push(value
+ ' != ' + repr(attribute
.value
));
255 conditions
.push(value
+ ' != null');
258 throw 'Unknown operator ' + attribute
.operator
+ ' in selector';
263 return conditions
.join(' && ');
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());
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
);
278 siblings
= MochiKit
.Base
.filter(function (node
) {
279 return node
.tagName
== element
.tagName
;
283 siblings
= MochiKit
.Iter
.reversed(siblings
);
286 var actualIndex
= MochiKit
.Base
.findIdentical(siblings
, element
);
287 return ((actualIndex
+ 1 - b
) / a
) % 1 == 0;
289 return b
== MochiKit
.Base
.findIdentical(siblings
, element
) + 1;
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;
299 /** @id MochiKit.Selector.Selector.prototype.findElements */
300 findElements: function (scope
, axis
) {
303 if (axis
== undefined) {
307 function inScope(element
, scope
) {
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
== "~") {
316 while (sibling
= nextSiblingElement(sibling
)) {
317 if (element
== sibling
) {
323 throw "Invalid axis: " + axis
;
327 if (element
= MochiKit
.DOM
.getElement(this.params
.id
)) {
328 if (this.match(element
)) {
329 if (!scope
|| inScope(element
, scope
)) {
335 function nextSiblingElement(node
) {
336 node
= node
.nextSibling
;
337 while (node
&& node
.nodeType
!= 1) {
338 node
= node
.nextSibling
;
344 scope
= (scope
|| currentDocument()).getElementsByTagName(this.params
.tagName
|| '*');
345 } else if (axis
== ">") {
347 throw "> combinator not allowed without preceeding expression";
349 scope
= MochiKit
.Base
.filter(function (node
) {
350 return node
.nodeType
== 1;
351 }, scope
.childNodes
);
352 } else if (axis
== "+") {
354 throw "+ combinator not allowed without preceeding expression";
356 scope
= nextSiblingElement(scope
) && [nextSiblingElement(scope
)];
357 } else if (axis
== "~") {
359 throw "~ combinator not allowed without preceeding expression";
362 while (nextSiblingElement(scope
)) {
363 scope
= nextSiblingElement(scope
);
364 newscope
.push(scope
);
373 var results
= MochiKit
.Base
.filter(MochiKit
.Base
.bind(function (scopeElt
) {
374 return this.match(scopeElt
);
380 /** @id MochiKit.Selector.Selector.prototype.repr */
382 return 'Selector(' + this.expression
+ ')';
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
) {
394 return MochiKit
.Iter
.reduce(function (results
, expr
) {
395 if (match
= expr
.match(/^[>+~]$/)) {
396 nextScope
= match
[0];
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
));
406 }, expression
.replace(/(^\s+|\s+$)/g, '').split(/\s+/), [null]);
410 findDocElements: function () {
411 return MochiKit
.Selector
.findChildElements(MochiKit
.DOM
.currentDocument(), arguments
);
414 __new__: function () {
415 var m
= MochiKit
.Base
;
417 this.$$ = this.findDocElements
;
420 ":common": this.EXPORT
,
421 ":all": m
.concat(this.EXPORT
, this.EXPORT_OK
)
424 m
.nameFunctions(this);
428 MochiKit
.Selector
.__new__();
430 MochiKit
.Base
._exportSymbols(this, MochiKit
.Selector
);