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

Changeset 6366

Show
Ignore:
Timestamp:
03/09/07 04:12:13 (1 year ago)
Author:
sam
Message:

prototype: Merge the selector branch into trunk, bringing vast performance improvements, bug fixes, and near-complete CSS3 compliance to $$ and Selector. Closes #7568.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • spinoffs/prototype/trunk/CHANGELOG

    r6363 r6366  
    11*SVN* 
     2 
     3* Merge the selector branch into trunk, bringing vast performance improvements, bug fixes, and near-complete CSS3 compliance to $$ and Selector.  Closes #7568.  [Andrew Dupont] 
     4  Selector speed test: http://andrewdupont.net/test/double-dollar/ 
    25 
    36* Add support for JSON encoding and decoding.  Closes #7427.  [Tobie Langel] 
  • spinoffs/prototype/trunk/src/dom.js

    r6358 r6366  
    1919    return results; 
    2020  }; 
     21   
    2122  document.getElementsByClassName = function(className, parentElement) { 
    2223    var q = ".//*[contains(concat(' ', @class, ' '), ' " + className + " ')]"; 
    2324    return document._getElementsByXPath(q, parentElement); 
    2425  } 
     26   
    2527} else document.getElementsByClassName = function(className, parentElement) { 
    2628  var children = ($(parentElement) || document.body).getElementsByTagName('*'); 
     
    178180   
    179181  up: function(element, expression, index) { 
    180     return Selector.findElement($(element).ancestors(), expression, index); 
     182    var ancestors = $(element).ancestors(); 
     183    return expression ? Selector.findElement(ancestors, expression, index) : 
     184      ancestors[index || 0]; 
    181185  }, 
    182186   
    183187  down: function(element, expression, index) { 
    184     return Selector.findElement($(element).descendants(), expression, index); 
     188    var descendants = $(element).descendants(); 
     189    return expression ? Selector.findElement(descendants, expression, index) : 
     190      descendants[index || 0]; 
    185191  }, 
    186192 
    187193  previous: function(element, expression, index) { 
    188     return Selector.findElement($(element).previousSiblings(), expression, index); 
     194    var previousSiblings = $(element).previousSiblings(); 
     195    return expression ? Selector.findElement(previousSiblings, expression, index) : 
     196      previousSiblings[index || 0]; 
    189197  }, 
    190198   
    191199  next: function(element, expression, index) { 
    192     return Selector.findElement($(element).nextSiblings(), expression, index); 
     200    var nextSiblings = $(element).nextSiblings(); 
     201    return expression ? Selector.findElement(nextSiblings, expression, index) : 
     202      nextSiblings[index || 0]; 
    193203  }, 
    194204   
     
    204214  readAttribute: function(element, name) { 
    205215    element = $(element); 
    206     if (document.all && !window.opera) { 
     216    if (Prototype.Browser.IE) { 
    207217      var t = Element._attributeTranslations; 
    208218      if (t.values[name]) return t.values[name](element, name); 
    209219      if (t.names[name])  name = t.names[name]; 
    210220      var attribute = element.attributes[name]; 
    211       if(attribute) return attribute.nodeValue
     221      return attribute ? attribute.nodeValue : null
    212222    }     
    213223    return element.getAttribute(name); 
     
    399409    element._overflow = null; 
    400410    return element; 
    401   } 
     411  }   
    402412}; 
    403413 
     
    554564  Prototype.BrowserFeatures.ElementExtensions = true; 
    555565} 
     566 
     567Element.hasAttribute = function(element, attribute) { 
     568  if (element.hasAttribute) return element.hasAttribute(attribute); 
     569  return Element.Methods.Simulated.hasAttribute(element, attribute); 
     570}; 
    556571 
    557572Element.addMethods = function(methods) { 
  • spinoffs/prototype/trunk/src/selector.js

    r5957 r6366  
    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         } 
    57          
    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  
     5    this.expression = expression.strip(); 
     6    this.compileMatcher();     
     7  }, 
     8   
    759  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  
    96     return results; 
    97   }, 
    98  
     10    // Selectors with namespaced attributes can't use the XPath version 
     11    if (Prototype.BrowserFeatures.XPath && !(/\[[\w-]*?:/).test(this.expression)) 
     12      return this.compileXPathMatcher(); 
     13     
     14    var e = this.expression, ps = Selector.patterns, h = Selector.handlers,  
     15        c = Selector.criteria, le, p, m; 
     16 
     17    if (Selector._cache[e]) { 
     18      this.matcher = Selector._cache[e]; return; 
     19    } 
     20    this.matcher = ["this.matcher = function(root) {",  
     21                    "var r = root, h = Selector.handlers, c = false, n;"]; 
     22 
     23    while (e && le != e && (/\S/).test(e)) { 
     24      le = e; 
     25      for (var i in ps) { 
     26        p = ps[i]; 
     27        if (m = e.match(p)) { 
     28          this.matcher.push(typeof c[i] == 'function' ? c[i](m) : 
     29              new Template(c[i]).evaluate(m)); 
     30          e = e.replace(m[0], ''); 
     31          break; 
     32        } 
     33      } 
     34    } 
     35     
     36    this.matcher.push("return h.unique(n);\n}"); 
     37    eval(this.matcher.join('\n')); 
     38    Selector._cache[this.expression] = this.matcher; 
     39  }, 
     40   
     41  compileXPathMatcher: function() { 
     42    var e = this.expression, ps = Selector.patterns, 
     43        x = Selector.xpath, le, p, m; 
     44 
     45    if (Selector._cache[e]) { 
     46      this.xpath = Selector._cache[e]; return; 
     47    } 
     48 
     49    this.matcher = ['.//*']; 
     50    while (e && le != e && (/\S/).test(e)) { 
     51      le = e; 
     52      for (var i in ps) { 
     53        if (m = e.match(ps[i])) { 
     54          this.matcher.push(typeof x[i] == 'function' ? x[i](m) :  
     55            new Template(x[i]).evaluate(m)); 
     56          e = e.replace(m[0], ''); 
     57          break; 
     58        } 
     59      } 
     60    } 
     61     
     62    this.xpath = this.matcher.join(''); 
     63    Selector._cache[this.expression] = this.xpath; 
     64  }, 
     65   
     66  findElements: function(root) { 
     67    root = root || document; 
     68    if (this.xpath) return document._getElementsByXPath(this.xpath, root); 
     69    return this.matcher(root); 
     70  }, 
     71   
     72  match: function(element) { 
     73    return this.findElements(document).include(element); 
     74  }, 
     75   
    9976  toString: function() { 
    10077    return this.expression; 
     78  }, 
     79   
     80  inspect: function() { 
     81    return "#<Selector:" + this.expression.inspect() + ">"; 
    10182  } 
    102 } 
     83}; 
    10384 
    10485Object.extend(Selector, { 
     86  _cache: {}, 
     87   
     88  xpath: { 
     89    descendant:   "//*", 
     90    child:        "/*", 
     91    adjacent:     "/following-sibling::*[1]", 
     92    laterSibling: '/following-sibling::*', 
     93    tagName:      function(m) {  
     94      if (m[1] == '*') return ''; 
     95      return "[local-name()='" + m[1].toLowerCase() +  
     96             "' or local-name()='" + m[1].toUpperCase() + "']"; 
     97    }, 
     98    className:    "[contains(concat(' ', @class, ' '), ' #{1} ')]", 
     99    id:           "[@id='#{1}']", 
     100    attrPresence: "[@#{1}]", 
     101    attr: function(m) { 
     102      m[3] = m[5] || m[6]; 
     103      return new Template(Selector.xpath.operators[m[2]]).evaluate(m); 
     104    }, 
     105    pseudo: function(m) { 
     106      var h = Selector.xpath.pseudos[m[1]]; 
     107      if (!h) return ''; 
     108      if (typeof h === 'function') return h(m); 
     109      return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m); 
     110    }, 
     111    operators: { 
     112      '=':  "[@#{1}='#{3}']", 
     113      '!=': "[@#{1}!='#{3}']", 
     114      '^=': "[starts-with(@#{1}, '#{3}')]", 
     115      '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']", 
     116      '*=': "[contains(@#{1}, '#{3}')]", 
     117      '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]", 
     118      '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]" 
     119    }, 
     120    pseudos: { 
     121      'first-child': '[not(preceding-sibling::*)]', 
     122      'last-child':  '[not(following-sibling::*)]', 
     123      'only-child':  '[not(preceding-sibling::* or following-sibling::*)]', 
     124      'empty':       "[count(*) = 0 and (count(text()) = 0 or translate(text(), ' \t\r\n', '') = '')]", 
     125      'checked':     "[@checked]", 
     126      'disabled':    "[@disabled]", 
     127      'enabled':     "[not(@disabled)]", 
     128      'not': function(m) { 
     129        if (!m[6]) return ''; 
     130        var p = Selector.patterns, x = Selector.xpath; 
     131        for (var i in p) { 
     132          if (mm = m[6].match(p[i])) { 
     133            var ss = typeof x[i] == 'function' ? x[i](mm) : new Template(x[i]).evaluate(mm); 
     134            m[6] = ss.substring(1, ss.length - 1); 
     135            break; 
     136          } 
     137        } 
     138        return "[not(" + m[6] + ")]"; 
     139      }, 
     140      'nth-child':      function(m) {  
     141        return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m); 
     142      }, 
     143      'nth-last-child': function(m) { 
     144        return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m); 
     145      }, 
     146      'nth-of-type':    function(m) { 
     147        return Selector.xpath.pseudos.nth("position() ", m); 
     148      }, 
     149      'nth-last-of-type': function(m) { 
     150        return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m); 
     151      }, 
     152      'first-of-type':  function(m) {  
     153        m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m); 
     154      }, 
     155      'last-of-type':   function(m) { 
     156        m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m); 
     157      }, 
     158      'only-of-type':   function(m) { 
     159        var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m); 
     160      }, 
     161      nth: function(predicate, m) { 
     162        var mm, formula = m[6]; 
     163        if (formula == 'even') formula = '2n+0'; 
     164        if (formula == 'odd')  formula = '2n+1'; 
     165        if (mm = formula.match(/^(\d+)$/)) // digit only 
     166          predicate += "= " + mm[1]; 
     167        if (mm = formula.match(/^(\d+)?n(\+(\d+))?/)) { // an+b 
     168          var a = mm[1] ? Number(mm[1]) : 1; 
     169          var b = mm[3] ? Number(mm[3]) : 0; 
     170          predicate += "mod " + a + " = " + b; 
     171        } 
     172        return "[" + predicate + "]";         
     173      } 
     174    } 
     175  }, 
     176   
     177  criteria: { 
     178    tagName:      'n = h.tagName(n, r, "#{1}", c);   c = false;',   
     179    className:    'n = h.className(n, r, "#{1}", c); c = false;', 
     180    id:           'n = h.id(n, r, "#{1}", c);        c = false;', 
     181    attrPresence: 'n = h.attrPresence(n, r, "#{1}"); c = false;', 
     182    attr: function(m) { 
     183      m[3] = m[5] || m[6]; 
     184      return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}"); c = false;').evaluate(m); 
     185    }, 
     186    pseudo:       'n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;', 
     187    descendant:   'c = "descendant";', 
     188    child:        'c = "child";', 
     189    adjacent:     'c = "adjacent";', 
     190    laterSibling: 'c = "laterSibling";' 
     191  }, 
     192 
     193  patterns: { 
     194    // combinators must be listed first 
     195    // (and descendant needs to be last combinator) 
     196    laterSibling: /^\s*~\s*/, 
     197    child:        /^\s*>\s*/, 
     198    adjacent:     /^\s*\+\s*/, 
     199    descendant:   /^\s/, 
     200 
     201    // selectors follow 
     202    tagName:      /^\s*(\*|[\w\-]+)(\b|$)?/, 
     203    id:           /^#([\w\-\*]+)(\b|$)/, 
     204    className:    /^\.([\w\-\*]+)(\b|$)/, 
     205    pseudo:       /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$)/, 
     206    attrPresence: /^\[([\w]+)\]/, 
     207    attr:         /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\]]*?)\4|([^'"][^\]]*?)))?\]/ 
     208  }, 
     209   
     210  handlers: { 
     211    // UTILITY FUNCTIONS 
     212    // joins two collections 
     213    concat: function(a, b) { 
     214      for (var i = 0, node; node = b[i]; i++) 
     215        a.push(node); 
     216      return a; 
     217    }, 
     218     
     219    // marks an array of nodes for counting 
     220    mark: function(nodes) { 
     221      for (var i = 0, node; node = nodes[i]; i++) 
     222        node._counted = true; 
     223      return nodes; 
     224    }, 
     225     
     226    unmark: function(nodes) { 
     227      for (var i = 0, node; node = nodes[i]; i++) 
     228        node._counted = undefined;     
     229      return nodes;   
     230    }, 
     231 
     232    // mark each child node with its position (for nth calls) 
     233    // "ofType" flag indicates whether we're indexing for nth-of-type 
     234    // rather than nth-child 
     235    index: function(parentNode, reverse, ofType) { 
     236      parentNode._counted = true; 
     237      if (reverse) { 
     238        for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) { 
     239          node = nodes[i]; 
     240          if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++; 
     241        } 
     242      } else { 
     243        for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++) 
     244          if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++; 
     245      } 
     246    }, 
     247     
     248    // filters out duplicates and extends all nodes 
     249    unique: function(nodes) { 
     250      if (nodes.length == 0) return nodes; 
     251      var results = [nodes[0]], n; 
     252      nodes[0]._counted = true;       
     253      for (var i = 0, l = nodes.length; i < l; i++) { 
     254        n = nodes[i]; 
     255        if (!n._counted) { 
     256          n._counted = true; 
     257          results.push(Element.extend(n)); 
     258        } 
     259      } 
     260      return Selector.handlers.unmark(results); 
     261    }, 
     262     
     263    // COMBINATOR FUNCTIONS 
     264    descendant: function(nodes) { 
     265      var h = Selector.handlers; 
     266      for (var i = 0, results = [], node; node = nodes[i]; i++) 
     267        h.concat(results, Element.descendants(node)); 
     268      return results; 
     269    }, 
     270     
     271    child: function(nodes) { 
     272      var h = Selector.handlers; 
     273      for (var i = 0, results = [], node; node = nodes[i]; i++) 
     274        h.concat(results, Element.immediateDescendants(node)); 
     275      return results; 
     276    }, 
     277     
     278    adjacent: function(nodes) { 
     279      for (var i = 0, results = [], node; node = nodes[i]; i++) { 
     280        var next = this.nextElementSibling(node); 
     281        if (next) results.push(next); 
     282      } 
     283      return results; 
     284    }, 
     285     
     286    laterSibling: function(nodes) { 
     287      var h = Selector.handlers; 
     288      for (var i = 0, results = [], node; node = nodes[i]; i++) 
     289        h.concat(results, Element.nextSiblings(node)); 
     290      return results; 
     291    }, 
     292 
     293    nextElementSibling: function(node) { 
     294      while (node = node.nextSibling) 
     295              if (node.nodeType == 1) return node; 
     296      return null; 
     297    }, 
     298     
     299    previousElementSibling: function(node) { 
     300      while (node = node.previousSibling) 
     301        if (node.nodeType == 1) return node; 
     302      return null; 
     303    }, 
     304     
     305    // TOKEN FUNCTIONS 
     306    tagName: function(nodes, root, tagName, combinator) { 
     307      tagName = tagName.toUpperCase(); 
     308      var results = [], h = Selector.handlers; 
     309      if (nodes) { 
     310        if (combinator) { 
     311          // fastlane for ordinary descendant combinators 
     312          if (combinator == "descendant") { 
     313            for (var i = 0, node; node = nodes[i]; i++) 
     314              h.concat(results, node.getElementsByTagName(tagName)); 
     315            return results; 
     316          } else nodes = this[combinator](nodes); 
     317          if (tagName == "*") return nodes; 
     318        } 
     319        for (var i = 0, node; node = nodes[i]; i++) 
     320          if (node.tagName.toUpperCase() == tagName) results.push(node); 
     321        return results; 
     322      } else return root.getElementsByTagName(tagName); 
     323    }, 
     324     
     325    id: function(nodes, root, id, combinator) { 
     326      var targetNode = $(id), h = Selector.handlers; 
     327      if (!nodes && root == document) return targetNode ? [targetNode] : []; 
     328      if (nodes) { 
     329        if (combinator) { 
     330          if (combinator == 'child') { 
     331            for (var i = 0, node; node = nodes[i]; i++) 
     332              if (targetNode.parentNode == node) return [targetNode]; 
     333          } else if (combinator == 'descendant') { 
     334            for (var i = 0, node; node = nodes[i]; i++) 
     335              if (Element.descendantOf(targetNode, node)) return [targetNode]; 
     336          } else if (combinator == 'adjacent') { 
     337            for (var i = 0, node; node = nodes[i]; i++) 
     338              if (Selector.handlers.previousElementSibling(targetNode) == node) 
     339                return [targetNode]; 
     340          } else nodes = h[combinator](nodes); 
     341        }  
     342        for (var i = 0, node; node = nodes[i]; i++) 
     343          if (node == targetNode) return [targetNode]; 
     344        return []; 
     345      } 
     346      return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : []; 
     347    }, 
     348 
     349    className: function(nodes, root, className, combinator) { 
     350      if (nodes && combinator) nodes = this[combinator](nodes); 
     351      return Selector.handlers.byClassName(nodes, root, className); 
     352    }, 
     353 
     354    byClassName: function(nodes, root, className) { 
     355      if (!nodes) nodes = Selector.handlers.descendant([root]); 
     356      var needle = ' ' + className + ' '; 
     357      for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) { 
     358        nodeClassName = node.className; 
     359        if (nodeClassName.length == 0) continue; 
     360        if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle)) 
     361          results.push(node); 
     362      } 
     363      return results; 
     364    },     
     365     
     366    attrPresence: function(nodes, root, attr) { 
     367      var results = []; 
     368      for (var i = 0, node; node = nodes[i]; i++) 
     369        if (Element.hasAttribute(node, attr)) results.push(node); 
     370      return results;       
     371    }, 
     372     
     373    attr: function(nodes, root, attr, value, operator) { 
     374      var handler = Selector.operators[operator], results = []; 
     375      for (var i = 0, node; node = nodes[i]; i++) { 
     376        var nodeValue = Element.readAttribute(node, attr); 
     377        if (nodeValue === null) continue; 
     378        if (handler(nodeValue, value)) results.push(node); 
     379      } 
     380      return results; 
     381    }, 
     382                 
     383    pseudo: function(nodes, name, value, root, combinator) { 
     384      if (combinator) nodes = this[combinator](nodes); 
     385      return Selector.pseudos[name](nodes, value, root); 
     386    } 
     387  }, 
     388   
     389  pseudos: { 
     390    'first-child': function(nodes, value, root) { 
     391      for (var i = 0, results = [], node; node = nodes[i]; i++) { 
     392        if (Selector.handlers.previousElementSibling(node)) continue; 
     393          results.push(node); 
     394      } 
     395      return results; 
     396    }, 
     397    'last-child': function(nodes, value, root) { 
     398      for (var i = 0, results = [], node; node = nodes[i]; i++) { 
     399        if (Selector.handlers.nextElementSibling(node)) continue; 
     400          results.push(node); 
     401      } 
     402      return results; 
     403    }, 
     404    'only-child': function(nodes, value, root) { 
     405      var h = Selector.handlers; 
     406      for (var i = 0, results = [], node; node = nodes[i]; i++) 
     407        if (!h.previousElementSibling(node) && !h.nextElementSibling(node)) 
     408          results.push(node);   
     409      return results; 
     410    }, 
     411    'nth-child':        function(nodes, formula, root) {  
     412      return Selector.pseudos.nth(nodes, formula, root);  
     413    },     
     414    'nth-last-child':   function(nodes, formula, root) {  
     415      return Selector.pseudos.nth(nodes, formula, root, true); 
     416    },     
     417    'nth-of-type':      function(nodes, formula, root) {  
     418      return Selector.pseudos.nth(nodes, formula, root, false, true); 
     419    }, 
     420    'nth-last-of-type': function(nodes, formula, root) {  
     421      return Selector.pseudos.nth(nodes, formula, root, true, true); 
     422    },     
     423    'first-of-type':    function(nodes, formula, root) {  
     424      return Selector.pseudos.nth(nodes, "1", root, false, true); 
     425    },     
     426    'last-of-type':     function(nodes, formula, root) {  
     427      return Selector.pseudos.nth(nodes, "1", root, true, true); 
     428    }, 
     429    'only-of-type':     function(nodes, formula, root) { 
     430      var p = Selector.pseudos; 
     431      return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root); 
     432    }, 
     433     
     434    // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type 
     435    nth: function(nodes, formula, root, reverse, ofType) { 
     436      if (formula == 'even') formula = '2n+0'; 
     437      if (formula == 'odd')  formula = '2n+1'; 
     438      var h = Selector.handlers, results = [], indexed = [], m; 
     439      h.mark(nodes); 
     440      for (var i = 0, node; node = nodes[i]; i++) { 
     441        if (!node.parentNode._counted) { 
     442          h.index(node.parentNode, reverse, ofType); 
     443          indexed.push(node.parentNode); 
     444        } 
     445      } 
     446      if (formula.match(/^\d+$/)) { // just a number 
     447        formula = Number(formula); 
     448        for (var i = 0, node; node = nodes[i]; i++) 
     449          if (node.nodeIndex == formula) results.push(node); 
     450      } else if (m = formula.match(/^(\d+)?n(\+(\d+))?$/)) { // an+b 
     451        var a = m[1] ? Number(m[1]) : 1; 
     452        var b = m[3] ? Number(m[3]) : 0; 
     453        for (var i = 0, node; node = nodes[i]; i++) 
     454          if (node.nodeIndex % a == b) results.push(node); 
     455      } 
     456      h.unmark(nodes); 
     457      h.unmark(indexed);     
     458      return results;   
     459    }, 
     460         
     461    'empty': function(nodes, value, root) { 
     462      for (var i = 0, results = [], node; node = nodes[i]; i++) { 
     463        // IE treats comments as element nodes 
     464        if (node.tagName == '!' || (node.firstChild && !node.innerHTML.match(/^\s*$/))) continue; 
     465        results.push(node); 
     466      } 
     467      return results; 
     468    }, 
     469     
     470    'not': function(nodes, selector, root) { 
     471      var h = Selector.handlers, exclusions = $A(nodes), selectorType, m; 
     472      for (var i in Selector.patterns) { 
     473        if (m = selector.match(Selector.patterns[i])) { 
     474          selectorType = i; break; 
     475        } 
     476      } 
     477      switch(selectorType) { 
     478        case 'className': case 'tagName': case 'id': // fallthroughs 
     479        case 'attrPresence': exclusions = h[selectorType](exclusions, root, m[1], false); break; 
     480        case 'attr': m[3] = m[5] || m[6]; exclusions = h.attr(exclusions, root, m[1], m[3], m[2]); break;         
     481        case 'pseudo': exclusions = h.pseudo(exclusions, m[1], m[6], root, false); break;    
     482        // only 'simple selectors' (one token) allowed in a :not clause 
     483        default: throw 'Illegal selector in :not clause.'; 
     484      } 
     485      h.mark(exclusions); 
     486      for (var i = 0, results = [], node; node = nodes[i]; i++) 
     487        if (!node._counted) results.push(node); 
     488      h.unmark(exclusions); 
     489      return results; 
     490    }, 
     491     
     492    'enabled': function(nodes, value, root) { 
     493      for (var i = 0, results = [], node; node = nodes[i]; i++) 
     494        if (!node.disabled) results.push(node); 
     495      return results; 
     496    }, 
     497     
     498    'disabled': function(nodes, value, root) { 
     499      for (var i = 0, results = [], node; node = nodes[i]; i++) 
     500        if (node.disabled) results.push(node); 
     501      return results; 
     502    }, 
     503     
     504    'checked': function(nodes, value, root) { 
     505      for (var i = 0, results = [], node; node = nodes[i]; i++) 
     506        if (node.checked) results.push(node); 
     507      return results; 
     508    } 
     509  }, 
     510     
     511  operators: { 
     512    '=':  function(nv, v) { return nv == v; }, 
     513    '!=': function(nv, v) { return nv != v; }, 
     514    '^=': function(nv, v) { return nv.startsWith(v); }, 
     515    '$=': function(nv, v) { return nv.endsWith(v); }, 
     516    '*=': function(nv, v) { return nv.include(v); }, 
     517    '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); }, 
     518    '|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); } 
     519  }, 
     520 
    105521  matchElements: function(elements, expression) { 
    106     var selector = new Selector(expression); 
    107     return elements.select(selector.match.bind(selector)).map(Element.extend); 
     522    var matches = new Selector(expression).findElements(), h = Selector.handlers; 
     523    h.mark(matches); 
     524    for (var i = 0, results = [], element; element = elements[i]; i++) 
     525      if (element._counted) results.push(element); 
     526    h.unmark(matches); 
     527    return results; 
    108528  }, 
    109529   
    110530  findElement: function(elements, expression, index) { 
    111     if (typeof expression == 'number') index = expression, expression = false; 
     531    if (typeof expression == 'number') {  
     532      index = expression; expression = false; 
     533    } 
    112534    return Selector.matchElements(elements, expression || '*')[index || 0]; 
    113535  }, 
    114536   
    115537  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(); 
     538    var exprs = expressions.join(','), expressions = []; 
     539    exprs.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) { 
     540      expressions.push(m[1].strip()); 
     541    }); 
     542    var results = [], h = Selector.handlers;     
     543    for (var i = 0, l = expressions.length, selector; i < l; i++) { 
     544      selector = new Selector(expressions[i].strip()); 
     545      h.concat(results, selector.findElements(element)); 
     546    } 
     547    return (l > 1) ? h.unique(results) : results; 
    124548  } 
    125549}); 
  • spinoffs/prototype/trunk/test/lib/unittest.js

    r5649 r6366  
    306306  assertEnumEqual: function(expected, actual) { 
    307307    var message = arguments[2] || "assertEnumEqual"; 
    308     try { $A(expected).length == $A(actual).length &&  
     308    expected = $A(expected); 
     309    actual = $A(actual); 
     310    try { expected.length == actual.length &&  
    309311      expected.zip(actual).all(function(pair) { return pair[0] == pair[1] }) ? 
    310312        this.pass() : this.fail(message + ': expected ' + Test.Unit.inspect(expected) +  
  • spinoffs/prototype/trunk/test/unit/selector.html

    r5957 r6366  
    11<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
    2         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
    3 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> 
     2                                "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
     3<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" 
     4 xmlns:html="http://www.w3.org/1999/xhtml"> 
    45<head> 
    5   <title>Prototype Unit test file</title> 
    6   <meta http-equiv="content-type" content="text/html; charset=utf-8" /> 
    7   <script src="../../dist/prototype.js" type="text/javascript"></script> 
    8   <script src="../lib/unittest.js" type="text/javascript"></script> 
    9   <link rel="stylesheet" href="../test.css" type="text/css" /> 
    10   <style type="text/css" media="screen"> 
    11   /* <![CDATA[ */ 
    12     #testcss1 { font-size:11px; color: #f00; } 
    13     #testcss2 { font-size:12px; color: #0f0; display: none; } 
    14   /* ]]> */ 
    15   </style> 
     6       <title>Prototype Unit test file</title> 
     7       <meta http-equiv="content-type" content="text/html; charset=utf-8" /> 
     8       <script src="../../dist/prototype.js" type="text/javascript"></script> 
     9       <script src="../lib/unittest.js" type="text/javascript"></script> 
     10       <link rel="stylesheet" href="../test.css" type="text/css" /> 
     11       <style type="text/css" media="screen"> 
     12       /* <![CDATA[ */ 
     13               #testcss1 { font-size:11px; color: #f00; } 
     14               #testcss2 { font-size:12px; color: #0f0; display: none; } 
     15       /* ]]> */ 
     16       </style> 
    1617</head> 
    1718<body> 
    1819<h1>Prototype Unit test file</h1> 
    1920<p> 
    20   Test of utility functions in selector.js 
     21       Test of utility functions in selector.js 
    2122</p> 
    2223 
    2324<div id="fixtures" style="display: none"> 
    24   <h1 class="title">Some title <span>here</span></h1> 
    25   <p id="p" class="first summary"> 
    26     <strong id="strong">This</strong> is a short blurb 
    27     <a id="link_1" class="first internal" href="#">with a link</a> or  
    28     <a id="link_2" class="internal highlight" href="#"><em id="em">two</em></a>. 
    29     Or <cite id="with_title" title="hello world!">three</cite>. 
    30   </p> 
    31   <ul id="list"> 
    32     <li id="item_1" class="first"><a id="link_3" href="#" class="external"><span id="span">Another link</span></a></li> 
    33     <li id="item_2">Some text</li> 
     25       <h1 class="title">Some title <span>here</span></h1> 
     26       <p id="p" class="first summary"> 
     27               <strong id="strong">This</strong> is a short blurb 
     28               <a id="link_1" class="first internal" rel="external nofollow" href="#">with a link</a> or  
     29               <a id="link_2" class="internal highlight" href="#"><em id="em">two</em></a>. 
     30               Or <cite id="with_title" title="hello world!">a citation</cite>. 
     31       </p> 
     32       <ul id="list"> 
     33               <li id="item_1" class="first"><a id="link_3" href="#" class="external"><span id="span">Another link</span></a></li> 
     34               <li id="item_2">Some text</li> 
    3435                <li id="item_3" xml:lang="es-us" class="">Otra cosa</li> 
    35   </ul> 
    36  
    37   <!-- this form has a field with the name 'id', 
    38     therefore its ID property won't be 'troubleForm': --> 
    39   <form id="troubleForm"><input type="hidden" name="id" /></form> 
    40 </div> 
     36        </ul> 
     37 
     38        <!-- this form has a field with the name 'id', 
     39                therefore its ID property won't be 'troubleForm': --> 
     40        <form id="troubleForm"> 
     41                <input type="hidden" name="id" id="hidden" /> 
     42                <input type="text" name="disabled_text_field" id="disabled_text_field" disabled="disabled" /> 
     43                <input type="text" name="enabled_text_field" id="enabled_text_field" /> 
     44                <input type="checkbox" name="checkboxes" id="checked_box" checked="checked" value="Checked" /> 
     45                <input type="checkbox" name="checkboxes" id="unchecked_box" value="Unchecked"/> 
     46                <input type="radio" name="radiobuttons" id="checked_radio" checked="checked" value="Checked" /> 
     47                <input type="radio" name="radiobuttons" id="unchecked_radio" value="Unchecked" /> 
     48        </form> 
     49         
     50        <div id="level1"> 
     51                <span id="level2_1"> 
     52                        <span id="level3_1"></span> 
     53                        <!-- This comment should be ignored by the adjacent selector --> 
     54                        <span id="level3_2"></span> 
     55                </span> 
     56                <span id="level2_2"> 
     57                        <em id="level_only_child">               
     58                        </em> 
     59                </span> 
     60                <div id="level2_3"></div> 
     61        </div> <!-- #level1 --> 
     62 
     63        <div id="dupContainer"> 
     64                <span id="dupL1"> 
     65                        <span id="dupL2"> 
     66                                <span id="dupL3"> 
     67                                        <span id="dupL4"> 
     68                                                <span id="dupL5"></span> 
     69                                        </span> 
     70                                </span> 
     71                        </span> 
     72                </span> 
     73        </div> <!-- #dupContainer --> 
     74 
     75        <div id="grandfather"> grandfather               
     76                <div id="father" class="brothers men"> father                     
     77                        <div id="son"> son </div> 
     78                </div> 
     79                <div id="uncle" class="brothers men"> uncle </div> 
     80        </div>   
     81 
     82        <form id="commaParent" title="commas,are,good"> 
     83                <input type="hidden" id="commaChild" name="foo" value="#commaOne,#commaTwo" /> 
     84                <input type="hidden" id="commaTwo" name="foo2" value="oops" /> 
     85        </form> 
     86         
     87</div> <!-- #fixtures --> 
    4188 
    4289<!-- Log output --> 
     
    4693<script type="text/javascript" language="javascript" charset="utf-8"> 
    4794// <![CDATA[ 
    48   new Test.Unit.Runner({ 
    49     testSelectorWithTagName: function() {with(this) { 
    50       assertEnumEqual($A(document.getElementsByTagName('li')), $$('li')); 
    51       assertEnumEqual([$('strong')], $$('strong')); 
    52       assertEnumEqual([], $$('nonexistent')); 
    53       assertEnumEqual($A(document.getElementsByTagName('*')), $$('*')); 
    54     }}, 
    55      
    56     testSelectorWithId: function() {with(this) { 
    57       assertEnumEqual([$('fixtures')], $$('#fixtures')); 
    58       assertEnumEqual([], $$('#nonexistent')); 
    59       assertEnumEqual([$('troubleForm')], $$('#troubleForm')); 
    60     }}, 
    61      
    62     testSelectorWithClassName: function() {with(this) { 
    63       assertEnumEqual($('p', 'link_1', 'item_1'), $$('.first')); 
    64       assertEnumEqual([], $$('.second')); 
    65     }}, 
    66      
    67     testSelectorWithTagNameAndId: function() {with(this) { 
    68       assertEnumEqual([$('strong')], $$('strong#strong')); 
    69       assertEnumEqual([], $$('p#strong')); 
    70     }}, 
    71      
    72     testSelectorWithTagNameAndClassName: function() {with(this) { 
    73       assertEnumEqual($('link_1', 'link_2'), $$('a.internal')); 
    74       assertEnumEqual([$('link_2')], $$('a.internal.highlight')); 
    75       assertEnumEqual([$('link_2')], $$('a.highlight.internal')); 
    76       assertEnumEqual([], $$('a.highlight.internal.nonexistent')); 
    77     }}, 
    78      
    79     testSelectorWithIdAndClassName: function() {with(this) { 
    80       assertEnumEqual([$('link_2')], $$('#link_2.internal')); 
    81       assertEnumEqual([$('link_2')], $$('.internal#link_2')); 
    82       assertEnumEqual([$('link_2')], $$('#link_2.internal.highlight')); 
    83       assertEnumEqual([], $$('#link_2.internal.nonexistent')); 
    84     }}, 
    85      
    86     testSelectorWithTagNameAndIdAndClassName: function() {with(this) { 
    87       assertEnumEqual([$('link_2')], $$('a#link_2.internal')); 
    88       assertEnumEqual([$('link_2')], $$('a.internal#link_2')); 
    89       assertEnumEqual([$('item_1')], $$('li#item_1.first')); 
    90       assertEnumEqual([], $$('li#item_1.nonexistent')); 
    91       assertEnumEqual([], $$('li#item_1.first.nonexistent')); 
    92     }}, 
    93      
    94     test$$MatchesAncestryWithTokensSeparatedByWhitespace: function() {with(this) { 
    95       assertEnumEqual($('em', 'span'), $$('#fixtures a *')); 
    96       assertEnumEqual([$('p')], $$('div#fixtures p')); 
    97     }}, 
    98      
    99     test$$CombinesResultsWhenMultipleExpressionsArePassed: function() {with(this) { 
    100       assertEnumEqual($('link_1', 'link_2', 'item_1', 'item_2', 'item_3'), $$('#p a', ' ul#list li ')); 
    101     }}, 
    102  
    103     testSelectorWithTagNameAndAttributeExistence: function() {with(this) { 
    104       assertEnumEqual($$('#fixtures h1'), $$('h1[class]')); 
    105       assertEnumEqual($$('#fixtures h1'), $$('h1[CLASS]')); 
    106       assertEnumEqual([$('item_3')], $$('li#item_3[class]')); 
    107     }}, 
    108      
    109     testSelectorWithTagNameAndSpecificAttributeValue: function() {with(this) { 
    110       assertEnumEqual($('link_1', 'link_2', 'link_3'), $$('a[href="#"]')); 
    111       assertEnumEqual($('link_1', 'link_2', 'link_3'), $$('a[href=#]')); 
    112     }}, 
    113      
    114     testSelectorWithTagNameAndWhitespaceTokenizedAttributeValue: function() {with(this) { 
    115       assertEnumEqual($('link_1', 'link_2'), $$('a[class~="internal"]')); 
    116       assertEnumEqual($('link_1', 'link_2'), $$('a[class~=internal]')); 
    117     }}, 
    118      
    119     testSelectorWithUniversalAndHyphenTokenizedAttributeValue: function() {with(this) { 
    120       assertEnumEqual([$('item_3')], $$('*[xml:lang|="es"]')); 
    121       assertEnumEqual([$('item_3')], $$('*[xml:lang|="ES"]')); 
    122     }}, 
    123      
    124     testSelectorWithTagNameAndNegatedAttributeValue: function() {with(this) { 
    125       assertEnumEqual([], $$('a[href!=#]')); 
    126     }}, 
    127      
    128     test$$WithNestedAttributeSelectors: function() {with(this) { 
    129       assertEnumEqual([$('strong')], $$('div[style] p[id] strong')); 
    130     }}, 
    131      
    132     testSelectorWithMultipleConditions: function() {with(this) { 
    133       assertEnumEqual([$('link_3')], $$('a[class~=external][href="#"]')); 
    134       assertEnumEqual([], $$('a[class~=external][href!="#"]')); 
    135     }}, 
    136      
    137     testSelectorMatchElements: function() {with(this) { 
    138       assertElementsMatch(Selector.matchElements($('list').descendants(), 'li'), '#item_1', '#item_2', '#item_3'); 
    139       assertElementsMatch(Selector.matchElements($('fixtures').descendants(), 'a.internal'), '#link_1', '#link_2'); 
    140       assertEnumEqual([], Selector.matchElements($('fixtures').descendants(), 'p.last')); 
    141     }}, 
    142      
    143     testSelectorFindElement: function() {with(this) { 
    144       assertElementMatches(Selector.findElement($('list').descendants(), 'li'), 'li#item_1.first'); 
    145       assertElementMatches(Selector.findElement($('list').descendants(), 'li', 1), 'li#item_2'); 
    146       assertElementMatches(Selector.findElement($('list').descendants(), 'li#item_3'), 'li'); 
    147       assertEqual(undefined, Selector.findElement($('list').descendants(), 'em')); 
    148     }}, 
    149  
    150     testSelectorWithSpaceInAttributeValue: function() {with(this) { 
    151       assertEnumEqual([$('with_title')], $$('cite[title="hello world!"]')); 
    152     }}     
    153   }, 'testlog'); 
     95 
     96