1

Workaround:

If this helps anyone in the future, I'm posting a workaround here.

Simplest solution also seems to be the best: At the end of the script, added an event listener. This triggers an ImagesLoaded routine. Which then triggers a timeout. This way, if the connection is slow, the timeout doesn't start until all images have been loaded.

(
function ImagesLoaded(w, undefined){
    //place ImagesLoaded.js code here//;}
function timeoutFunction(callfunction, msDelay){
    setTimeout(callfunction, msDelay);}
function ReplaceElements(){
    //code below//;}
document.addEventListener('load', ImagesLoaded(document).always(timeoutFunction(ReplaceElements, 2000)));}(window));
```

Original issue:

Recently posted a thread, got a lot of good feedback. This is a cleaner version of that thread.

Issue: addEventListener('load') does not cause the script to wait until window has loaded before firing.

I'm running Tampermonkey, a browser extension that uses JavaScript to alter things on a website to your preferred layout. On the site ScienceDirect.com, there is one (never more than one) DOI link. I've made a script which looks for that DOI link, and replaces it with a different link. The last line of code; "window.addEventListener('load', replaceElement);" should only fire the "replaceElement" function AFTER loading.

The element is successfully replaced, and the correct (new) innerHTML is displayed for a fraction of a second. Then the innerHTML reverts back to the original innerHTML. It seems like, for some reason, the script does't wait for the entire page to load despite the addEventListener('load') function.

Since there is always only one DOI link on a page, and it's always placed inside the 'div' object called 'doi-link' I use this to find the 'a' element, and the loop is ended after the first match. Again, as far as I can tell, the problem is premature firing of the replaceElement function. I've tried working with different formatting, moving the "addEventListener" to the beginning of the document, nothing seems to work. The last time I had a similar problem, that was because of formatting. I had written it as ('load', ()); when it should have been ('load', ); Tried both versions, no difference.

Tried moving the "addEventListener" so it was inside the function, no difference.

Someone suggested that the page was triggered to reload, perhaps because it was submitted as a form. So I added "return false;", no difference. Now I'm just scratching my head, would love to hear your ideas or suggestions.

The code below is the minimum to reproduce the issue. There is only one element.id, since the link only occurs once on any page. After finding & replacing the link, [f] is increased to end the "for"-loop.

The element is successfully replaced. For a fraction of a second, the correct (new) innerHTML is displayed. Then it reverts. Seems like the addEventListener isn't waiting. No matter how I try, the innerHTML of my new 'a' link reverts back to the innerHTML of the 'a' element that used to be there.

// ==UserScript==
// @name         ScienceDriect DOI Button
// @description  Add sci-hub button on article page. Add sci-hub button after article link. Support Google scholar, bing academic and baidu xueshu. Jump CNKI English article to Chinese article.
// @include      https://sciencedirect.com/*
// @include      http://sciencedirect.com/*
// @include      https://www.sciencedirect.com/*
// @include      http://www.sciencedirect.com/*
// @require      http://code.jquery.com/jquery-3.4.1.min.js
// @require      https://unpkg.com/imagesloaded@4.1.4/imagesloaded.pkgd.js
// @require      http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js
// @require      http://cdnjs.cloudflare.com/ajax/libs/sugar/1.3/sugar.min.js
// @grant        GM_setClipboard
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// ==/UserScript==

(
function(w, undefined) {
'use strict';

var ILID = ('il' + Math.random()).replace(/0\./g, '');
var EVENTS = 'load error';
var ALLOWED_NODE_TYPES = [
    1, // ELEMENT_NODE
    9, // DOCUMENT_NODE
    11 // DOCUMENT_FRAGMENT_NODE
];

/**
 * Return type of the value.
 *
 * @param  {Mixed} value
 *
 * @return {String}
 */
function type(value) {
    if (value == null) {
        return String(value);
    }
    if (typeof value === 'object' || typeof value === 'function') {
        return (value instanceof w.NodeList && 'nodelist') ||
            (value instanceof w.HTMLCollection && 'htmlcollection') ||
            Object.prototype.toString.call(value).match(/\s([a-z]+)/i)[1].toLowerCase();
    }
    return typeof value;
}

/**
 * Convert array-like objects into an array.
 *
 * @param  {Mixed} collection
 *
 * @return {Array}
 */
function toArray(collection) {
    switch (type(collection)) {
        case 'array':
            return collection;
        case 'undefined':
            return [];
        case 'nodelist':
        case 'htmlcollection':
        case 'arguments':
            var arr = [];
            for (var i = 0, l = collection.length; i < l; i++) {
                if (i in collection) {
                    arr.push(collection[i]);
                }
            }
            return arr;
        default:
            return [collection];
    }
}

/**
 * Check whether the value is in an array.
 *
 * @param  {Mixed} value
 * @param  {Array} array
 *
 * @return {Boolean}
 */
function inArray(value, array) {
    if (type(array) !== 'array') {
        return -1;
    }
    if (array.indexOf) {
        return array.indexOf(value);
    }
    for (var i=0, l = array.length; i < l; i++) {
        if (array[i] === value) {
            return i;
        }
    }
    return -1;
}

/**
 * Callback proxy.
 *
 * Ensures that callback will receive a specific context.
 *
 * @param {Mixed}    context
 * @param {Function} callback
 */
function proxy(context, callback) {
    return function () {
        return callback.apply(context, arguments);
    };
}

/**
 * Add event listeners to element.
 *
 * @param  {Node}     element
 * @param  {Event}    eventName
 * @param  {Function} handler
 *
 * @return {Void}
 */
function bind(element, eventName, handler) {
    listener(element, eventName, handler);
}

/**
 * Remove event listeners from element.
 *
 * @param  {Node}     element
 * @param  {Event}    eventName
 * @param  {Function} handler
 *
 * @return {Void}
 */
function unbind(element, eventName, handler) {
    listener(element, eventName, handler, 1);
}

/**
 * Manage element event listeners.
 *
 * @param  {Node}     element
 * @param  {Event}    eventName
 * @param  {Function} handler
 * @param  {Bool}     remove
 *
 * @return {Void}
 */
function listener(element, eventName, handler, remove) {
    var events = eventName.split(' ');
    for (var i = 0, l = events.length; i < l; i++) {
        if (element.addEventListener) {
            element[remove ? 'removeEventListener' : 'addEventListener'](events[i], handler, false);
        } else if (element.attachEvent) {
            element[remove ? 'detachEvent' : 'attachEvent']('on' + events[i], handler);
        }
    }
}

/**
 * Callbacks handler.
 */
function Callbacks() {
    var self = this;
    var callbacks = {};
    var i, l;

    /**
     * Registers callbacks.
     *
     * @param  {Mixed} name
     * @param  {Mixed} fn
     *
     * @return {Void}
     */
    self.on = function (name, fn) {
        callbacks[name] = callbacks[name] || [];
        if (type(fn) === 'function' && inArray(fn, callbacks[name]) === -1) {
            callbacks[name].push(fn);
        }
    };

    /**
     * Remove one or all callbacks.
     *
     * @param  {String} name
     * @param  {Mixed}  fn
     *
     * @return {Void}
     */
    self.off = function (name, fn) {
        callbacks[name] = callbacks[name] || [];
        if (fn === undefined) {
            callbacks[name].length = 0;
        } else {
            var index = inArray(fn, callbacks[name]);
            if (index !== -1) {
                callbacks[name].splice(index, 1);
            }
        }
    };

    /**
     * Trigger callbacks for event.
     *
     * @param  {String} name
     * @param  {Mixed}  context
     * @param  {Mixed}  argN
     *
     * @return {Void}
     */
    self.trigger = function (name, context) {
        if (callbacks[name]) {
            for (i = 0, l = callbacks[name].length; i < l; i++) {
                callbacks[name][i].apply(context, Array.prototype.slice.call(arguments, 2));
            }
        }
    };
}

/**
 * Executes callback(s) when images have finished with loading.
 *
 * @param  {NodeList} collection Collection of containers, images, or both.
 * @param  {Function} options    ImagesLoaded options.
 *
 * @return {Void}
 */
function ImagesLoaded(collection, options) {
    // Fill unassigned options with defaults
    options = options || {};
    for (var key in ImagesLoaded.defaults) {
        if (!options.hasOwnProperty(key)) {
            options[key] = ImagesLoaded.defaults[key];
        }
    }

    // Private variables
    var self = this instanceof ImagesLoaded ? this : {};
    var callbacks = new Callbacks();
    var tIndex;

    // Element holders
    self.images = [];
    self.loaded = [];
    self.pending = [];
    self.proper = [];
    self.broken = [];

    // States
    self.isPending = true;
    self.isDone = false;
    self.isFailed = false;

    // Extract images
    collection = toArray(collection);
    for (var c = 0, cl = collection.length; c < cl; c++) {
        if (collection[c].nodeName === 'IMG') {
            self.images.push(collection[c]);
        } else if (inArray(collection[c].nodeType, ALLOWED_NODE_TYPES) !== -1) {
            self.images = self.images.concat(toArray(collection[c].getElementsByTagName('img')));
        }
    }

    /**
     * Registers or executes callback for done state.
     *
     * @param  {Function} callback
     *
     * @return {ImagesLoaded}
     */
    self.done = function (callback) {
        if (self.isPending) {
            callbacks.on('done', callback);
        } else if (self.isDone && type(callback) === 'function') {
            callback.call(self);
        }
        return self;
    };

    /**
     * Registers or executes callback for fail state.
     *
     * @param  {Function} callback
     *
     * @return {ImagesLoaded}
     */
    self.fail = function (callback) {
        if (self.isPending) {
            callbacks.on('fail', callback);
        } else if (self.isFailed && type(callback) === 'function') {
            callback.call(self);
        }
        return self;
    };

    /**
     * Registers or executes callback for done state.
     *
     * @param  {Function} callback
     *
     * @return {ImagesLoaded}
     */
    self.always = function (callback) {
        if (self.isPending) {
            callbacks.on('always', callback);
        } else if (type(callback) === 'function') {
            callback.call(self);
        }
        return self;
    };

    /**
     * Registers or executes callback for done state.
     *
     * @param  {Function} callback
     *
     * @return {ImagesLoaded}
     */
    self.progress = function (callback) {
        if (self.isPending) {
            callbacks.on('progress', callback);
        }
        // Retroactivity
        for (var i = 0, l = self.loaded.length; i < l; i++) {
            callback.call(self, self.loaded[i], self.loaded[i][ILID].isBroken);
        }
        return self;
    };

    /**
     * Executes proper callbacks when all images has finished with loading.
     *
     * @return {Void}
     */
    function doneLoading() {
        if (!self.isPending) {
            return;
        }
        // Clear timeout
        clearTimeout(tIndex);
        // Mark states
        self.isPending = false;
        self.isDone = self.images.length === self.proper.length;
        self.isFailed = !self.isDone;
        // Trigger callbacks
        callbacks.trigger(self.isDone ? 'done' : 'fail', self);
        callbacks.trigger('always', self);
    }

    /**
     * Terminates the determination process prematurely.
     *
     * @return {Void}
     */
    function terminate() {
        // Mark still pending images as broken
        while (self.pending.length) {
            imgLoaded(self.pending[0], 1);
        }
    }

    /**
     * Image load event handler.
     *
     * @param  {Event} event
     *
     * @return {Void}
     */
    function imgLoadedHandler(event) {
        /*jshint validthis:true */
        event = event || w.event;
        // Unbind loaded handler from temporary image
        unbind(this[ILID].tmpImg || {}, EVENTS, imgLoadedHandler);
        // Leave the temporary image for garbage collection
        this[ILID].tmpImg = null;
        // Don't proceed if image is already loaded
        if (inArray(this, self.loaded) === -1) {
            imgLoaded(this, event.type !== 'load');
        }
    }

    /**
     * Mark image as loaded.
     *
     * @param  {Node}    img      Image element.
     * @param  {Boolean} isBroken Whether the image is broken.
     *
     * @return {Void}
     */
    function imgLoaded(img, isBroken) {
        var pendingIndex = inArray(img, self.pending);
        if (pendingIndex === -1) {
            return;
        } else {
            self.pending.splice(pendingIndex, 1);
        }
        // Store element in loaded images array
        self.loaded.push(img);
        // Keep track of broken and properly loaded images
        self[isBroken ? 'broken' : 'proper'].push(img);
        // Cache image state for future calls
        img[ILID].isBroken = isBroken;
        img[ILID].src = img.src;
        // Trigger progress callback
        setTimeout(function () {
            callbacks.trigger('progress', self, img, isBroken);
        });
        // Call doneLoading
        if (self.images.length === self.loaded.length) {
            setTimeout(doneLoading);
        }
    }

    /**
     * Checks the status of all images.
     *
     * @return {Void}
     */
    function check() {
        // If no images, trigger immediately
        if (!self.images.length) {
            doneLoading();
            return;
        }
        // Actually check the images
        var img;
        for (var i = 0, il = self.images.length; i < il; i++) {
            img = self.images[i];
            img[ILID] = img[ILID] || {};
            // Add image to pending array
            self.pending.push(img);
            // Find out whether this image has been already checked for status.
            // If it was, and src has not changed, call imgLoaded.
            if (img[ILID].isBroken !== undefined && img[ILID].src === img.src) {
                imgLoaded(img, img[ILID].isBroken);
                continue;
            }
            // If complete is true and browser supports natural sizes,
            // try to check for image status manually.
            if (img.complete && img.naturalWidth !== undefined) {
                imgLoaded(img, img.naturalWidth === 0);
                continue;
            }
            // If none of the checks above matched, simulate loading on detached element.
            img[ILID].tmpImg = document.createElement('img');
            bind(img[ILID].tmpImg, EVENTS, proxy(img, imgLoadedHandler));
            img[ILID].tmpImg.src = img.src;
        }
    }

    // Defer the images check to next process tick to give people time to bind progress callbacks.
    setTimeout(check);
    // Set the timeout
    setTimeout(terminate, options.timeout);
    // Return the instance
    return self;
}

// Default options
ImagesLoaded.defaults = {
    timeout: 10000 // Automatically fail images loading when this time has passed.
};

// Expose globally
w.ImagesLoaded = ImagesLoaded;
    function replaceElement(){
        var ParentElement=document.getElementById('doi-link');
        var Array = ParentElement.childNodes;
        //look for the element containing the link
        for(var f=0; f<Array.length; f++){
            if(Array[f].href.indexOf('doi.org/') != -1){
                var newA = document.createElement('a');
                newA.href=Array[f].href.replace('doi.org','sci-hub.tw');
                newA.id='clickme';
                newA.innerHTML='Sci-Hub';
                newA.style = 'background:#3d85c6;background:linear-gradient(#3d85c6,#073763);border-radius:5px;padding:8px 20px;color:#ffffff;display:inline-block;font:normal bold 24px/1 "Calibri", sans-serif;text-align:center;text-shadow:1px 1px #000000;';
                newA.role=('button');
                f=Array.length;
//when the object has been found(there is only one on any page),
//f is increased to exit the 'for' loop.
            }
       };
        //create a new 'DIV' element, append new 'A' element & replace the old 'DIV' element.
        var newDIV=document.createElement('div');
        newDIV.id="newdivelement";
        newDIV.appendChild(newA);
        ParentElement.replaceWith(newDIV);}

window.addEventListener('load', replaceElement);
function startup(ms){
    setTimeout(rune,ms);}

document.addEventListener('load', ImagesLoaded(document).always(startup(2000)));}(window));

  • 1
    Try making the trigger something manual, just to make sure that if you wait long enough, it DOES work as intended. Because it could be that the original host is doing something to that link that forces it to update, even after the page is loaded. – Calvin Godfrey Jun 10 '19 at 18:48
  • Was going to suggest a similar approach - checking for any `XHR` requests that might be made after replacement. – Tony M Jun 10 '19 at 18:49
  • Thank you all for your help! I'm leaving this thread here, for reference, if anyone does figure out a solution. Have a nice evening! – Krister Johnson Jun 10 '19 at 18:55
  • imagesLoadedJS is a good solution to this. Fires an event once media is loaded. https://imagesloaded.desandro.com/ – gilbert-v Jun 10 '19 at 19:13
  • have you tried using promises? – Matt Berg Jun 11 '19 at 23:54
  • Haven't tried promises yet, looking into it. The page DOES finish loading, firing the 'load' event. But it continues fetching additional elements for 2-5 seconds, resetting the innerHTML. – Krister Johnson Jun 12 '19 at 00:38
  • I see `window.addEventListener('load'...` used, but I also see `document.addEventListener('load'...` at the end of this question. You may have to change `document` to `window`, depending on your needs. See https://stackoverflow.com/questions/588040/window-onload-vs-document-onload – jabbascript Nov 26 '19 at 21:36

0 Answers0