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