Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to loop over all elements on a page including pseudo elements?

Tags:

javascript

css

How would I loop over all elements including psuedo elements? I am aware I can use getComputedStyle(element,pseudoEl) to get its content, however I have been unable to find a way to get all pseudo elements on the page so that I can use the afore mentioned function to get their content/styling. Seems to be a simple problem, but have been unable to find any solution.

like image 853
David Mulder Avatar asked Nov 27 '13 17:11

David Mulder


People also ask

How do I loop through all DOM elements on a page?

We can use the document. getElementsByTagName or document. querySelectorAll method to get all elements on a page. Then we can loop through them all with the for-of loop.

Can you have multiple after pseudo-elements?

You can't add two ::after pseudo-elements to one DOM element. You can however add an ::before additionally.

How do you get an array of all the DOM elements with a certain selector?

To get all DOM elements by an attribute, use the querySelectorAll method, e.g. document. querySelectorAll('[class="box"]') . The querySelectorAll method returns a NodeList containing the elements that match the specified selector.

Can I have multiple before pseudo-elements for the same element?

1, an element can only have at most one of any kind of pseudo-element at any time. (This means an element can have both a :before and an :after pseudo-element — it just cannot have more than one of each kind.)


3 Answers

You are on the right track. Looping over all DOM elements is fairly easy using either getElementsByTagName("*") or querySelectorAll("*"). And then we have to look at each of those elements whether they have a pseudo-element. Which all do as @zzzzBov mentioned.

Although you didn't mention it explicitly, but I assume the :before and :after pseudo elements are those you are mostly interested in. So we take the advantage of the fact that you have to use the content property to actually use pseudo elements: We just simply check whether it's set or not. Hopefully this little script helps you:

var allElements = document.getElementsByTagName("*");

for (var i=0, max=allElements.length; i < max; i++) {
    var before = window.getComputedStyle(allElements[i], ':before');  
    var after = window.getComputedStyle(allElements[i], ':after'); 
    if(before.content){
        // found :before
        console.log(before.content);
    }
    if(after.content){
        // found :after
        console.log(after.content);
    }
}
like image 120
Max Avatar answered Oct 20 '22 15:10

Max


