Skip to content

Commit

Permalink
Integrate support for the W3C Selectors API into the Selector class. …
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
savetheclocktower committed Mar 27, 2008
1 parent 855e273 commit 03c1530
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 17 deletions.
2 changes: 2 additions & 0 deletions 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]
Expand Down
57 changes: 45 additions & 12 deletions src/selector.js
Expand Up @@ -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() {
Expand All @@ -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;

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
},

Expand Down
9 changes: 4 additions & 5 deletions test/unit/selector.html
Expand Up @@ -387,14 +387,14 @@ <h1 class="title">Some title <span>here</span></h1>

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() {
Expand All @@ -407,7 +407,6 @@ <h1 class="title">Some title <span>here</span></h1>
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'))
Expand Down

0 comments on commit 03c1530

Please sign in to comment.