How to make my userscript work on specific pages of a site that uses the history api?

1.7k views Asked by At

(This is in continuation of this discussion)

I've been trying to make a script (for instagram) that shows how many images out of total are currently visible when viewing a profile page (example profile). It shows the counter in the upper right corner of the screen and supports infinite scrolling.

Here it is:

// ==UserScript==
// @name        Instagram - visible images counter
// @name        Instagram - visible images counter
// @include     https://instagram.com/*
// @grant       none
// ==/UserScript==

var p = document.getElementsByClassName('-cx-PRIVATE-PostsGridItem__root');
var m = document.getElementsByClassName('-cx-PRIVATE-PostsStatistic__count');
var ww = m[0].innerHTML.replace(/(\d+),(?=\d{3}(\D|$))/g, "$1");                       // Regex to strip the thousand comma seperator from the posts number
var z = (p.length/ww)*100;
var counter = p.length+' / '+m[0].innerHTML +' that is ' +z.toFixed(1) + '%';
var div = document.createElement("div");
div.style.top = "1px";
div.style.right = "1px";
div.style.position = "fixed";
document.body.appendChild(div);
div.id="mycounter";
mycounter = document.getElementById('mycounter');
mycounter.innerHTML = counter;
mycounter.style.top = "1px";
mycounter.style.right = "1px";
mycounter.style.position = "fixed";

/// ---------------------------------
/// mutation observer -monitors the Posts grid for infinite scrolling event- 
/// ---------------------------------
var target1 = document.querySelector('.-cx-PRIVATE-PostsGrid__root');
var observer1 = new MutationObserver(function (mutations) {
  mutations.forEach(function (mutation) {
    p=document.getElementsByClassName('-cx-PRIVATE-PostsGridItem__root');
    m = document.getElementsByClassName('-cx-PRIVATE-PostsStatistic__count');
    ww = m[0].innerHTML.replace(/(\d+),(?=\d{3}(\D|$))/g, "$1");
    z = (p.length/ww)*100;
    counter = p.length+' / '+m[0].innerHTML +' that is ' +z.toFixed(1) + '%';    
    mycounter.innerHTML = counter;
  });
})
var config = { attributes: true, childList: true, characterData: true }
observer1.observe(target1, config);

My script works ok, but I have this issue:

Instagram, after it's recent redesign -I think-, seems to use single-page application workflow,
i.e fetches the clicked link content and replaces the current page with it, and then changes the browser's current URL, all without actually reloading the page.

So, my script only works when I open a profile URL in a new tab.
It doesn't work when opening a profile URL in the same tab while in my timeline (https://instagram.com)).
In other words, it doesn't work (after I open https://instagram.com and login)
if I click to view a profile URL of a user I follow, eg. https://instagram.com/instagram/

How can I fix this?

Someone has kindly suggested these 3 ways:

  1. Try an event handler for some event fired by the Instagram site like pjax:end on github, for example.
  2. Use a mutation observer that waits a profile-specific html element.
  3. Or use setInterval to check location.href periodically.*


