silverstripe-framework/thirdparty/jquery-concrete/jquery.selector.matches.js
2009-11-21 02:26:09 +00:00

354 lines
8.7 KiB
JavaScript

/*
This attempts to do the opposite of Sizzle.
Sizzle is good for finding elements for a selector, but not so good for telling if an individual element matches a selector
*/
(function($) {
/**** CAPABILITY TESTS ****/
var div = document.createElement('div');
div.innerHTML = '<form id="test"><input name="id" type="text"/></form>';
// In IE 6-7, getAttribute often does the wrong thing (returns similar to el.attr), so we need to use getAttributeNode on that browser
var getAttributeDodgy = div.firstChild.getAttribute('id') !== 'test';
// Does browser support Element.firstElementChild, Element.previousElementSibling, etc.
var hasElementTraversal = div.firstElementChild && div.firstElementChild.tagName == 'FORM';
// Does browser support Element.children
var hasChildren = div.children && div.children[0].tagName == 'FORM';
var FUNC_IN = /^\s*function\s*\([^)]*\)\s*{/;
var FUNC_OUT = /}\s*$/;
var funcToString = function(f) {
return (''+f).replace(FUNC_IN,'').replace(FUNC_OUT,'');
}
// Can we use Function#toString ?
try {
var testFunc = function(){ return 'good' };
if ((new Function('',funcToString(testFunc)))() != 'good') funcToString = false;
}
catch(e) { funcToString = false; console.log(e.message);/*pass*/ }
/**** INTRO ****/
var GOOD = /GOOD/g;
var BAD = /BAD/g;
var STARTS_WITH_QUOTES = /^['"]/g;
var join = function(js) {
return js.join('\n');
}
var join_complex = function(js) {
code = new String(js.join('\n')); // String objects can have properties set. strings can't
code.complex = true;
return code;
}
/**** ATTRIBUTE ACCESSORS ****/
var getAttr;
// Good browsers
if (!getAttributeDodgy) {
getAttr = function(attr){ return 'var _'+attr+' = el.getAttribute("'+attr+'");' ; }
}
// IE 6, 7
else {
// On IE 6 + 7, getAttribute still has to be called with DOM property mirror name, not attribute name. Map attributes to those names
var getAttrIEMap = { 'class': 'className', 'for': 'htmlFor' };
getAttr = function(attr) {
var ieattr = getAttrIEMap[attr] || attr;
return 'var _'+attr+' = el.getAttribute("'+ieattr+'",2) || (el.getAttributeNode("'+attr+'")||{}).nodeValue;';
}
}
/**** ATTRIBUTE COMPARITORS ****/
var attrchecks = {
'-': '!_K',
'=': '_K != "V"',
'!=': '_K == "V"',
'~=': '__K.indexOf(" V ") == -1',
'^=': '!_K || _K.indexOf("V") != 0',
'*=': '!_K || _K.indexOf("V") == -1',
'$=': '!_K || _K.substr(_K.length-"V".length) != "V"'
}
/**** STATE TRACKER ****/
var State = $.selector.State = Base.extend({
init: function(){
this.reset();
},
reset: function() {
this.attrs = {}; this.wsattrs = {};
},
prev: function(){
this.reset();
if (hasElementTraversal) return 'el = el.previousElementSibling';
return 'while((el = el.previousSibling) && el.nodeType != 1) {}';
},
next: function() {
this.reset();
if (hasElementTraversal) return 'el = el.nextElementSibling';
return 'while((el = el.nextSibling) && el.nodeType != 1) {}';
},
prevLoop: function(body){
this.reset();
if (hasElementTraversal) return join([ 'while(el = el.previousElementSibling){', body]);
return join([
'while(el = el.previousSibling){',
'if (el.nodeType != 1) continue;',
body,
]);
},
parent: function() {
this.reset();
return 'el = el.parentNode;';
},
parentLoop: function(body) {
this.reset();
return join([
'while((el = el.parentNode) && el.nodeType == 1){',
body,
'}'
]);
},
uses_attr: function(attr) {
if (this.attrs[attr]) return;
this.attrs[attr] = true;
return getAttr(attr);
},
uses_wsattr: function(attr) {
if (this.wsattrs[attr]) return;
this.wsattrs[attr] = true;
return join([this.uses_attr(attr), 'var __'+attr+' = " "+_'+attr+'+" ";']);
},
save: function(lbl) {
return 'var el'+lbl+' = el;';
},
restore: function(lbl) {
this.reset();
return 'el = el'+lbl+';';
}
});
/**** PSEUDO-CLASS DETAILS ****/
var pseudoclschecks = {
'first-child': join([
'var cel = el;',
'while(cel = cel.previousSibling){ if (cel.nodeType === 1) BAD; }',
]),
'last-child': join([
'var cel = el;',
'while(cel = cel.nextSibling){ if (cel.nodeType === 1) BAD; }'
]),
'nth-child': function(a,b) {
var get_i = join([
'var i = 1, cel = el;',
'while(cel = cel.previousSibling){',
'if (cel.nodeType === 1) i++;',
'}',
]);
if (a == 0) return join([
get_i,
'if (i- '+b+' != 0) BAD;'
]);
else if (b == 0 && a >= 0) return join([
get_i,
'if (i%'+a+' != 0 || i/'+a+' < 0) BAD;'
]);
else if (b == 0 && a < 0) return join([
'BAD;'
]);
else return join([
get_i,
'if ((i- '+b+')%'+a+' != 0 || (i- '+b+')/'+a+' < 0) BAD;'
]);
}
};
// Needs to refence contents of object, so must be injected after definition
pseudoclschecks['only-child'] = join([
pseudoclschecks['first-child'],
pseudoclschecks['last-child']
]);
/**** SimpleSelector ****/
$.selector.SimpleSelector.addMethod('compile', function(el) {
var js = [];
/* Check against element name */
if (this.tag && this.tag != '*') {
js[js.length] = 'if (el.tagName != "'+this.tag.toUpperCase()+'") BAD;';
}
/* Check against ID */
if (this.id) {
js[js.length] = el.uses_attr('id');
js[js.length] = 'if (_id !== "'+this.id+'") BAD;';
}
/* Build className checking variable */
if (this.classes.length) {
js[js.length] = el.uses_wsattr('class');
/* Check against class names */
$.each(this.classes, function(i, cls){
js[js.length] = 'if (__class.indexOf(" '+cls+' ") == -1) BAD;';
})
}
/* Check against attributes */
$.each(this.attrs, function(i, attr){
js[js.length] = (attr[1] == '~=') ? el.uses_wsattr(attr[0]) : el.uses_attr(attr[0]);
var check = attrchecks[ attr[1] || '-' ];
check = check.replace( /K/g, attr[0]).replace( /V/g, attr[2] && attr[2].match(STARTS_WITH_QUOTES) ? attr[2].slice(1,-1) : attr[2] );
js[js.length] = 'if ('+check+') BAD;';
});
/* Check against nots */
$.each(this.nots, function(i, not){
var lbl = ++lbl_id;
var func = join([
'l'+lbl+':{',
not.compile(el).replace(BAD, 'break l'+lbl).replace(GOOD, 'BAD'),
'}'
]);
if (!(not instanceof $.selector.SimpleSelector)) func = join([
el.save(lbl),
func,
el.restore(lbl)
])
js[js.length] = func;
});
/* Check against pseudo-classes */
$.each(this.pseudo_classes, function(i, pscls){
var check = pseudoclschecks[pscls[0]];
if (check) {
js[js.length] = ( typeof check == 'function' ? check.apply(this, pscls[1]) : check );
}
else if (check = $.find.selectors.filters[pscls[0]]) {
if (funcToString) {
js[js.length] = funcToString(check).replace(/elem/g,'el').replace(/return([^;]+);/,'if (!($1)) BAD;');
}
else {
js[js.length] = 'if (!$.find.selectors.filters.'+pscls[0]+'(el)) BAD;'
}
}
});
js[js.length] = 'GOOD';
/* Pass */
return join(js);
});
var lbl_id = 0;
/** Turns an compiled fragment into the first part of a combination */
function as_subexpr(f) {
if (f.complex)
return join([
'l'+(++lbl_id)+':{',
f.replace(GOOD, 'break l'+lbl_id),
'}',
]);
else
return f.replace(GOOD, '');
}
var combines = {
' ': function(el, f1, f2) {
return join_complex([
f2,
'while(true){',
el.parent(),
'if (!el || el.nodeType !== 1) BAD;',
f1.compile(el).replace(BAD, 'continue'),
'}'
]);
},
'>': function(el, f1, f2) {
return join([
f2,
el.parent(),
'if (!el || el.nodeType !== 1) BAD;',
f1.compile(el)
]);
},
'~': function(el, f1, f2) {
return join_complex([
f2,
el.prevLoop(),
f1.compile(el).replace(BAD, 'continue'),
'}',
'BAD;'
]);
},
'+': function(el, f1, f2) {
return join([
f2,
el.prev(),
'if (!el) BAD;',
f1.compile(el)
]);
}
};
$.selector.Selector.addMethod('compile', function(el) {
l = this.parts.length;
expr = this.parts[--l].compile(el);
while (l) {
combinator = this.parts[--l];
expr = combines[combinator](el, this.parts[--l], as_subexpr(expr));
}
return expr;
});
$.selector.SelectorsGroup.addMethod('compile', function(el) {
var expr = [], lbl = ++lbl_id;
for (var i=0; i < this.parts.length; i++) {
expr[expr.length] = join([
i == 0 ? el.save(lbl) : el.restore(lbl),
'l'+lbl+'_'+i+':{',
this.parts[i].compile(el).replace(BAD, 'break l'+lbl+'_'+i),
'}'
]);
}
expr[expr.length] = 'BAD;';
return join(expr);
});
$.selector.SelectorBase.addMethod('matches', function(el){
this.matches = new Function('el', join([
'if (!el) return false;',
this.compile(new State()).replace(BAD, 'return false').replace(GOOD, 'return true')
]));
return this.matches(el);
});
})(jQuery);