0

I have built a Chrome extension that intentionally hides thumbnail images upon initial page load, and that works well. But it also hides the "Load more" button after clicking it once and prevents more thumbnails from loading, even though there are many more videos/thumbnails that should be loaded.

Here is the part of my extension code that executes after clicking Load more, and then it hides the Load more button:

var insertedNodes = [];
var observer = new MutationObserver(function (mutations) {
    mutations.forEach(function (mutation) {
        // loop through each chunk of html that changes on the page
        for (var i = 0; i < mutation.addedNodes.length; i++){
            newHTMLChunk = mutation.addedNodes[i];

            if (!newHTMLChunk.tagName) 
                continue;

            qHTML = $('<div/>').html(newHTMLChunk).contents();
            var vID = qHTML.find('div').attr("data-context-item-id");

            if(vID){
                console.log("dynamic vID: " + vID);
                qHTML.find('img').hide();
            }
        }
    })
});
observer.observe(document, {
    attributes: true,
    childList: true,
    characterData: true,
    subtree:true
});

Here is what it looks like after initial page load, with my extension running: enter image description here

After clicking the Load more button, notice that the button disappears and no more thumbs load: enter image description here

Here is the html for YouTube's Load more button, which doesn't seem to have anything that would match the img selector to get hidden:

<span class="yt-uix-button-content">
    <span class="load-more-loading hid">
        <span class="yt-spinner">
            <span class="yt-spinner-img  yt-sprite" title="Loading icon"></span>       
            Loading...
        </span>
    </span>
    <span class="load-more-text">
        Load more
    </span>
</span>

