// written by Russell Gooday (c)2011
(function(doc, global){
var Qselect, utils, splitToParts, find, filter, mapFind, child, relative,
attribute, groupChildren, propDelete,
hasOwn = Object.prototype.hasOwnProperty,
push = Array.prototype.push,
slice = Array.prototype.slice,
rxSplitParts = new RegExp(
'(\\w+:[-\\w]+(?:\\([^)]+\\))?)|'
+ '((?:\\w+)?\\[[^\\]]+])|'
+ '(>)|'
+ '(#[-\\w]+)|'
+ '((?:\\w+)?\\.[-\\w]+)|'
+ '(\\w+)', 'g'),
partMatches = ['selector', 'pseudo', 'attribute', 'relative', 'id', 'className', 'tag'];
// -------------- Generic utils ----------------------------------
utils = {
// make Array
slice : (function(doc){
try {
slice.call(doc.createElement('div'));
return function( obj ) { return slice.call( obj ); };
} catch(e) {
return function( obj ) {
var ret = [], len = obj.length;
if (len) { while (len--) { ret[len] = obj[len]; } }
return ret;
};
}
}(doc)),
// forEach
forEach : function( arr, fn, context ) {
var that = context || null,
len = arr.length, i = 0;
for (; i < len; i++) { fn.call(that, arr[i], i, arr); }
},
// shallow copy
extend: function(from) {
var to = arguments[1] || this, prop;
for (prop in from) {
if ( hasOwn.call(from, prop) ) { to[prop] = from[prop]; }
}
}
};
// ---------------- End Generic utils ----------------------------
// ---------------- Split to Parts -------------------------------
// returns an array of tokens. for example
// 'ul li:first-child' becomes
// [0] {find: 'tag', selector: 'ul'}
// [1] {find: 'pseudo', selector: 'li:first-child'}
// These are processed by search.
splitToParts = function(selector) {
// Match Types
// ['selector',
// 1:'pseudo', 2:'attribute', 3:'relative', 4:'id', 5:'className', 6:'tag']
var regEx = rxSplitParts,
matches = partMatches,
len = matches.length,
parts = [],
match, i = 1;
// reset regEx counter for exec
regEx.lastIndex = 0;
while (match = regEx.exec(selector)) {
// loop through match types
for (i = 1; i < len; i++) {
if (match[i]) {
parts.push({find: matches[i], selector: match[0]}); break;
}
}
}
return parts;
};
// ----------------------------------- End Split to Parts --------------------------------------------
// ----------------------------------- Qselect -------------------------------------------------------
Qselect = function( selector, root ) {
if ( !(this instanceof Qselect) ) { return new Qselect( selector, root ); }
var parts,
root = root || doc;
selector = selector.replace(/^[ ]|[ ]$/g, '');
if( Qselect.cache[selector] ) { return Qselect.cache[selector]; }
// Need to also handle where root maybe an html collection
if (root instanceof Qselect) {
parts = splitToParts(root.selector.match(/([^\s]+)$/)[1] + ' ' + selector);
root = root.els;
} else {
parts = splitToParts(selector);
}
this.els = Qselect.search(parts, root);
this.selector = selector; //Todo this is flawed. Needs to be root + selector
Qselect.cache[selector] = this;
};
// Add utils as static properties of Qselect
utils.extend(utils, Qselect);
Qselect.extend({
rxClasses : {
"CLASS": /(\w+)?\.([-\w]+)/,
"PSEUDO": /(\w+):([-\w]+)(?:\(([^)]+)\))?/,
"ATTRIBUTE": /(\w+)?\[/ // Todo Finish this expression
},
cache : {}, // needs work.
// The main search method.
search : function search( selector, root ){
var query = selector[0],
rest = selector.slice(1),
results = [],
els = (root.length) ? root : false,
len, i = 0;
if (!els) {
if (query.find === 'relative') {
els = mapFind.relative(root, query.selector, rest[0]);
rest = rest.slice(1);
} else {
els = mapFind[query.find](root, query.selector);
}
}
for (len = els.length; i < len; i++) {
if (rest[0]) { push.apply( results, search(rest, els[i]) ); }
else { results.push (els[i]); }
}
return results;
}
});
Qselect.prototype.get = function(){ return this.els; };
filter = {
attr : function (elems, attr, name) {
var results = [], len = elems.length, i = 0;
for (; i < len; i++) {
if (elems[i][attr] === name) { results.push(elems[i]); }
}
return results;
}
};
mapFind = {
id : function(root, selector){ return find.id(root, selector.slice(1)); },
tag : function(root, selector){ return find.tag(root, selector); },
className : function(root, selector){
var match = selector.match(Qselect.rxClasses.CLASS),
className = match[2],
tag = match[1];
return (!tag)
? find.className( root, className )
: filter.attr( find.className(root, className), 'nodeName', tag.toUpperCase() );
},
pseudo : function(root, selector){
var match = selector.match(Qselect.rxClasses.PSEUDO),
tag = match[1], pseudo = match[2], expr = match[3];
return find.pseudo( root, tag, pseudo, expr );
},
attribute : function(root, selector){
// Todo
},
relative : function(root, selector, query){
return find.relative(root, selector, query);
}
};
find = {
// ID
id : function (root, id) { return [root.getElementById(id)]; },
// TAG
tag : function (root, tag) { return Qselect.slice(root.getElementsByTagName(tag)); },
// CLASSNAME
className : (function() {
if (doc.getElementsByClassName) {
return function (root, className) {
return Qselect.slice(root.getElementsByClassName(className));
};
}
return function (root, className) {
var toMatch = new RegExp("\\b" + className + "\\b"),
elems = root.getElementsByTagName('*'),
len = elems.length,
results = [],
elem = elems[0],
i = 0, j = 0;
for (; i < len; elem = elems[++i]) {
if (toMatch.test(elem.className)) { results[j++] = elem; }
}
return results;
};
}(doc)),
// PSEUDO
pseudo : function( root, tag, pseudo, expr ) {
var els = root.getElementsByTagName(tag),
filtered = [],
len, el, i = 0, j = 0;
if (/(first|last)-child/.test(pseudo)) {
len = els.length; el = els[0];
for (; i < len; el = els[++i]) { if (child[pseudo](el)) { filtered[j++] = el; } }
return filtered;
}
// Note: May re-write pseudo to take a node and return a boolean instead.
// That way can eliminate this extra conditional branching.
if (/nth-child/.test(pseudo)) {
Qselect.forEach(groupChildren(els), function(els){
push.apply(filtered, child[pseudo](els, expr));
});
}
return filtered;
},
// only takes '>' at the moment.
relative: function(root, selector, query) {
var els = mapFind[query.find](root, query.selector),
len = els.length,
filtered = [],
i = 0, j = 0;
for (; i < len; i++) {
if(relative[selector]( root, els[i] )) { filtered[j++] = els[i]; }
}
return filtered;
}
};
child = {
// split nth-child expression in to matches
rxExpr : /(-(?=\d*n\+\d+))?(\d*)(?:(n)(?:([-+])(\d+))?)?/,
exprCache : {odd: 0, even: 1},
"first-child" : function(el) {
var currElem = el;
while (currElem = currElem.previousSibling) {
if (currElem.nodeType === 1) { return false; }
}
return true;
},
"last-child" : function(el) {
var currElem = el;
while (currElem = currElem.nextSibling) {
if (currElem.nodeType === 1) { return false; }
}
return true;
},
"nth-child" : function(els, expr) {
var filtered = [],
len = els.length,
e = {}, n = 0, i = 0, j = 0;
// if just a quick index value e.g. (5). return that element
if (/^\d+$/.test(expr)){ return [els[expr]]; }
if (/^odd|even$/.test(expr)) { n = 2; i = child.exprCache[expr]; }
else {
// if the matched/split expression isn't cached.
if (!child.exprCache[expr]) {
child.rxExpr.test(expr);
// For clarity have labeled the matches
child.exprCache[expr] = {
oper1 : RegExp.$1, // -
num : RegExp.$2, // 3
n : RegExp.$3, // n
oper2 : RegExp.$4, // +
offset : RegExp.$5 // 9
};
}
// n is the step i.e. 2n is a step of 2
// i is the first element.
e = child.exprCache[expr];
n = parseInt(e.num || 1, 10);
i = parseInt((e.oper2)
? (e.oper2 === '+')
? e.offset : (e.num || e.offset) - e.offset
: (e.num || 1), 10)-1;
}
if (!e.oper1) { for ( ; i < len; i += n ) { filtered[j++] = els[i]; } }
else { for (; i > -1; i -= n ) { filtered.unshift(els[i]); } }
return filtered;
}
};
attribute = {
};
relative = {
">" : function( root, node ){
if (node.parentNode === root) { return true; }
return false;
}
};
// groups elements in parent->children groups
// need this for nth-child
groupChildren = function(els){
var len = els.length,
groups = [], parents = [],
pId = 0, // unique id for parentNodes
el, pNode, i = 0;
for (; i < len; i++) {
el = els[i];
pNode = el.parentNode;
if ( pNode._pId === undefined ) {
pNode._pId = pId;
groups[pId] = []; // add new parent group
parents[pId++] = pNode; // store parent node for clean up later
}
groups[pNode._pId].push(el);
}
// cleanup parents and remove unique ids
while(parents.length) { propDelete(parents.pop(), '_pId'); }
return groups;
};
// delete property from element
propDelete = (function(el, prop){
try {
var body = doc.body, tmpId = '_tmpId:' + new Date();
body[tmpId] = true;
delete body[tmpId]; // try this
return function(el, prop){ delete el[prop]; };
} catch(e){
body.removeAttribute(tmpId);
return function(el,prop){ el.removeAttribute(prop); };
}
}());
// Todo: Add to framework, rather than window.
global.Qselect = Qselect;
}(window.document, window));
Bookmarks