// ==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);
});
}