Splitting CSS selectors (for use with MutationObserver)

Perry Mitchell / 2016-08-30 21:32:01
Splitting CSS selectors (for use with MutationObserver)

CSS se­lec­tors are ver­sa­tile query strings that browsers can use to lo­cate el­e­ments ac­cord­ing to many var­i­ous as­pects. Selectors can be used to lo­cate el­e­ments that have a cer­tain re­la­tion­ship with their par­ents, chil­dren and sib­lings, cer­tain at­trib­utes or val­ues and even cer­tain states (radio but­tons etc.). Selector query strings can also con­tain mul­ti­ple se­lec­tors sep­a­rated by com­mas.

A lot of how the front-end soft­ware at Kiosked works is to do with these query strings, as they help make the prod­uct more dy­namic by al­low­ing us to spec­ify el­e­ments to in­ter­act with at run­time. As browser tech­nol­ogy has pro­gressed, we’ve fre­quently up­graded our in­ter­ac­tion mech­a­nisms to more pow­er­ful and ef­fi­cient al­ter­na­tives. One such up­grade was the use of the MutationObserver API, which al­lows us to sub­scribe to DOM al­ter­ation no­ti­fi­ca­tions. This has a huge ad­van­tage over other meth­ods like in­ter­vals as we only need to re­act to changes rather than check for them in an in­fi­nite loop.

MutationObservers are very pow­er­ful, but we’re in­ter­ested in el­e­ments strewn about the DOM so it can’t ex­actly help us nar­row down ex­actly what we want to lis­ten to. We have our CSS se­lec­tors, but lis­ten­ing to only the changes rel­e­vant to those se­lec­tors is a chal­lenge.

Let’s take a look at a query se­lec­tor: article.content div.main > p.article, div.sidebar span.header ~ span.info. There’s a lot go­ing on here, but when it boils down to it, there’s only a cou­ple of se­lec­tors that re­ally tell us what el­e­ments are be­ing se­lected: p and span.info. Of course the con­text mat­ters here, but when us­ing some­thing like a MutationObserver to pump hun­dreds or thou­sands of no­ti­fi­ca­tions into our hands about changes, it’s prob­a­bly OK to check their lo­ca­tion later once we’ve de­ter­mined they’re at least the right el­e­ment we care about.

We need to first break this se­lec­tor up into use­ful peaces be­fore we can be­gin to use it with a MutationObserver, and css-se­lec­tor-split­ter is a li­brary that can as­sist with this. Using css-se­lec­tor-split­ter we can first break up the se­lec­tor into its 2 sub-se­lec­tors:

const splitSelector = require("css-selector-splitter");

let selectors = splitSelector("article.content div.main > p.article, div.sidebar span.header ~ span.info");
console.log(selectors); // ["article.content div.main > p.article", "div.sidebar span.header ~ span.info"]

Now that we have our se­lec­tors split, we can think about what we re­ally need to make the MutationObserver more use­ful. What we re­ally care about are the ac­tual se­lected el­e­ments, as the MutationObserver will only give us those. p.article and span.info hold enough in­for­ma­tion to al­low us to nar­row down our search through the waves of po­ten­tial no­ti­fi­ca­tions. We can split these se­lec­tors fur­ther:

selectors.forEach(function(selector) {
    let breakup = splitSelector.splitSelectorBlocks(selector),
        mostImportantPart = breakup.selectors.pop();
    console.log(mostImportantPart); // "p.article", "span.info"
});

Now that we have this in­for­ma­tion, we can ex­tract the nec­es­sary parts we need to iden­tify any in­ter­est­ing el­e­ments that come through the ob­server. Let’s start with tak­ing out the nec­es­sary in­for­ma­tion:

var mostImportantPart = "p.article",
    classes = [],
    ids = [];
var match,
    regex = new RegExp("(\\.|#)(\\w+)", "g");
while (match = regex.exec(mostImportantPart)) {
    if (match[1] === "#") {
        ids.push(match[2]);
    } else if (match[1] === ".") {
        classes.push(match[2]);
    }
}
console.log(classes); // ["article"]

And with this, we can start watch­ing for new nodes:

MutationObserver = window.MutationObserver || window.WebKitMutationObserver;

var obs = new MutationObserver(function(mutations, observer) {
    // look through all mutations that just occured
    for (var i = 0; i < mutations.length; i += 1) {
        // look through all added nodes of this mutation
        for (var j = 0; j < mutations[i].addedNodes.length; j += 1) {
            var node = mutations[i].addedNodes[j],
                nodeClasses = (node.className || "").split(/\s+/g),
                nodeID = node.id || "",
                interesting = false;
            for (var k = 0; k < classes.length; k += 1) {
                if (nodeClasses.indexOf(classes[k]) >= 0) {
                    interesting = true;
                    break;
                }
            }
            if (ids.indexOf(nodeID) >= 0) {
                interesting = true;
            }
            if (interesting) {
                // do something with node
            }
        }
    }
});

obs.observe(document.body, {
    childList: true,    // addition and removal of elements
    subtree: true       // target and target's descendants
});

Once we’ve de­fined our call­back for the ob­server, be can be­gin ob­serv­ing within an el­e­ment by call­ing observe(element, options). options de­fines what in­for­ma­tion we’re in­ter­ested in hear­ing about, whilst element is the par­ent that we’re lis­ten­ing within. Let’s gen­er­ate some el­e­ments to lis­ten for:

var mainDiv = document.querySelector("div.main");
[
    "test",
    "article",
    "other"
].forEach(function(elClass) {
    var el = document.createElement("p");
    el.className = elClass;
    mainDiv.appendChild(el);
});

As we’re only lis­ten­ing for the article class, only one of these should ap­pear as in­ter­est­ing in our code above.

Mutation ob­servers are highly pow­er­ful and quite ef­fi­cient, and can make very ver­sa­tile tools when cou­pled with the right ap­pli­ca­tion de­sign. CSS se­lec­tors can be an easy way of rep­re­sent­ing im­por­tant in­for­ma­tion - and with css-se­lec­tor-split­ter, they can be bro­ken up into com­po­nents that can be used with MutationObserver as well as other tools.