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