| 39 | | /*===== |
| | 39 | ;(function(){ |
| | 40 | // define everything in a closure for compressability reasons. "d" is an |
| | 41 | // alias to "dojo" since it's so frequently used. This seems a |
| | 42 | // transformation that the build system could perform on a per-file basis. |
| | 43 | |
| | 44 | //////////////////////////////////////////////////////////////////////// |
| | 45 | // Utility code |
| | 46 | //////////////////////////////////////////////////////////////////////// |
| | 47 | |
| | 48 | var d = dojo; |
| | 49 | var childNodesName = dojo.isIE ? "children" : "childNodes"; |
| | 50 | |
| | 51 | var getQueryParts = function(query){ |
| | 52 | // summary: state machine for query tokenization |
| | 53 | if(">~+".indexOf(query.charAt(query.length-1)) >= 0){ |
| | 54 | query += " *" |
| | 55 | } |
| | 56 | query += " "; // ensure that we terminate the state machine |
| | 57 | |
| | 58 | var ts = function(s, e){ |
| | 59 | return d.trim(query.slice(s, e)); |
| | 60 | } |
| | 61 | |
| | 62 | // the overall data graph of the full query, as represented by queryPart objects |
| | 63 | var qparts = []; |
| | 64 | // state keeping vars |
| | 65 | var inBrackets = -1; |
| | 66 | var inParens = -1; |
| | 67 | var inMatchFor = -1; |
| | 68 | var inPseudo = -1; |
| | 69 | var inClass = -1; |
| | 70 | var inId = -1; |
| | 71 | var inTag = -1; |
| | 72 | var lc = ""; // the last character |
| | 73 | var cc = ""; // the current character |
| | 74 | var pStart; |
| | 75 | // iteration vars |
| | 76 | var x = 0; // index in the query |
| | 77 | var ql = query.length; |
| | 78 | var currentPart = null; // data structure representing the entire clause |
| | 79 | var _cp = null; // the current pseudo or attr matcher |
| | 80 | |
| | 81 | var endTag = function(){ |
| | 82 | if(inTag >= 0){ |
| | 83 | var tv = (inTag == x) ? null : ts(inTag, x).toLowerCase(); |
| | 84 | currentPart[ (">~+".indexOf(tv) < 0) ? "tag" : "oper" ] = tv; |
| | 85 | inTag = -1; |
| | 86 | } |
| | 87 | } |
| | 88 | |
| | 89 | var endId = function(){ |
| | 90 | if(inId >= 0){ |
| | 91 | currentPart.id = ts(inId, x).replace(/\\/g, ""); |
| | 92 | inId = -1; |
| | 93 | } |
| | 94 | } |
| | 95 | |
| | 96 | var endClass = function(){ |
| | 97 | if(inClass >= 0){ |
| | 98 | currentPart.classes.push(ts(inClass+1, x).replace(/\\/g, "")); |
| | 99 | inClass = -1; |
| | 100 | } |
| | 101 | } |
| | 102 | |
| | 103 | var endAll = function(){ |
| | 104 | endId(); endTag(); endClass(); |
| | 105 | } |
| | 106 | |
| | 107 | for(; lc=cc, cc=query.charAt(x),x<ql; x++){ |
| | 108 | if(lc == "\\"){ continue; } |
| | 109 | if(!currentPart){ |
| | 110 | // NOTE: I hate all this alloc, but it's shorter than writing tons of if's |
| | 111 | pStart = x; |
| | 112 | currentPart = { |
| | 113 | query: null, |
| | 114 | pseudos: [], |
| | 115 | attrs: [], |
| | 116 | classes: [], |
| | 117 | tag: null, |
| | 118 | oper: null, |
| | 119 | id: null |
| | 120 | }; |
| | 121 | inTag = x; |
| | 122 | } |
| | 123 | |
| | 124 | if(inBrackets >= 0){ |
| | 125 | // look for a the close first |
| | 126 | if(cc == "]"){ |
| | 127 | if(!_cp.attr){ |
| | 128 | _cp.attr = ts(inBrackets+1, x); |
| | 129 | }else{ |
| | 130 | _cp.matchFor = ts((inMatchFor||inBrackets+1), x); |
| | 131 | } |
| | 132 | var cmf = _cp.matchFor; |
| | 133 | if(cmf){ |
| | 134 | if( (cmf.charAt(0) == '"') || (cmf.charAt(0) == "'") ){ |
| | 135 | _cp.matchFor = cmf.substring(1, cmf.length-1); |
| | 136 | } |
| | 137 | } |
| | 138 | currentPart.attrs.push(_cp); |
| | 139 | _cp = null; // necessaray? |
| | 140 | inBrackets = inMatchFor = -1; |
| | 141 | }else if(cc == "="){ |
| | 142 | var addToCc = ("|~^$*".indexOf(lc) >=0 ) ? lc : ""; |
| | 143 | _cp.type = addToCc+cc; |
| | 144 | _cp.attr = ts(inBrackets+1, x-addToCc.length); |
| | 145 | inMatchFor = x+1; |
| | 146 | } |
| | 147 | // now look for other clause parts |
| | 148 | }else if(inParens >= 0){ |
| | 149 | if(cc == ")"){ |
| | 150 | if(inPseudo >= 0){ |
| | 151 | _cp.value = ts(inParens+1, x); |
| | 152 | } |
| | 153 | inPseudo = inParens = -1; |
| | 154 | } |
| | 155 | }else if(cc == "#"){ |
| | 156 | endAll(); |
| | 157 | inId = x+1; |
| | 158 | }else if(cc == "."){ |
| | 159 | endAll(); |
| | 160 | inClass = x; |
| | 161 | }else if(cc == ":"){ |
| | 162 | endAll(); |
| | 163 | inPseudo = x; |
| | 164 | }else if(cc == "["){ |
| | 165 | endAll(); |
| | 166 | inBrackets = x; |
| | 167 | _cp = { |
| | 168 | /*===== |
| | 169 | attr: null, type: null, matchFor: null |
| | 170 | =====*/ |
| | 171 | }; |
| | 172 | }else if(cc == "("){ |
| | 173 | if(inPseudo >= 0){ |
| | 174 | _cp = { |
| | 175 | name: ts(inPseudo+1, x), |
| | 176 | value: null |
| | 177 | } |
| | 178 | currentPart.pseudos.push(_cp); |
| | 179 | } |
| | 180 | inParens = x; |
| | 181 | }else if(cc == " " && lc != cc){ |
| | 182 | // note that we expect the string to be " " terminated |
| | 183 | endAll(); |
| | 184 | if(inPseudo >= 0){ |
| | 185 | currentPart.pseudos.push({ name: ts(inPseudo+1, x) }); |
| | 186 | } |
| | 187 | currentPart.hasLoops = ( |
| | 188 | currentPart.pseudos.length || |
| | 189 | currentPart.attrs.length || |
| | 190 | currentPart.classes.length ); |
| | 191 | currentPart.query = ts(pStart, x); |
| | 192 | currentPart.tag = (currentPart["oper"]) ? null : (currentPart.tag || "*"); |
| | 193 | qparts.push(currentPart); |
| | 194 | currentPart = null; |
| | 195 | } |
| | 196 | } |
| | 197 | return qparts; |
| | 198 | }; |
| | 199 | |
| | 200 | |
| | 201 | //////////////////////////////////////////////////////////////////////// |
| | 202 | // XPath query code |
| | 203 | //////////////////////////////////////////////////////////////////////// |
| | 204 | |
| | 205 | // this array is a lookup used to generate an attribute matching function. |
| | 206 | // There is a similar lookup/generator list for the DOM branch with similar |
| | 207 | // calling semantics. |
| | 208 | var xPathAttrs = { |
| | 209 | "*=": function(attr, value){ |
| | 210 | return "[contains(@"+attr+", '"+ value +"')]"; |
| | 211 | }, |
| | 212 | "^=": function(attr, value){ |
| | 213 | return "[starts-with(@"+attr+", '"+ value +"')]"; |
| | 214 | }, |
| | 215 | "$=": function(attr, value){ |
| | 216 | return "[substring(@"+attr+", string-length(@"+attr+")-"+(value.length-1)+")='"+value+"']"; |
| | 217 | }, |
| | 218 | "~=": function(attr, value){ |
| | 219 | return "[contains(concat(' ',@"+attr+",' '), ' "+ value +" ')]"; |
| | 220 | }, |
| | 221 | "|=": function(attr, value){ |
| | 222 | return "[contains(concat(' ',@"+attr+",' '), ' "+ value +"-')]"; |
| | 223 | }, |
| | 224 | "=": function(attr, value){ |
| | 225 | return "[@"+attr+"='"+ value +"']"; |
| | 226 | } |
| | 227 | }; |
| | 228 | |
| | 229 | // takes a list of attribute searches, the overall query, a function to |
| | 230 | // generate a default matcher, and a closure-bound method for providing a |
| | 231 | // matching function that generates whatever type of yes/no distinguisher |
| | 232 | // the query method needs. The method is a bit tortured and hard to read |
| | 233 | // because it needs to be used in both the XPath and DOM branches. |
| | 234 | var handleAttrs = function( attrList, |
| | 235 | query, |
| | 236 | getDefault, |
| | 237 | handleMatch){ |
| | 238 | d.forEach(query.attrs, function(attr){ |
| | 239 | var matcher; |
| | 240 | // type, attr, matchFor |
| | 241 | if(attr.type && attrList[attr.type]){ |
| | 242 | matcher = attrList[attr.type](attr.attr, attr.matchFor); |
| | 243 | }else if(attr.attr.length){ |
| | 244 | matcher = getDefault(attr.attr); |
| | 245 | } |
| | 246 | if(matcher){ handleMatch(matcher); } |
| | 247 | }); |
| | 248 | } |
| | 249 | |
| | 250 | var buildPath = function(query){ |
| | 251 | var xpath = "."; |
| | 252 | var qparts = getQueryParts(d.trim(query)); |
| | 253 | while(qparts.length){ |
| | 254 | var tqp = qparts.shift(); |
| | 255 | var prefix; |
| | 256 | var postfix = ""; |
| | 257 | // FIXME: need to add support for ~ and + |
| | 258 | if(tqp.oper == ">"){ |
| | 259 | prefix = "/"; |
| | 260 | // prefix = "/child::*"; |
| | 261 | tqp = qparts.shift(); |
| | 262 | }else if(tqp.oper == "~"){ |
| | 263 | prefix = "/following-sibling::"; // get element following siblings |
| | 264 | tqp = qparts.shift(); |
| | 265 | }else if(tqp.oper == "+"){ |
| | 266 | // FIXME: |
| | 267 | // fails when selecting subsequent siblings by node type |
| | 268 | // because the position() checks the position in the list |
| | 269 | // of matching elements and not the localized siblings |
| | 270 | prefix = "/following-sibling::"; |
| | 271 | postfix = "[position()=1]"; |
| | 272 | tqp = qparts.shift(); |
| | 273 | }else{ |
| | 274 | prefix = "//"; |
| | 275 | // prefix = "/descendant::*" |
| | 276 | } |
| | 277 | |
| | 278 | // get the tag name (if any) |
| | 279 | |
| | 280 | xpath += prefix + tqp.tag + postfix; |
| | 281 | |
| | 282 | // check to see if it's got an id. Needs to come first in xpath. |
| | 283 | if(tqp.id){ |
| | 284 | xpath += "[@id='"+tqp.id+"'][1]"; |
| | 285 | } |
| | 286 | |
| | 287 | d.forEach(tqp.classes, function(cn){ |
| | 288 | var cnl = cn.length; |
| | 289 | var padding = " "; |
| | 290 | if(cn.charAt(cnl-1) == "*"){ |
| | 291 | padding = ""; cn = cn.substr(0, cnl-1); |
| | 292 | } |
| | 293 | xpath += |
| | 294 | "[contains(concat(' ',@class,' '), ' "+ |
| | 295 | cn + padding + "')]"; |
| | 296 | }); |
| | 297 | |
| | 298 | handleAttrs(xPathAttrs, tqp, |
| | 299 | function(condition){ |
| | 300 | return "[@"+condition+"]"; |
| | 301 | }, |
| | 302 | function(matcher){ |
| | 303 | xpath += matcher; |
| | 304 | } |
| | 305 | ); |
| | 306 | |
| | 307 | // FIXME: need to implement pseudo-class checks!! |
| | 308 | }; |
| | 309 | return xpath; |
| | 310 | }; |
| | 311 | |
| | 312 | var _xpathFuncCache = {}; |
| | 313 | var getXPathFunc = function(path){ |
| | 314 | if(_xpathFuncCache[path]){ |
| | 315 | return _xpathFuncCache[path]; |
| | 316 | } |
| | 317 | |
| | 318 | var doc = d.doc; |
| | 319 | // var parent = d.body(); // FIXME |
| | 320 | // FIXME: don't need to memoize. The closure scope handles it for us. |
| | 321 | var xpath = buildPath(path); |
| | 322 | |
| | 323 | var tf = function(parent){ |
| | 324 | // XPath query strings are memoized. |
| | 325 | var ret = []; |
| | 326 | var xpathResult; |
| | 327 | try{ |
| | 328 | xpathResult = doc.evaluate(xpath, parent, null, |
| | 329 | // XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null); |
| | 330 | XPathResult.ANY_TYPE, null); |
| | 331 | }catch(e){ |
| | 332 | console.debug("failure in exprssion:", xpath, "under:", parent); |
| | 333 | console.debug(e); |
| | 334 | } |
| | 335 | var result = xpathResult.iterateNext(); |
| | 336 | while(result){ |
| | 337 | ret.push(result); |
| | 338 | result = xpathResult.iterateNext(); |
| | 339 | } |
| | 340 | return ret; |
| | 341 | } |
| | 342 | return _xpathFuncCache[path] = tf; |
| | 343 | }; |
| | 344 | |
| | 345 | /* |
| | 346 | d.xPathMatch = function(query){ |
| | 347 | // XPath based DOM query system. Handles a small subset of CSS |
| | 348 | // selectors, subset is identical to the non-XPath version of this |
| | 349 | // function. |
| | 350 | |
| | 351 | // FIXME: need to add support for alternate roots |
| | 352 | return getXPathFunc(query)(); |
| | 353 | } |
| | 354 | */ |
| | 355 | |
| | 356 | //////////////////////////////////////////////////////////////////////// |
| | 357 | // DOM query code |
| | 358 | //////////////////////////////////////////////////////////////////////// |
| | 359 | |
| | 360 | var _filtersCache = {}; |
| | 361 | var _simpleFiltersCache = {}; |
| | 362 | |
| | 363 | // the basic building block of the yes/no chaining system. agree(f1, f2) |
| | 364 | // generates a new function which returns the boolean results of both of |
| | 365 | // the passed functions to a single logical-anded result. |
| | 366 | var agree = function(first, second){ |
| | 367 | if(!first){ return second; } |
| | 368 | if(!second){ return first; } |
| | 369 | |
| | 370 | return function(){ |
| | 371 | return first.apply(window, arguments) && second.apply(window, arguments); |
| | 372 | } |
| | 373 | } |
| | 374 | |
| | 375 | var _childElements = function(root){ |
| | 376 | var ret = []; |
| | 377 | var te, x=0, tret = root[childNodesName]; |
| | 378 | while(te=tret[x++]){ |
| | 379 | if(te.nodeType == 1){ ret.push(te); } |
| | 380 | } |
| | 381 | return ret; |
| | 382 | } |
| | 383 | |
| | 384 | var _nextSiblings = function(root, single){ |
| | 385 | var ret = []; |
| | 386 | var te = root; |
| | 387 | while(te = te.nextSibling){ |
| | 388 | if(te.nodeType == 1){ |
| | 389 | ret.push(te); |
| | 390 | if(single){ break; } |
| | 391 | } |
| | 392 | } |
| | 393 | return ret; |
| | 394 | } |
| | 395 | |
| | 396 | var _filterDown = function(element, queryParts, matchArr, idx){ |
| | 397 | // NOTE: |
| | 398 | // in the fast path! this function is called recursively and for |
| | 399 | // every run of a query. |
| | 400 | var nidx = idx+1; |
| | 401 | var isFinal = (queryParts.length == nidx); |
| | 402 | var tqp = queryParts[idx]; |
| | 403 | |
| | 404 | // see if we can constrain our next level to direct children |
| | 405 | if(tqp.oper){ |
| | 406 | var ecn = (tqp.oper == ">") ? |
| | 407 | _childElements(element) : |
| | 408 | _nextSiblings(element, (tqp.oper == "+")); |
| | 409 | |
| | 410 | if(!ecn || !ecn.length){ |
| | 411 | return; |
| | 412 | } |
| | 413 | nidx++; |
| | 414 | isFinal = (queryParts.length == nidx); |
| | 415 | // kinda janky, too much array alloc |
| | 416 | var tf = getFilterFunc(queryParts[idx+1]); |
| | 417 | // for(var x=ecn.length-1, te; x>=0, te=ecn[x]; x--){ |
| | 418 | for(var x=0, ecnl=ecn.length, te; x<ecnl, te=ecn[x]; x++){ |
| | 419 | if(tf(te)){ |
| | 420 | if(isFinal){ |
| | 421 | matchArr.push(te); |
| | 422 | }else{ |
| | 423 | _filterDown(te, queryParts, matchArr, nidx); |
| | 424 | } |
| | 425 | } |
| | 426 | /* |
| | 427 | if(x==0){ |
| | 428 | break; |
| | 429 | } |
| | 430 | */ |
| | 431 | } |
| | 432 | } |
| | 433 | |
| | 434 | // otherwise, keep going down, unless we'er at the end |
| | 435 | var candidates = getElementsFunc(tqp)(element); |
| | 436 | if(isFinal){ |
| | 437 | while(candidates.length){ |
| | 438 | matchArr.push(candidates.shift()); |
| | 439 | } |
| | 440 | /* |
| | 441 | candidates.unshift(0, matchArr.length-1); |
| | 442 | matchArr.splice.apply(matchArr, candidates); |
| | 443 | */ |
| | 444 | }else{ |
| | 445 | // if we're not yet at the bottom, keep going! |
| | 446 | while(candidates.length){ |
| | 447 | _filterDown(candidates.shift(), queryParts, matchArr, nidx); |
| | 448 | } |
| | 449 | } |
| | 450 | } |
| | 451 | |
| | 452 | var filterDown = function(elements, queryParts){ |
| | 453 | var ret = []; |
| | 454 | |
| | 455 | // for every root, get the elements that match the descendant selector |
| | 456 | // for(var x=elements.length-1, te; x>=0, te=elements[x]; x--){ |
| | 457 | var x = elements.length - 1, te; |
| | 458 | while(te = elements[x--]){ |
| | 459 | _filterDown(te, queryParts, ret, 0); |
| | 460 | } |
| | 461 | return ret; |
| | 462 | } |
| | 463 | |
| | 464 | var getFilterFunc = function(q){ |
| | 465 | // note: query can't have spaces! |
| | 466 | if(_filtersCache[q.query]){ |
| | 467 | return _filtersCache[q.query]; |
| | 468 | } |
| | 469 | var ff = null; |
| | 470 | |
| | 471 | // does it have a tagName component? |
| | 472 | if(q.tag){ |
| | 473 | if(q.tag == "*"){ |
| | 474 | ff = agree(ff, |
| | 475 | function(elem){ |
| | 476 | return (elem.nodeType == 1); |
| | 477 | } |
| | 478 | ); |
| | 479 | }else{ |
| | 480 | // tag name match |
| | 481 | ff = agree(ff, |
| | 482 | function(elem){ |
| | 483 | return ( |
| | 484 | (elem.nodeType == 1) && |
| | 485 | (q.tag == elem.tagName.toLowerCase()) |
| | 486 | ); |
| | 487 | // return isTn; |
| | 488 | } |
| | 489 | ); |
| | 490 | } |
| | 491 | } |
| | 492 | |
| | 493 | // does the node have an ID? |
| | 494 | if(q.id){ |
| | 495 | ff = agree(ff, |
| | 496 | function(elem){ |
| | 497 | return ( |
| | 498 | (elem.nodeType == 1) && |
| | 499 | (elem.id == q.id) |
| | 500 | ); |
| | 501 | } |
| | 502 | ); |
| | 503 | } |
| | 504 | |
| | 505 | if(q.hasLoops){ |
| | 506 | // if we have other query param parts, make sure we add them to the |
| | 507 | // filter chain |
| | 508 | ff = agree(ff, getSimpleFilterFunc(q)); |
| | 509 | } |
| | 510 | |
| | 511 | return _filtersCache[q.query] = ff; |
| | 512 | } |
| | 513 | |
| | 514 | var getNodeIndex = function(node){ |
| | 515 | // NOTE: |
| | 516 | // we could have a more accurate caching mechanism by invalidating |
| | 517 | // caches after the query has finished, but I think that'd lead to |
| | 518 | // significantly more cache churn than the cache would provide |
| | 519 | // value for in the common case. Generally, we're more |
| | 520 | // conservative (and therefore, more accurate) than jQuery and |
| | 521 | // DomQuery WRT node node indexes, but there may be corner cases |
| | 522 | // in which we fall down. How much we care about them is TBD. |
| | 523 | |
| | 524 | var pn = node.parentNode; |
| | 525 | var pnc = pn.childNodes; |
| | 526 | |
| | 527 | // check to see if we can trust the cache. If not, re-key the whole |
| | 528 | // thing and return our node match from that. |
| | 529 | |
| | 530 | var nidx = -1; |
| | 531 | var child = pn.firstChild; |
| | 532 | if(!child){ |
| | 533 | return nidx; |
| | 534 | } |
| | 535 | |
| | 536 | var ci = node["__cachedIndex"]; |
| | 537 | var cl = pn["__cachedLength"]; |
| | 538 | |
| | 539 | // only handle cache building if we've gone out of sync |
| | 540 | if(((typeof cl == "number")&&(cl != pnc.length))||(typeof ci != "number")){ |
| | 541 | // rip though the whole set, building cache indexes as we go |
| | 542 | pn["__cachedLength"] = pnc.length; |
| | 543 | var idx = 1; |
| | 544 | do{ |
| | 545 | // we only assign indexes for nodes with nodeType == 1, as per: |
| | 546 | // http://www.w3.org/TR/css3-selectors/#nth-child-pseudo |
| | 547 | // only elements are counted in the search order, and they |
| | 548 | // begin at 1 for the first child's index |
| | 549 | |
| | 550 | if(child === node){ |
| | 551 | nidx = idx; |
| | 552 | } |
| | 553 | if(child.nodeType == 1){ |
| | 554 | child["__cachedIndex"] = idx; |
| | 555 | idx++; |
| | 556 | } |
| | 557 | child = child.nextSibling; |
| | 558 | }while(child); |
| | 559 | }else{ |
| | 560 | // NOTE: |
| | 561 | // could be incorrect in some cases (node swaps involving the |
| | 562 | // passed node, etc.), but we ignore those due to the relative |
| | 563 | // unlikelihood of that occuring |
| | 564 | nidx = ci; |
| | 565 | } |
| | 566 | return nidx; |
| | 567 | } |
| | 568 | |
| | 569 | var firedCount = 0; |
| | 570 | |
| | 571 | var blank = ""; |
| | 572 | var _getAttr = function(elem, attr){ |
| | 573 | if(attr == "class"){ |
| | 574 | return elem.className || blank; |
| | 575 | } |
| | 576 | if(attr == "for"){ |
| | 577 | return elem.htmlFor || blank; |
| | 578 | } |
| | 579 | return elem.getAttribute(attr, 2) || blank; |
| | 580 | } |
| | 581 | |
| | 582 | var attrs = { |
| | 583 | "*=": function(attr, value){ |
| | 584 | return function(elem){ |
| | 585 | // E[foo*="bar"] |
| | 586 | // an E element whose "foo" attribute value contains |
| | 587 | // the substring "bar" |
| | 588 | return (_getAttr(elem, attr).indexOf(value)>=0); |
| | 589 | } |
| | 590 | }, |
| | 591 | "^=": function(attr, value){ |
| | 592 | // E[foo^="bar"] |
| | 593 | // an E element whose "foo" attribute value begins exactly |
| | 594 | // with the string "bar" |
| | 595 | return function(elem){ |
| | 596 | return (_getAttr(elem, attr).indexOf(value)==0); |
| | 597 | } |
| | 598 | }, |
| | 599 | "$=": function(attr, value){ |
| | 600 | // E[foo$="bar"] |
| | 601 | // an E element whose "foo" attribute value ends exactly |
| | 602 | // with the string "bar" |
| | 603 | var tval = " "+value; |
| | 604 | return function(elem){ |
| | 605 | var ea = " "+_getAttr(elem, attr); |
| | 606 | return (ea.lastIndexOf(v |