// ==UserScript==
// @name Faris Handy Webdev JavaScript functions
// @namespace http://tampermonkey.net/
// @version 0.3.1
// @description A bunch of useful JavaScript functions
// @description This is not a regular script for you to run! Only use this via the @require keyword.
// @author Faris Hijazi
// @grant GM_xmlhttpRequest
// @grant GM_download
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_setClipboard
// @grant unsafeWindow
// @grant window.close
// @grant window.focus
// @run-at document-start
// @include *
// @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
// ==/UserScript==
/**/
if (typeof unsafeWindow === "undefined") unsafeWindow = window;
unsafeWindow.URL_REGEX_STR = `(//|http(s?:))[\\d\\w?%\\-_/\\\\=.]+?`;
unsafeWindow.IMAGE_URL_REGEX = new RegExp(`(//|http(s?:))[\\d\\w?%\\-_/\\\\=.]+?\\.(jpg|png|jpeg|gif|tiff|&f=1)`, 'gim');
// /(\/\/|http(s?:))[\d\w?%\-_\/\\=.]+?\.(jpg|png|jpeg|gif|tiff|&f=1)/gim;
unsafeWindow.VID_URL_REGEX = /(\/\/|http(s?:))[\d\w?%\-_\/\\=.]+?\.(mov|webm|mp4|wmv|&f=1)/gim;
var useEncryptedGoogle = /encrypted.google.com/.test(location.hostname);
var googleBaseURL = `https://${/google\./.test(location.hostname) ? location.hostname :
((useEncryptedGoogle ? "encrypted" : "www") + ".google.com")}`;
unsafeWindow.gImgSearchURL = `${googleBaseURL}/search?&hl=en&tbm=isch&q=`;
unsafeWindow.GIMG_REVERSE_SEARCH_URL = `${googleBaseURL}/searchbyimage?&image_url=`;
if (typeof GM_setClipboard !== 'undefined') {
unsafeWindow.setClipboard = GM_setClipboard;
unsafeWindow.GM_setClipboard = GM_setClipboard;
}
if (typeof GM_xmlhttpRequest !== 'undefined') {
unsafeWindow.GM_xmlhttpRequest =
/**
* Description
GM_xmlhttpRequest is a cross-origin version of XMLHttpRequest. The beauty of this function is that a user script can make requests that do not use the same-origin policy, creating opportunities for powerful mashups.
Restrictions
GM_xmlhttpRequest restricts access to the http, https, ftp, data, blob, and moz-blob protocols.
If a script uses one or more @domains then the GM_xmlhttpRequest api will be restricted to those domains.
If the url provided does not pass the above criteria then a error will be thrown when calling GM_xmlhttpRequest
Arguments
Object details
A single object with properties defining the request behavior.
String method: Optional. The HTTP method to utilize. Currently only "GET" and "POST" are supported. Defaults to "GET".
String url: The URL to which the request will be sent. This value may be relative to the page the user script is running on.
Function onload: Optional. A function called if the request finishes successfully. Passed a Scriptish response object (see below).
Function onerror: Optional. A function called if the request fails. Passed a Scriptish response object (see below).
Function onreadystatechange: Optional. A function called whenever the request's readyState changes. Passed a Scriptish response object (see below).
String data: Optional. Content to send as the body of the request.
Object headers: Optional. An object containing headers to be sent as part of the request.
Boolean binary: Optional. Forces the request to send data as binary. Defaults to false.
Boolean makePrivate: Optional. Forces the request to be a private request (same as initiated from a private window). (0.1.9+)
Boolean mozBackgroundRequest: Optional. If true security dialogs will not be shown, and the request will fail. Defaults to true.
String user: Optional. The user name to use for authentication purposes. Defaults to the empty string "".
String password: Optional. The password to use for authentication purposes. Defaults to the empty string "".
String overrideMimeType: Optional. Overrides the MIME type returned by the server.
Boolean ignoreCache: Optional. Forces a request to the server, bypassing the cache. Defaults to false.
Boolean ignoreRedirect: Optional. Forces the request to ignore both temporary and permanent redirects.
Boolean ignoreTempRedirect: Optional. Forces the request to ignore only temporary redirects.
Boolean ignorePermanentRedirect: Optional. Forces the request to ignore only permanent redirects.
Boolean failOnRedirect: Optional. Forces the request to fail if a redirect occurs.
Integer redirectionLimit: Optional. Range allowed: 0-10. Forces the request to fail if a certain number of redirects occur.
Note: A redirectionLimit of 0 is equivalent to setting failOnRedirect to true.
Note: If both are set, redirectionLimit will take priority over failOnRedirect.
Note: When ignore*Redirect is set and a redirect is encountered the request will still succeed, and subsequently call onload. failOnRedirect or redirectionLimit exhaustion, however, will produce an error when encountering a redirect, and subsequently call onerror.
Response Object
This is the response object passed to the onload, onerror, and onreadystatechange callbacks described for the details object above.
String responseText: The response to the request in text form.
String responseJSON: If the content type is JSON (example: application/json, text/x-json, and more..) then responseJSON will be available.
Integer readyState: The state of the request. Refer to https://developer.mozilla.org/en/XMLHttpRequest#Properties
String responseHeaders: The string value of all response headers. null if no response has been received.
Integer status: The HTTP status code from the server. null if the request hasn't yet completed, or resulted in an error.
String statusText: The entire HTTP status response string from the server. null if the request hasn't yet completed, or resulted in an error.
String finalUrl: The final URL used for the request. Takes redirects into account. null if the request hasn't yet completed, or resulted in an error.
For "onprogress" only:
Boolean lengthComputable: Whether it is currently possible to know the total size of the response.
Integer loaded: The number of bytes loaded thus far.
Integer total: The total size of the response.
Returns
*/
GM_xmlhttpRequest;
}
unsafeWindow.log = console.debug;
unsafeWindow.setLog = newDebugState => debug = (typeof newDebugState === "boolean") ? newDebugState : debug;
unsafeWindow.matchSite = matchSite;
unsafeWindow.createElement = createElement;
unsafeWindow.loadScript = loadScript;
unsafeWindow.Proxy = {
fileStack: url => (`https://process.filestackapi.com/AhTgLagciQByzXpFGRI0Az/${encodeURIComponent(url.trim())}`),
steemitimages: url => /\.(jpg|jpeg|tiff|png|gif)($|\?)/i.test(url) ? (`https://steemitimages.com/0x0/${url.trim()}`) : url,
ddg: ddgProxy
};
unsafeWindow.ddgProxy = ddgProxy;
unsafeWindow.getOGZscalarUrl = getOGZscalarUrl;
unsafeWindow.reverseDdgProxy = reverseDdgProxy;
unsafeWindow.isDdgUrl = isDdgUrl;
unsafeWindow.targetIsInput = targetIsInput;
unsafeWindow.createAndAddAttribute = createAndAddAttribute;
unsafeWindow.getGImgReverseSearchURL = getGImgReverseSearchURL;
unsafeWindow.toDdgProxy = () => location.href = ddgProxy(location.href);
unsafeWindow.isIterable = obj => obj != null && typeof obj[Symbol.iterator] == 'function';
unsafeWindow.GM_setValue = GM_setValue;
unsafeWindow.GM_getValue = GM_getValue;
unsafeWindow.q = q;
unsafeWindow.qa = qa;
unsafeWindow.siteSearchUrl = siteSearchUrl;
unsafeWindow.getAbsoluteURI = getAbsoluteURI;
/**Returns the HOSTNAME of a website url*/
unsafeWindow.getHostname = getHostname;
/***/
unsafeWindow.openAllLinks = function () {
Array.from(document.links).forEach(function (link) {
if (link.hasAttribute("href")) {
window.open(link.href);
}
});
};
/**Returns a DuckDuckGo proxy url (attempts to unblock the url)*/
function ddgProxy(href) {
return isDdgUrl(href) || /^(javascript)/i.test(href) ? href : (`https://proxy.duckduckgo.com/iu/?u=${encodeURIComponent(href)}&f=1`);
}
/**Opens the url via fetch(), then performs a callback giving it the document element*/
unsafeWindow.fetchElement = fetchElement;
/**Opens the url via xmlhttpRequest, then performs a callback giving it the document element*/
unsafeWindow.xmlRequestElement = xmlRequestElement;
unsafeWindow.onLoadDim = onLoadDim;
unsafeWindow.addCss = addCss;
/**
* @author https://codepen.io/frosas/
* Also works with scripts from other sites if they have CORS enabled (look for the header Access-Control-Allow-Origin: *).
*
* // Usage
* var url = 'https://raw.githubusercontent.com/buzamahmooza/Helpful-Web-Userscripts/master/GM_dummy_functions.js?token=AZoN2Rl0UPDtcrOIgaESbGp_tuHy51Hmks5bpijqwA%3D%3D';
* loadGitHubScript(url).then((event) => { });
*/
function loadGitHubScript(url) {
return fetch(url).
then(res => res.blob()).
then(body => loadScript(URL.createObjectURL(body)));
function loadScript(url) {
new Promise(function(resolve, reject) {
var script = document.createElement('script');
script.src = url;
script.onload = resolve;
script.onerror = function(){
console.warn("couldn't load script: ", url);
if(typeof reject === 'function')
reject();
}; // TODO Not sure it really works
document.head.appendChild(script);
});
}
}
/**@deprecated doesn't actually succeed*/
unsafeWindow.addJs = function addJs(js, id) {
const jsScript = document.createElement('script');
jsScript.appendChild(document.createTextNode(js));
if (!!id) jsScript.id = id;
jsScript.classList.add('addJs');
return document.getElementsByTagName('head')[0].appendChild(jsScript);
};
unsafeWindow.observe = observe;
unsafeWindow.gfycatPage2GifUrl = function (gfycatPageUrl) {
if (!/https:\/\/gfycat\.com\/gifs\/detail\/.+/.test(gfycatPageUrl)) {
throw error("Not a gfycat home url:" + gfycatPageUrl);
}
return `https://thumbs.gfycat.com/${gfycatPageUrl.split('/').pop()}-size_restricted.gif`;
};
unsafeWindow.preloader = preloader;
unsafeWindow.waitForElement = waitForElement;
unsafeWindow.includeJs = includeJs;
/**
* Appends a style element to the head of the document containing the given cssStr text
* @param cssStr
* @param id
* @return {HTMLStyleElement}
*/
function addCss(cssStr, id) {
const style = document.createElement('style');
if (style.styleSheet) {
style.styleSheet.cssText = cssStr;
} else {
style.appendChild(document.createTextNode(cssStr));
}
if (!!id) style.id = id;
style.classList.add('addCss');
return document.getElementsByTagName('head')[0].appendChild(style);
}
unsafeWindow.disableStyles = disableStyles;
function disableStyles(enable) {
console.log('Disabling styles');
for (const styleEl of document.querySelectorAll('style, link')) {
styleEl.disabled = !enable
}
}
unsafeWindow.createAndGetNavbar = createAndGetNavbar;
/**
* Creates a static navbar at the top of the page.
* Useful for adding buttons and controls to it
* @param callback this callback should be used when instantly adding content to the navbar,
* do NOT just take the returned value and start adding elements.
* @return {HTMLDivElement} returns the parent navbar element
*/
function createAndGetNavbar(callback) {
// Settings up the navbar
// language=CSS
addCss(`
div#topnav {
position: fixed;
z-index: 1000;
min-height: 50px;
top: 0;
right: 0;
left: 0;
background: #525252;
font-size: 14px;
}
div#topnav-content {
margin-left: 115px;
padding: 10px;
font-family: inherit;
font-stretch: extra-condensed;
font-size: 20px;
}`, "navbar-css");
function adjustTopMargin() {
document.body.style.top = `${q('#topnav').offsetHeight}px`;
}
const navbar = document.createElement(`div`);
navbar.id = "topnav";
const navbarContentDiv = document.createElement('div');
navbarContentDiv.id = "topnav-content";
navbar.appendChild(navbarContentDiv);
document.body.firstElementChild.before(navbar);
window.addEventListener('resize', adjustTopMargin);
document.body.style.position = "relative";
// keep trying to use the callback, works when the navbarContentDiv is finally added
var interval = setInterval(function () {
const topnavContentDiv = q('#topnav-content');
if (topnavContentDiv) {
clearInterval(interval);
if (callback)
callback(topnavContentDiv);
adjustTopMargin();
}
}, 100);
return navbar;
}
unsafeWindow.setStyleInHTML = setStyleInHTML;
/**
* This will set the style of an element by force, by manipulating the style HTML attribute.
* This gives you more control, you can set the exact text you want in the HTML element (like giving a style priority via "!important").
* Example calls:
* setStyleByHTML(el, "background-image", "url(http://www.example.com/cool.png)")
* setStyleByHTML(el, "{ background-image : url(http://www.example.com/cool.png) }")
* @param {HTMLElement} el
* @param {String} styleProperty
* @param {String} styleValue
* @return el
*/
function setStyleInHTML(el, styleProperty, styleValue, printVerbose) {
styleProperty = styleProperty.trim().replace(/^.*{|}.*$/g, '');
const split = styleProperty.split(':');
if (!styleValue && split.length > 1) {
styleValue = split.pop();
styleProperty = split.pop();
}
if (el.hasAttribute('style')) {
const styleText = el.getAttribute('style');
const styleArgument = `${styleProperty}: ${styleValue};`;
let newStyle = new RegExp(styleProperty, 'i').test(styleText) ?
styleText.replace(new RegExp(`${styleProperty}:.+?;`, 'im'), styleArgument) :
`${styleText} ${styleArgument}`;
el.setAttribute('style', newStyle);
if(printVerbose) console.debug(
'adding to style ', `"${styleArgument}"`,
'\nnewStyle:', `"${newStyle}"`,
'\nelement:', el
);
}
return el;
}
Math.clamp = function (a, min, max) {
return a < min ? min :
a > max ? max : a;
};
/**
* @param targetElement
* @param callback
* @param options mutationObserver options{ childList: boolean, subtree: boolean, attributes: boolean, characterData: boolean }
* @returns the mutationObserver object
*/
function observe(targetElement, callback, options) {
if (!targetElement) targetElement = document.body;
if (!options) {
options = {
childList: true, subtree: true,
attributes: false, characterData: false
};
}
const mutationsHandler = function (mutations) {
for (const mutation of mutations) {
if (mutation.addedNodes.length) {
callback(mutation.target);
}
callback();
}
};
callback(targetElement);
const mutationObserver = new MutationObserver(mutationsHandler);
mutationObserver.observe(targetElement, options);
return mutationObserver;
}
function getGImgReverseSearchURL(url) {
// console.debug('gImgReverseSearchURL=', gImgReverseSearchURL);
return url ? GIMG_REVERSE_SEARCH_URL + encodeURIComponent(url.trim()) : "";
}
unsafeWindow.nodeDepth = nodeDepth;
/**
* returns the number of nodes between the child and parent
* @param child
* @param parent the parent (direct or indirect) of the child element
* @param currentDepth used for recursion only, do NOT modify it's value
* @return {number}
*/
function nodeDepth(child, parent = document, currentDepth = 0) {
if (!child || !parent) throw "Both the child and parent must non-null.";
if (!parent.contains(child)) throw "The given parent does not contain the child.";
currentDepth++;
return child.parentNode == parent ?
currentDepth :
nodeDepth(child.parentNode, parent, currentDepth);
}
/**Returns the href wrapped with proxy.DuckDuckGo.com */
function reverseDdgProxy(href) {
var s = href;
if (isZscalarUrl(href)) s = getOGZscalarUrl(href); // extra functionality:
if (isDdgUrl(href)) {
s = new URL(location.href).searchParams.get('u');
}
// https://proxy.duckduckgo.com/iu/?u=
if (s && s[0]) {
return decodeURIComponent(s[0]);
} else {
console.log('Was unable to reverseDDGProxy for URL:', href);
return s;
}
}
unsafeWindow.regexBetween = function (precedingRegEx, betweenRegEx, proceedingRegEx, regexOptions) {
return new RegExp(`(?<=(${precedingRegEx}))(${!betweenRegEx ? ".+?" : betweenRegEx})(?=(${proceedingRegEx}))`, regexOptions);
};
unsafeWindow.extend = typeof($) == 'undefined' ? null : $.extend;
function preloader(imgUrls) {
console.log('imgs passed:', imgUrls);
let imgObjs = [];
// start preloading
for (const url of imgUrls) {
// create object
let imageObj = new Image();
imageObj.src = url;
imageObj.onload = (function () {
console.log('ImageLoaded:', this.src, this);
});
imgObjs.push(imageObj);
}
}
// http://code.jquery.com/jquery.js
function includeJs(src) {
const script = document.createElement('script');
script.setAttribute('src', src);
document.getElementsByTagName('head')[0].appendChild(script);
return script;
}
//TODO: fix and test it
/**@WIP
* @param {function} elementGetter a function to get the wanted element (or event a condition function)
* that will be called to test if the element has appeared yet. (should return true only when the element appears)
* @param callback the elementGetter will be passed as the first argument
* @return {MutationObserver}
*/
function waitForElement(elementGetter, callback) {
const observer = new MutationObserver(function (mutations, me) {
// mutations.forEach(function (mutation) {
// if (!mutation.addedNodes) return false;
function handleSuccess(node) {
// console.debug('Wanted node found:', node);
callback(node);
me.disconnect();
}
// for (let i = 0; i < mutation.addedNodes.length; i++) {
// var node = mutation.addedNodes[i];
var node = (typeof(elementGetter) === 'function') ? elementGetter() : q(elementGetter);
try {
if (node) {
if (node.length !== undefined && node.length !== 0) {
for (const n of node)
handleSuccess(n);
} else if (node.length === undefined) {
handleSuccess(node);
}
}
} catch (e) {
console.warn(e);
}
// }
// });
});
observer.observe(document.body, {
childList: true
, subtree: true
, attributes: false
, characterData: false
});
return observer;
// stop watching using: observer.disconnect();
}
/**
* cross-browser wheel delta
* Returns the mousewheel scroll delta as -1 (wheelUp) or 1 (wheelDown) (cross-browser support)
* @param {MouseWheelEvent} wheelEvent
* @return {number} -1 or 1
*/
unsafeWindow.getWheelDelta = function getWheelDelta(wheelEvent) {
// cross-browser wheel delta
wheelEvent = window.event || wheelEvent; // old IE support
return Math.max(-1, Math.min(1, (wheelEvent.wheelDelta || -wheelEvent.detail)));
};
unsafeWindow.elementUnderMouse = function elementUnderMouse(wheelEvent) {
return document.elementFromPoint(wheelEvent.clientX, wheelEvent.clientY);
};
/** Create an element by typing it's inner HTML.
For example: var myAnchor = createElement('<a href="https://example.com">Go to example.com</a>');
* @param html
* @param callback optional callback, invoked once the element is created, the element is passed.
* @return {HTMLElement}
*/
function createElement(html, callback) {
const div = document.createElement('div');
div.innerHTML = (html).trim();
const element = div.firstElementChild;
if (!!callback && callback.call)
callback.call(null, element);
return element;
}
/* todo: remove, this thing is terrible and has no point */
function matchSite(siteRegex) {
let result = location.href.match(siteRegex);
if (!!result) console.debug("Site matched regex: " + siteRegex);
return result;
}
function siteSearchUrl(query) {
if (query) {
return gImgSearchURL + "site:" + encodeURIComponent(query.trim());
}
}
/**
* removes all coded functionality to the element by removing it and reappending it's outerHTML
*/
function clearElementFunctions(element) {
const outerHTML = element.outerHTML;
element.after(createElement(outerHTML));
element.remove();
}
unsafeWindow.clearElementFunctions = clearElementFunctions;
/**abbreviation for querySelectorAll()
* @param selector
* @param node
* @return {set<HTMLElement>} */
function qa(selector, node = document) {
return node.querySelectorAll(selector);
}
/**abbreviation for querySelector()
* @param selector
* @param node
* @return {HTMLElement} */
function q(selector, node = document) {
return node.querySelector(selector);
}
unsafeWindow.getIncrementedUrl = getIncrementedUrl;
/** Returns a modified url as a string, either incremented(++) or decremented(--)
* @param {string} href the input url you want to modify
* @param {number} incrAmount (optional) the amount to increment.
* Default: increment by 1 (++).
* If the result in the url becomes negative, it will be overridden to 0.
* @return {string} incremented/decremented url string */
function getIncrementedUrl(href, incrAmount) {
var e, s;
let IB = incrAmount ? incrAmount : 1;
function isDigit(c) {
return ("0" <= c && c <= "9")
}
var match = href.match(/&f=1$/);
var tip = !!match ? match[0] : "";
let L = href.replace(tip, "");
let LL = L.length;
for (e = LL - 1; e >= 0; --e) if (isDigit(L.charAt(e))) {
for (s = e - 1; s >= 0; --s) if (!isDigit(L.charAt(s))) break;
break;
}
++s;
if (e < 0) return;
let oldNum = L.substring(s, e + 1);
let newNum = (parseInt(oldNum, 10) + IB);
if (newNum < 0) newNum = 0;
let newNumStr = "" + newNum;
while (newNumStr.length < oldNum.length)
newNumStr = "0" + newNumStr;
return (L.substring(0, s) + "" + newNumStr + "" + L.slice(e + 1) + "" + tip);
}
unsafeWindow.printElementTextAttributes = printElementTextAttributes;
function printElementTextAttributes(el) {
console.log(
'innerText:', el.innerText,
'\nOuterText:', el.outerHTML,
'\nInnerHTML:', el.innerHTML,
'\nouterHTML:', el.outerHTML
);
}
function isZscalarUrl(zscalarUrl) {
return /https:\/\/zscaler\.kfupm\.edu\.sa\/Default\.aspx\?url=/.test(zscalarUrl);
}
/**
* @param zscalarUrl {string}
* @returns {string} the original link that ZScalar is blocking
*/
function getOGZscalarUrl(zscalarUrl) {
if (!isZscalarUrl(zscalarUrl)) {
return zscalarUrl;
} // not a zscalar url
zscalarUrl = zscalarUrl.trim();
let x = decodeURIComponent(('' + zscalarUrl).substring(46, zscalarUrl.indexOf('&referer')));
// let x = decodeURIComponent(('' + zscalarUrl).substring(46, zscalarUrl.indexOf('&referer')));
console.debug('Extracted ZScalar original link:', x);
return x;
}
/*function loadScript(url, callback) {
let script = document.createElement("script");
script.type = "text/javascript";
if (script.readyState) { //IE
script.onreadystatechange = function () {
if (script.readyState === "loaded" ||
script.readyState === "complete") {
script.onreadystatechange = null;
callback();
}
};
} else { //Others
script.onload = function () {
callback();
};
}
script.src = url;
document.getElementsByTagName("head")[0].appendChild(script);
}*/
function loadScript(url, callback, type) {
if (!callback) callback = () => console.log('Script laoded:', url);
// Adding the script tag to the head as suggested before
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
if (!type) type = 'text/javascript';
script.type = type;
script.src = url;
// Then bind the event to the callback function.
// There are several events for cross browser compatibility.
script.onreadystatechange = callback;
script.onload = callback;
// Fire the loading
head.appendChild(script);
return script;
}
unsafeWindow.loadModule = loadModule;
unsafeWindow.getElementsWithText = getElementsWithText;
window.document.getElementsWithText = getElementsWithText;
/**
* Iterates through all HTMLElements and returns the ones that contains innerText matching the given regex/substr
* @param textRegex
* @param preliminarySelector a node selector to narrow down the search from the start
* @return {[]}
*/
function getElementsWithText(textRegex, preliminarySelector) {
if (!textRegex) {
console.error('must have an input value');
return;
}
const matchingEls = [];
const useSubstring = (typeof textRegex === 'string');
function elementContainsText(element, regex) {
return !(regex && element && element.innerText) ? false :
(useSubstring ? element.innerText.indexOf(regex) > -1 : element.innerText.match(textRegex));
}
for (const el of document.querySelectorAll(preliminarySelector || '*')) {
if (elementContainsText(el, textRegex)) {
var nothingContainsEl = true;
for (var i = 0; i < matchingEls.length; i++)
if (matchingEls[i] && matchingEls[i].contains(el) && !Object.is(matchingEls[i], el)) {
matchingEls[i] = null; // that old parentEl is now gone, we only want the deepest node
nothingContainsEl = false;
}
matchingEls.push(el);
}
}
return matchingEls.filter(el => el != null);
}
function loadModule(url, callback) {
return loadScript(url, callback, 'module');
}
/**
* Creates and adds an attributeNode to the element (if the element doesn't have that attribute)
* sets the attribute value
* @param node
* @param attributeName
* @param attributeValue
*/
function createAndAddAttribute(node, attributeName, attributeValue) {
if (!node) {
console.error('Node is null, cannot add attribute.');
return;
}
if (!node.hasAttribute(attributeName)) {
var attr = document.createAttribute(attributeName);
attr.value = attributeValue;
node.setAttributeNode(attr);
}
if (!!attributeValue) {
node.setAttribute(attributeName, attributeValue);
}
}
/** Deal with relative URIs (URIs starting with "/" or "//") */
function getAbsoluteURI(inputUrl) {
let reconstructedUri = inputUrl
.replace(new RegExp(`^//`), `${location.protocol}//`) // If string starts with "//", replace with protocol
.replace(new RegExp(`^/`), `${getHostname(location.href)}/`);// convert relative uri (precede with hostname if URI starts with "/")
// add protocol if one is not found
if (!/^https?/.test(reconstructedUri))
reconstructedUri = `https://${reconstructedUri}`;
return reconstructedUri;
}
/**
* Returns true if the event target is an input element (such as a textfield).
* This is useful when you want to remap letter keys only when you are not typing in a text field :)
* @param event
* @return {boolean}
*/
function targetIsInput(event) {
const ignores = document.getElementsByTagName('input');
const target = event.target;
for (let ignore of ignores)
if (target === ignore || ignore.contains(target)) {
// console.log('The target recieving the keycode is of type "input", so it will not recieve your keystroke', target);
return true;
}
return false;
}
/** Calls the callback function, passing to it the width and height: "callback(w, h)"
* @param url
* @param callback callback(width, height, url, imgNode, args)
* @param imgNode
* @param args gets passed to the callback
*/
function onLoadDim(url, callback, imgNode, args) {
var img = new Image();
if (!url) {
console.warn('Url is invalid');
return;
}
if (typeof url !== "string") {
url = !!url.src ? url.src : url.href;
}
if (typeof callback === 'function') {
img.addEventListener('load', function () {
callback(this.naturalWidth, this.naturalHeight, url, imgNode, args);
});
} else {
console.error('onLoad() callback passed should be of type "function".');
}
img.src = url;
}
/**@deprecated Opens the url via xmlhttpRequest, then performs a callback giving it the document element*/
function xmlRequestElement(url, callback) {
if (typeof callback !== 'function') console.error("The callback is not a function", callback);
const req = new XMLHttpRequest();
req.open('GET', url);
req.send();
req.onreadystatechange = function () {
if (req.readyState === req.DONE) {
const pageHTML = req.responseText;
const doc = document.createElement('html');
doc.innerHTML = pageHTML;
console.log('Recieved document for page ', url + ":", doc);
callback(doc, url);
}
};
}
unsafeWindow.fetchDoc = fetchDoc;
function fetchDoc(url, callback) {
fetch(url, {
mode: 'no-cors',
method: 'get'
}
).then((res) => res.text())
.then((text) => {
var doc = document.createElement('html');
doc.innerHTML = text;
if (callback && typeof callback === 'function')
callback(doc);
});
}
/**Opens the url, then performs a callback giving it the document element
* @param {string} url
* @param {function} callback passes: (doc, url, args) to the callback function when the response is complete
* @param {o} args Options object.
* "args": Arguments to pass to the callback (Array or Object type)
* @returns returns the callback result
*/
function fetchElement(url, callback, args) {
if (typeof callback !== 'function') console.error('Callback is not a function.!');
fetch(url).then(
response => response.text() // .json(), etc.
// same as function(response) {return response.text();}
).then(function (html) {
var doc = document.implementation.createHTMLDocument('');
doc.open();
doc.write(html);
doc.close();
try {
return callback(doc, url, args);
} catch (e) {
console.error(e);
return (html);
}
}
);
}
function isDdgUrl(url) {
return /^https:\/\/proxy\.duckduckgo\.com/.test(url);
}
function unicodeToChar(text) {
return text.replace(/\\u[\dA-F]{4}/gim,
match => String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16)));
}
// don't make public because it conflicts with DDGP Unblocker script
/** A class containing static functions to manipulate the srcset attribute */
unsafeWindow.SrcSet = class SrcSet {
/**
* @author https://github.com/sindresorhus/srcset/
* @param arr
* @return {*}
*/
static deepUnique(arr) {
return arr.sort().filter(function (el, i) {
return JSON.stringify(el) !== JSON.stringify(arr[i - 1]);
});
}
/**
* @author https://github.com/sindresorhus/srcset/
* @param str
* @return {*}
*/
static parse(str) {
return this.deepUnique(str.split(',').map(function (el) {
var ret = {};
el.trim().split(/\s+/).forEach(function (el, i) {
if (i === 0) {
return ret.url = el;
}
var value = el.substring(0, el.length - 1);
var postfix = el[el.length - 1];
var intVal = parseInt(value, 10);
var floatVal = parseFloat(value);
if (postfix === 'w' && /^[\d]+$/.test(value)) {
ret.width = intVal;
} else if (postfix === 'h' && /^[\d]+$/.test(value)) {
ret.height = intVal;
} else if (postfix === 'x' && !isNaN(floatVal)) {
ret.density = floatVal;
} else {
throw new Error('Invalid srcset descriptor: ' + el + '.');
}
});
return ret;
}));
}
static stringify(arr) {
return (arr.map(function (el) {
if (!el.url) {
throw new Error('URL is required.');
}
var ret = [el.url];
if (el.width) {
ret.push(el.width + 'w');
}
if (el.height) {
ret.push(el.height + 'h');
}
if (el.density) {
ret.push(el.density + 'x');
}
return ret.join(' ');
})).join(', ');
}
};
unsafeWindow.cookieUtils = {
setCookie: function setCookie(name,value,days) {
var expires = "";
if (days) {
var date = new Date();
date.setTime(date.getTime() + (days*24*60*60*1000));
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + (value || "") + expires + "; path=/";
return document.cookie;
},
getCookie: function getCookie(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for(var i=0;i < ca.length;i++) {
var c = ca[i];
while (c.charAt(0)==' ') c = c.substring(1,c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
}
return null;
},
eraseCookie: function eraseCookie(name) {
document.cookie = name+'=; Max-Age=-99999999;';
return document.cookie;
}
};
unsafeWindow.url2location = url2location;
/**
* Retrieves an object with parsed URL data
* @param url
* @return {
* {port: string,
* protocol: string,
* href: string,
* origin: string,
* pathname: string,
* hash: string,
* search: string}
* }
*/
function url2location(url) {
const a = document.createElement("a");
a.href = url;
return {
port: a.port,
protocol: a.protocol,
href: a.href,
origin: a.origin,
pathname: a.pathname,
hash: a.hash,
search: a.search
}
}
/** @param href
* @param keepPrefix defaults to false
* www.example.com
* if true: example.com
* if false: www.example.com
* @returns {string} Returns the hostname of the site URL */
function getHostname(href, keepPrefix) {
const a = document.createElement("a");
a.href = href;
// if (keepPrefix) console.debug("getHostname href =", href);
return a.hostname;
}
/** Inclomplete
* @type {boolean} */
unsafeWindow.freezeGif = freezeGif;
function freezeGif(img, unfreeze) {
function createElementAndCallback(type, callback) {
const element = document.createElement(type);
callback(element);
return element;
}
var width = img.width,
height = img.height,
canvas = createElementAndCallback('canvas', function (clone) {
clone.width = width;
clone.height = height;
}),
attr,
i = 0;
var freeze = function () {
canvas.getContext('2d').drawImage(img, 0, 0, width, height);
for (i = 0; i < img.attributes.length; i++) {
attr = img.attributes[i];
if (attr.name !== '"') { // test for invalid attributes
canvas.setAttribute(attr.name, attr.value);
}
}
canvas.classList.add('freeze-gif');
canvas.style.position = 'absolute';
img.parentNode.insertBefore(canvas, img);
img.style.visibility = 'hidden';
// img.style.opacity = 0;
};
var unfreezeGif = function () {
console.log('unfreezing', img);
const freezeCanvas = img.closest('.freeze-gif');
if (!freezeCanvas) {
console.error('Couldn\'t find freezeCanvas while unfreezing this gif:', img);
} else {
freezeCanvas.style.visibility = 'hidden';
}
// img.style.opacity = 100;
img.style.visibility = 'visible';
};
if (unfreeze) {
unfreezeGif();
} else {
if (img.complete) {
freeze();
} else {
img.addEventListener('load', freeze, true);
}
}
}
function getIframeDoc(iframe) {
return iframe.contentDocument || iframe.contentWindow ? iframe.contentWindow.document : null;
}
unsafeWindow.removeClickListeners = removeClickListeners;
function removeClickListeners(selector) {
if (!!unsafeWindow.$) {
unsafeWindow.$(!selector ? "*" : selector)
.unbind("click")
.off("click")
.removeAttr("onclick");
} else {
console.warn('unsafeWindow.$ is not defined');
}
}
/**
* @WIP TODO: Complete this function
*/
function removeEventListeners(eventTarget) {
var eventType = "click";
eventTarget = window;
for (const eventType in window.getEventListeners(eventTarget)) {
const eventListeners = getEventListeners(eventTarget);
if (eventListeners.hasOwnProperty(eventType))
for (const o of eventListeners[eventType]) {
console.log('before:', o);
o.listener = null;
console.log('after:', o);
}
}
// noinspection JSUnresolvedFunction
let listeners = eventTarget.getEventListeners(eventTarget);
listeners.forEach(function (listener) {
console.log('removing listener:', listener);
eventTarget.removeEventListener("click", listener, false);
});
}
unsafeWindow.removeDoubleSpaces = removeDoubleSpaces;
unsafeWindow.cleanGibberish = cleanGibberish;
unsafeWindow.isBase64ImageData = isBase64ImageData;
unsafeWindow.cleanDates = cleanDates;
unsafeWindow.downloadScripts = function downloadScripts() {
var scriptUrls = Array.from(document.querySelectorAll('script'))
.map(script => script.src ? script.src : window.URL.createObjectURL(new Blob([script.innerHTML], {type: 'text/plain'}))
);
zipFiles(scriptUrls);
};
unsafeWindow.escapeEncodedChars = escapeEncodedChars;
/** Escapes Unicode chars, and anything starting with \x, \u,
* @param text
* @return {*} */
function escapeEncodedChars(text) {
return text
.replace(/\\[u][\dA-F]{4}/gim, match => String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16)))
.replace(/\\[x][\dA-F]{2}/gim, match => String.fromCharCode(parseInt(match.replace(/\\x/g, ''), 16)));
}
/**
* Returns true if the string is an image data string
* @param str
* @returns {boolean}
*/
function isBase64ImageData(str) {
return /^data:image\/.{1,5};base64/.test(str);
}
function removeDoubleSpaces(str) {
return !!str ? str.replace(/(\s{2,})/g, " ") : str;
}
function cleanDates(str) {
return !!str ? removeDoubleSpaces(str.replace(/\d*\.([^.]+)\.\d*/g, ' ')) : str;
}
function cleanGibberish(str, minWgr, debug=false) {
if (str) {
const gibberishRegex = /(\W{2,})|(\d{3,})|(\d+\w{1,5}\d+){2,}/g;
let noGibberish = removeDoubleSpaces(str.replace(gibberishRegex, " ")),
/**
* The minimum word2gibberish ratio to exit the loop
* @type {number|*}
*/
minWgr = 0.4 || minWgr;
if (noGibberish.length < 3) return str;
/**
* WGR: Word to Gibberish Ratio (between 0 and 1)
* 0: No gibberish (Good)
* 1: 100% Gibberish (Bad)
* @type {number}
*/
let wgr = (str.length - noGibberish.length) / str.length;
if(debug) console.debug(
'cleanGibberish(' + str + ')' +
'\nOriginal:', str,
'\nNoGibberish:', noGibberish,
'\nRatio:', wgr
);
return wgr > minWgr ?
cleanGibberish(noGibberish, minWgr) :
(str.length > 3 ? str : "");
}
return "";
}
var getCssImage = (element) => !element ? null : element.style["background-image"].replace(/(['"]?\)$)|(^url\(["']?)/g, '');
unsafeWindow.getCssImages = () => Array.from(document.querySelectorAll('[style*="background-image"]')).map(getCssImage);
unsafeWindow.observeDocument = function observeDocument(callback, options) {
callback(document.body);
options = typeof(extend)!=='function'? {}: extend(options, {
singleCallbackPerMutation: false
});
new MutationObserver(
/** @param mutations */
function mutationCallback(mutations) {
for (const mutation of mutations) {
if (!mutation.addedNodes.length)
continue;
callback(mutation.target);
if (options.singleCallbackPerMutation === true) {
break;
}
}
}
).observe(document.body, {
childList: true,
subtree: true,
attributes: true,
characterData: false
}
);
};
unsafeWindow.observeIframe = function observeIframe(iframe, observerInit, observerOptions, args) {
// older browsers don't get responsive iframe height, for now
if (!window.MutationObserver) return;
console.debug('Attaching an iframe observer...', iframe, '\n\n');
var iframeObserver = new MutationObserver(function (mutations, observer) {
console.debug(
'Observed mutation in iframe:', iframe,
'\nmutations:', mutations
);
observerInit(mutations, observer, args);
});
var interval = setInterval(function () {
if (iframe.contentWindow && iframe.contentWindow.document) {
iframeObserver.observe(iframe.contentWindow.document, observerOptions || {
attributes: true,
subtree: true,
childList: true,
characterData: true
});
console.log('Successfully added observer to iframe!', iframe);
clearInterval(interval);
}
}, 100);
};
unsafeWindow.observeAllFrames = function observeAllFrames(callback) {
callback(document.body);
callback(document);
let mutationObserver = new MutationObserver(function (mutations) {
for (const mutation of mutations) {
if (mutation.addedNodes.length) {
callback(mutation.target);
}
}
});
const mutationOptions = {
childList: true, subtree: true,
attributes: true, characterData: true
};
mutationObserver.observe(document, mutationOptions);
for (const iframe of document.querySelectorAll('iframe')) {
callback(iframe.body);
mutationObserver.observe(iframe, mutationOptions);
}
};
/**
* @param {function} condition
* @param {function} action
* @param {number} interval
*/
function waitFor(condition, action, interval) {
if (typeof condition === 'undefined') {
console.error('"condition" should be a function type:', condition);
return false;
}
if (typeof action === 'undefined') {
console.error('"condition" should be a function type:', action);
return false;
}
if (!interval) interval = 50;
var checkExist = setInterval(function () {
if (condition) {
console.log("Exists!");
clearInterval(checkExist);
action();
}
}, interval);
}
function downloadUsingXmlhttpRequest(url, opts) {
var imgUrl = url || "http://static.jsbin.com/images/dave.min.svg?4.1.4";
GM_xmlhttpRequest({
method: 'GET',
url: imgUrl,
onload: function (respDetails) {
var binResp = customBase64Encode(respDetails.responseText);
/*-- Here, we just demo that we have a valid base64 encoding
by inserting the image into the page.
We could just as easily AJAX-off the data instead.
*/
var zImgPara = document.createElement('p');
var zTargetNode = document.querySelector("body *"); //1st child
zImgPara.innerHTML = 'Image: <img src="data:image/png;base64,'
+ binResp + '">';
zTargetNode.parentNode.insertBefore(zImgPara, zTargetNode);
},
overrideMimeType: 'text/plain; charset=x-user-defined'
});
function customBase64Encode(inputStr) {
var
bbLen = 3,
enCharLen = 4,
inpLen = inputStr.length,
inx = 0,
jnx,
keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+ "0123456789+/=",
output = "",
paddingBytes = 0;
var
bytebuffer = new Array(bbLen),
encodedCharIndexes = new Array(enCharLen);
while (inx < inpLen) {
for (jnx = 0; jnx < bbLen; ++jnx) {
/*--- Throw away high-order byte, as documented at:
https://developer.mozilla.org/En/Using_XMLHttpRequest#Handling_binary_data
*/
if (inx < inpLen) {
bytebuffer[jnx] = inputStr.charCodeAt(inx++) & 0xff;
} else {
bytebuffer[jnx] = 0;
}
}
/*--- Get each encoded character, 6 bits at a time.
index 0: first 6 bits
index 1: second 6 bits
(2 least significant bits from inputStr byte 1
+ 4 most significant bits from byte 2)
index 2: third 6 bits
(4 least significant bits from inputStr byte 2
+ 2 most significant bits from byte 3)
index 3: forth 6 bits (6 least significant bits from inputStr byte 3)
*/
encodedCharIndexes[0] = bytebuffer[0] >> 2;
encodedCharIndexes[1] = ((bytebuffer[0] & 0x3) << 4) | (bytebuffer[1] >> 4);
encodedCharIndexes[2] = ((bytebuffer[1] & 0x0f) << 2) | (bytebuffer[2] >> 6);
encodedCharIndexes[3] = bytebuffer[2] & 0x3f;
//--- Determine whether padding happened, and adjust accordingly.
paddingBytes = inx - (inpLen - 1);
switch (paddingBytes) {
case 1:
// Set last character to padding char
encodedCharIndexes[3] = 64;
break;
case 2:
// Set last 2 characters to padding char
encodedCharIndexes[3] = 64;
encodedCharIndexes[2] = 64;
break;
default:
break; // No padding - proceed
}
/*--- Now grab each appropriate character out of our keystring,
based on our index array and append it to the output string.
*/
for (jnx = 0; jnx < enCharLen; ++jnx)
output += keyStr.charAt(encodedCharIndexes[jnx]);
}
return output;
}
}
unsafeWindow.iterateOverURLPattern = function iterateOverURLPattern(inputURL) {
inputURL = inputURL || '';
var bracketsContent = inputURL.match(/\[.+]/);
if (bracketsContent.length)
var [start, end] = bracketsContent[0].replace(/[\[\]]/g, '').split('-');
console.debug(
'text in brackets:', bracketsContent,
'\nstart, end:', `["${start}", "${end}"]`
);
let urls = [];
for (var i = start; i <= end; i++) {
var newNum = "" + i;
while (newNum.length < start.length) newNum = "0" + newNum;
urls.push(inputURL.replace(/\[.+]/, newNum));
}
return urls;
};
/*
CSS for top navbars:
.fixed-position {
position: fixed;
top: 0px;
z-index: 16777271;
}
*/
/*global self */
/*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */
/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/src/FileSaver.js */
/* FileSaver.js
* A saveAs() FileSaver implementation.
* 1.3.8
* 2018-03-22 14:03:47
*
* By Eli Grey, https://eligrey.com
* License: MIT
* See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md
*
*/
// noinspection ThisExpressionReferencesGlobalObjectJS
var saveAs = saveAs || (function (view) {
"use strict";
// IE <10 is explicitly unsupported
if (typeof view === "undefined" || typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) {
return;
}
var
doc = view.document
// only get URL when necessary in case Blob.js hasn't overridden it yet
,
get_URL = function () {
return view.URL || view.webkitURL || view;
},
save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a"),
can_use_save_link = "download" in save_link,
click = function (node) {
var event = new MouseEvent("click");
node.dispatchEvent(event);
},
is_safari = /constructor/i.test(view.HTMLElement) || view.safari,
is_chrome_ios = /CriOS\/[\d]+/.test(navigator.userAgent),
setImmediate = view.setImmediate || view.setTimeout,
throw_outside = function (ex) {
setImmediate(function () {
throw ex;
}, 0);
},
force_saveable_type = "application/octet-stream"
// the Blob API is fundamentally broken as there is no "downloadfinished" event to subscribe to
,
arbitrary_revoke_timeout = 1000 * 40 // in ms
,
revoke = function (file) {
var revoker = function () {
if (typeof file === "string") { // file is an object URL
get_URL().revokeObjectURL(file);
} else { // file is a File
file.remove();
}
};
setTimeout(revoker, arbitrary_revoke_timeout);
},
dispatch = function (filesaver, event_types, event) {
event_types = [].concat(event_types);
var i = event_types.length;
while (i--) {
var listener = filesaver["on" + event_types[i]];
if (typeof listener === "function") {
try {
listener.call(filesaver, event || filesaver);
} catch (ex) {
throw_outside(ex);
}
}
}
},
auto_bom = function (blob) {
// prepend BOM for UTF-8 XML and text/* types (including HTML)
// note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF
if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
return new Blob([String.fromCharCode(0xFEFF), blob], {
type: blob.type
});
}
return blob;
},
FileSaver = function (blob, name, no_auto_bom) {
if (!no_auto_bom) {
blob = auto_bom(blob);
}
// First try a.download, then web filesystem, then object URLs
var
filesaver = this,
type = blob.type,
force = type === force_saveable_type,
object_url, dispatch_all = function () {
dispatch(filesaver, "writestart progress write writeend".split(" "));
}
// on any filesys errors revert to saving with object URLs
,
fs_error = function () {
if ((is_chrome_ios || (force && is_safari)) && view.FileReader) {
// Safari doesn't allow downloading of blob urls
var reader = new FileReader();
reader.onloadend = function () {
var url = is_chrome_ios ? reader.result : reader.result.replace(/^data:[^;]*;/, 'data:attachment/file;');
var popup = view.open(url, '_blank');
if (!popup) view.location.href = url;
url = undefined; // release reference before dispatching
filesaver.readyState = filesaver.DONE;
dispatch_all();
};
reader.readAsDataURL(blob);
filesaver.readyState = filesaver.INIT;
return;
}
// don't create more object URLs than needed
if (!object_url) {
object_url = get_URL().createObjectURL(blob);
}
if (force) {
view.location.href = object_url;
} else {
var opened = view.open(object_url, "_blank");
if (!opened) {
// Apple does not allow window.open, see https://developer.apple.com/library/safari/documentation/Tools/Conceptual/SafariExtensionGuide/WorkingwithWindowsandTabs/WorkingwithWindowsandTabs.html
view.location.href = object_url;
}
}
filesaver.readyState = filesaver.DONE;
dispatch_all();
revoke(object_url);
};
filesaver.readyState = filesaver.INIT;
if (can_use_save_link) {
object_url = get_URL().createObjectURL(blob);
setImmediate(function () {
save_link.href = object_url;
save_link.download = name;
click(save_link);
dispatch_all();
revoke(object_url);
filesaver.readyState = filesaver.DONE;
}, 0);
return;
}
fs_error();
},
FS_proto = FileSaver.prototype,
saveAs = function (blob, name, no_auto_bom) {
return new FileSaver(blob, name || blob.name || "download", no_auto_bom);
};
// IE 10+ (native saveAs)
if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) {
return function (blob, name, no_auto_bom) {
name = name || blob.name || "download";
if (!no_auto_bom) {
blob = auto_bom(blob);
}
return navigator.msSaveOrOpenBlob(blob, name);
};
}
// todo: detect chrome extensions & packaged apps
//save_link.target = "_blank";
FS_proto.abort = function () {
};
FS_proto.readyState = FS_proto.INIT = 0;
FS_proto.WRITING = 1;
FS_proto.DONE = 2;
FS_proto.error =
FS_proto.onwritestart =
FS_proto.onprogress =
FS_proto.onwrite =
FS_proto.onabort =
FS_proto.onerror =
FS_proto.onwriteend =
null;
return saveAs;
}(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this));
/*`self` is undefined in Firefox for Android content script context
while `this` is nsIContentFrameMessageManager
with an attribute `content` that corresponds to the window*/
if (typeof module !== "undefined" && module.exports) {
module.exports.saveAs = saveAs;
} else if ((typeof define !== "undefined" && define !== null) && (define.amd !== null)) {
define([], function () {
return saveAs;
});
}
/* Example:
* var blob = new Blob(["Hello, world!"], {type: "text/plain;charset=utf-8"});
* saveAs(blob, "hello world.txt");
*/
unsafeWindow.saveAs = saveAs;
/* mousetrap v1.6.2 craig.is/killing/mice */
/*global define:false */
/**
* Copyright 2012-2017 Craig Campbell
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Mousetrap is a simple keyboard shortcut library for Javascript with
* no external dependencies
*
* @version 1.6.2
* @url craig.is/killing/mice
*/
(function (window, document, undefined) {
// Check if mousetrap is used inside browser, if not, return
if (!window) {
return;
}
/**
* mapping of special keycodes to their corresponding keys
*
* everything in this dictionary cannot use keypress events
* so it has to be here to map to the correct keycodes for
* keyup/keydown events
*
* @type {Object}
*/
var _MAP = {
8: 'backspace',
9: 'tab',
13: 'enter',
16: 'shift',
17: 'ctrl',
18: 'alt',
20: 'capslock',
27: 'esc',
32: 'space',
33: 'pageup',
34: 'pagedown',
35: 'end',
36: 'home',
37: 'left',
38: 'up',
39: 'right',
40: 'down',
45: 'ins',
46: 'del',
91: 'meta',
93: 'meta',
224: 'meta'
};
/**
* mapping for special characters so they can support
*
* this dictionary is only used incase you want to bind a
* keyup or keydown event to one of these keys
*
* @type {Object}
*/
var _KEYCODE_MAP = {
106: '*',
107: '+',
109: '-',
110: '.',
111: '/',
186: ';',
187: '=',
188: ',',
189: '-',
190: '.',
191: '/',
192: '`',
219: '[',
220: '\\',
221: ']',
222: '\''
};
/**
* this is a mapping of keys that require shift on a US keypad
* back to the non shift equivelents
*
* this is so you can use keyup events with these keys
*
* note that this will only work reliably on US keyboards
*
* @type {Object}
*/
var _SHIFT_MAP = {
'~': '`',
'!': '1',
'@': '2',
'#': '3',
'$': '4',
'%': '5',
'^': '6',
'&': '7',
'*': '8',
'(': '9',
')': '0',
'_': '-',
'+': '=',
':': ';',
'\"': '\'',
'<': ',',
'>': '.',
'?': '/',
'|': '\\'
};
/**
* this is a list of special strings you can use to map
* to modifier keys when you specify your keyboard shortcuts
*
* @type {Object}
*/
var _SPECIAL_ALIASES = {
'option': 'alt',
'command': 'meta',
'return': 'enter',
'escape': 'esc',
'plus': '+',
'mod': /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl'
};
/**
* variable to store the flipped version of _MAP from above
* needed to check if we should use keypress or not when no action
* is specified
*
* @type {Object|undefined}
*/
var _REVERSE_MAP;
/**
* loop through the f keys, f1 to f19 and add them to the map
* programatically
*/
for (var i = 1; i < 20; ++i) {
_MAP[111 + i] = 'f' + i;
}
/**
* loop through to map numbers on the numeric keypad
*/
for (i = 0; i <= 9; ++i) {
// This needs to use a string cause otherwise since 0 is falsey
// mousetrap will never fire for numpad 0 pressed as part of a keydown
// event.
//
// @see https://github.com/ccampbell/mousetrap/pull/258
_MAP[i + 96] = i.toString();
}
/**
* cross browser add event method
*
* @param {Element|HTMLDocument} object
* @param {string} type
* @param {Function} callback
* @returns void
*/
function _addEvent(object, type, callback) {
if (object.addEventListener) {
object.addEventListener(type, callback, false);
return;
}
try {
object.attachEvent('on' + type, callback);
} catch (e) {
console.error(e);
}
}
/**
* takes the event and returns the key character
*
* @param {Event} e
* @return {string}
*/
function _characterFromEvent(e) {
// for keypress events we should return the character as is
if (e.type == 'keypress') {
var character = String.fromCharCode(e.which);
// if the shift key is not pressed then it is safe to assume
// that we want the character to be lowercase. this means if
// you accidentally have caps lock on then your key bindings
// will continue to work
//
// the only side effect that might not be desired is if you
// bind something like 'A' cause you want to trigger an
// event when capital A is pressed caps lock will no longer
// trigger the event. shift+a will though.
if (!e.shiftKey) {
character = character.toLowerCase();
}
return character;
}
// for non keypress events the special maps are needed
if (_MAP[e.which]) {
return _MAP[e.which];
}
if (_KEYCODE_MAP[e.which]) {
return _KEYCODE_MAP[e.which];
}
// if it is not in the special map
// with keydown and keyup events the character seems to always
// come in as an uppercase character whether you are pressing shift
// or not. we should make sure it is always lowercase for comparisons
return String.fromCharCode(e.which).toLowerCase();
}
/**
* checks if two arrays are equal
*
* @param {Array} modifiers1
* @param {Array} modifiers2
* @returns {boolean}
*/
function _modifiersMatch(modifiers1, modifiers2) {
return modifiers1.sort().join(',') === modifiers2.sort().join(',');
}
/**
* takes a key event and figures out what the modifiers are
*
* @param {Event} e
* @returns {Array}
*/
function _eventModifiers(e) {
var modifiers = [];
if (e.shiftKey) {
modifiers.push('shift');
}
if (e.altKey) {
modifiers.push('alt');
}
if (e.ctrlKey) {
modifiers.push('ctrl');
}
if (e.metaKey) {
modifiers.push('meta');
}
return modifiers;
}
/**
* prevents default for this event
*
* @param {Event} e
* @returns void
*/
function _preventDefault(e) {
if (e.preventDefault) {
e.preventDefault();
return;
}
e.returnValue = false;
}
/**
* stops propogation for this event
*
* @param {Event} e
* @returns void
*/
function _stopPropagation(e) {
if (e.stopPropagation) {
e.stopPropagation();
return;
}
e.cancelBubble = true;
}
/**
* determines if the keycode specified is a modifier key or not
*
* @param {string} key
* @returns {boolean}
*/
function _isModifier(key) {
return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
}
/**
* reverses the map lookup so that we can look for specific keys
* to see what can and can't use keypress
*
* @return {Object}
*/
function _getReverseMap() {
if (!_REVERSE_MAP) {
_REVERSE_MAP = {};
for (var key in _MAP) {
// pull out the numeric keypad from here cause keypress should
// be able to detect the keys from the character
if (key > 95 && key < 112) {
continue;
}
if (_MAP.hasOwnProperty(key)) {
_REVERSE_MAP[_MAP[key]] = key;
}
}
}
return _REVERSE_MAP;
}
/**
* picks the best action based on the key combination
*
* @param {string} key - character for key
* @param {Array} modifiers
* @param {string=} action passed in
*/
function _pickBestAction(key, modifiers, action) {
// if no action was picked in we should try to pick the one
// that we think would work best for this key
if (!action) {
action = _getReverseMap()[key] ? 'keydown' : 'keypress';
}
// modifier keys don't work as expected with keypress,
// switch to keydown
if (action == 'keypress' && modifiers.length) {
action = 'keydown';
}
return action;
}
/**
* Converts from a string key combination to an array
*
* @param {string} combination like "command+shift+l"
* @return {Array}
*/
function _keysFromString(combination) {
if (combination === '+') {
return ['+'];
}
combination = combination.replace(/\+{2}/g, '+plus');
return combination.split('+');
}
/**
* Gets info for a specific key combination
*
* @param {string} combination key combination ("command+s" or "a" or "*")
* @param {string=} action
* @returns {Object}
*/
function _getKeyInfo(combination, action) {
var keys;
var key;
var i;
var modifiers = [];
// take the keys from this pattern and figure out what the actual
// pattern is all about
keys = _keysFromString(combination);
for (i = 0; i < keys.length; ++i) {
key = keys[i];
// normalize key names
if (_SPECIAL_ALIASES[key]) {
key = _SPECIAL_ALIASES[key];
}
// if this is not a keypress event then we should
// be smart about using shift keys
// this will only work for US keyboards however
if (action && action != 'keypress' && _SHIFT_MAP[key]) {
key = _SHIFT_MAP[key];
modifiers.push('shift');
}
// if this key is a modifier then add it to the list of modifiers
if (_isModifier(key)) {
modifiers.push(key);
}
}
// depending on what the key combination is
// we will try to pick the best event for it
action = _pickBestAction(key, modifiers, action);
return {
key: key,
modifiers: modifiers,
action: action
};
}
function _belongsTo(element, ancestor) {
if (element === null || element === document) {
return false;
}
if (element === ancestor) {
return true;
}
return _belongsTo(element.parentNode, ancestor);
}
function Mousetrap(targetElement) {
var self = this;
targetElement = targetElement || document;
if (!(self instanceof Mousetrap)) {
return new Mousetrap(targetElement);
}
/**
* element to attach key events to
*
* @type {Element}
*/
self.target = targetElement;
/**
* a list of all the callbacks setup via Mousetrap.bind()
*
* @type {Object}
*/
self._callbacks = {};
/**
* direct map of string combinations to callbacks used for trigger()
*
* @type {Object}
*/
self._directMap = {};
/**
* keeps track of what level each sequence is at since multiple
* sequences can start out with the same sequence
*
* @type {Object}
*/
var _sequenceLevels = {};
/**
* variable to store the setTimeout call
*
* @type {null|number}
*/
var _resetTimer;
/**
* temporary state where we will ignore the next keyup
*
* @type {boolean|string}
*/
var _ignoreNextKeyup = false;
/**
* temporary state where we will ignore the next keypress
*
* @type {boolean}
*/
var _ignoreNextKeypress = false;
/**
* are we currently inside of a sequence?
* type of action ("keyup" or "keydown" or "keypress") or false
*
* @type {boolean|string}
*/
var _nextExpectedAction = false;
/**
* resets all sequence counters except for the ones passed in
*
* @param {Object} doNotReset
* @returns void
*/
function _resetSequences(doNotReset) {
doNotReset = doNotReset || {};
var activeSequences = false,
key;
for (key in _sequenceLevels) {
if (doNotReset[key]) {
activeSequences = true;
continue;
}
_sequenceLevels[key] = 0;
}
if (!activeSequences) {
_nextExpectedAction = false;
}
}
/**
* finds all callbacks that match based on the keycode, modifiers,
* and action
*
* @param {string} character
* @param {Array} modifiers
* @param {Event|Object} e
* @param {string=} sequenceName - name of the sequence we are looking for
* @param {string=} combination
* @param {number=} level
* @returns {Array}
*/
function _getMatches(character, modifiers, e, sequenceName, combination, level) {
var i;
var callback;
var matches = [];
var action = e.type;
// if there are no events related to this keycode
if (!self._callbacks[character]) {
return [];
}
// if a modifier key is coming up on its own we should allow it
if (action == 'keyup' && _isModifier(character)) {
modifiers = [character];
}
// loop through all callbacks for the key that was pressed
// and see if any of them match
for (i = 0; i < self._callbacks[character].length; ++i) {
callback = self._callbacks[character][i];
// if a sequence name is not specified, but this is a sequence at
// the wrong level then move onto the next match
if (!sequenceName && callback.seq && _sequenceLevels[callback.seq] != callback.level) {
continue;
}
// if the action we are looking for doesn't match the action we got
// then we should keep going
if (action != callback.action) {
continue;
}
// if this is a keypress event and the meta key and control key
// are not pressed that means that we need to only look at the
// character, otherwise check the modifiers as well
//
// chrome will not fire a keypress if meta or control is down
// safari will fire a keypress if meta or meta+shift is down
// firefox will fire a keypress if meta or control is down
if ((action == 'keypress' && !e.metaKey && !e.ctrlKey) || _modifiersMatch(modifiers, callback.modifiers)) {
// when you bind a combination or sequence a second time it
// should overwrite the first one. if a sequenceName or
// combination is specified in this call it does just that
//
// @todo make deleting its own method?
var deleteCombo = !sequenceName && callback.combo == combination;
var deleteSequence = sequenceName && callback.seq == sequenceName && callback.level == level;
if (deleteCombo || deleteSequence) {
self._callbacks[character].splice(i, 1);
}
matches.push(callback);
}
}
return matches;
}
/**
* actually calls the callback function
*
* if your callback function returns false this will use the jquery
* convention - prevent default and stop propogation on the event
*
* @param {Function} callback
* @param {Event} e
* @returns void
*/
function _fireCallback(callback, e, combo, sequence) {
// if this event should not happen stop here
if (self.stopCallback(e, e.target || e.srcElement, combo, sequence)) {
return;
}
if (callback(e, combo) === false) {
_preventDefault(e);
_stopPropagation(e);
}
}
/**
* handles a character key event
*
* @param {string} character
* @param {Array} modifiers
* @param {Event} e
* @returns void
*/
self._handleKey = function (character, modifiers, e) {
var callbacks = _getMatches(character, modifiers, e);
var i;
var doNotReset = {};
var maxLevel = 0;
var processedSequenceCallback = false;
// Calculate the maxLevel for sequences so we can only execute the longest callback sequence
for (i = 0; i < callbacks.length; ++i) {
if (callbacks[i].seq) {
maxLevel = Math.max(maxLevel, callbacks[i].level);
}
}
// loop through matching callbacks for this key event
for (i = 0; i < callbacks.length; ++i) {
// fire for all sequence callbacks
// this is because if for example you have multiple sequences
// bound such as "g i" and "g t" they both need to fire the
// callback for matching g cause otherwise you can only ever
// match the first one
if (callbacks[i].seq) {
// only fire callbacks for the maxLevel to prevent
// subsequences from also firing
//
// for example 'a option b' should not cause 'option b' to fire
// even though 'option b' is part of the other sequence
//
// any sequences that do not match here will be discarded
// below by the _resetSequences call
if (callbacks[i].level != maxLevel) {
continue;
}
processedSequenceCallback = true;
// keep a list of which sequences were matches for later
doNotReset[callbacks[i].seq] = 1;
_fireCallback(callbacks[i].callback, e, callbacks[i].combo, callbacks[i].seq);
continue;
}
// if there were no sequence matches but we are still here
// that means this is a regular match so we should fire that
if (!processedSequenceCallback) {
_fireCallback(callbacks[i].callback, e, callbacks[i].combo);
}
}
// if the key you pressed matches the type of sequence without
// being a modifier (ie "keyup" or "keypress") then we should
// reset all sequences that were not matched by this event
//
// this is so, for example, if you have the sequence "h a t" and you
// type "h e a r t" it does not match. in this case the "e" will
// cause the sequence to reset
//
// modifier keys are ignored because you can have a sequence
// that contains modifiers such as "enter ctrl+space" and in most
// cases the modifier key will be pressed before the next key
//
// also if you have a sequence such as "ctrl+b a" then pressing the
// "b" key will trigger a "keypress" and a "keydown"
//
// the "keydown" is expected when there is a modifier, but the
// "keypress" ends up matching the _nextExpectedAction since it occurs
// after and that causes the sequence to reset
//
// we ignore keypresses in a sequence that directly follow a keydown
// for the same character
var ignoreThisKeypress = e.type == 'keypress' && _ignoreNextKeypress;
if (e.type == _nextExpectedAction && !_isModifier(character) && !ignoreThisKeypress) {
_resetSequences(doNotReset);
}
_ignoreNextKeypress = processedSequenceCallback && e.type == 'keydown';
};
/**
* handles a keydown event
*
* @param {Event} e
* @returns void
*/
function _handleKeyEvent(e) {
// normalize e.which for key events
// @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
if (typeof e.which !== 'number') {
e.which = e.keyCode;
}
var character = _characterFromEvent(e);
// no character found then stop
if (!character) {
return;
}
// need to use === for the character check because the character can be 0
if (e.type == 'keyup' && _ignoreNextKeyup === character) {
_ignoreNextKeyup = false;
return;
}
self.handleKey(character, _eventModifiers(e), e);
}
/**
* called to set a 1 second timeout on the specified sequence
*
* this is so after each key press in the sequence you have 1 second
* to press the next key before you have to start over
*
* @returns void
*/
function _resetSequenceTimer() {
clearTimeout(_resetTimer);
_resetTimer = setTimeout(_resetSequences, 1000);
}
/**
* binds a key sequence to an event
*
* @param {string} combo - combo specified in bind call
* @param {Array} keys
* @param {Function} callback
* @param {string=} action
* @returns void
*/
function _bindSequence(combo, keys, callback, action) {
// start off by adding a sequence level record for this combination
// and setting the level to 0
_sequenceLevels[combo] = 0;
/**
* callback to increase the sequence level for this sequence and reset
* all other sequences that were active
*
* @param {string} nextAction
* @returns {Function}
*/
function _increaseSequence(nextAction) {
return function () {
_nextExpectedAction = nextAction;
++_sequenceLevels[combo];
_resetSequenceTimer();
};
}
/**
* wraps the specified callback inside of another function in order
* to reset all sequence counters as soon as this sequence is done
*
* @param {Event} e
* @returns void
*/
function _callbackAndReset(e) {
_fireCallback(callback, e, combo);
// we should ignore the next key up if the action is key down
// or keypress. this is so if you finish a sequence and
// release the key the final key will not trigger a keyup
if (action !== 'keyup') {
_ignoreNextKeyup = _characterFromEvent(e);
}
// weird race condition if a sequence ends with the key
// another sequence begins with
setTimeout(_resetSequences, 10);
}
// loop through keys one at a time and bind the appropriate callback
// function. for any key leading up to the final one it should
// increase the sequence. after the final, it should reset all sequences
//
// if an action is specified in the original bind call then that will
// be used throughout. otherwise we will pass the action that the
// next key in the sequence should match. this allows a sequence
// to mix and match keypress and keydown events depending on which
// ones are better suited to the key provided
for (var i = 0; i < keys.length; ++i) {
var isFinal = i + 1 === keys.length;
var wrappedCallback = isFinal ? _callbackAndReset : _increaseSequence(action || _getKeyInfo(keys[i + 1]).action);
_bindSingle(keys[i], wrappedCallback, action, combo, i);
}
}
/**
* binds a single keyboard combination
*
* @param {string} combination
* @param {Function} callback
* @param {string=} action
* @param {string=} sequenceName - name of sequence if part of sequence
* @param {number=} level - what part of the sequence the command is
* @returns void
*/
function _bindSingle(combination, callback, action, sequenceName, level) {
// store a direct mapped reference for use with Mousetrap.trigger
self._directMap[combination + ':' + action] = callback;
// make sure multiple spaces in a row become a single space
combination = combination.replace(/\s+/g, ' ');
var sequence = combination.split(' ');
var info;
// if this pattern is a sequence of keys then run through this method
// to reprocess each pattern one key at a time
if (sequence.length > 1) {
_bindSequence(combination, sequence, callback, action);
return;
}
info = _getKeyInfo(combination, action);
// make sure to initialize array if this is the first time
// a callback is added for this key
self._callbacks[info.key] = self._callbacks[info.key] || [];
// remove an existing match if there is one
_getMatches(info.key, info.modifiers, {type: info.action}, sequenceName, combination, level);
// add this call back to the array
// if it is a sequence put it at the beginning
// if not put it at the end
//
// this is important because the way these are processed expects
// the sequence ones to come first
self._callbacks[info.key][sequenceName ? 'unshift' : 'push']({
callback: callback,
modifiers: info.modifiers,
action: info.action,
seq: sequenceName,
level: level,
combo: combination
});
}
/**
* binds multiple combinations to the same callback
*
* @param {Array} combinations
* @param {Function} callback
* @param {string|undefined} action
* @returns void
*/
self._bindMultiple = function (combinations, callback, action) {
for (var i = 0; i < combinations.length; ++i) {
_bindSingle(combinations[i], callback, action);
}
};
// start!
_addEvent(targetElement, 'keypress', _handleKeyEvent);
_addEvent(targetElement, 'keydown', _handleKeyEvent);
_addEvent(targetElement, 'keyup', _handleKeyEvent);
}
/**
* binds an event to mousetrap
*
* can be a single key, a combination of keys separated with +,
* an array of keys, or a sequence of keys separated by spaces
*
* be sure to list the modifier keys first to make sure that the
* correct key ends up getting bound (the last key in the pattern)
*
* @param {string|Array} keys
* @param {Function} callback
* @param {string=} action - 'keypress', 'keydown', or 'keyup'
* @returns void
*/
Mousetrap.prototype.bind = function (keys, callback, action) {
var self = this;
keys = keys instanceof Array ? keys : [keys];
self._bindMultiple.call(self, keys, callback, action);
return self;
};
/**
* unbinds an event to mousetrap
*
* the unbinding sets the callback function of the specified key combo
* to an empty function and deletes the corresponding key in the
* _directMap dict.
*
* TODO: actually remove this from the _callbacks dictionary instead
* of binding an empty function
*
* the keycombo+action has to be exactly the same as
* it was defined in the bind method
*
* @param {string|Array} keys
* @param {string} action
* @returns void
*/
Mousetrap.prototype.unbind = function (keys, action) {
var self = this;
return self.bind.call(self, keys, function () {
}, action);
};
/**
* triggers an event that has already been bound
*
* @param {string} keys
* @param {string=} action
* @returns void
*/
Mousetrap.prototype.trigger = function (keys, action) {
var self = this;
if (self._directMap[keys + ':' + action]) {
self._directMap[keys + ':' + action]({}, keys);
}
return self;
};
/**
* resets the library back to its initial state. this is useful
* if you want to clear out the current keyboard shortcuts and bind
* new ones - for example if you switch to another page
*
* @returns void
*/
Mousetrap.prototype.reset = function () {
var self = this;
self._callbacks = {};
self._directMap = {};
return self;
};
/**
* should we stop this event before firing off callbacks
*
* @param {Event} e
* @param {Element} element
* @return {boolean}
*/
Mousetrap.prototype.stopCallback = function (e, element) {
var self = this;
// if the element has the class "mousetrap" then no need to stop
if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
return false;
}
if (_belongsTo(element, self.target)) {
return false;
}
// stop for input, select, and textarea
return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable;
};
/**
* exposes _handleKey publicly so it can be overwritten by extensions
*/
Mousetrap.prototype.handleKey = function () {
var self = this;
return self._handleKey.apply(self, arguments);
};
/**
* allow custom key mappings
*/
Mousetrap.addKeycodes = function (object) {
for (var key in object) {
if (object.hasOwnProperty(key)) {
_MAP[key] = object[key];
}
}
_REVERSE_MAP = null;
};
/**
* Init the global mousetrap functions
*
* This method is needed to allow the global mousetrap functions to work
* now that mousetrap is a constructor function.
*/
Mousetrap.init = function () {
var documentMousetrap = Mousetrap(document);
for (var method in documentMousetrap) {
if (method.charAt(0) !== '_') {
Mousetrap[method] = (function (method) {
return function () {
return documentMousetrap[method].apply(documentMousetrap, arguments);
};
}(method));
}
}
};
Mousetrap.init();
// expose mousetrap to the global object
window.Mousetrap = Mousetrap;
// expose as a common js module
if (typeof module !== 'undefined' && module.exports) {
module.exports = Mousetrap;
}
// expose mousetrap as an AMD module
if (typeof define === 'function' && define.amd) {
define(function () {
return Mousetrap;
});
}
})(typeof window !== 'undefined' ? window : null, typeof window !== 'undefined' ? document : null);
unsafeWindow.Mousetrap = Mousetrap;
unsafeWindow.fetchSimilarHeaders = fetchSimilarHeaders;
function fetchSimilarHeaders(callback) {
var request = new XMLHttpRequest();
request.onreadystatechange = function () {
if (request.readyState === 4) {
//
// The following headers may often be similar
// to those of the original page request...
//
if (callback && typeof callback === 'function') {
callback(request.getAllResponseHeaders());
}
}
};
//
// Re-request the same page (document.location)
// We hope to get the same or similar response headers to those which
// came with the current page, but we have no guarantee.
// Since we are only after the headers, a HEAD request may be sufficient.
//
request.open('HEAD', document.location, true);
request.send(null);
}
String.prototype.escapeSpecialChars = function () {
return this.replace(/\\n/g, "\\n")
.replace(/\\'/g, "\\'")
.replace(/\\"/g, '\\"')
.replace(/\\&/g, "\\&")
.replace(/\\r/g, "\\r")
.replace(/\\t/g, "\\t")
.replace(/\\b/g, "\\b")
.replace(/\\f/g, "\\f");
};
function headers2Object(headers) {
if (!headers) return {};
const jsonParseEscape = function (str) {
return str.replace(/\n/g, "\\n")
.replace(/\'/g, "\\'")
.replace(/\"/g, '\\"')
.replace(/\&/g, "\\&")
.replace(/\r/g, "\\r")
.replace(/\t/g, "\\t")
.replace(/\f/g, "\\f");
};
var jsonStr = '{\n' +
headers.trim().split("\n").filter(line => line.length > 2)
.map(
line => " " + [line.slice(0, line.indexOf(':')), line.slice(line.indexOf(':') + 1)]
.map(part => '"' + jsonParseEscape(part.trim()) + '"').join(':')
)
.join(",\n") +
'\n}';
console.log('jsonStr:', jsonStr);
return JSON.parse(jsonStr);
}
unsafeWindow.fetchUsingProxy = fetchUsingProxy;
/**
* @param url
* Found from: https://stackoverflow.com/questions/43871637/no-access-control-allow-origin-header-is-present-on-the-requested-resource-whe
* @param callback
* @see https://cors-anywhere.herokuapp.com/
*/
function fetchUsingProxy(url, callback) {
const proxyurl = "https://cors-anywhere.herokuapp.com/";
callback = callback || (contents => console.log(contents));
fetch(proxyurl + url) // https://cors-anywhere.herokuapp.com/https://example.com
.then(response => response.text())
.then(callback)
.catch(() => console.error(`Can’t access ${url} response. Blocked by browser?`))
}
unsafeWindow.getModKeys = getModifierKeys;
unsafeWindow.KeyEvent = {
DOM_VK_BACKSPACE: 8,
DOM_VK_TAB: 9,
DOM_VK_ENTER: 13,
DOM_VK_SHIFT: 16,
DOM_VK_CTRL: 17,
DOM_VK_ALT: 18,
DOM_VK_PAUSE_BREAK: 19,
DOM_VK_CAPS_LOCK: 20,
DOM_VK_ESCAPE: 27,
DOM_VK_PGUP: 33, DOM_VK_PAGE_UP: 33,
DOM_VK_PGDN: 34, DOM_VK_PAGE_DOWN: 34,
DOM_VK_END: 35,
DOM_VK_HOME: 36,
DOM_VK_LEFT: 37, DOM_VK_LEFT_ARROW: 37,
DOM_VK_UP: 38, DOM_VK_UP_ARROW: 38,
DOM_VK_RIGHT: 39, DOM_VK_RIGHT_ARROW: 39,
DOM_VK_DOWN: 40, DOM_VK_DOWN_ARROW: 40,
DOM_VK_INSERT: 45,
DOM_VK_DEL: 46, DOM_VK_DELETE: 46,
DOM_VK_0: 48, DOM_VK_ALPHA0: 48,
DOM_VK_1: 49, DOM_VK_ALPHA1: 49,
DOM_VK_2: 50, DOM_VK_ALPHA2: 50,
DOM_VK_3: 51, DOM_VK_ALPHA3: 51,
DOM_VK_4: 52, DOM_VK_ALPHA4: 52,
DOM_VK_5: 53, DOM_VK_ALPHA5: 53,
DOM_VK_6: 54, DOM_VK_ALPHA6: 54,
DOM_VK_7: 55, DOM_VK_ALPHA7: 55,
DOM_VK_8: 56, DOM_VK_ALPHA8: 56,
DOM_VK_9: 57, DOM_VK_ALPHA9: 57,
DOM_VK_A: 65,
DOM_VK_B: 66,
DOM_VK_C: 67,
DOM_VK_D: 68,
DOM_VK_E: 69,
DOM_VK_F: 70,
DOM_VK_G: 71,
DOM_VK_H: 72,
DOM_VK_I: 73,
DOM_VK_J: 74,
DOM_VK_K: 75,
DOM_VK_L: 76,
DOM_VK_M: 77,
DOM_VK_N: 78,
DOM_VK_O: 79,
DOM_VK_P: 80,
DOM_VK_Q: 81,
DOM_VK_R: 82,
DOM_VK_S: 83,
DOM_VK_T: 84,
DOM_VK_U: 85,
DOM_VK_V: 86,
DOM_VK_W: 87,
DOM_VK_X: 88,
DOM_VK_Y: 89,
DOM_VK_Z: 90,
DOM_VK_LWIN: 91, DOM_VK_LEFT_WINDOW: 91,
DOM_VK_RWIN: 92, DOM_VK_RIGHT_WINDOW: 92,
DOM_VK_SELECT: 93,
DOM_VK_NUMPAD0: 96,
DOM_VK_NUMPAD1: 97,
DOM_VK_NUMPAD2: 98,
DOM_VK_NUMPAD3: 99,
DOM_VK_NUMPAD4: 100,
DOM_VK_NUMPAD5: 101,
DOM_VK_NUMPAD6: 102,
DOM_VK_NUMPAD7: 103,
DOM_VK_NUMPAD8: 104,
DOM_VK_NUMPAD9: 105,
DOM_VK_MULTIPLY: 106,
DOM_VK_ADD: 107,
DOM_VK_SUBTRACT: 109,
DOM_VK_DECIMAL_POINT: 110,
DOM_VK_DIVIDE: 111,
DOM_VK_F1: 112,
DOM_VK_F2: 113,
DOM_VK_F3: 114,
DOM_VK_F4: 115,
DOM_VK_F5: 116,
DOM_VK_F6: 117,
DOM_VK_F7: 118,
DOM_VK_F8: 119,
DOM_VK_F9: 120,
DOM_VK_F10: 121,
DOM_VK_F11: 122,
DOM_VK_F12: 123,
DOM_VK_NUM_LOCK: 144,
DOM_VK_SCROLL_LOCK: 145,
DOM_VK_SEMICOLON: 186,
DOM_VK_EQUALS: 187, DOM_VK_EQUAL_SIGN: 187,
DOM_VK_COMMA: 188,
DOM_VK_DASH: 189,
DOM_VK_PERIOD: 190,
DOM_VK_FORWARD_SLASH: 191,
DOM_VK_GRAVE_ACCENT: 192,
DOM_VK_OPEN_BRACKET: 219,
DOM_VK_BACK_SLASH: 220,
DOM_VK_CLOSE_BRACKET: 221,
DOM_VK_SINGLE_QUOTE: 222
};
/**
* Order of key strokes in naming convention: Ctrl > Shift > Alt > Meta
* @param keyEvent
* @returns {{CTRL_ONLY: boolean, SHIFT_ONLY: boolean, ALT_ONLY: boolean, META_ONLY: boolean, NONE: boolean}}
*/
function getModifierKeys(keyEvent) {
/** @type {{CTRL_ONLY: boolean, SHIFT_ONLY: boolean, ALT_ONLY: boolean, NONE: boolean}} */
return {
CTRL_SHIFT: keyEvent.ctrlKey && !keyEvent.altKey && keyEvent.shiftKey && !keyEvent.metaKey,
CTRL_ALT: keyEvent.ctrlKey && keyEvent.altKey && !keyEvent.shiftKey && !keyEvent.metaKey,
ALT_SHIFT: !keyEvent.ctrlKey && keyEvent.altKey && keyEvent.shiftKey && !keyEvent.metaKey,
CTRL_ONLY: keyEvent.ctrlKey && !keyEvent.altKey && !keyEvent.shiftKey && !keyEvent.metaKey,
CTRL_ALT_SHIFT: keyEvent.ctrlKey && keyEvent.altKey && keyEvent.shiftKey && !keyEvent.metaKey,
SHIFT_ONLY: !keyEvent.ctrlKey && !keyEvent.altKey && keyEvent.shiftKey && !keyEvent.metaKey,
ALT_ONLY: !keyEvent.ctrlKey && keyEvent.altKey && !keyEvent.shiftKey && !keyEvent.metaKey,
META_ONLY: !keyEvent.ctrlKey && !keyEvent.altKey && !keyEvent.shiftKey && keyEvent.metaKey,
NONE: !keyEvent.ctrlKey && !keyEvent.shiftKey && !keyEvent.altKey && !keyEvent.metaKey,
targetIsInput: (function targetIsInput() {
const ignores = document.getElementsByTagName('input');
const target = keyEvent.target;
for (let ignore of ignores)
if (target === ignore || ignore.contains(target)) {
// console.log('The target recieving the keycode is of type "input", so it will not recieve your keystroke', target);
return true;
}
return false;
})()
};
}
function publicizeSymbols(...parameters) {
for (const parameter of parameters) {
unsafeWindow[parameter] = parameter;
}
}
function mapObject(o) {
var map = new Map();
for (const key in (o)) {
if (o.hasOwnProperty(key))
map.set(key, o[key]);
}
return map;
}
function getObjOfType(targetInstance, parentObj) {
var list = [];
for (const o in parentObj) if (o instanceof targetInstance) {
return o;
}
return list;
}
function getNestedMembers(parentObject, targetType, list) {
if (!parentObject) {
console.error("parentObject is not defined:", parent);
return;
}
list = list || [];
for (const member in parentObject) {
const typeofObj = typeof member;
if (typeofObj === "object") {
getNestedMembers(member, targetType, list);
} else if (typeofObj !== 'undefined') {
if (targetType && typeofObj !== targetType)
continue;
list.push(member);
}
}
return list;
}