13

I'm using a MutationObserver to look for added images to a web page. As many images are displayed via the CSS background-image property, I'm checking the current/computed CSS style in addition to looking for img tags, for example...

var hasBackgroundImage = function(node) {
    var nodeStyle = node.currentStyle || getComputedStyle(node, null);
    return (
        nodeStyle &&
        nodeStyle.backgroundImage &&
        nodeStyle.backgroundImage != "none"
        );
};

However, it seems there is a delay between a node triggering a mutation event and the CSS rules being applied so there appears to be no backgroundImage during the mutation event even though there will be at some point later. I noticed this when the code worked during debugging (stepping with breakpoints) but didn't work at runtime. Delaying mutation event processing with setTimeout also works but to be certain the delay needs to be quite large and changes from page to page (I'm not sure exactly when the CSS rules are guaranteed to be applied). A possible cause may simply be the delayed load or injection of CSS content.

What's the best way to achieve this functionality? I guess I'm after an on-style-change mutation, but I doubt this exists.

jozxyqk
  • 16,424
  • 12
  • 91
  • 180
  • It might help if we know the reason you're attempting to observe the mutations... – prodigitalson Dec 04 '13 at 05:21
  • @prodigitalson I'm attempting to modify all images on a web page as part of a browser extension. I'd like it to be as seamless and robust as possible. I'm assuming the mutation observer will give events for images added dynamically, otherwise I'd just walk the whole page once. – jozxyqk Dec 04 '13 at 05:28

2 Answers2

18

This works for me...

  1. Use a mutation observer to catch changes to the style attribute...

    var observer = new MutationObserver(parseMutations);
    observer.observe(document, {
        ...
        attributes:    true,
        attributeFilter: ["style"]
    });
    
    ...
    
        if (mutation.attributeName) //we'll assume it's "style"
            parseNode(mutation.target); //check for style.backgroundImage and call filterNode()
    

    This works for both setAttribute("style", ...) and element.style.whatever = something.

  2. Catch new style and link elements with the mutation observer, add an onload event and parse applicable nodes...

    var stylenodes = ["STYLE", "LINK"];
    
    ...
    
    for (var i = 0; i < mutation.addedNodes.length; i++)
    {
        var node = mutation.addedNodes[i];
        var nodeName = node.nodeName.toUpperCase();
        if (stylenodes.indexOf(nodeName) !== -1)
            node.addEventListener("load", styleLoaded);
    
    ...
    
    //catch loading of stylenodes and parse all new rules
    var currentLoadedStyles = [];
    var styleLoaded = function() {
        //check all styles and look for one that has just added some rules
        for (var i = 0; i < document.styleSheets.length; ++i)
        {
            if (document.styleSheets[i].rules && document.styleSheets[i].rules.length > 0 && currentLoadedStyles.indexOf(document.styleSheets[i]) == -1)
            {
                currentLoadedStyles.push(document.styleSheets[i]);
                parseNewStyle(document.styleSheets[i].rules);
            }
        }
    };
    
    //look for rules with background images and re-filter all nodes it applies to
    var parseNewStyle = function(rules) {
        for (var i = 0; i < rules.length; ++i)
        {
            //if any rule contains a background-image (could look for anything here really)
            if (rules[i].style && rules[i].style.backgroundImage && rules[i].style.backgroundImage != "none")
            {
                //get all affected nodes and re-parse them
                var nodes = document.querySelectorAll(rules[i].selectorText);
                for (var j = 0; j < nodes.length; ++j)
                    filterNode(nodes[j]);
            }
        }
    };
    
jozxyqk
  • 16,424
  • 12
  • 91
  • 180
  • 1
    That seems to work only if the `style` attribute is modified specifically, not if the computed style changes (f.e. due to stylesheet changes). – trusktr Jun 03 '16 at 09:38
  • 1
    @trusktr this will handle changes from new stylesheets. To handle modifications to styles you can use another mutation observer. Also check for changes to external references by src attribute. A more complete example is here: https://github.com/pknowles/ImageFilter/blob/master/imagefinder.js – jozxyqk Jun 03 '16 at 10:02
  • 2
    I see what you mean. So the mutation observer is on the `style` elements, or observing the content of `style` attributes. But, what I'd like is observing computedStyle without observing stylesheets or style attributes. I guess Object.observe would have been good for this, but that's removed from spec. – trusktr Jun 07 '16 at 20:15
  • 1
    @trusktr yeah that'd be nice, but short of polling for changes I'm not sure how. – jozxyqk Jun 07 '16 at 22:08
  • note that some sites (ie gmail) load tens of thousands of styles and currentLoadedStyles can get very big – kofifus Oct 08 '17 at 04:24
0

I used https://github.com/keithclark/ComputedStyleObserver to observe the computed style "display" and make changes depending on its value. Here is the code: (in my case it was a Vue.js project)

setup(){
    let observer = null;
    const collapsableItemsOnNavbar = ref(null);

    onMounted(() =>{            
        // This is to make sure that the Profile menu displays correctly between screen sizes
        const callback = entries => {
            entries.forEach(entry => {
                console.log(`Property '${entry.property}' changed from '${entry.previousValue}' to '${entry.value}'`);
                nextTick(() => {
                    let displayType = entry.value;
                    console.log(displayType);
                    if(displayType === 'none' || displayType === 'block'){
                        // We are in a small screen, don't display the profile picture
                        app.onNavbarCollapseChanged(true);
                    }
                    else{
                        // We are in a bigger screen, display the profile picture
                        app.onNavbarCollapseChanged(false);
                    }
                });
            });
        }

        observer = new ComputedStyleObserver(callback, ['display']);
        observer.observe(collapsableItemsOnNavbar.value);
    });
    onBeforeUnmount(() =>{
        observer.disconnect();
    });

    return {
        collapsableItemsOnNavbar,
    };
},
Murat Aykanat
  • 1,648
  • 4
  • 29
  • 38