1

I'm trying to append an <img> element into a <span> in the content script of my chrome extension. However, it seems that the append only has an effect when I append into the body of the document, as explained in content-script.js. To reproduce this, click the following link and open dev tools:

http://kart.finn.no/?mapType=norge&tab=soek_i_annonser&searchKey=search_id_realestate_lettings&mapTitle=Kart+over+bolig+til+leie+&ztr=1&

Search for the <img> whose id is seenIcon. It will be defined when appending into the body, but undefined in all other cases.

manifest.json

{
    "manifest_version": 2,

    "name": "Finn.no Property Blacklist",
    "description": "Hides finn.no property search results that you've marked as \"seen\".",
    "version": "1.0",

    "permissions": [
        "activeTab",
        "http://kart.finn.no/*",
        "storage"
    ],
    "content_scripts": [
        {
            "matches": ["http://kart.finn.no/*"],
            "js": ["content-script.js"]
        }
    ],
    "web_accessible_resources": [
        "*.png"
    ]
}

content-script.js

console.log("content script!")

function getIconSpanIfExists() {
    var spanClassName = "imagePoi";
    var matchingElements = document.getElementsByClassName(spanClassName);
    if (matchingElements.length > 1) {
        console.error(failureMessage("wasn't expecting more than one element with class name " + spanClassName));
        return null;
    }

    if (matchingElements.length === 0) {
        return null;
    }

    return matchingElements[0].parentNode;
}

function htmlReady() {
    return getIconSpanIfExists();
}

function addIcons() {
    var iconSpan = getIconSpanIfExists();

    // Append into body - works.
//    var icon = document.createElement("img");
//    icon.id = "seenIcon";
//    icon.src = chrome.extension.getURL("seen.png");
//    document.body.appendChild(icon);
//    console.log("appended " + icon.id + " into body");

    // Append into span - doesn't work, even though it says childNodes.length is 2.
    var icon = document.createElement("img");
    icon.id = "seenIcon";
    icon.src = chrome.extension.getURL("seen.png");
    icon.style.left = "200px";
    icon.style.top = "200px";
    iconSpan.appendChild(icon);
    console.log("appended " + icon.id + " into span with class imagePoi" + " new children: " + iconSpan.childNodes.length);

    // Modify innerHTML of span - doesn't work, even though innerHTML has the icon.
//    iconSpan.innerHTML += "\n<img id=\"seenIcon\""
//        + "src=\"" + chrome.extension.getURL("seen.png") + "\""
//        + "style=\"left: 200px; top: 200px;\">";
//    console.log(iconSpan.parentNode.id, iconSpan.innerHTML);
}

function init() {
    console.log("initialising content script");

    if (!htmlReady()) {
        console.log("not all HTML is loaded yet; waiting");

        var timer = setInterval(waitForHtml, 200);

        function waitForHtml() {
            console.log("waiting for required HTML elements...");
            if (htmlReady()) {
                clearInterval(timer);
                console.log("... found them!");
                addIcons();
            }
        }

        return;
    }
}

if (document.readyState === "complete") {
    console.log("document is complete")
    init();
} else {
    console.log("document is not yet ready; adding listener")
    window.addEventListener("load", init, false);
}

seen.png

seen.png

Why are the changes not reflected in the DOM?

Mitch
  • 23,716
  • 9
  • 83
  • 122

1 Answers1

3

The node is recreated by the site, so you need to wait a little after it's first appearance and only then add the new image.

I've tested it with a simple userscript with MutationObserver which adds the new icon each time .imagePoi is added to the document, including the first two appearances and subsequent on zoom-in/out.

setMutationHandler(document, '.imagePoi', function(observer, node) {
  node.parentNode.insertAdjacentHTML('beforeend',
    '<img src="http://www.dna-bioscience.co.uk/images/check-mark.gif">');
});

function setMutationHandler(baseNode, selector, cb) {
  var ob = new MutationObserver(function(mutations) {
    for (var i=0, ml=mutations.length, m; (i<ml) && (m=mutations[i]); i++)
      for (var j=0, nodes=m.addedNodes, nl=nodes.length, n; (j<nl) && (n=nodes[j]); j++)
        if (n.nodeType == 1) 
          if (n = n.matches(selector) ? n : n.querySelector(selector))
            if (!cb(ob, n))
              return;
  });
  ob.observe(baseNode, {subtree:true, childList:true});
}

You can simplify the handler by utilizing the fact that .imagePoi's mutation record's target has finnPoiLayer in its class list. But it'll be prone to breaking when site layout slightly changes.

wOxxOm
  • 65,848
  • 11
  • 132
  • 136
  • I used `MutationObserver` originally, and ended up with several chains of functions that look very similar to the `setMutationHandler()` function above, so I switched to a simple timer as per [this](http://stackoverflow.com/questions/13917047/how-to-get-a-content-script-to-load-after-a-pages-javascript-has-executed/13917682#comment51201657_13917682) advice. I didn't know that you could observe nested children being added with `MutationObserver`... I see that the `subtree` argument could have been quite useful to avoid those chains. :) – Mitch Jul 26 '15 at 06:26
  • I tried this out, but the `` still doesn't get created for me using my original link. I added a couple of `console.log()`s in the `load` callback and before the `insertAdjacentHTML()` call, but nothing got printed. I also gave the `` an `id`, but couldn't find it after the page had loaded. – Mitch Jul 26 '15 at 06:36
  • Your code works with the `if (document.readyState === "complete")` check, thanks! It might be worth adding that to your answer in case someone else has the same problem. – Mitch Jul 26 '15 at 06:40
  • By the way, why do you disconnect it the first time? Wouldn't it be enough to leave it connected? – Mitch Jul 26 '15 at 07:06
  • I disconnect it to skip doing the job the first time but apparently that's not needed. – wOxxOm Jul 26 '15 at 07:10