After some performance testing, my recommendation is:

  • In most circumstances, use Max K's solution. The performance is good enough in most circumstances, it's reliable and it clocks in at under 15 LOC (mine is about 70).
  • Use the solution below if you really need to squeeze out every little millisecond and you know (because you've actually tested it) that it's faster.

The (usually) faster solution

You already know how to get a list of every element in the document using document.querySelectorAll('*'). This works in most circumstances, but for larger documents in which only a few elements have pseudo-elements it can be slow.

In this situation, we can approach the problem from a different angle. First, we loop through the document stylesheets and construct a dictionary of selectors associated with before or after pseudo-elements:

function getPseudoElementSelectors() {
    var matchPseudoSelector = /:{1,2}(after|before)/,
        found = { before: [], after: [] };

    if (!(document.styleSheets && document.styleSheets.length)) return found;

    return Array.from(document.styleSheets)
        .reduce(function(pseudoSelectors, sheet) {
            try {
                if (!sheet.cssRules) return pseudoSelectors;

                // Get an array of all individual selectors.
                var ruleSelectors = Array.from(sheet.cssRules)
                    .reduce(function(selectors, rule) {
                        return (rule && rule.selectorText)
                            ? selectors.concat(rule.selectorText.split(','))
                            : selectors;
                    }, []);

                // Construct a dictionary of rules with pseudo-elements.
                var rulePseudoSelectors = ruleSelectors.reduce(function(selectors, selector) {

                    // Check if this selector has a pseudo-element.
                    if (matchPseudoSelector.test(selector)) {
                        var pseudoElement = matchPseudoSelector.exec(selector)[1],
                            cleanSelector = selector.replace(matchPseudoSelector, '').trim();

                        selectors[pseudoElement].push(cleanSelector);
                    }

                    return selectors;
                }, { before: [], after: [] });

                pseudoSelectors.before = pseudoSelectors.before.concat(rulePseudoSelectors.before);
                pseudoSelectors.after = pseudoSelectors.after.concat(rulePseudoSelectors.after);

            // Quietly handle errors from accessing cross-origin stylesheets.
            } catch (e) { if (console && console.warn) console.warn(e); }

            return pseudoSelectors;

        }, found);
}

We can use this dictionary to get an array of pseudo-elements defined on elements matching those selectors:

function getPseudoElements() {
    var selectors = getPseudoElementSelectors(),
        names = ['before', 'after']

    return names.reduce(function(pseudoElements, name) {
        if (!selectors[name].length) return pseudoElements;

        var selector = selectors[name].join(','),
            elements = Array.from(document.querySelectorAll(selector));

        return pseudoElements.concat(
            elements.reduce(function(withContent, el) {
                var pseudo = getComputedStyle(el, name);

                // Add to array if element has content defined.
                return (pseudo.content.length)
                    ? withContent.concat(pseudo)
                    : withContent;
            }, [])
        );
    }, []);
}

Finally, a little utility function I used to convert the array-like objects returned by most DOM methods into actual arrays:

Array.from = Array.from || function(arrayish) {
    return [].slice.call(arrayish);
};

Et voilà! Calling getPseudoElements() returns an array of CSS style declarations corresponding to pseudo-elements defined in the document without looping through and checking every element.

jsFiddle demo

Caveats

It would be too much to hope that this approach would account for everything. There are a few things to bear in mind:

  • It only returns the before and after pseudo-elements, though it would be easy to adapt it to include others, or even a configurable list.
  • Cross-domain stylesheets without the appropriate CORS headers will raise a (suppressed) security exception and won't be included.
  • Only pseudo-elements set up in your CSS will be picked up; those set up directly in JavaScript won't be.
  • Some odd selectors (for example, something like li[data-separator=","]:after) will be mangled, though I'm pretty sure I could bulletproof the script against most of these with a little work.

Performance

Performance will vary depending on the number of rules in your stylesheets and the number of elements matching selectors that define a pseudo-element. If you have big stylesheets, relatively small documents or a higher proportion of elements with pseudo-elements, Max K's solution might be faster.

I tested this a little on a few sites to give an idea of the difference in performance under different circumstances. Below are the results of running each function in a loop 1000 times in the console (Chrome 31):

  • Google (UK)
    • getPseudoElementsByCssSelectors: 757ms
    • getPseudoElements: 1071ms
  • Yahoo! UK
    • getPseudoElementsByCssSelectors: 59ms
    • getPseudoElements: 5492ms
  • MSN UK
    • getPseudoElementsByCssSelectors: 341ms
    • getPseudoElements: 12752ms
  • Stack Overflow
    • getPseudoElementsByCssSelectors: 22ms
    • getPseudoElements: 10908ms
  • Gmail
    • getPseudoElementsByCssSelectors: 42910ms
    • getPseudoElements: 11684ms
  • Nicholas Gallagher's pure CSS GUI icons demo
    • getPseudoElementsByCssSelectors: 2761ms
    • getPseudoElements: 948ms

Code used to test performance

Notice that Max K's solution beats the pants off of mine in the last two examples. I was expecting it with Nicholas Gallagher's CSS icons page, but not Gmail! It turns out that Gmail has a combined total of nearly 110 selectors that specify pseudo-elements over 5 stylesheets with a total of over 9,600 selectors combined, which dwarfs the number of actual elements used (approximately 2,800).

It's worth noting that even in the slowest case, Max's solution still doesn't take much more than 10ms to run once, which isn't bad considering that it's a quarter of the length of mine and has none of the caveats.

like image 34
Jordan Gray Avatar answered Oct 20 '22 15:10

Jordan Gray


Max K shared a solution where all elements are checked for their computed style which is a concept I have been using as a temporary solution myself for the last day already. The HUGE disadvantage is the performance overhead as all elements are checked for the computed style of the non exisiting pseudo elements two times(my script is taking twice as long to execute for the off chance that there are pseudo elements available).

Either way, just thought I would share the slightly more generalized version I have been using for the last couple of days

var loopOverAllStyles = function(container,cb){
    var hasPseudo = function(el){
        var cs;
        return {
            after: (cs = getComputedStyle(el,"after"))["content"].length ? csa : false,
            before: (cs = getComputedStyle(el,"before"))["content"].length ? csb : false
        };
    }
    var allElements = container.querySelectorAll("*");
    for(var i=0;i<allElements.length;i++){
        cb(allElements[i],"element",getComputedStyle(allElements[i]));
        var pcs = hasPseudo(allElements[i]);
        if(pcs.after) cb(allElements[i],"after",pcs.after);
        if(pcs.before) cb(allElements[i],"before",pcs.before);
    }
}

loopOverAllStyles(document,function(el,type,computedStyle){
    console.log(arguments);
});
like image 2
David Mulder Avatar answered Oct 20 '22 14:10

David Mulder