Selector.js (15739B)
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);