So, I've been trying various approaches (including the suggestions #2 and #3) but no success.
(about suggestion #1: I can't find any element similar to pjax:end).
So, what I've tried so far:


  1. (based on suggestion #2) adding another mutation observer to check whether the element that shows the 'posts count' element exists, and if yes, then run my code.

    var target0 = document.querySelector('.-cx-PRIVATE-PostsStatistic__count');
    
    var observer0 = new MutationObserver(function (mutations) {
      mutations.forEach(function (mutation) {    
            myCode();
      });
    })
    
    var config0 = { attributes: true, childList: true, characterData: true }
    
    observer0.observe(target0, config0);
    

  1. (based on suggestion #3) checking location.href every 1 second whether the current location is a profile (i.e. not the timeline (https://instagram.com/). If true then clear the periodic function and run my previous code.

    var interval = setInterval(testHref() , 1000);
    
    function testHref(){
        if (location.href != "https://instagram.com/")
            clearInterval(interval);
            myCode();   
    }
    

  1. simply adding a 1 sec delay on top of my code (and changing the @require rule to apply only on profile URLs // @include https://instagram.com/*/), but no success:

    setTimeout(function() { }, 1000);
    

  1. I've even tried using the waitForKeyElements utility function, which detects and handles AJAXed content.
    I thought it's much easier to implement this way, working as a simple "wait until an element exists" (I just used the main profile pic as the selector to wait for, because I couldn't find any relevant AJAX selector. Also I didn't use jNode inside my code).
    So I just enclosed my whole code in a single function visibleCounter, and added a waitForKeyElements line (see below), but unfortunately it also doesn't work:

    waitForKeyElements (".-cx-PRIVATE-ProfilePage__avatar", visibleCounter);         
    
    function visibleCounter(jNode){
        myCode()
    }
    
2

There are 2 answers

0
darkred On BEST ANSWER

I solved this using the arrive.js library. It provides events to watch for DOM elements creation and removal. It makes use of Mutation Observers internally. The library does not depend on jQuery, you can replace jQuery elements in the examples below with pure javascript elements and it would work fine.

I quote this comment by its author:

I've always found MutationObserver api a bit complex so I've built a library, arrive.js, to provide a simpler api to listen for elements creation/removal.

as well as just two of its uses:

Watch for elements creation:

Use arrive event to watch for elements creation:

// watch for creation of an element which satisfies the selector ".test-elem"
$(document).arrive(".test-elem", function() {
    // 'this' refers to the newly created element
    var $newElem = $(this);
});

and

Watch for elements removal

Use leave event to watch for elements removal. The first arugument to leave must not be a descendent or child selector i.e. you cannot pass .page .test-elem, instead, pass .test-elem. It's because of a limitation in MutationObserver's api.

// watch for removal of an element which satisfies the selector ".test-elem"
$(".container-1").leave(".test-elem", function() {
    var $removedElem = $(this);
});  [1]: https://github.com/uzairfarooq/arrive

And, this is the complete script:

// ==UserScript==
// @name        Instagram - visible images counter
// @include     https://www.instagram.com/*
// @grant       none
// @require     https://code.jquery.com/jquery-3.0.0.min.js
// @require     https://greasyfork.org/scripts/21927-arrive-js/code/arrivejs.js?version=139586
// ==/UserScript==




function showCounter() {
    var visibleCount = $( "a[href*='taken-by']" ).length;   // Count of visible images
    var totalString = $("span:contains('posts')").eq(1).children().eq(0).html();    // The 'total' value (it's a string)
    var total = totalString.replace(/(\d+),(?=\d{3}(\D|$))/g, '$1');    // Apply regex to 'total' string to strip the thousand comma seperator
    if (visibleCount > total){
        visibleCount = total;
    }
    var visiblePercent = ((visibleCount / total) * 100).toFixed(1); // Visible images as percentage
    var counter = visibleCount + ' / ' + totalString + ' that is ' + visiblePercent + '%';
    return counter;
}




function createDiv(){
    // Creation of the counter element
    document.body.appendChild(div);
    div.innerHTML = showCounter();      // Initial display of the counter
    div.style.top = '1px';
    div.style.right = '1px';
    div.style.position = 'fixed';
}



function observer(){

    /// ---------------------------------
    /// mutation observer -monitors the Posts grid for infinite scrolling event-.
    /// ---------------------------------
    observer1 = new MutationObserver(function(mutations) {
        mutations.forEach(function(mutation) {
            div.innerHTML = showCounter();                      // In each infinite scrolling event, re-calculate counter
        });
    }).observe($('article').children().eq(1).children()[0],     // target of the observer
        {
            // attributes: true,
            childList: true,
            // characterData: true,
        }); // config of the observer

}






var div = document.createElement('div');    // global variable
var observer1;                              // global variable

if (document.URL !== 'https://www.instagram.com/' &&
    document.URL.indexOf('https://www.instagram.com/p/') === -1 ){
    createDiv();
    observer();
}



$(document).arrive('article ._5axto', function() {      // 'article .5axto'
    createDiv();
    observer();
});



$(document).leave('article ._5axto', function() {
    div.remove();
    observer1.disconnect();
});
0
Nafiur Rahman Khadem On

Copied from my answer here:

For a similar use case, I used MutationObserver and checked node.baseURI of each node in each mutation.addedNodes.

(async function() {
    'use strict';
    const selector = '.quiz--answered';
    const observer = new MutationObserver(mutations => {
        if (!mutations.some(mutation => Array.from(mutation.addedNodes).some(node => node.baseURI.includes('/quiz/')))) {
            return;
        }
        const elm = document.querySelector(selector);
        if (elm) {
            // work with elm
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });
})();