Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Changeset 6183

Show
Ignore:
Timestamp:
02/21/07 03:24:50 (1 year ago)
Author:
andrew
Message:

First commit on the selector branch. Complete overhaul of $$ as started in #7568. Still not complete, but implements nearly all of CSS3 (missing only a few pseudoclasses).

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • spinoffs/prototype/branches/selector/src/selector.js

    r5957 r6183  
    11var Selector = Class.create(); 
     2 
    23Selector.prototype = { 
    34  initialize: function(expression) { 
    4     this.params = {classNames: []}; 
    5     this.expression = expression.toString().strip(); 
    6     this.parseExpression(); 
    7     this.compileMatcher(); 
    8   }, 
    9  
    10   parseExpression: function() { 
    11     function abort(message) { throw 'Parse error in selector: ' + message; } 
    12  
    13     if (this.expression == '')  abort('empty expression'); 
    14  
    15     var params = this.params, expr = this.expression, match, modifier, clause, rest; 
    16     while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!]?=)(?:"([^"]*)"|([^\]\s]*)))?\]$/i)) { 
    17       params.attributes = params.attributes || []; 
    18       params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''}); 
    19       expr = match[1]; 
    20     } 
    21  
    22     if (expr == '*') return this.params.wildcard = true; 
    23      
    24     while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+)(.*)/i)) { 
    25       modifier = match[1], clause = match[2], rest = match[3]; 
    26       switch (modifier) { 
    27         case '#':       params.id = clause; break; 
    28         case '.':       params.classNames.push(clause); break; 
    29         case '': 
    30         case undefined: params.tagName = clause.toUpperCase(); break; 
    31         default:        abort(expr.inspect()); 
    32       } 
    33       expr = rest; 
    34     } 
    35      
    36     if (expr.length > 0) abort(expr.inspect()); 
    37   }, 
    38  
    39   buildMatchExpression: function() { 
    40     var params = this.params, conditions = [], clause; 
    41  
    42     if (params.wildcard) 
    43       conditions.push('true'); 
    44     if (clause = params.id) 
    45       conditions.push('element.readAttribute("id") == ' + clause.inspect()); 
    46     if (clause = params.tagName) 
    47       conditions.push('element.tagName.toUpperCase() == ' + clause.inspect()); 
    48     if ((clause = params.classNames).length > 0) 
    49       for (var i = 0, length = clause.length; i < length; i++) 
    50         conditions.push('element.hasClassName(' + clause[i].inspect() + ')'); 
    51     if (clause = params.attributes) { 
    52       clause.each(function(attribute) { 
    53         var value = 'element.readAttribute(' + attribute.name.inspect() + ')'; 
    54         var splitValueBy = function(delimiter) { 
    55           return value + ' && ' + value + '.split(' + delimiter.inspect() + ')'; 
    56         } 
     5    this.expression = expression.strip(); 
     6    this.compileMatcher();     
     7  }, 
     8   
     9  compileMatcher: function() { 
     10    // FIXME: There's no way to query namespaced attributes with  
     11    // document.evaluate, so I think punting back to the non-XPath approach 
     12    // is the only option 
     13    if (Prototype.BrowserFeatures.XPath && !(/\[[\w-]*?:/).test(this.expression)) 
     14      return this.compileXPathMatcher(); 
     15     
     16    var e = this.expression, ps = Selector.patterns, h = Selector.handlers,  
     17        c = Selector.criteria, le, p, m; 
     18 
     19    if (Selector._cache[e]) { 
     20      this.matcher = Selector._cache[e]; 
     21      return; 
     22    } 
     23    this.nodes   = undefined;     
     24    this.matcher = ["this.matcher = function(root) {\n",  
     25                    "var n = this.nodes, r = root,\n", 
     26                    "h = Selector.handlers, d = false;\n"]; 
     27 
     28    while (e && (/\S/).test(e) && le != e) { 
     29      le = e; 
     30      for (var i in ps) { 
     31        p = ps[i]; 
     32        if (m = e.match(p)) { 
     33          this.matcher.push(typeof c[i] == 'function' ? c[i](m) : 
     34              new Template(c[i]).evaluate(m)); 
     35          e = e.replace(m[0], ''); 
     36          break; 
     37        } 
     38      } 
     39    } 
     40     
     41    this.matcher.push("return h.unique(n);\n}"); 
     42    eval(this.matcher.join('')); 
     43    Selector._cache[this.expression] = this.matcher; 
     44  }, 
     45   
     46  compileXPathMatcher: function() { 
     47    var e = this.expression, ps = Selector.patterns,  
     48        x = Selector.xpath, le, p, m; 
     49 
     50    if (Selector._cache[e]) { 
     51      this.xpath = Selector._cache[e]; 
     52      return; 
     53    } 
     54    this.matcher = [".//"]; 
     55    while (e && (/\S/).test(e) && le != e) { 
     56      le = e; 
     57      for (var i in ps) { 
     58        if (m = e.match(ps[i])) { 
     59          this.matcher.push(typeof x[i] == 'function' ? x[i](m) :  
     60            new Template(x[i]).evaluate(m)); 
     61          e = e.replace(m[0], ''); 
     62          break; 
     63        } 
     64      } 
     65    } 
     66     
     67    this.xpath = this.matcher.join(''); 
     68 
     69    // FIXME: there's got to be a better way to do this... 
     70    // adjacency needs "[1]" as the first predicate 
     71    this.xpath = this.xpath.replace(/following-sibling::\[1\](.*?)(\[|\/|$)/g, 
     72      'following-sibling::$1[1]$2'); 
     73    // any axes not immediately followed by tag names need "*" 
     74    this.xpath = this.xpath.replace(/(\/\/|::|\/)\[/g, '$1*['); 
     75    Selector._cache[this.expression] = this.xpath; 
     76  }, 
     77   
     78  findElements: function(root) { 
     79    root = root || document; 
     80    if (this.xpath) 
     81      return document._getElementsByXPath(this.xpath, root); 
     82    return this.matcher(root); 
     83  }, 
     84   
     85  match: function(element) { 
     86    this.results = this.results || this.findElements(document); 
     87    return this.results.include(element); 
     88    //return this.findElements(document).include(element); 
     89  } 
     90}; 
     91 
     92Object.extend(Selector, { 
     93  _cache: {}, 
     94   
     95  xpath: { 
     96    descendant:   "//", 
     97    child:        "/", 
     98    adjacent:     "/following-sibling::[1]", 
     99    sibling:      '/following-sibling::', 
     100    tagName:      "#{1}", 
     101    className:    "[contains(concat(' ', @class, ' '), ' #{1} ')]", 
     102    id:           "[@id='#{1}']", 
     103    attrPresence: "[@#{1}]", 
     104    attr: function(m) { 
     105      m[3] = m[5] || m[6]; 
     106      return new Template(Selector.xpath.operators[m[2]]).evaluate(m); 
     107    }, 
     108    pseudo: function(m) { 
     109      var h = Selector.xpath.pseudos[m[1]]; 
     110      if (!h) return; 
     111      if (typeof h === 'function') return h(m); 
     112      return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m); 
     113    }, 
     114     
     115    operators: { 
     116      '=':  "[@#{1}='#{3}']", 
     117      '!=': "[@#{1}!='#{3}']", 
     118      '^=': "[substring(@#{1}, 1, string-length('#{3}'))='#{3}']", 
     119      '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']", 
     120      '*=': "[contains(@#{1}, '#{3}')]", 
     121      '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]", 
     122      '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]" 
     123    }, 
     124    pseudos: { 
     125      'first-child': '[position()=1]', 
     126      'last-child':  '[position()=last()]', 
     127      'empty':       '[not(node())]', 
     128      'checked':     "[@checked='checked']", 
     129      'enabled':     "[not(@disabled='disabled')]", 
     130      'disabled':    "[@disabled='disabled']", 
     131      'not':         "[not(#{4})]", 
     132      'not':         function(m) { 
     133        if (!m[4]) return; 
     134        for (var i in Selector.patterns) { 
     135          if (mm = m[4].match(Selector.patterns[i])) { 
     136            var ss = new Template(Selector.xpath[i]).evaluate(mm); 
     137            // FIXME: This is an ugly way to trim the brackets 
     138            m[4] = ss.substring(1, ss.length - 1);             
     139          } 
     140        } 
     141        return "[not(" + m[4] + ")]"; 
     142      }, 
     143      'nth-child':   function(m) { 
     144        var predicate, mm, formula = m[4]; 
     145        if (["odd", "even"].include(formula)) 
     146          predicate = "position() mod 2 = " + (formula == 'odd' ? 1 : 0); 
     147        if (mm = formula.match(/^(\d+)$/)) // digit only 
     148          predicate = "position() = " + mm[1]; 
     149        if (mm = formula.match(/^(\d+)?n(\+(\d+))?/)) { // an+b 
     150          var a = m[1] !== undefined ? Number(m[1]) : 1; 
     151          var b = m[3] !== undefined ? Number(m[2]) : 0; 
     152          predicate = "position() mod " + mm[1] + " = " + mm[3]; 
     153        } 
     154        return "[" + predicate + "]"; 
     155      } 
     156    } 
     157  }, 
     158   
     159  criteria: { 
     160    tagName:      'n = h.tagName(n, r, "#{1}", d);   d = false;',   
     161    className:    'n = h.className(n, r, "#{1}", d); d = false;', 
     162    id:           'n = h.id(n, r, "#{1}", d);        d = false;', 
     163    attrPresence: 'n = h.attrPresence(n, r, "#{1}"); d = false;', 
     164    attr: function(m) { 
     165      m[3] = m[5] || m[6]; 
     166      return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}"); d = false;').evaluate(m); 
     167    }, 
     168    pseudo:       'n = h.pseudo(n, "#{1}", "#{4}", r, d); d = false;', 
     169    descendant:   'd = "descendant";', 
     170    child:        'd = "child";', 
     171    adjacent:     'd = "adjacent";', 
     172    sibling:      'd = "sibling";' 
     173  }, 
     174 
     175  patterns: { 
     176    // combinators must be listed first 
     177    // (and descendant needs to be last combinator) 
     178    sibling:      /^\s*~\s*/, 
     179    child:        /^\s*>\s*/, 
     180    adjacent:     /^\s*\+\s*/, 
     181    descendant:   /^\s/, 
     182 
     183    // selectors follow 
     184    tagName:      /^\s*(\*|[\w\-]+)(\b|$)?/, 
     185    id:           /^#([\w\-\*]+)(\b|$)/, 
     186    className:    /^\.([\w\-\*]+)(\b|$)/, 
     187    attrPresence: /^\[([\w]+)\]/, 
     188    pseudo:       /^:((first|last|nth|only)-child|empty|checked|enabled|disabled|not)(\((.*?)\))?(\b|$)/, 
     189    attr:         /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\]]*?)\4|([^'"][^\]]*?)))?\]/ 
     190  }, 
     191   
     192  handlers: { 
     193    // returns a flat list of all descendants of the node collection 
     194    descendant: function(nodes) { 
     195      for (var i = 0, results = [], node; node = nodes[i]; i++) 
     196        Selector.handlers.concat(results, Element.descendants(node)); 
     197      return results; 
     198    }, 
     199     
     200    child: function(nodes) { 
     201      var results = []; 
     202      for (var i = 0, node; node = nodes[i]; i++) 
     203        Selector.handlers.concat(results, Element.immediateDescendants(node)); 
     204      return results; 
     205    }, 
     206     
     207    adjacent: function(nodes) { 
     208      var results = []; 
     209      for (var i = 0, node; node = nodes[i]; i++) { 
     210        var next = this.nextElementSibling(node); 
     211        if (next) results.push(next); 
     212      } 
     213      return results; 
     214    }, 
     215     
     216    sibling: function(nodes) { 
     217      var results = []; 
     218      for (var i = 0, node; node = nodes[i]; i++) 
     219        Selector.handlers.concat(results, Element.siblings(node)); 
     220      return results; 
     221    }, 
     222     
     223    tagName: function(nodes, root, tagName, combinator) { 
     224      tagName = tagName.toUpperCase(); 
     225      var results = [], h = Selector.handlers; 
     226      if (nodes) { 
     227        if (combinator) { 
     228          // fastlane for ordinary descendant combinators 
     229          if (combinator == "descendant") { 
     230            for (var i = 0, node; node = nodes[i]; i++) { 
     231              var subnodes = node.getElementsByTagName(tagName); 
     232              h.concat(results, subnodes); 
     233            } 
     234            return results; 
     235          } 
     236          nodes = this[combinator](nodes); 
     237        } 
     238        for (var i = 0, node; node = nodes[i]; i++) { 
     239          if (node.tagName.toUpperCase() == tagName) 
     240            results.push(node); 
     241        } 
     242        return results; 
     243      } 
     244      return root.getElementsByTagName(tagName); 
     245    }, 
     246     
     247    className: function(nodes, root, className, combinator) { 
     248      var results = [], h = Selector.handlers; 
     249      if (nodes) { 
     250        if (combinator) nodes = this[combinator](nodes); 
     251        return h.byClassName(nodes, root, className); 
     252      } 
     253      return h.byClassName(nodes, root, className); 
     254      //return document.getElementsByClassName(className, root); 
     255    }, 
     256     
     257    id: function(nodes, root, id, combinator) { 
     258      var targetNode = $(id); 
     259      if (!nodes && root == document) return [targetNode]; 
     260      if (nodes) { 
     261        if (combinator) { 
     262          if (combinator == 'child') { 
     263            for (var i = 0, node; node = nodes[i]; i++) 
     264              if (targetNode.parentNode == node) return [targetNode]; 
     265          } else if (combinator == 'descendant') { 
     266            for (var i = 0, node; node = nodes[i]; i++) 
     267              if (Element.descendantOf(targetNode, node)) return [targetNode]; 
     268          } else if (combinator == 'adjacent') { 
     269            for (var i = 0, node; node = nodes[i]; i++) 
     270              if (targetNode.previousSibling == node) return [targetNode]; 
     271          } else if (combinator == 'sibling') { 
     272            if (nodes.include(targetNode)) return [targetNode]; 
     273          } else nodes = Selector.handlers[combinator](nodes); 
     274        }  
     275        for (var i = 0, node; node = nodes[i]; i++) 
     276          if (node == targetNode) return [targetNode]; 
     277        return []; 
     278      } 
     279      return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : []; 
     280    }, 
     281     
     282    attrPresence: function(nodes, root, attr) { 
     283      var results = []; 
     284      for (var i = 0, node; node = nodes[i]; i++) { 
     285        if (!node.hasAttribute) { 
     286          if (Element.Methods.Simulated.hasAttribute(node, attr)) 
     287            results.push(node); 
     288        } else if (node.hasAttribute(attr)) results.push(node); 
     289      } 
     290      return results; 
     291    }, 
     292     
     293    attr: function(nodes, root, attr, value, operator) { 
     294      var handler = Selector.operators[operator], results = []; 
     295      for (var i = 0, node; node = nodes[i]; i++) { 
     296        var nodeValue = Element.readAttribute(node, attr); 
     297        if (nodeValue === null) continue; 
     298        if (handler(nodeValue, value)) results.push(node); 
     299      } 
     300      return results; 
     301    }, 
     302     
     303    // joins two collections 
     304    concat: function(a, b) { 
     305      for (var i = 0, node; node = b[i]; i++) 
     306        a.push(node); 
     307      return a; 
     308    }, 
     309     
     310    // marks an array of nodes for counting 
     311    mark: function(nodes) { 
     312      for (var i = 0, node; node = nodes[i]; i++) 
     313        node._counted = true; 
     314      return nodes; 
     315    }, 
     316     
     317    unmark: function(nodes) { 
     318      for (var i = 0, node; node = nodes[i]; i++) 
     319        node._counted = undefined;     
     320      return nodes;   
     321    }, 
     322     
     323    // filters out duplicates and extends all nodes 
     324    unique: function(nodes) { 
     325      if (nodes.length == 0) return nodes; 
     326      var results = [nodes[0]], n; 
     327      nodes[0]._counted = true;       
     328      for (var i = 0, l = nodes.length; i < l; i++) { 
     329        n = nodes[i]; 
     330        if (!n._counted) { 
     331          n._counted = true; 
     332          results.push(Element.extend(n)); 
     333        } 
     334      } 
     335      return Selector.handlers.unmark(results); 
     336    }, 
     337     
     338    byClassName: function(nodes, root, className) { 
     339      if (!nodes) nodes = Selector.handlers.descendant([root]); 
     340      var needle = [" ", className, " "].join(''); 
     341      for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) { 
     342        nodeClassName = node.className; 
     343        if (nodeClassName.length == 0) continue; 
     344        if (nodeClassName == className ||  
     345          [" ", nodeClassName, " "].join('').include(needle)) 
     346          results.push(node); 
     347      } 
     348      return results; 
     349    }, 
    57350         
    58         switch (attribute.operator) { 
    59           case '=':       conditions.push(value + ' == ' + attribute.value.inspect()); break; 
    60           case '~=':      conditions.push(splitValueBy(' ') + '.include(' + attribute.value.inspect() + ')'); break; 
    61           case '|=':      conditions.push( 
    62                             splitValueBy('-') + '.first().toUpperCase() == ' + attribute.value.toUpperCase().inspect() 
    63                           ); break; 
    64           case '!=':      conditions.push(value + ' != ' + attribute.value.inspect()); break; 
    65           case '': 
    66           case undefined: conditions.push('element.hasAttribute(' + attribute.name.inspect() + ')'); break; 
    67           default:        throw 'Unknown operator ' + attribute.operator + ' in selector'; 
    68         } 
    69       }); 
    70     } 
    71  
    72     return conditions.join(' && '); 
    73   }, 
    74  
    75   compileMatcher: function() { 
    76     this.match = new Function('element', 'if (!element.tagName) return false; \ 
    77       element = $(element); \ 
    78       return ' + this.buildMatchExpression()); 
    79   }, 
    80  
    81   findElements: function(scope) { 
    82     var element; 
    83  
    84     if (element = $(this.params.id)) 
    85       if (this.match(element)) 
    86         if (!scope || Element.childOf(element, scope)) 
    87           return [element]; 
    88  
    89     scope = (scope || document).getElementsByTagName(this.params.tagName || '*'); 
    90  
    91     var results = []; 
    92     for (var i = 0, length = scope.length; i < length; i++) 
    93       if (this.match(element = scope[i])) 
    94         results.push(Element.extend(element)); 
    95  
     351    nextElementSibling: function(node) { 
     352      do { 
     353        var node = node.nextSibling; 
     354      } while (node.nodeType != 1 && node.nextSibling);  
     355      return (node.nodeType == 1) ? node : null; 
     356    }, 
     357     
     358    previousElementSibling: function(node) { 
     359      do { 
     360        var node = node.previousSibling; 
     361      } while (node.nodeType != 1 && node.previousSibling); 
     362      return (node.nodeType == 1) ? node : null; 
     363    }, 
     364     
     365    pseudo: function(nodes, name, value, root, combinator) { 
     366      if (combinator) nodes = this[combinator](nodes); 
     367      return Selector.pseudos[name](nodes, value, root, combinator); 
     368    }, 
     369     
     370    index: function(node) { 
     371      node._counted = true; 
     372      for (var i = 0, j = 1, nodes = node.childNodes; node = nodes[i]; i++) 
     373        if (node.nodeType == 1) node.nodeIndex = j++; 
     374    } 
     375  }, 
     376   
     377  pseudos: { 
     378    'first-child': function(nodes, value, root) { 
     379      for (var i = 0, results = [], node; node = nodes[i]; i++) { 
     380        if (!Selector.handlers.previousElementSibling(node)) 
     381          results.push(node); 
     382      } 
     383      return results; 
     384    }, 
     385     
     386    'last-child': function(nodes, value, root) { 
     387      for (var i = 0, results = [], node; node = nodes[i]; i++) { 
     388        if (!Selector.handlers.nextElementSibling(node)) 
     389          results.push(node); 
     390      } 
     391      return results; 
     392    }, 
     393     
     394    'nth-child': function(nodes, formula, root) { 
     395      var h = Selector.handlers, results = [], indexed = [], m; 
     396       
     397      // index nodes ahead of time 
     398      for (var i = 0, node; node = nodes[i]; i++) { 
     399        if (!node.parentNode._counted) { 
     400          h.index(node.parentNode); 
     401          indexed.push(node.parentNode); 
     402        } 
     403      }       
     404      if (formula == 'odd' || formula == 'even') { 
     405        var mod = (formula == 'odd') ? 1 : 0; 
     406        for (var i = 0, node; node = nodes[i]; i++) 
     407          if ((node.nodeIndex + 1) % 2 == mod) results.push(node); 
     408      } 
     409       
     410      if (formula.match(/^\d+$/)) { // just a number 
     411        formula = Number(formula); 
     412        for (var i = 0, node; node = nodes[i]; i++) 
     413          if (node.nodeIndex == formula) results.push(node); 
     414      } 
     415 
     416      if (m = formula.match(/^(\d+)?n(\+(\d+))?$/)) { // an+b 
     417        var a = m[1] !== undefined ? Number(m[1]) : 1; 
     418        var b = m[3] !== undefined ? Number(m[2]) : 0; 
     419        for (var i = 0, node; node = nodes[i]; i++) 
     420          if (node.nodeIndex % a == b) results.push(node); 
     421      } 
     422 
     423      // clean up indexing 
     424      h.unmark(indexed); 
     425      return results; 
     426    }, 
     427     
     428    'only-child': function(nodes, value, root) { 
     429      var h = Selector.handlers; 
     430      for (var i = 0, results = [], node; node = nodes[i]; i++) { 
     431        if (!h.previousElementSibling(node) && 
     432          !h.nextElementSibling(node)) { 
     433          results.push(node);   
     434        } 
     435      } 
     436      return results; 
     437    }, 
     438     
     439    'empty': function(nodes, value, root) { 
     440      for (var i = 0, results = [], node; node = nodes[i]; i++) { 
     441        if (node.firstChild || node.innerHTML.length > 0) continue; 
     442        results.push(node); 
     443      } 
     444      return results; 
     445    }, 
     446     
     447    'not': function(nodes, selector, root) { 
     448      var h = Selector.handlers; 
     449 
     450      var matches = new Selector(selector).findElements(root);       
     451      h.mark(matches);       
     452      for (var i = 0, results = [], node; node = nodes[i]; i++) 
     453        if (!node._counted) results.push(node); 
     454      h.unmark(matches); 
     455      return results; 
     456    }       
     457  }, 
     458     
     459  operators: { 
     460    '=':  function(nv, v) { return nv == v; }, 
     461    '!=': function(nv, v) { return nv != v; }, 
     462    '^=': function(nv, v) { return nv.startsWith(v); }, 
     463    '$=': function(nv, v) { return nv.endsWith(v); }, 
     464    '*=': function(nv, v) { return nv.include(v); }, 
     465    '~=': function(nv, v) { return nv.split(/\s/).include(v); }, 
     466    '|=': function(nv, v) { return nv.toUpperCase().split('-').include(v.toUpperCase()); } 
     467  }, 
     468 
     469  matchElements: function(elements, expression) { 
     470    var selector = new Selector(expression), results = []; 
     471    for (var i = 0, l = elements.length; i < l; i++) { 
     472      if (selector.match(elements[i])) 
     473        results.push(Element.extend(elements[i])); 
     474    } 
    96475    return results; 
    97476  }, 
    98  
    99   toString: function() { 
    100     return this.expression; 
    101   } 
    102 } 
    103  
    104 Object.extend(Selector, { 
    105   matchElements: function(elements, expression) { 
    106     var selector = new Selector(expression); 
    107     return elements.select(selector.match.bind(selector)).map(Element.extend); 
    108   }, 
    109477   
    110478  findElement: function(elements, expression, index) { 
    111     if (typeof expression == 'number') index = expression, expression = false; 
     479    if (typeof expression == 'number') {  
     480      index = expression; expression = false; 
     481    } 
    112482    return Selector.matchElements(elements, expression || '*')[index || 0]; 
    113483  }, 
    114484   
    115485  findChildElements: function(element, expressions) { 
    116     return expressions.map(function(expression) { 
    117       return expression.match(/[^\s"]+(?:"[^"]*"[^\s"]+)*/g).inject([null], function(results, expr) { 
    118         var selector = new Selector(expr); 
    119         return results.inject([], function(elements, result) { 
    120           return elements.concat(selector.findElements(result || element)); 
    121         }); 
    122       }); 
    123     }).flatten(); 
     486    expressions = expressions.join(',').split(','); 
     487    var results = [], selector; 
     488    for (var i = 0, l = expressions.length; i < l; i++) { 
     489      selector = new Selector(expressions[i].strip()); 
     490      results  = results.concat(selector.findElements(element)); 
     491    } 
     492    return results; 
    124493  } 
    125494}); 
     
    128497  return Selector.findChildElements(document, $A(arguments)); 
    129498} 
     499 
     500 
     501// TODO: 
     502// (remaining pseudoclasses) 
     503// nth-last-child 
     504// nth-of-type 
     505// nth-last-of-type 
     506// 
  • spinoffs/prototype/branches/selector/test/unit/selector.html

    r5957 r6183  
    3838    therefore its ID property won't be 'troubleForm': --> 
    3939  <form id="troubleForm"><input type="hidden" name="id" /></form> 
    40 </div> 
     40   
     41   
     42  <div id="grandfather"> grandfather     
     43    <div id="father" class="brothers men"> father       
     44      <div id="son"> son </div> 
     45    </div> 
     46    <div id="uncle" class="brothers men"> uncle </div> 
     47  </div>   
     48</div> <!-- #fixtures --> 
    4149 
    4250<!-- Log output --> 
     
    4755// <![CDATA[ 
    4856  new Test.Unit.Runner({ 
     57     
    4958    testSelectorWithTagName: function() {with(this) { 
    5059      assertEnumEqual($A(document.getElementsByTagName('li')), $$('li')); 
     
    126135    }}, 
    127136     
     137    testSelectorWithSubstringAttributeValues: function() {with(this) { 
     138      assertEnumEqual($('father', 'uncle'), $$('div[class^=bro]'), 'matching beginning of string'); 
     139      assertEnumEqual($('father', 'uncle'), $$('div[class$=men]'), 'matching end of string'); 
     140      assertEnumEqual($('father', 'uncle'), $$('div[class*="ers m"]'), 'matching substring') 
     141    }}, 
     142     
    128143    test$$WithNestedAttributeSelectors: function() {with(this) { 
    129144      assertEnumEqual([$('strong')], $$('div[style] p[id] strong')); 
     
    150165    testSelectorWithSpaceInAttributeValue: function() {with(this) { 
    151166      assertEnumEqual([$('with_title')], $$('cite[title="hello world!"]')); 
    152     }}     
     167    }}, 
     168     
     169    testChildCombinator: function() {with(this) { 
     170      assertEnumEqual($('link_1', 'link_2'), $$('p.first > a')); 
     171      assertEnumEqual($('father', 'uncle'), $$('div#grandfather > div')); 
     172    }}, 
     173     
     174    testAdjacencyCombinator: function() {with(this) { 
     175      assertEnumEqual([$('uncle')], $$('div.brothers + div.brothers')); 
     176      assertEnumEqual([$('uncle')], $$('div.brothers + div'));       
     177    }}, 
     178     
     179    testGeneralSiblingCombinator: function() {with(this) { 
     180      assertEnumEqual([$('list')], $$('h1 ~ ul')); 
     181    }}, 
     182 
     183    testUniquenessOfResultSet: function() {with(this) { 
     184      assertEnumEqual($$('div div'), $$('div div').uniq()); 
     185    }} 
     186         
    153187  }, 'testlog'); 
    154188// ]]>