Detect-Nav

Detect url change on Chrome/Firefox (Greasemonkey, Firemonkey, Violentmonkey, Tampermonkey)

// ==UserScript==
// @name         Detect-Nav
// @description  Detect url change on Chrome/Firefox (Greasemonkey, Firemonkey, Violentmonkey, Tampermonkey)
// @author       GTK
// @namespace    https://greasyfork.org/en/users/1352961-gtk
// @version      0.1
// @match        *://*/*
// @grant        none
// @run-at       document-start
// @noframes
// ==/UserScript==


// ============================================================
// `GMCompat` Compatibility shim
//  adapted from `https://github.com/chocolateboy/gm-compat`
// ============================================================

const $unsafeWindow = (typeof unsafeWindow !== 'undefined') ? unsafeWindow.wrappedJSObject || unsafeWindow : window;
const GMCompat = Object.freeze({
    unsafeWindow:  $unsafeWindow,

    CLONE_INTO_OPTIONS: {
        cloneFunctions: true,
        target: $unsafeWindow,
        wrapReflectors: true
    },

    EXPORT_FUNCTION_OPTIONS: {
        target: $unsafeWindow,
    },

    apply: function($this, fn, _args) {
        const args = [].slice.call(_args)
        return fn.apply($this, this.cloneInto(args))
    },

    call: function($this, fn, ..._args) {
        const args = this.cloneInto(_args)
        return fn.call($this, ...args)
    },

    cloneInto: function(object, _options) {
        const options = Object.assign({}, this.CLONE_INTO_OPTIONS, _options)
        const _cloneInto = (typeof cloneInto === 'function') ? cloneInto : object => object
        return _cloneInto(object, options.target, options)
    },

    export: function(value, options) {
        return (typeof value === 'function') ? this.exportFunction(value, options) : this.cloneInto(value, options)
    },

    exportFunction: function(fn, _options) {
        const options = Object.assign({}, this.EXPORT_FUNCTION_OPTIONS, _options)
        const _exportFunction = (typeof exportFunction === 'function') ? exportFunction : (fn, { defineAs, target = $unsafeWindow } = {}) => { return defineAs ? (target[defineAs] = fn) : fn }

        return _exportFunction(fn, options.target, options)
    },

    unwrap: function(value) {
        return value ? (value.wrappedJSObject || value) : value
    }
});


// ============================================================
//  Navigation detection & CustomEvent dispatch
// ============================================================

if (window.navigation) {
    console.info('Using Navigation API.');
    window.navigation.addEventListener('navigatesuccess', e => {
        notify(e.type, e.currentTarget.currentEntry.url);
    });

} else if (window.onurlchange === null) {
    console.info('Using window.onurlchange.');
    window.addEventListener('urlchange', e => {
        notify('urlchange', e.url);
    });

} else {
    console.info('Using patch.');
    (window.location.hostname === 'www.youtube.com' ? handleYoutube : patchHistory)();
}



let oldUrl;
function notify(method, url){
    const absUrl = new URL(url || window.location.href, window.location.origin).href;
    const detail = GMCompat.export({ method: method, oldUrl: oldUrl, newUrl: absUrl });
    const event = new CustomEvent('detectnavigate', { bubbles: true, detail: detail });
    document.dispatchEvent(event);
    oldUrl = absUrl;
}

function patchHistory(){
    ['pushState', 'replaceState'].forEach(method => {
        const original = GMCompat.unsafeWindow.history[method];
        const patched = function(){
            GMCompat.apply(this, original, arguments);
            notify(method, arguments[2]);
        };

        GMCompat.unsafeWindow.history[method] = GMCompat.export(patched);
    });

    window.addEventListener('popstate', e => {
        notify(e.type);
    });
}

function handleYoutube(){
    window.addEventListener('yt-navigate-finish', e => {
        notify(e.type, e.detail.response.url);
    });
}