Here is an example of the contents of newHTMLChunk.innerHTML (notice there is only 1 img element, and that's the one I want to hide without hiding the Load More button and without preventing more thumbs from loading):

<div class="yt-lockup clearfix  yt-lockup-video yt-lockup-grid vve-check" data-context-item-id="n3Qb30awpLs" data-visibility-tracking="QLvJwrX0-4a6nwE=">
<div class="yt-lockup-dismissable">
    <div class="yt-lockup-thumbnail">
        <span class=" spf-link  ux-thumb-wrap contains-addto">
            <a href="/watch?v=n3Qb30awpLs" class="yt-uix-sessionlink" aria-hidden="true" data-sessionlink="ei=PdyEWNiaGozB-AOT64qQCA&amp;feature=c4-videos-u">
                <span class="video-thumb  yt-thumb yt-thumb-196">
                    <span class="yt-thumb-default">
                        <span class="yt-thumb-clip">
                            <img onload=";__ytRIL(this)" alt="" aria-hidden="true" width="196" src="https://i.ytimg.com/vi/n3Qb30awpLs/hqdefault.jpg?custom=true&amp;w=336&amp;…amp;jpg444=true&amp;jpgq=90&amp;sp=68&amp;sigh=CiLl7jzBOIIFewRt-KAOEs-bKwA" data-ytimg="1">
                                <span class="vertical-align"></span>
                            </span>
                        </span>
                    </span>
                </a>
                <span class="video-time" aria-hidden="true">
                    <span aria-label="103 seconds">1:43</span>
                </span>
                <span class="thumb-menu dark-overflow-action-menu video-actions">
                    <button class="yt-uix-button-reverse flip addto-watch-queue-menu spf-nolink hide-until-delayloaded yt-uix-button yt-uix-button-dark-overflow-action-menu yt-uix-button-size-default yt-uix-button-has-icon no-icon-markup yt-uix-button-empty" type="button" aria-haspopup="true" aria-expanded="false" onclick=";return false;">
                        <span class="yt-uix-button-arrow yt-sprite"></span>
                        <ul class="watch-queue-thumb-menu yt-uix-button-menu yt-uix-button-menu-dark-overflow-action-menu hid">
                            <li role="menuitem" class="overflow-menu-choice addto-watch-queue-menu-choice addto-watch-queue-play-next yt-uix-button-menu-item" data-action="play-next" onclick=";return false;" data-video-ids="n3Qb30awpLs">
                                <span class="addto-watch-queue-menu-text">Play next</span>
                            </li>
                            <li role="menuitem" class="overflow-menu-choice addto-watch-queue-menu-choice addto-watch-queue-play-now yt-uix-button-menu-item" data-action="play-now" onclick=";return false;" data-video-ids="n3Qb30awpLs">
                                <span class="addto-watch-queue-menu-text">Play now</span>
                            </li>
                        </ul>
                    </button>
                </span>
                <button class="yt-uix-button yt-uix-button-size-small yt-uix-button-default yt-uix-button-empty yt-uix-button-has-icon no-icon-markup addto-button video-actions spf-nolink hide-until-delayloaded addto-watch-later-button yt-uix-tooltip" type="button" onclick=";return false;" role="button" title="Watch Later" data-video-ids="n3Qb30awpLs"></button>
                <button class="yt-uix-button yt-uix-button-size-small yt-uix-button-default yt-uix-button-empty yt-uix-button-has-icon no-icon-markup addto-button addto-queue-button video-actions spf-nolink hide-until-delayloaded addto-tv-queue-button yt-uix-tooltip" type="button" onclick=";return false;" title="Queue" data-video-ids="n3Qb30awpLs" data-style="tv-queue"></button>
            </span>
        </div>
        <div class="yt-lockup-content">
            <h3 class="yt-lockup-title ">
                <a class="yt-uix-sessionlink yt-uix-tile-link  spf-link  yt-ui-ellipsis yt-ui-ellipsis-2" dir="ltr" title="Josh’s Afterlife Analogy" aria-describedby="description-id-407587" data-sessionlink="ei=PdyEWNiaGozB-AOT64qQCA&amp;feature=c4-videos-u" href="/watch?v=n3Qb30awpLs">Josh’s Afterlife Analogy</a>
                <span class="accessible-description" id="description-id-407587"> - Duration: 103 seconds.</span>
            </h3>
            <div class="yt-lockup-meta">
                <ul class="yt-lockup-meta-info">
                    <li>16,475 views</li>
                    <li>2 months ago</li>
                </ul>
            </div>
        </div>
    </div>
    <div class="yt-lockup-notifications-container hid" style="height:110px"></div>
</div>

As I step through the code in Chrome's debugger I'm finding that the entire div around each newly-loaded thumbnail is being hidden, not just the image. So, in fact, the additional thumbs are loading, but then the whole div is hidden (not just the img element), and all subsequent divs slide to the left, one by one, until it appears that nothing new loaded. The final div hide actually hides the button as well even though there is no image in that button. Why is it doing this?

How can I change this so that (1) the additional video thumbnails load and display as expected and only the img element inside is hidden; and (2) the Load more button remains visible until there are truly no more thumbs to load?

HerrimanCoder
  • 6,835
  • 24
  • 78
  • 158
  • Set a breakpoint and debug the code properly. BTW your MutationObserver callback is highly inefficient. – wOxxOm Jan 22 '17 at 15:10
  • How would you make it more efficient? – HerrimanCoder Jan 22 '17 at 15:14
  • Observe only the things you need, don't use jQuery inside, don't recreate the html chunks, [and so on](http://stackoverflow.com/a/39332340/). Anyway that's a separate issue that you can pursue once you fix the main one. – wOxxOm Jan 22 '17 at 15:18
  • Beware some of the added nodes are text nodes (not html elements) so check the type first: `if (!newHTMLChunk.tagName) continue;` You might need an additional check for DocumentFragment. – wOxxOm Jan 22 '17 at 15:25
  • Thanks wOxxOm, I have removed an offending line and am debugging. See my edit above. – HerrimanCoder Jan 22 '17 at 15:35
  • I cleaned up my code and modified my original post and provided more details -- I'm still having the same problem as originally noted, though. – HerrimanCoder Jan 22 '17 at 16:27
  • I think the problem is your usage of jQuery `.html(node)` which *moves* the specified DOM `node` inside a detached container instead of copying. Anyway, the very idea of using jQuery inside MutaationObserver callback is bad. You can perform the checks using `.matches` and `.querySelector`. – wOxxOm Jan 22 '17 at 16:37
  • wOxxOm give me a working example, posted as an answer, and I'll award the answer. – HerrimanCoder Jan 22 '17 at 22:09

2 Answers2

1

The most likely issue with your MutationObserver callback is that jQuery .html(DOMnode) method moves the provided DOMnode from the page document into a detached (non-rendered) node qHTML.

Also, MutationObserver callback has to be extremely fast in order not to slow down page loading,
see Performance of MutationObserver to detect nodes in entire DOM.
That is, use jQuery only on elements you want, not on every added node.

onElementAdded('div[data-context-item-id]', document, function(elements) {
    elements.forEach(function(element) {
        var vID = element.dataset.contextItemId;
        if (vID) {
            console.log("dynamic vID: " + vID);
            $(element).find('img').hide();
        }
    });
});

function onElementAdded(selector, baseNode, callback) {
    new MutationObserver(function(mutations) {
        var found = [];
        for (var mutation, i = 0; (mutation = mutations[i++]); ) {
            var addedNodes = mutation.addedNodes[i];
            for (var node, j = 0; (node = addedNodes[j++]); ) {
                // skip non-element nodes
                if (!node.tagName) {
                    continue;
                }
                // does the added node itself match the selector?
                if (node.matches(selector)) {
                    found.push(node);
                    continue;
                }
                // does the added node contain child elements matching the selector?
                // (since qS is much faster than qSA it makes sense to use it first)
                if (node.querySelector(selector)) {
                    var children = node.querySelectorAll(selector);
                    // prevent stack overflow
                    if (children.length < 1000) {
                        Array.prototype.push.apply(found, children);
                    } else {
                        found = found.concat(children);
                    }
                }
            }
        }
        if (found.length) {
            callback(found);
        }
    }).observe(baseNode || document, {
        childList: true,
        subtree: true,
    });
}

FWIW, if the goal is just hiding the images inside div elements with data-context-item-id attribute then you can do it with a simple one-time CSS style injection (either in manifest.json or by manually adding a <style> element): div[data-context-item-id] img { display: none; }

wOxxOm
  • 65,848
  • 11
  • 132
  • 136
0

I solved it with the below, with much help from wOxxOm's comments (but not the posted answer). It works perfectly now, and at least somewhat more efficient. wOxxOm, I upvoted your answer, and I still welcome any comments you have. I tried your .observe() changes and it didn't seem to work, but I may have done it wrong.

var insertedNodes = [];
var observer = new MutationObserver(function (mutations) {
    mutations.forEach(function (mutation) {
        // loop through each chunk of html that changes on the page
        for (var i = 0; i < mutation.addedNodes.length; i++){
            newHTMLChunk = mutation.addedNodes[i];

            if (!newHTMLChunk.tagName) 
                continue;

            if(newHTMLChunk.getElementsByTagName('div').length > 0){
                if(newHTMLChunk.getElementsByTagName('div')[0]){
                    var vID = newHTMLChunk.getElementsByTagName('div')[0].getAttribute('data-context-item-id');

                    if(vID){
                        $("div[data-context-item-id='" + vID + "'] img").hide();
                    }
                }
            }
        }
    })
});
observer.observe(document, {
    attributes: true,
    childList: true,
    characterData: true,
    subtree:true
});
HerrimanCoder
  • 6,835
  • 24
  • 78
  • 158