Right taking a plunge in the deepend. Maybe a big ask this.
I’m having a go at writing a selector engine. It’s for personal use with my own sites, so doesn’t really need to be idiot proof.
It will be a fallback for querySelectorAll.
I started out just wanting something simple i.e. find ‘ul li.myClass’, but that wasn’t enough.
This selector is intended to be a module along with an events module, which will bolt onto a framework. The framework constructor will be along the lines of jquery with a dummy constructor, prototype.init etc.
My real question is about the design or design pattern of this particular module. It’s all getting a little bit messy and inconsistent.
There’s a line I keep reading which is ‘favour composition over inheritence’. I’m still none the wiser regarding that, but do get the impression that sticking rigidly to inheritence is problematic. Just not sure how or where to go about that.
So in conclusion pretty clueless:confused:
Rather than waffling on here’s the code. If extra comments or details are needed let me know. Also I’m not looking for exhaustive answers or re-writes, but maybe just a nudge in the right direction.
// 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));
This is a simple test I’m running on the code. I’ve just added the ability to use the returned object as the root for subsequent selections. As pointed out by Raffles. Still needs work though.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>qSelect test</title>
<style type='text/css'></style>
</head>
<body id = 'main'>
<div class = 'testLists'>
<h2>List 1</h2>
<ul class = 'uList1'>
<li>Item 1</li><li>Item 2</li><li>Item 3</li>
<li>Item 4
<ul class = 'uList2'>
<li>Item 1</li><li>Item 2</li><li>Item 3</li>
<li>Item 4</li><li>Item 5</li><li>Item 6</li>
</ul>
</li>
<li>Item 5</li><li>Item 6</li>
<li>Item 7</li>
</ul>
<h2>List 2</h2>
<ul class = 'uList3'>
<li>Item 1</li><li>Item 2</li><li>Item 3</li>
<li>Item 4</li><li>Item 5</li><li>Item 6</li>
</ul>
</div>
<!--qSelect is intended to be a selector module, which will be added to a simple framework-->
<script type = 'text/javascript' src = 'qSelect.js'></script>
<script type='text/javascript'>
// Just a quick function to test Qselect
function testQselect(selector, root, style){
var elems = Qselect(selector, root).get(), //get Elements here
style = style.match(/\\s?([^:]+):\\s?(.+)/);
Qselect.forEach(elems, function(el){
el.style[style[1]] = style[2];
});
}
// A test to see if root is functioning properly
var list1 = Qselect('div.testLists ul.uList1'),
list3 = Qselect('div.testLists ul.uList3');
testQselect('li:nth-child(odd)', list1, 'background: #ccf');
testQselect('li:nth-child(2n)', list3, 'background: #cfc');
testQselect('> li', list1, 'listStyle: none');
</script>
</body>
</html>
Thank you very much
RLM