Nth-child element filter

A work in-progress. Have written code to handle the filtering of child elements. It will form part of a CSS3 patch for IE8’s querySelectorAll.

This is the ‘filter elements’ portion of the code. It takes a node-list, a call back to filter results, a flag for ‘of-type’ selectors and finally a reverse flag to handle last-child and last-child-of selectors.

filterElems = function (els, fn, ofType, reverse) {

    // reversing the node-list to make it easy to search for last-child
    var elems = (reverse)? slice.call(els).reverse() : slice.call(els),
        paren,
        children,
        results = [], i,

        elsOnly = function(els) { return filter(els, function(el){ if (el.nodeType === 1) return true; }); },

        // Matches node-list elements to a parent-node. These matches are then filtered
        // with a supplied callback function and stored in results. Unmatched nodes are
        // returned ready for the next search and match.
        filterEls = function(el) {
            if (el.parentNode === paren) {

                // if 'of-type' pass index 'i' otherwise match element against child index and pass that instead,
                if (fn(el, (ofType)? i += 1 : indexOf(children, el))) { results.push(el); }

            } else { return true; }
        };

    while (elems.length) {

        paren = elems[0].parentNode; i = -1;

        // children in IE8 wrongly includes comment nodes so need to filter them out.
        children = (reverse) ? slice.call(elsOnly(paren.children)).reverse() : slice.call(elsOnly(paren.children));

        elems = filter(elems, filterEls);
    }
    return results;
};

I have tested it in IE10’s IE8 mode and all appears to be functioning correctly.

I guess there are a few question marks. One. The simple method to find last-child elements was to call a first-child callback on a reversed array. It works, but not a 100% on this. Certainly a simple approach.

Two. IE8 wrongly includes comment nodes in the children node-list. So I filter out all but element nodes prior to searching and matching. Again makes things simpler for me, but not sure if this is the best approach. Maybe these could be eliminated during search and match.

If there are any howlers in there would like to know.

Here is a test. Due to IE8 have had to include polyfills for array functions. These will be utilised throughout the framework so are not limited to just child selection.

<!DOCTYPE html>
<html lang="en">
<head> <meta charset="utf-8">
<title>Root QuerySelector</title>
<style>
h1, p {margin: 3px 0px;}

#childTest2 {margin-left: 30px;}
</style>
</head>
<body>
<div id='childTest1'>
  <h1>Heading 1</h1>
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
  <div id='childTest2'>
    <h1>Heading 2</h1>
    <p>Paragraph 1</p>
    <p>Paragraph 2</p>
    <p>Paragraph 3</p>
  </div>
  <p>Paragraph 3</p>
  <p>Paragraph 4</p>
  <p>Paragraph 5</p>
</div>

<div id='childTest3'>
  <h1>Heading 3</h1>
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
  <p>Paragraph 3</p>
  <p>Paragraph 4</p>
</div>

