diff --git a/CHANGELOG b/CHANGELOG index 563b15e..ff224f0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,5 @@ +* Integrate support for the W3C Selectors API into the Selector class. Will now use the API when possible (browser supports the API *and* recognizes the given selector). Means minor changes to the semantics of :enabled, :disabled, and :empty in order to comply with CSS spec. + * Avoid re-extending element in Element#getDimensions. [kangax] * Prevent Hash#toQueryString from serializing objets. [kangax, Tobie Langel] diff --git a/src/selector.js b/src/selector.js index a8c48c8..424da5f 100644 --- a/src/selector.js +++ b/src/selector.js @@ -5,7 +5,17 @@ var Selector = Class.create({ initialize: function(expression) { this.expression = expression.strip(); - this.compileMatcher(); + + if (this.shouldUseSelectorsAPI()) { + this.mode = 'selectorsAPI'; + } else if (this.shouldUseXPath()) { + this.mode = 'xpath'; + this.compileXPathMatcher(); + } else { + this.mode = "normal"; + this.compileMatcher(); + } + }, shouldUseXPath: function() { @@ -20,16 +30,30 @@ var Selector = Class.create({ // XPath can't do namespaced attributes, nor can it read // the "checked" property from DOM nodes - if ((/(\[[\w-]*?:|:checked)/).test(this.expression)) + if ((/(\[[\w-]*?:|:checked)/).test(e)) return false; return true; }, - compileMatcher: function() { - if (this.shouldUseXPath()) - return this.compileXPathMatcher(); + shouldUseSelectorsAPI: function() { + if (!Prototype.BrowserFeatures.SelectorsAPI) return false; + if (!Selector._div) Selector._div = new Element('div'); + + // Make sure the browser treats the selector as valid. Test on an + // isolated element to minimize cost of this check. + + try { + Selector._div.querySelector(this.expression); + } catch(e) { + return false; + } + + return true; + }, + + compileMatcher: function() { var e = this.expression, ps = Selector.patterns, h = Selector.handlers, c = Selector.criteria, le, p, m; @@ -86,8 +110,16 @@ var Selector = Class.create({ findElements: function(root) { root = root || document; - if (this.xpath) return document._getElementsByXPath(this.xpath, root); - return this.matcher(root); + var results; + + switch (this.mode) { + case 'selectorsAPI': + return $A(root.querySelectorAll(this.expression)); + case 'xpath': + return document._getElementsByXPath(this.xpath, root); + default: + return this.matcher(root); + } }, match: function(element) { @@ -178,10 +210,10 @@ Object.extend(Selector, { 'first-child': '[not(preceding-sibling::*)]', 'last-child': '[not(following-sibling::*)]', 'only-child': '[not(preceding-sibling::* or following-sibling::*)]', - 'empty': "[count(*) = 0 and (count(text()) = 0 or translate(text(), ' \t\r\n', '') = '')]", + 'empty': "[count(*) = 0 and (count(text()) = 0)]", 'checked': "[@checked]", - 'disabled': "[@disabled]", - 'enabled': "[not(@disabled)]", + 'disabled': "[(@disabled) and (@type!='hidden')]", + 'enabled': "[not(@disabled) and (@type!='hidden')]", 'not': function(m) { var e = m[6], p = Selector.patterns, x = Selector.xpath, le, v; @@ -575,7 +607,7 @@ Object.extend(Selector, { 'empty': function(nodes, value, root) { for (var i = 0, results = [], node; node = nodes[i]; i++) { // IE treats comments as element nodes - if (node.tagName == '!' || (node.firstChild && !node.innerHTML.match(/^\s*$/))) continue; + if (node.tagName == '!' || node.firstChild) continue; results.push(node); } return results; @@ -593,7 +625,8 @@ Object.extend(Selector, { 'enabled': function(nodes, value, root) { for (var i = 0, results = [], node; node = nodes[i]; i++) - if (!node.disabled) results.push(node); + if (!node.disabled && (!node.type || node.type !== 'hidden')) + results.push(node); return results; }, diff --git a/test/unit/selector.html b/test/unit/selector.html index 7f0fef8..50e4c55 100644 --- a/test/unit/selector.html +++ b/test/unit/selector.html @@ -387,14 +387,14 @@ testSelectorWithEnabledDisabledChecked: function() { this.assertEnumEqual([$('disabled_text_field')], $$('#troubleForm > *:disabled')); - this.assertEnumEqual($('troubleForm').getInputs().without($('disabled_text_field')), $$('#troubleForm > *:enabled')); + this.assertEnumEqual($('troubleForm').getInputs().without($('disabled_text_field'), $('hidden')), $$('#troubleForm > *:enabled')); this.assertEnumEqual($('checked_box', 'checked_radio'), $$('#troubleForm *:checked')); }, testSelectorWithEmpty: function() { - $('level3_1').innerHTML = "\t\n\n\r\n\t "; - this.assertEnumEqual($('level3_1', 'level3_2', 'level_only_child', 'level2_3'), $$('#level1 *:empty')); - this.assertEnumEqual([$('level_only_child')], $$('#level_only_child:empty')); + $('level3_1').innerHTML = ""; + this.assertEnumEqual($('level3_1', 'level3_2', 'level2_3'), $$('#level1 *:empty')); + this.assertEnumEqual([], $$('#level_only_child:empty'), 'newlines count as content!'); }, testIdenticalResultsFromEquivalentSelectors: function() { @@ -407,7 +407,6 @@ this.assertEnumEqual($$('ul > li:nth-child(odd)'), $$('ul > li:nth-child(2n+1)')); this.assertEnumEqual($$('ul > li:first-child'), $$('ul > li:nth-child(1)')); this.assertEnumEqual($$('ul > li:last-child'), $$('ul > li:nth-last-child(1)')); - this.assertEnumEqual($$('#troubleForm *:enabled'), $$('#troubleForm *:not(:disabled)')); this.assertEnumEqual($$('ul > li:nth-child(n-999)'), $$('ul > li')); this.assertEnumEqual($$('ul>li'), $$('ul > li')); this.assertEnumEqual($$('#p a:not(a[rel$="nofollow"])>em'), $$('#p a:not(a[rel$="nofollow"]) > em'))