<script>
/*jslint boss: true, laxbreak: true*/
var toString = {}.toString,

    // ---------------------------- Array Methods/IE 8 Poly-fills -----------------------------------------
    // Note: Are used throughout framework.
    slice = (function(doc){

        try {
            // will fail in <IE9
            return ([].slice.call(doc.documentElement) && [].slice);

        } catch(e) {
            // based on MDN shim
            return function(begin, end){

                var len = this.length,
                    i, results = [], delta;
                    begin = (begin || 0);
                    end = (end || len);

                if (toString.call(this) === '[object Array]') {
                    return [].slice.call(this, begin, end);
                }

                begin = (begin >= 0) ? begin : len + begin;
                end = (end > 0) ? end : len + begin;
                delta = end - begin;

                if (delta > 0) {
                    for ( i = 0; i < delta; i += 1 ) { results[i] = this[begin + i]; }
                }
                return results;
            };
        }
    }(window.document)),

    forEach = function (obj, fn, context) {

        var len, i, proceed, args;

        if (len = (typeof obj === 'object' && obj.length)) {

            context = (context || obj);

            for (i = 0; i < len; i += 1) {

                proceed = fn.call(context, obj[i], i , obj);

                if (proceed === false) { break; }
            }
        }
        return obj;
    },

    filter = function(obj, fn, context){

        var len, i, proceed, results = [];

        if (len = (typeof obj === 'object' && obj.length)) {

            context = (context || obj);

            for (i = 0; i < len; i += 1) {

                if (proceed = fn.call(context, obj[i], i , obj)) { results.push(obj[i]); }

                else if (proceed === false) { break; }
            }
        }
        return results;
    },

    indexOf = (function(){

        return (Array.prototype.indexOf)

            ? function(obj, item, start) { return obj.indexOf(item, (start || 0)); }

            : function(obj, item, start) {
                for (var i = (start || 0), len = obj.length; i < len; i += 1) {
                    if (obj[i] === item) { return i; }
                }
                return -1;
            };
    }()),

    // --------------------------------------- Filter Child Elements ------------------------------------------------

    filterElems = function (els, fn, ofType, reverse) {

        var elems = (reverse)? slice.call(els).reverse() : slice.call(els),
            paren,
            children,
            results = [], i,

            elsOnly = function(els) { return filter(els, function(el){ if (el.nodeType === 1) return true; }); },

            filterEls = function(el) {
                if (el.parentNode === paren) {

                    if (fn(el, (ofType)? i += 1 : indexOf(children, el))) { results.push(el); }

                } else { return true; }
            };

        while (elems.length) {

            paren = elems[0].parentNode; i = -1;

            children = (reverse) ? slice.call(elsOnly(paren.children)).reverse() : slice.call(elsOnly(paren.children));

            elems = filter(elems, filterEls);
        }
        return results;
    };

    // ------------------------------------ Child methods to test ----------------------------------------------------------------

var firstChild = function(els) { return filterElems(els, function(el, i){ if (i === 0) { return true; } } ); },

    lastChild = function(els) { return filterElems(els, function(el, i){ if (i === 0) { return true; } }, false, true );},

    firstChildOfType = function(els) { return filterElems(els, function(el, i){ if (i === 0) { return true; } }, true); },

    lastChildOfType = function(els) { return filterElems(els, function(el, i){ if (i === 0) { return true; } }, true, true); },

    // --------------------------------- Basic nth-child tests ------------------------------------------------------
    // --- Note: nth-child will be built into one method and will also handle other expressions such as 2n+1 etc. ---

    nthChildEven = function(els) { return filterElems(els, function(el, i){ if (((i + 1) % 2) === 0) { return true; } });},

    nthChildOdd = function(els) { return filterElems(els, function(el, i){ if (((i + 0) % 2) === 0) { return true; } });},

    nthChildOfTypeEven = function(els) { return filterElems(els, function(el, i){ if (((i + 1) % 2) === 0) { return true; } }, true);},

    nthChildOfTypeOdd = function(els) { return filterElems(els, function(el, i){ if (((i + 0) % 2) === 0) { return true; } }, true);},

    nthChildNum = function(els, num) { return filterElems(els, function(el, i){ if (i === (num -1)) { return true; } });},

    nthChildOfTypeNum = function(els, num) { return filterElems(els, function(el, i){ if (i === (num -1)) { return true; } }, true);};

// -------------- Some tests --------------------------------

var paras = document.getElementsByTagName('p');

// first-child. Note: Will not match as first-children are headings not paragraphs.
forEach(firstChild(paras), function(el) { el.style.color = 'red'; });

// first-child-of-type.
forEach(firstChildOfType(paras), function(el) { el.style.color = 'white'; });

// last-child.
forEach(lastChild(paras), function(el) { el.style.color = 'yellow'; });

// nth-child(odd).
forEach(nthChildOdd(paras), function(el) { el.style.backgroundColor = 'teal'; });

// nth-child(even).
forEach(nthChildEven(paras), function(el) { el.style.backgroundColor = 'slateblue'; });

// nth-child-of-type(even).
forEach(nthChildOfTypeEven(paras), function(el) { el.style.outline = '2px solid red'; });

// nth-child-of-type(num)
forEach(nthChildOfTypeNum(paras, 2), function(el) { el.style.color = 'cyan'; el.style.fontStyle = 'italic';});

Next stage is to build a proper nth-child method…

Cheers for any feedback:)