Faris Handy Webdev JavaScript functions

A bunch of useful JavaScript functions

当前为 2018-10-14 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Faris Handy Webdev JavaScript functions
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.3
  5. // @description A bunch of useful JavaScript functions
  6. // @description This is not a regular script for you to run! Only use this via the @require keyword.
  7. // @author Faris Hijazi
  8. // @grant GM_xmlhttpRequest
  9. // @grant GM_download
  10. // @grant GM_setValue
  11. // @grant GM_getValue
  12. // @grant GM_setClipboard
  13. // @grant unsafeWindow
  14. // @grant window.close
  15. // @grant window.focus
  16. // @run-at document-start
  17. // @include *
  18. // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
  19. // ==/UserScript==
  20.  
  21. /**/
  22. if (typeof unsafeWindow === "undefined") unsafeWindow = window;
  23.  
  24. unsafeWindow.URL_REGEX_STR = `(//|http(s?:))[\\d\\w?%\\-_/\\\\=.]+?`;
  25. unsafeWindow.IMAGE_URL_REGEX = new RegExp(`(//|http(s?:))[\\d\\w?%\\-_/\\\\=.]+?\\.(jpg|png|jpeg|gif|tiff|&f=1)`, 'gim');
  26. // /(\/\/|http(s?:))[\d\w?%\-_\/\\=.]+?\.(jpg|png|jpeg|gif|tiff|&f=1)/gim;
  27. unsafeWindow.VID_URL_REGEX = /(\/\/|http(s?:))[\d\w?%\-_\/\\=.]+?\.(mov|webm|mp4|wmv|&f=1)/gim;
  28.  
  29. var useEncryptedGoogle = /encrypted.google.com/.test(location.hostname);
  30. var googleBaseURL = `https://${/google\./.test(location.hostname) ? location.hostname :
  31. ((useEncryptedGoogle ? "encrypted" : "www") + ".google.com")}`;
  32.  
  33. unsafeWindow.gImgSearchURL = `${googleBaseURL}/search?&hl=en&tbm=isch&q=`;
  34. unsafeWindow.GIMG_REVERSE_SEARCH_URL = `${googleBaseURL}/searchbyimage?&image_url=`;
  35.  
  36. if (typeof GM_setClipboard !== 'undefined') {
  37. unsafeWindow.setClipboard = GM_setClipboard;
  38. unsafeWindow.GM_setClipboard = GM_setClipboard;
  39. }
  40. if (typeof GM_xmlhttpRequest !== 'undefined') {
  41. unsafeWindow.GM_xmlhttpRequest =
  42. /**
  43. * Description
  44. 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.
  45.  
  46. Restrictions
  47. GM_xmlhttpRequest restricts access to the http, https, ftp, data, blob, and moz-blob protocols.
  48.  
  49. If a script uses one or more @domains then the GM_xmlhttpRequest api will be restricted to those domains.
  50.  
  51. If the url provided does not pass the above criteria then a error will be thrown when calling GM_xmlhttpRequest
  52.  
  53. Arguments
  54. Object details
  55. A single object with properties defining the request behavior.
  56.  
  57. String method: Optional. The HTTP method to utilize. Currently only "GET" and "POST" are supported. Defaults to "GET".
  58. 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.
  59. Function onload: Optional. A function called if the request finishes successfully. Passed a Scriptish response object (see below).
  60. Function onerror: Optional. A function called if the request fails. Passed a Scriptish response object (see below).
  61. Function onreadystatechange: Optional. A function called whenever the request's readyState changes. Passed a Scriptish response object (see below).
  62. String data: Optional. Content to send as the body of the request.
  63. Object headers: Optional. An object containing headers to be sent as part of the request.
  64. Boolean binary: Optional. Forces the request to send data as binary. Defaults to false.
  65. Boolean makePrivate: Optional. Forces the request to be a private request (same as initiated from a private window). (0.1.9+)
  66. Boolean mozBackgroundRequest: Optional. If true security dialogs will not be shown, and the request will fail. Defaults to true.
  67. String user: Optional. The user name to use for authentication purposes. Defaults to the empty string "".
  68. String password: Optional. The password to use for authentication purposes. Defaults to the empty string "".
  69. String overrideMimeType: Optional. Overrides the MIME type returned by the server.
  70. Boolean ignoreCache: Optional. Forces a request to the server, bypassing the cache. Defaults to false.
  71. Boolean ignoreRedirect: Optional. Forces the request to ignore both temporary and permanent redirects.
  72. Boolean ignoreTempRedirect: Optional. Forces the request to ignore only temporary redirects.
  73. Boolean ignorePermanentRedirect: Optional. Forces the request to ignore only permanent redirects.
  74. Boolean failOnRedirect: Optional. Forces the request to fail if a redirect occurs.
  75. Integer redirectionLimit: Optional. Range allowed: 0-10. Forces the request to fail if a certain number of redirects occur.
  76. Note: A redirectionLimit of 0 is equivalent to setting failOnRedirect to true.
  77. Note: If both are set, redirectionLimit will take priority over failOnRedirect.
  78.  
  79. 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.
  80.  
  81. Response Object
  82. This is the response object passed to the onload, onerror, and onreadystatechange callbacks described for the details object above.
  83.  
  84. String responseText: The response to the request in text form.
  85. String responseJSON: If the content type is JSON (example: application/json, text/x-json, and more..) then responseJSON will be available.
  86. Integer readyState: The state of the request. Refer to https://developer.mozilla.org/en/XMLHttpRequest#Properties
  87. String responseHeaders: The string value of all response headers. null if no response has been received.
  88. Integer status: The HTTP status code from the server. null if the request hasn't yet completed, or resulted in an error.
  89. String statusText: The entire HTTP status response string from the server. null if the request hasn't yet completed, or resulted in an error.
  90. 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.
  91. For "onprogress" only:
  92.  
  93. Boolean lengthComputable: Whether it is currently possible to know the total size of the response.
  94. Integer loaded: The number of bytes loaded thus far.
  95. Integer total: The total size of the response.
  96. Returns
  97. */
  98. GM_xmlhttpRequest;
  99. }
  100.  
  101. unsafeWindow.log = console.debug;
  102. unsafeWindow.setLog = newDebugState => debug = (typeof newDebugState === "boolean") ? newDebugState : debug;
  103. unsafeWindow.matchSite = matchSite;
  104. unsafeWindow.createElement = createElement;
  105. unsafeWindow.loadScript = loadScript;
  106. unsafeWindow.Proxy = {
  107. fileStack: url => (`https://process.filestackapi.com/AhTgLagciQByzXpFGRI0Az/${encodeURIComponent(url.trim())}`),
  108. steemitimages: url => /\.(jpg|jpeg|tiff|png|gif)($|\?)/i.test(url) ? (`https://steemitimages.com/0x0/${url.trim()}`) : url,
  109. ddg: ddgProxy
  110. };
  111. unsafeWindow.ddgProxy = ddgProxy;
  112. unsafeWindow.getOGZscalarUrl = getOGZscalarUrl;
  113. unsafeWindow.reverseDdgProxy = reverseDdgProxy;
  114. unsafeWindow.isDdgUrl = isDdgUrl;
  115. unsafeWindow.targetIsInput = targetIsInput;
  116. unsafeWindow.createAndAddAttribute = createAndAddAttribute;
  117. unsafeWindow.getGImgReverseSearchURL = getGImgReverseSearchURL;
  118.  
  119. unsafeWindow.toDdgProxy = () => location.href = ddgProxy(location.href);
  120. unsafeWindow.isIterable = obj => obj != null && typeof obj[Symbol.iterator] == 'function';
  121. unsafeWindow.GM_setValue = GM_setValue;
  122. unsafeWindow.GM_getValue = GM_getValue;
  123.  
  124. unsafeWindow.q = q;
  125. unsafeWindow.qa = qa;
  126. unsafeWindow.siteSearchUrl = siteSearchUrl;
  127. unsafeWindow.getAbsoluteURI = getAbsoluteURI;
  128.  
  129. /**Returns the HOSTNAME of a website url*/
  130. unsafeWindow.getHostname = getHostname;
  131. /***/
  132. unsafeWindow.openAllLinks = function () {
  133. Array.from(document.links).forEach(function (link) {
  134. if (link.hasAttribute("href")) {
  135. window.open(link.href);
  136. }
  137. });
  138. };
  139.  
  140. /**Returns a DuckDuckGo proxy url (attempts to unblock the url)*/
  141. function ddgProxy(href) {
  142. return isDdgUrl(href) || /^(javascript)/i.test(href) ? href : (`https://proxy.duckduckgo.com/iu/?u=${encodeURIComponent(href)}&f=1`);
  143. }
  144.  
  145. /**Opens the url via fetch(), then performs a callback giving it the document element*/
  146. unsafeWindow.fetchElement = fetchElement;
  147. /**Opens the url via xmlhttpRequest, then performs a callback giving it the document element*/
  148. unsafeWindow.xmlRequestElement = xmlRequestElement;
  149. unsafeWindow.onLoadDim = onLoadDim;
  150. unsafeWindow.addCss = addCss;
  151.  
  152. /**
  153. * @author https://codepen.io/frosas/
  154. * Also works with scripts from other sites if they have CORS enabled (look for the header Access-Control-Allow-Origin: *).
  155. *
  156. * // Usage
  157. * var url = 'https://raw.githubusercontent.com/buzamahmooza/Helpful-Web-Userscripts/master/GM_dummy_functions.js?token=AZoN2Rl0UPDtcrOIgaESbGp_tuHy51Hmks5bpijqwA%3D%3D';
  158. * loadGitHubScript(url).then((event) => { });
  159. */
  160. function loadGitHubScript(url) {
  161. return fetch(url).
  162. then(res => res.blob()).
  163. then(body => loadScript(URL.createObjectURL(body)));
  164. function loadScript(url) {
  165. new Promise(function(resolve, reject) {
  166. var script = document.createElement('script');
  167. script.src = url;
  168. script.onload = resolve;
  169. script.onerror = function(){
  170. console.warn("couldn't load script: ", url);
  171. if(typeof reject === 'function')
  172. reject();
  173. }; // TODO Not sure it really works
  174. document.head.appendChild(script);
  175. });
  176. }
  177. }
  178.  
  179.  
  180.  
  181. /**@deprecated doesn't actually succeed*/
  182. unsafeWindow.addJs = function addJs(js, id) {
  183. const jsScript = document.createElement('script');
  184. jsScript.appendChild(document.createTextNode(js));
  185. if (!!id) jsScript.id = id;
  186. jsScript.classList.add('addJs');
  187. return document.getElementsByTagName('head')[0].appendChild(jsScript);
  188. };
  189. unsafeWindow.observe = observe;
  190. unsafeWindow.gfycatPage2GifUrl = function (gfycatPageUrl) {
  191. if (!/https:\/\/gfycat\.com\/gifs\/detail\/.+/.test(gfycatPageUrl)) {
  192. throw error("Not a gfycat home url:" + gfycatPageUrl);
  193. }
  194. return `https://thumbs.gfycat.com/${gfycatPageUrl.split('/').pop()}-size_restricted.gif`;
  195. };
  196. unsafeWindow.preloader = preloader;
  197. unsafeWindow.waitForElement = waitForElement;
  198. unsafeWindow.includeJs = includeJs;
  199. /**
  200. * Appends a style element to the head of the document containing the given cssStr text
  201. * @param cssStr
  202. * @param id
  203. * @return {HTMLStyleElement}
  204. */
  205. function addCss(cssStr, id) {
  206. const style = document.createElement('style');
  207. if (style.styleSheet) {
  208. style.styleSheet.cssText = cssStr;
  209. } else {
  210. style.appendChild(document.createTextNode(cssStr));
  211. }
  212. if (!!id) style.id = id;
  213. style.classList.add('addCss');
  214. return document.getElementsByTagName('head')[0].appendChild(style);
  215. }
  216. unsafeWindow.disableStyles = disableStyles;
  217. function disableStyles(enable) {
  218. console.log('Disabling styles');
  219. for (const styleEl of document.querySelectorAll('style, link')) {
  220. styleEl.disabled = !enable
  221. }
  222. }
  223.  
  224. unsafeWindow.createAndGetNavbar = createAndGetNavbar;
  225. /**
  226. * Creates a static navbar at the top of the page.
  227. * Useful for adding buttons and controls to it
  228. * @param callback this callback should be used when instantly adding content to the navbar,
  229. * do NOT just take the returned value and start adding elements.
  230. * @return {HTMLDivElement} returns the parent navbar element
  231. */
  232. function createAndGetNavbar(callback) {
  233. // Settings up the navbar
  234. // language=CSS
  235. addCss(`
  236. div#topnav {
  237. position: fixed;
  238. z-index: 1000;
  239. min-height: 50px;
  240. top: 0;
  241. right: 0;
  242. left: 0;
  243. background: #525252;
  244. font-size: 14px;
  245. }
  246.  
  247. div#topnav-content {
  248. margin-left: 115px;
  249. padding: 10px;
  250. font-family: inherit;
  251. font-stretch: extra-condensed;
  252. font-size: 20px;
  253. }`, "navbar-css");
  254.  
  255. function adjustTopMargin() {
  256. document.body.style.top = `${q('#topnav').offsetHeight}px`;
  257. }
  258.  
  259. const navbar = document.createElement(`div`);
  260. navbar.id = "topnav";
  261. const navbarContentDiv = document.createElement('div');
  262. navbarContentDiv.id = "topnav-content";
  263.  
  264. navbar.appendChild(navbarContentDiv);
  265.  
  266. document.body.firstElementChild.before(navbar);
  267.  
  268. window.addEventListener('resize', adjustTopMargin);
  269.  
  270. document.body.style.position = "relative";
  271.  
  272. // keep trying to use the callback, works when the navbarContentDiv is finally added
  273. var interval = setInterval(function () {
  274. const topnavContentDiv = q('#topnav-content');
  275. if (topnavContentDiv) {
  276. clearInterval(interval);
  277. if (callback)
  278. callback(topnavContentDiv);
  279. adjustTopMargin();
  280. }
  281. }, 100);
  282. return navbar;
  283. }
  284.  
  285.  
  286. unsafeWindow.setStyleInHTML = setStyleInHTML;
  287. /**
  288. * This will set the style of an element by force, by manipulating the style HTML attribute.
  289. * This gives you more control, you can set the exact text you want in the HTML element (like giving a style priority via "!important").
  290. * Example calls:
  291. * setStyleByHTML(el, "background-image", "url(http://www.example.com/cool.png)")
  292. * setStyleByHTML(el, "{ background-image : url(http://www.example.com/cool.png) }")
  293. * @param {HTMLElement} el
  294. * @param {String} styleProperty
  295. * @param {String} styleValue
  296. * @return el
  297. */
  298. function setStyleInHTML(el, styleProperty, styleValue) {
  299. styleProperty = styleProperty.trim().replace(/^.*{|}.*$/g, '');
  300.  
  301. const split = styleProperty.split(':');
  302. if (!styleValue && split.length > 1) {
  303. styleValue = split.pop();
  304. styleProperty = split.pop();
  305. }
  306.  
  307. if (el.hasAttribute('style')) {
  308. const styleText = el.getAttribute('style');
  309. const styleArgument = `${styleProperty}: ${styleValue};`;
  310.  
  311. let newStyle = new RegExp(styleProperty, 'i').test(styleText) ?
  312. styleText.replace(new RegExp(`${styleProperty}:.+?;`, 'im'), styleArgument) :
  313. `${styleText} ${styleArgument}`;
  314.  
  315. el.setAttribute('style', newStyle);
  316.  
  317. console.debug(
  318. 'adding to style ', `"${styleArgument}"`,
  319. '\nnewStyle:', `"${newStyle}"`,
  320. '\nelement:', el
  321. );
  322. }
  323. return el;
  324. }
  325. Math.clamp = function (a, min, max) {
  326. return a < min ? min :
  327. a > max ? max : a;
  328. };
  329.  
  330. /**
  331. * @param targetElement
  332. * @param callback
  333. * @param options mutationObserver options{ childList: boolean, subtree: boolean, attributes: boolean, characterData: boolean }
  334. * @returns the mutationObserver object
  335. */
  336. function observe(targetElement, callback, options) {
  337. if (!targetElement) targetElement = document.body;
  338. if (!options) {
  339. options = {
  340. childList: true, subtree: true,
  341. attributes: false, characterData: false
  342. };
  343. }
  344. const mutationsHandler = function (mutations) {
  345. for (const mutation of mutations) {
  346. if (mutation.addedNodes.length) {
  347. callback(mutation.target);
  348. }
  349. callback();
  350. }
  351. };
  352. callback(targetElement);
  353. const mutationObserver = new MutationObserver(mutationsHandler);
  354. mutationObserver.observe(targetElement, options);
  355. return mutationObserver;
  356. }
  357.  
  358. function getGImgReverseSearchURL(url) {
  359. // console.debug('gImgReverseSearchURL=', gImgReverseSearchURL);
  360. return url ? GIMG_REVERSE_SEARCH_URL + encodeURIComponent(url.trim()) : "";
  361. }
  362.  
  363. unsafeWindow.nodeDepth = nodeDepth;
  364. /**
  365. * returns the number of nodes between the child and parent
  366. * @param child
  367. * @param parent the parent (direct or indirect) of the child element
  368. * @param currentDepth used for recursion only, do NOT modify it's value
  369. * @return {number}
  370. */
  371. function nodeDepth(child, parent = document, currentDepth = 0) {
  372. if (!child || !parent) throw "Both the child and parent must non-null.";
  373. if (!parent.contains(child)) throw "The given parent does not contain the child.";
  374.  
  375. currentDepth++;
  376. return child.parentNode == parent ?
  377. currentDepth :
  378. nodeDepth(child.parentNode, parent, currentDepth);
  379. }
  380.  
  381. /**Returns the href wrapped with proxy.DuckDuckGo.com */
  382. function reverseDdgProxy(href) {
  383. var s = href;
  384. if (isZscalarUrl(href)) s = getOGZscalarUrl(href); // extra functionality:
  385. if (isDdgUrl(href)) {
  386. s = new URL(location.href).searchParams.get('u');
  387. }
  388. // https://proxy.duckduckgo.com/iu/?u=
  389. if (s && s[0]) {
  390. return decodeURIComponent(s[0]);
  391. } else {
  392. console.log('Was unable to reverseDDGProxy for URL:', href);
  393. return s;
  394. }
  395. }
  396.  
  397. unsafeWindow.regexBetween = function (precedingRegEx, betweenRegEx, proceedingRegEx, regexOptions) {
  398. return new RegExp(`(?<=(${precedingRegEx}))(${!betweenRegEx ? ".+?" : betweenRegEx})(?=(${proceedingRegEx}))`, regexOptions);
  399. };
  400. unsafeWindow.extend = typeof($) == 'undefined' ? null : $.extend;
  401.  
  402. function preloader(imgUrls) {
  403. console.log('imgs passed:', imgUrls);
  404. let imgObjs = [];
  405. // start preloading
  406. for (const url of imgUrls) {
  407. // create object
  408. let imageObj = new Image();
  409. imageObj.src = url;
  410. imageObj.onload = (function () {
  411. console.log('ImageLoaded:', this.src, this);
  412. });
  413. imgObjs.push(imageObj);
  414. }
  415. }
  416.  
  417. // http://code.jquery.com/jquery.js
  418. function includeJs(src) {
  419. const script = document.createElement('script');
  420. script.setAttribute('src', src);
  421. document.getElementsByTagName('head')[0].appendChild(script);
  422. return script;
  423. }
  424.  
  425. //TODO: fix and test it
  426. /**@WIP
  427. * @param {function} elementGetter a function to get the wanted element (or event a condition function)
  428. * that will be called to test if the element has appeared yet. (should return true only when the element appears)
  429. * @param callback the elementGetter will be passed as the first argument
  430. * @return {MutationObserver}
  431. */
  432. function waitForElement(elementGetter, callback) {
  433. const observer = new MutationObserver(function (mutations, me) {
  434. // mutations.forEach(function (mutation) {
  435. // if (!mutation.addedNodes) return false;
  436.  
  437. function handleSuccess(node) {
  438. // console.debug('Wanted node found:', node);
  439. callback(node);
  440. me.disconnect();
  441. }
  442.  
  443. // for (let i = 0; i < mutation.addedNodes.length; i++) {
  444. // var node = mutation.addedNodes[i];
  445. var node = (typeof(elementGetter) === 'function') ? elementGetter() : q(elementGetter);
  446. try {
  447. if (node) {
  448. if (node.length !== undefined && node.length !== 0) {
  449. for (const n of node)
  450. handleSuccess(n);
  451. } else if (node.length === undefined) {
  452. handleSuccess(node);
  453. }
  454. }
  455. } catch (e) {
  456. console.warn(e);
  457. }
  458. // }
  459. // });
  460. });
  461.  
  462. observer.observe(document.body, {
  463. childList: true
  464. , subtree: true
  465. , attributes: false
  466. , characterData: false
  467. });
  468. return observer;
  469. // stop watching using: observer.disconnect();
  470. }
  471.  
  472. /**
  473. * cross-browser wheel delta
  474. * Returns the mousewheel scroll delta as -1 (wheelUp) or 1 (wheelDown) (cross-browser support)
  475. * @param {MouseWheelEvent} wheelEvent
  476. * @return {number} -1 or 1
  477. */
  478. unsafeWindow.getWheelDelta = function getWheelDelta(wheelEvent) {
  479. // cross-browser wheel delta
  480. wheelEvent = window.event || wheelEvent; // old IE support
  481. return Math.max(-1, Math.min(1, (wheelEvent.wheelDelta || -wheelEvent.detail)));
  482. };
  483. unsafeWindow.elementUnderMouse = function elementUnderMouse(wheelEvent) {
  484. return document.elementFromPoint(wheelEvent.clientX, wheelEvent.clientY);
  485. };
  486.  
  487. /** Create an element by typing it's inner HTML.
  488. For example: var myAnchor = createElement('<a href="https://example.com">Go to example.com</a>');
  489. * @param html
  490. * @param callback optional callback, invoked once the element is created, the element is passed.
  491. * @return {HTMLElement}
  492. */
  493. function createElement(html, callback) {
  494. const div = document.createElement('div');
  495. div.innerHTML = (html).trim();
  496. const element = div.firstElementChild;
  497. if (!!callback && callback.call)
  498. callback.call(null, element);
  499.  
  500. return element;
  501. }
  502. /* todo: remove, this thing is terrible and has no point */
  503. function matchSite(siteRegex) {
  504. let result = location.href.match(siteRegex);
  505. if (!!result) console.debug("Site matched regex: " + siteRegex);
  506. return result;
  507. }
  508. function siteSearchUrl(query) {
  509. if (query) {
  510. return gImgSearchURL + "site:" + encodeURIComponent(query.trim());
  511. }
  512. }
  513.  
  514. /**
  515. * removes all coded functionality to the element by removing it and reappending it's outerHTML
  516. */
  517. function clearElementFunctions(element) {
  518. const outerHTML = element.outerHTML;
  519. element.after(createElement(outerHTML));
  520. element.remove();
  521. }
  522. unsafeWindow.clearElementFunctions = clearElementFunctions;
  523.  
  524. /**abbreviation for querySelectorAll()
  525. * @param selector
  526. * @param node
  527. * @return {set<HTMLElement>} */
  528. function qa(selector, node = document) {
  529. return node.querySelectorAll(selector);
  530. }
  531. /**abbreviation for querySelector()
  532. * @param selector
  533. * @param node
  534. * @return {HTMLElement} */
  535. function q(selector, node = document) {
  536. return node.querySelector(selector);
  537. }
  538.  
  539. unsafeWindow.getIncrementedUrl = getIncrementedUrl;
  540. /** Returns a modified url as a string, either incremented(++) or decremented(--)
  541. * @param {string} href the input url you want to modify
  542. * @param {number} incrAmount (optional) the amount to increment.
  543. * Default: increment by 1 (++).
  544. * If the result in the url becomes negative, it will be overridden to 0.
  545. * @return {string} incremented/decremented url string */
  546. function getIncrementedUrl(href, incrAmount) {
  547. var e, s;
  548. let IB = incrAmount ? incrAmount : 1;
  549.  
  550. function isDigit(c) {
  551. return ("0" <= c && c <= "9")
  552. }
  553. var match = href.match(/&f=1$/);
  554. var tip = !!match ? match[0] : "";
  555. let L = href.replace(tip, "");
  556. let LL = L.length;
  557. for (e = LL - 1; e >= 0; --e) if (isDigit(L.charAt(e))) {
  558. for (s = e - 1; s >= 0; --s) if (!isDigit(L.charAt(s))) break;
  559. break;
  560. }
  561. ++s;
  562. if (e < 0) return;
  563. let oldNum = L.substring(s, e + 1);
  564. let newNum = (parseInt(oldNum, 10) + IB);
  565. if (newNum < 0) newNum = 0;
  566. let newNumStr = "" + newNum;
  567. while (newNumStr.length < oldNum.length)
  568. newNumStr = "0" + newNumStr;
  569.  
  570. return (L.substring(0, s) + "" + newNumStr + "" + L.slice(e + 1) + "" + tip);
  571. }
  572.  
  573. unsafeWindow.printElementTextAttributes = printElementTextAttributes;
  574. function printElementTextAttributes(el) {
  575. console.log(
  576. 'innerText:', el.innerText,
  577. '\nOuterText:', el.outerHTML,
  578. '\nInnerHTML:', el.innerHTML,
  579. '\nouterHTML:', el.outerHTML
  580. );
  581. }
  582. function isZscalarUrl(zscalarUrl) {
  583. return /https:\/\/zscaler\.kfupm\.edu\.sa\/Default\.aspx\?url=/.test(zscalarUrl);
  584. }
  585. /**
  586. * @param zscalarUrl {string}
  587. * @returns {string} the original link that ZScalar is blocking
  588. */
  589. function getOGZscalarUrl(zscalarUrl) {
  590. if (!isZscalarUrl(zscalarUrl)) {
  591. return zscalarUrl;
  592. } // not a zscalar url
  593. zscalarUrl = zscalarUrl.trim();
  594. let x = decodeURIComponent(('' + zscalarUrl).substring(46, zscalarUrl.indexOf('&referer')));
  595. // let x = decodeURIComponent(('' + zscalarUrl).substring(46, zscalarUrl.indexOf('&referer')));
  596. console.debug('Extracted ZScalar original link:', x);
  597. return x;
  598. }
  599.  
  600. /*function loadScript(url, callback) {
  601. let script = document.createElement("script");
  602. script.type = "text/javascript";
  603. if (script.readyState) { //IE
  604. script.onreadystatechange = function () {
  605. if (script.readyState === "loaded" ||
  606. script.readyState === "complete") {
  607. script.onreadystatechange = null;
  608. callback();
  609. }
  610. };
  611. } else { //Others
  612. script.onload = function () {
  613. callback();
  614. };
  615. }
  616. script.src = url;
  617. document.getElementsByTagName("head")[0].appendChild(script);
  618. }*/
  619. function loadScript(url, callback, type) {
  620. if (!callback) callback = () => console.log('Script laoded:', url);
  621. // Adding the script tag to the head as suggested before
  622. var head = document.getElementsByTagName('head')[0];
  623. var script = document.createElement('script');
  624. if (!type) type = 'text/javascript';
  625. script.type = type;
  626. script.src = url;
  627.  
  628. // Then bind the event to the callback function.
  629. // There are several events for cross browser compatibility.
  630. script.onreadystatechange = callback;
  631. script.onload = callback;
  632.  
  633. // Fire the loading
  634. head.appendChild(script);
  635. return script;
  636. }
  637.  
  638. unsafeWindow.loadModule = loadModule;
  639. unsafeWindow.getElementsWithText = getElementsWithText;
  640. window.document.getElementsWithText = getElementsWithText;
  641. /**
  642. * Iterates through all HTMLElements and returns the ones that contains innerText matching the given regex/substr
  643. * @param textRegex
  644. * @param preliminarySelector a node selector to narrow down the search from the start
  645. * @return {[]}
  646. */
  647. function getElementsWithText(textRegex, preliminarySelector) {
  648. if (!textRegex) {
  649. console.error('must have an input value');
  650. return;
  651. }
  652. const matchingEls = [];
  653. const useSubstring = (typeof textRegex === 'string');
  654. function elementContainsText(element, regex) {
  655. return !(regex && element && element.innerText) ? false :
  656. (useSubstring ? element.innerText.indexOf(regex) > -1 : element.innerText.match(textRegex));
  657. }
  658.  
  659. for (const el of document.querySelectorAll(preliminarySelector || '*')) {
  660. if (elementContainsText(el, textRegex)) {
  661. var nothingContainsEl = true;
  662. for (var i = 0; i < matchingEls.length; i++)
  663. if (matchingEls[i] && matchingEls[i].contains(el) && !Object.is(matchingEls[i], el)) {
  664. matchingEls[i] = null; // that old parentEl is now gone, we only want the deepest node
  665. nothingContainsEl = false;
  666. }
  667. matchingEls.push(el);
  668. }
  669. }
  670. return matchingEls.filter(el => el != null);
  671. }
  672.  
  673. function loadModule(url, callback) {
  674. return loadScript(url, callback, 'module');
  675. }
  676. /**
  677. * Creates and adds an attributeNode to the element (if the element doesn't have that attribute)
  678. * sets the attribute value
  679. * @param node
  680. * @param attributeName
  681. * @param attributeValue
  682. */
  683. function createAndAddAttribute(node, attributeName, attributeValue) {
  684. if (!node) {
  685. console.error('Node is null, cannot add attribute.');
  686. return;
  687. }
  688.  
  689. if (!node.hasAttribute(attributeName)) {
  690. var attr = document.createAttribute(attributeName);
  691. attr.value = attributeValue;
  692. node.setAttributeNode(attr);
  693. }
  694. if (!!attributeValue) {
  695. node.setAttribute(attributeName, attributeValue);
  696. }
  697. }
  698.  
  699. /** Deal with relative URIs (URIs starting with "/" or "//") */
  700. function getAbsoluteURI(inputUrl) {
  701. let reconstructedUri = inputUrl
  702. .replace(new RegExp(`^//`), `${location.protocol}//`) // If string starts with "//", replace with protocol
  703. .replace(new RegExp(`^/`), `${getHostname(location.href)}/`);// convert relative uri (precede with hostname if URI starts with "/")
  704.  
  705. // add protocol if one is not found
  706. if (!/^https?/.test(reconstructedUri))
  707. reconstructedUri = `https://${reconstructedUri}`;
  708.  
  709. return reconstructedUri;
  710. }
  711.  
  712. /**
  713. * Returns true if the event target is an input element (such as a textfield).
  714. * This is useful when you want to remap letter keys only when you are not typing in a text field :)
  715. * @param event
  716. * @return {boolean}
  717. */
  718. function targetIsInput(event) {
  719. const ignores = document.getElementsByTagName('input');
  720. const target = event.target;
  721. for (let ignore of ignores)
  722. if (target === ignore || ignore.contains(target)) {
  723. // console.log('The target recieving the keycode is of type "input", so it will not recieve your keystroke', target);
  724. return true;
  725. }
  726. return false;
  727. }
  728.  
  729. /** Calls the callback function, passing to it the width and height: "callback(w, h)"
  730. * @param url
  731. * @param callback callback(width, height, url, imgNode, args)
  732. * @param imgNode
  733. * @param args gets passed to the callback
  734. */
  735. function onLoadDim(url, callback, imgNode, args) {
  736. var img = new Image();
  737. if (!url) {
  738. console.warn('Url is invalid');
  739. return;
  740. }
  741. if (typeof url !== "string") {
  742. url = !!url.src ? url.src : url.href;
  743. }
  744.  
  745. if (typeof callback === 'function') {
  746. img.addEventListener('load', function () {
  747. callback(this.naturalWidth, this.naturalHeight, url, imgNode, args);
  748. });
  749. } else {
  750. console.error('onLoad() callback passed should be of type "function".');
  751. }
  752. img.src = url;
  753. }
  754.  
  755. /**@deprecated Opens the url via xmlhttpRequest, then performs a callback giving it the document element*/
  756. function xmlRequestElement(url, callback) {
  757. if (typeof callback !== 'function') console.error("The callback is not a function", callback);
  758. const req = new XMLHttpRequest();
  759. req.open('GET', url);
  760. req.send();
  761. req.onreadystatechange = function () {
  762. if (req.readyState === req.DONE) {
  763. const pageHTML = req.responseText;
  764. const doc = document.createElement('html');
  765. doc.innerHTML = pageHTML;
  766.  
  767. console.log('Recieved document for page ', url + ":", doc);
  768.  
  769. callback(doc, url);
  770. }
  771. };
  772. }
  773. unsafeWindow.fetchDoc = fetchDoc;
  774. function fetchDoc(url, callback) {
  775. fetch(url, {
  776. mode: 'no-cors',
  777. method: 'get'
  778. }
  779. ).then((res) => res.text())
  780. .then((text) => {
  781. var doc = document.createElement('html');
  782. doc.innerHTML = text;
  783. if (callback && typeof callback === 'function')
  784. callback(doc);
  785. });
  786. }
  787. /**Opens the url, then performs a callback giving it the document element
  788. * @param {string} url
  789. * @param {function} callback passes: (doc, url, args) to the callback function when the response is complete
  790. * @param {o} args Options object.
  791. * "args": Arguments to pass to the callback (Array or Object type)
  792. * @returns returns the callback result
  793. */
  794. function fetchElement(url, callback, args) {
  795. if (typeof callback !== 'function') console.error('Callback is not a function.!');
  796. fetch(url).then(
  797. response => response.text() // .json(), etc.
  798. // same as function(response) {return response.text();}
  799. ).then(function (html) {
  800. var doc = document.implementation.createHTMLDocument('');
  801. doc.open();
  802. doc.write(html);
  803. doc.close();
  804. try {
  805. return callback(doc, url, args);
  806. } catch (e) {
  807. console.error(e);
  808. return (html);
  809. }
  810. }
  811. );
  812. }
  813.  
  814. function isDdgUrl(url) {
  815. return /^https:\/\/proxy\.duckduckgo\.com/.test(url);
  816. }
  817.  
  818. function unicodeToChar(text) {
  819. return text.replace(/\\u[\dA-F]{4}/gim,
  820. match => String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16)));
  821. }
  822.  
  823.  
  824. // don't make public because it conflicts with DDGP Unblocker script
  825.  
  826. /** A class containing static functions to manipulate the srcset attribute */
  827. unsafeWindow.SrcSet = class SrcSet {
  828. /**
  829. * @author https://github.com/sindresorhus/srcset/
  830. * @param arr
  831. * @return {*}
  832. */
  833. static deepUnique(arr) {
  834. return arr.sort().filter(function (el, i) {
  835. return JSON.stringify(el) !== JSON.stringify(arr[i - 1]);
  836. });
  837. }
  838. /**
  839. * @author https://github.com/sindresorhus/srcset/
  840. * @param str
  841. * @return {*}
  842. */
  843. static parse(str) {
  844. return this.deepUnique(str.split(',').map(function (el) {
  845. var ret = {};
  846.  
  847. el.trim().split(/\s+/).forEach(function (el, i) {
  848. if (i === 0) {
  849. return ret.url = el;
  850. }
  851.  
  852. var value = el.substring(0, el.length - 1);
  853. var postfix = el[el.length - 1];
  854. var intVal = parseInt(value, 10);
  855. var floatVal = parseFloat(value);
  856.  
  857. if (postfix === 'w' && /^[\d]+$/.test(value)) {
  858. ret.width = intVal;
  859. } else if (postfix === 'h' && /^[\d]+$/.test(value)) {
  860. ret.height = intVal;
  861. } else if (postfix === 'x' && !isNaN(floatVal)) {
  862. ret.density = floatVal;
  863. } else {
  864. throw new Error('Invalid srcset descriptor: ' + el + '.');
  865. }
  866. });
  867.  
  868. return ret;
  869. }));
  870. }
  871. static stringify(arr) {
  872. return (arr.map(function (el) {
  873. if (!el.url) {
  874. throw new Error('URL is required.');
  875. }
  876.  
  877. var ret = [el.url];
  878.  
  879. if (el.width) {
  880. ret.push(el.width + 'w');
  881. }
  882.  
  883. if (el.height) {
  884. ret.push(el.height + 'h');
  885. }
  886.  
  887. if (el.density) {
  888. ret.push(el.density + 'x');
  889. }
  890.  
  891. return ret.join(' ');
  892. })).join(', ');
  893. }
  894. };
  895.  
  896. unsafeWindow.cookieUtils = {
  897. setCookie: function setCookie(name,value,days) {
  898. var expires = "";
  899. if (days) {
  900. var date = new Date();
  901. date.setTime(date.getTime() + (days*24*60*60*1000));
  902. expires = "; expires=" + date.toUTCString();
  903. }
  904. document.cookie = name + "=" + (value || "") + expires + "; path=/";
  905. return document.cookie;
  906. },
  907. getCookie: function getCookie(name) {
  908. var nameEQ = name + "=";
  909. var ca = document.cookie.split(';');
  910. for(var i=0;i < ca.length;i++) {
  911. var c = ca[i];
  912. while (c.charAt(0)==' ') c = c.substring(1,c.length);
  913. if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
  914. }
  915. return null;
  916. },
  917. eraseCookie: function eraseCookie(name) {
  918. document.cookie = name+'=; Max-Age=-99999999;';
  919. return document.cookie;
  920. }
  921. };
  922.  
  923. unsafeWindow.url2location = url2location;
  924. /**
  925. * Retrieves an object with parsed URL data
  926. * @param url
  927. * @return {
  928. * {port: string,
  929. * protocol: string,
  930. * href: string,
  931. * origin: string,
  932. * pathname: string,
  933. * hash: string,
  934. * search: string}
  935. * }
  936. */
  937. function url2location(url) {
  938. const a = document.createElement("a");
  939. a.href = url;
  940. return {
  941. port: a.port,
  942. protocol: a.protocol,
  943. href: a.href,
  944. origin: a.origin,
  945. pathname: a.pathname,
  946. hash: a.hash,
  947. search: a.search
  948. }
  949. }
  950. /** @param href
  951. * @param keepPrefix defaults to false
  952. * www.example.com
  953. * if true: example.com
  954. * if false: www.example.com
  955. * @returns {string} Returns the hostname of the site URL */
  956. function getHostname(href, keepPrefix) {
  957. const a = document.createElement("a");
  958. a.href = href;
  959. // if (keepPrefix) console.debug("getHostname href =", href);
  960. return a.hostname;
  961. }
  962. /** Inclomplete
  963. * @type {boolean} */
  964. unsafeWindow.freezeGif = freezeGif;
  965. function freezeGif(img, unfreeze) {
  966. function createElementAndCallback(type, callback) {
  967. const element = document.createElement(type);
  968. callback(element);
  969. return element;
  970. }
  971. var width = img.width,
  972. height = img.height,
  973. canvas = createElementAndCallback('canvas', function (clone) {
  974. clone.width = width;
  975. clone.height = height;
  976. }),
  977. attr,
  978. i = 0;
  979.  
  980. var freeze = function () {
  981. canvas.getContext('2d').drawImage(img, 0, 0, width, height);
  982.  
  983. for (i = 0; i < img.attributes.length; i++) {
  984. attr = img.attributes[i];
  985.  
  986. if (attr.name !== '"') { // test for invalid attributes
  987. canvas.setAttribute(attr.name, attr.value);
  988. }
  989. }
  990. canvas.classList.add('freeze-gif');
  991. canvas.style.position = 'absolute';
  992.  
  993. img.parentNode.insertBefore(canvas, img);
  994. img.style.visibility = 'hidden';
  995. // img.style.opacity = 0;
  996. };
  997.  
  998. var unfreezeGif = function () {
  999. console.log('unfreezing', img);
  1000. const freezeCanvas = img.closest('.freeze-gif');
  1001.  
  1002. if (!freezeCanvas) {
  1003. console.error('Couldn\'t find freezeCanvas while unfreezing this gif:', img);
  1004. } else {
  1005. freezeCanvas.style.visibility = 'hidden';
  1006. }
  1007. // img.style.opacity = 100;
  1008. img.style.visibility = 'visible';
  1009. };
  1010.  
  1011. if (unfreeze) {
  1012. unfreezeGif();
  1013. } else {
  1014. if (img.complete) {
  1015. freeze();
  1016. } else {
  1017. img.addEventListener('load', freeze, true);
  1018. }
  1019. }
  1020. }
  1021. function getIframeDoc(iframe) {
  1022. return iframe.contentDocument || iframe.contentWindow ? iframe.contentWindow.document : null;
  1023. }
  1024.  
  1025. unsafeWindow.removeClickListeners = removeClickListeners;
  1026. function removeClickListeners(selector) {
  1027. if (!!unsafeWindow.$) {
  1028. unsafeWindow.$(!selector ? "*" : selector)
  1029. .unbind("click")
  1030. .off("click")
  1031. .removeAttr("onclick");
  1032. } else {
  1033. console.warn('unsafeWindow.$ is not defined');
  1034. }
  1035. }
  1036. /**
  1037. * @WIP TODO: Complete this function
  1038. */
  1039. function removeEventListeners(eventTarget) {
  1040. var eventType = "click";
  1041. eventTarget = window;
  1042.  
  1043. for (const eventType in window.getEventListeners(eventTarget)) {
  1044. const eventListeners = getEventListeners(eventTarget);
  1045. if (eventListeners.hasOwnProperty(eventType))
  1046. for (const o of eventListeners[eventType]) {
  1047. console.log('before:', o);
  1048. o.listener = null;
  1049. console.log('after:', o);
  1050. }
  1051. }
  1052.  
  1053.  
  1054. // noinspection JSUnresolvedFunction
  1055. let listeners = eventTarget.getEventListeners(eventTarget);
  1056. listeners.forEach(function (listener) {
  1057. console.log('removing listener:', listener);
  1058. eventTarget.removeEventListener("click", listener, false);
  1059. });
  1060. }
  1061.  
  1062. unsafeWindow.removeDoubleSpaces = removeDoubleSpaces;
  1063. unsafeWindow.cleanGibberish = cleanGibberish;
  1064. unsafeWindow.isBase64ImageData = isBase64ImageData;
  1065. unsafeWindow.cleanDates = cleanDates;
  1066.  
  1067. unsafeWindow.downloadScripts = function downloadScripts() {
  1068. var scriptUrls = Array.from(document.querySelectorAll('script'))
  1069. .map(script => script.src ? script.src : window.URL.createObjectURL(new Blob([script.innerHTML], {type: 'text/plain'}))
  1070. );
  1071. zipFiles(scriptUrls);
  1072. };
  1073.  
  1074. unsafeWindow.escapeEncodedChars = escapeEncodedChars;
  1075. /** Escapes Unicode chars, and anything starting with \x, \u,
  1076. * @param text
  1077. * @return {*} */
  1078. function escapeEncodedChars(text) {
  1079. return text
  1080. .replace(/\\[u][\dA-F]{4}/gim, match => String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16)))
  1081. .replace(/\\[x][\dA-F]{2}/gim, match => String.fromCharCode(parseInt(match.replace(/\\x/g, ''), 16)));
  1082. }
  1083. /**
  1084. * Returns true if the string is an image data string
  1085. * @param str
  1086. * @returns {boolean}
  1087. */
  1088. function isBase64ImageData(str) {
  1089. return /^data:image\/.{1,5};base64/.test(str);
  1090. }
  1091. function removeDoubleSpaces(str) {
  1092. return !!str ? str.replace(/(\s{2,})/g, " ") : str;
  1093. }
  1094.  
  1095. function cleanDates(str) {
  1096. return !!str ? removeDoubleSpaces(str.replace(/\d*\.([^.]+)\.\d*/g, ' ')) : str;
  1097. }
  1098. function cleanGibberish(str, minWgr, debug=false) {
  1099. if (str) {
  1100. const gibberishRegex = /(\W{2,})|(\d{3,})|(\d+\w{1,5}\d+){2,}/g;
  1101. let noGibberish = removeDoubleSpaces(str.replace(gibberishRegex, " ")),
  1102. /**
  1103. * The minimum word2gibberish ratio to exit the loop
  1104. * @type {number|*}
  1105. */
  1106. minWgr = 0.4 || minWgr;
  1107. if (noGibberish.length < 3) return str;
  1108. /**
  1109. * WGR: Word to Gibberish Ratio (between 0 and 1)
  1110. * 0: No gibberish (Good)
  1111. * 1: 100% Gibberish (Bad)
  1112. * @type {number}
  1113. */
  1114. let wgr = (str.length - noGibberish.length) / str.length;
  1115. if(debug) console.debug(
  1116. 'cleanGibberish(' + str + ')' +
  1117. '\nOriginal:', str,
  1118. '\nNoGibberish:', noGibberish,
  1119. '\nRatio:', wgr
  1120. );
  1121.  
  1122. return wgr > minWgr ?
  1123. cleanGibberish(noGibberish, minWgr) :
  1124. (str.length > 3 ? str : "");
  1125. }
  1126. return "";
  1127. }
  1128. var getCssImage = (element) => !element ? null : element.style["background-image"].replace(/(['"]?\)$)|(^url\(["']?)/g, '');
  1129. unsafeWindow.getCssImages = () => Array.from(document.querySelectorAll('[style*="background-image"]')).map(getCssImage);
  1130.  
  1131. unsafeWindow.observeDocument = function observeDocument(callback, options) {
  1132. callback(document.body);
  1133. options = typeof(extend)!=='function'? {}: extend(options, {
  1134. singleCallbackPerMutation: false
  1135. });
  1136. new MutationObserver(
  1137. /** @param mutations */
  1138. function mutationCallback(mutations) {
  1139. for (const mutation of mutations) {
  1140. if (!mutation.addedNodes.length)
  1141. continue;
  1142. callback(mutation.target);
  1143. if (options.singleCallbackPerMutation === true) {
  1144. break;
  1145. }
  1146. }
  1147. }
  1148. ).observe(document.body, {
  1149. childList: true,
  1150. subtree: true,
  1151. attributes: true,
  1152. characterData: false
  1153. }
  1154. );
  1155. };
  1156. unsafeWindow.observeIframe = function observeIframe(iframe, observerInit, observerOptions, args) {
  1157. // older browsers don't get responsive iframe height, for now
  1158. if (!window.MutationObserver) return;
  1159. console.debug('Attaching an iframe observer...', iframe, '\n\n');
  1160. var iframeObserver = new MutationObserver(function (mutations, observer) {
  1161. console.debug(
  1162. 'Observed mutation in iframe:', iframe,
  1163. '\nmutations:', mutations
  1164. );
  1165. observerInit(mutations, observer, args);
  1166. });
  1167.  
  1168. var interval = setInterval(function () {
  1169. if (iframe.contentWindow && iframe.contentWindow.document) {
  1170. iframeObserver.observe(iframe.contentWindow.document, observerOptions || {
  1171. attributes: true,
  1172. subtree: true,
  1173. childList: true,
  1174. characterData: true
  1175. });
  1176. console.log('Successfully added observer to iframe!', iframe);
  1177.  
  1178. clearInterval(interval);
  1179. }
  1180. }, 100);
  1181. };
  1182.  
  1183. unsafeWindow.observeAllFrames = function observeAllFrames(callback) {
  1184. callback(document.body);
  1185. callback(document);
  1186. let mutationObserver = new MutationObserver(function (mutations) {
  1187. for (const mutation of mutations) {
  1188. if (mutation.addedNodes.length) {
  1189. callback(mutation.target);
  1190. }
  1191. }
  1192. });
  1193. const mutationOptions = {
  1194. childList: true, subtree: true,
  1195. attributes: true, characterData: true
  1196. };
  1197. mutationObserver.observe(document, mutationOptions);
  1198. for (const iframe of document.querySelectorAll('iframe')) {
  1199. callback(iframe.body);
  1200. mutationObserver.observe(iframe, mutationOptions);
  1201. }
  1202. };
  1203.  
  1204. /**
  1205. * @param {function} condition
  1206. * @param {function} action
  1207. * @param {number} interval
  1208. */
  1209. function waitFor(condition, action, interval) {
  1210. if (typeof condition === 'undefined') {
  1211. console.error('"condition" should be a function type:', condition);
  1212. return false;
  1213. }
  1214. if (typeof action === 'undefined') {
  1215. console.error('"condition" should be a function type:', action);
  1216. return false;
  1217. }
  1218. if (!interval) interval = 50;
  1219.  
  1220. var checkExist = setInterval(function () {
  1221. if (condition) {
  1222. console.log("Exists!");
  1223. clearInterval(checkExist);
  1224. action();
  1225. }
  1226. }, interval);
  1227. }
  1228.  
  1229. function downloadUsingXmlhttpRequest(url, opts) {
  1230. var imgUrl = url || "http://static.jsbin.com/images/dave.min.svg?4.1.4";
  1231. GM_xmlhttpRequest({
  1232. method: 'GET',
  1233. url: imgUrl,
  1234. onload: function (respDetails) {
  1235. var binResp = customBase64Encode(respDetails.responseText);
  1236.  
  1237. /*-- Here, we just demo that we have a valid base64 encoding
  1238. by inserting the image into the page.
  1239. We could just as easily AJAX-off the data instead.
  1240. */
  1241. var zImgPara = document.createElement('p');
  1242. var zTargetNode = document.querySelector("body *"); //1st child
  1243.  
  1244. zImgPara.innerHTML = 'Image: <img src="data:image/png;base64,'
  1245. + binResp + '">';
  1246. zTargetNode.parentNode.insertBefore(zImgPara, zTargetNode);
  1247. },
  1248. overrideMimeType: 'text/plain; charset=x-user-defined'
  1249. });
  1250.  
  1251.  
  1252. function customBase64Encode(inputStr) {
  1253. var
  1254. bbLen = 3,
  1255. enCharLen = 4,
  1256. inpLen = inputStr.length,
  1257. inx = 0,
  1258. jnx,
  1259. keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
  1260. + "0123456789+/=",
  1261. output = "",
  1262. paddingBytes = 0;
  1263. var
  1264. bytebuffer = new Array(bbLen),
  1265. encodedCharIndexes = new Array(enCharLen);
  1266.  
  1267. while (inx < inpLen) {
  1268. for (jnx = 0; jnx < bbLen; ++jnx) {
  1269. /*--- Throw away high-order byte, as documented at:
  1270. https://developer.mozilla.org/En/Using_XMLHttpRequest#Handling_binary_data
  1271. */
  1272. if (inx < inpLen) {
  1273. bytebuffer[jnx] = inputStr.charCodeAt(inx++) & 0xff;
  1274. } else {
  1275. bytebuffer[jnx] = 0;
  1276. }
  1277. }
  1278.  
  1279. /*--- Get each encoded character, 6 bits at a time.
  1280. index 0: first 6 bits
  1281. index 1: second 6 bits
  1282. (2 least significant bits from inputStr byte 1
  1283. + 4 most significant bits from byte 2)
  1284. index 2: third 6 bits
  1285. (4 least significant bits from inputStr byte 2
  1286. + 2 most significant bits from byte 3)
  1287. index 3: forth 6 bits (6 least significant bits from inputStr byte 3)
  1288. */
  1289. encodedCharIndexes[0] = bytebuffer[0] >> 2;
  1290. encodedCharIndexes[1] = ((bytebuffer[0] & 0x3) << 4) | (bytebuffer[1] >> 4);
  1291. encodedCharIndexes[2] = ((bytebuffer[1] & 0x0f) << 2) | (bytebuffer[2] >> 6);
  1292. encodedCharIndexes[3] = bytebuffer[2] & 0x3f;
  1293.  
  1294. //--- Determine whether padding happened, and adjust accordingly.
  1295. paddingBytes = inx - (inpLen - 1);
  1296. switch (paddingBytes) {
  1297. case 1:
  1298. // Set last character to padding char
  1299. encodedCharIndexes[3] = 64;
  1300. break;
  1301. case 2:
  1302. // Set last 2 characters to padding char
  1303. encodedCharIndexes[3] = 64;
  1304. encodedCharIndexes[2] = 64;
  1305. break;
  1306. default:
  1307. break; // No padding - proceed
  1308. }
  1309.  
  1310. /*--- Now grab each appropriate character out of our keystring,
  1311. based on our index array and append it to the output string.
  1312. */
  1313. for (jnx = 0; jnx < enCharLen; ++jnx)
  1314. output += keyStr.charAt(encodedCharIndexes[jnx]);
  1315. }
  1316. return output;
  1317. }
  1318. }
  1319.  
  1320. unsafeWindow.iterateOverURLPattern = function iterateOverURLPattern(inputURL) {
  1321. inputURL = inputURL || '';
  1322.  
  1323. var bracketsContent = inputURL.match(/\[.+]/);
  1324. if (bracketsContent.length)
  1325. var [start, end] = bracketsContent[0].replace(/[\[\]]/g, '').split('-');
  1326.  
  1327. console.debug(
  1328. 'text in brackets:', bracketsContent,
  1329. '\nstart, end:', `["${start}", "${end}"]`
  1330. );
  1331.  
  1332. let urls = [];
  1333. for (var i = start; i <= end; i++) {
  1334. var newNum = "" + i;
  1335. while (newNum.length < start.length) newNum = "0" + newNum;
  1336. urls.push(inputURL.replace(/\[.+]/, newNum));
  1337. }
  1338. return urls;
  1339. };
  1340.  
  1341. /*
  1342. CSS for top navbars:
  1343.  
  1344. .fixed-position {
  1345. position: fixed;
  1346. top: 0px;
  1347. z-index: 16777271;
  1348. }
  1349.  
  1350. */
  1351.  
  1352.  
  1353. /*global self */
  1354.  
  1355. /*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */
  1356. /*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/src/FileSaver.js */
  1357. /* FileSaver.js
  1358. * A saveAs() FileSaver implementation.
  1359. * 1.3.8
  1360. * 2018-03-22 14:03:47
  1361. *
  1362. * By Eli Grey, https://eligrey.com
  1363. * License: MIT
  1364. * See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md
  1365. *
  1366. */
  1367. // noinspection ThisExpressionReferencesGlobalObjectJS
  1368. var saveAs = saveAs || (function (view) {
  1369. "use strict";
  1370. // IE <10 is explicitly unsupported
  1371. if (typeof view === "undefined" || typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) {
  1372. return;
  1373. }
  1374. var
  1375. doc = view.document
  1376. // only get URL when necessary in case Blob.js hasn't overridden it yet
  1377. ,
  1378. get_URL = function () {
  1379. return view.URL || view.webkitURL || view;
  1380. },
  1381. save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a"),
  1382. can_use_save_link = "download" in save_link,
  1383. click = function (node) {
  1384. var event = new MouseEvent("click");
  1385. node.dispatchEvent(event);
  1386. },
  1387. is_safari = /constructor/i.test(view.HTMLElement) || view.safari,
  1388. is_chrome_ios = /CriOS\/[\d]+/.test(navigator.userAgent),
  1389. setImmediate = view.setImmediate || view.setTimeout,
  1390. throw_outside = function (ex) {
  1391. setImmediate(function () {
  1392. throw ex;
  1393. }, 0);
  1394. },
  1395. force_saveable_type = "application/octet-stream"
  1396. // the Blob API is fundamentally broken as there is no "downloadfinished" event to subscribe to
  1397. ,
  1398. arbitrary_revoke_timeout = 1000 * 40 // in ms
  1399. ,
  1400. revoke = function (file) {
  1401. var revoker = function () {
  1402. if (typeof file === "string") { // file is an object URL
  1403. get_URL().revokeObjectURL(file);
  1404. } else { // file is a File
  1405. file.remove();
  1406. }
  1407. };
  1408. setTimeout(revoker, arbitrary_revoke_timeout);
  1409. },
  1410. dispatch = function (filesaver, event_types, event) {
  1411. event_types = [].concat(event_types);
  1412. var i = event_types.length;
  1413. while (i--) {
  1414. var listener = filesaver["on" + event_types[i]];
  1415. if (typeof listener === "function") {
  1416. try {
  1417. listener.call(filesaver, event || filesaver);
  1418. } catch (ex) {
  1419. throw_outside(ex);
  1420. }
  1421. }
  1422. }
  1423. },
  1424. auto_bom = function (blob) {
  1425. // prepend BOM for UTF-8 XML and text/* types (including HTML)
  1426. // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF
  1427. if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
  1428. return new Blob([String.fromCharCode(0xFEFF), blob], {
  1429. type: blob.type
  1430. });
  1431. }
  1432. return blob;
  1433. },
  1434. FileSaver = function (blob, name, no_auto_bom) {
  1435. if (!no_auto_bom) {
  1436. blob = auto_bom(blob);
  1437. }
  1438. // First try a.download, then web filesystem, then object URLs
  1439. var
  1440. filesaver = this,
  1441. type = blob.type,
  1442. force = type === force_saveable_type,
  1443. object_url, dispatch_all = function () {
  1444. dispatch(filesaver, "writestart progress write writeend".split(" "));
  1445. }
  1446. // on any filesys errors revert to saving with object URLs
  1447. ,
  1448. fs_error = function () {
  1449. if ((is_chrome_ios || (force && is_safari)) && view.FileReader) {
  1450. // Safari doesn't allow downloading of blob urls
  1451. var reader = new FileReader();
  1452. reader.onloadend = function () {
  1453. var url = is_chrome_ios ? reader.result : reader.result.replace(/^data:[^;]*;/, 'data:attachment/file;');
  1454. var popup = view.open(url, '_blank');
  1455. if (!popup) view.location.href = url;
  1456. url = undefined; // release reference before dispatching
  1457. filesaver.readyState = filesaver.DONE;
  1458. dispatch_all();
  1459. };
  1460. reader.readAsDataURL(blob);
  1461. filesaver.readyState = filesaver.INIT;
  1462. return;
  1463. }
  1464. // don't create more object URLs than needed
  1465. if (!object_url) {
  1466. object_url = get_URL().createObjectURL(blob);
  1467. }
  1468. if (force) {
  1469. view.location.href = object_url;
  1470. } else {
  1471. var opened = view.open(object_url, "_blank");
  1472. if (!opened) {
  1473. // Apple does not allow window.open, see https://developer.apple.com/library/safari/documentation/Tools/Conceptual/SafariExtensionGuide/WorkingwithWindowsandTabs/WorkingwithWindowsandTabs.html
  1474. view.location.href = object_url;
  1475. }
  1476. }
  1477. filesaver.readyState = filesaver.DONE;
  1478. dispatch_all();
  1479. revoke(object_url);
  1480. };
  1481. filesaver.readyState = filesaver.INIT;
  1482.  
  1483. if (can_use_save_link) {
  1484. object_url = get_URL().createObjectURL(blob);
  1485. setImmediate(function () {
  1486. save_link.href = object_url;
  1487. save_link.download = name;
  1488. click(save_link);
  1489. dispatch_all();
  1490. revoke(object_url);
  1491. filesaver.readyState = filesaver.DONE;
  1492. }, 0);
  1493. return;
  1494. }
  1495.  
  1496. fs_error();
  1497. },
  1498. FS_proto = FileSaver.prototype,
  1499. saveAs = function (blob, name, no_auto_bom) {
  1500. return new FileSaver(blob, name || blob.name || "download", no_auto_bom);
  1501. };
  1502.  
  1503. // IE 10+ (native saveAs)
  1504. if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) {
  1505. return function (blob, name, no_auto_bom) {
  1506. name = name || blob.name || "download";
  1507.  
  1508. if (!no_auto_bom) {
  1509. blob = auto_bom(blob);
  1510. }
  1511. return navigator.msSaveOrOpenBlob(blob, name);
  1512. };
  1513. }
  1514.  
  1515. // todo: detect chrome extensions & packaged apps
  1516. //save_link.target = "_blank";
  1517.  
  1518. FS_proto.abort = function () {
  1519. };
  1520. FS_proto.readyState = FS_proto.INIT = 0;
  1521. FS_proto.WRITING = 1;
  1522. FS_proto.DONE = 2;
  1523.  
  1524. FS_proto.error =
  1525. FS_proto.onwritestart =
  1526. FS_proto.onprogress =
  1527. FS_proto.onwrite =
  1528. FS_proto.onabort =
  1529. FS_proto.onerror =
  1530. FS_proto.onwriteend =
  1531. null;
  1532.  
  1533. return saveAs;
  1534. }(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this));
  1535. /*`self` is undefined in Firefox for Android content script context
  1536. while `this` is nsIContentFrameMessageManager
  1537. with an attribute `content` that corresponds to the window*/
  1538. if (typeof module !== "undefined" && module.exports) {
  1539. module.exports.saveAs = saveAs;
  1540. } else if ((typeof define !== "undefined" && define !== null) && (define.amd !== null)) {
  1541. define([], function () {
  1542. return saveAs;
  1543. });
  1544. }
  1545. /* Example:
  1546. * var blob = new Blob(["Hello, world!"], {type: "text/plain;charset=utf-8"});
  1547. * saveAs(blob, "hello world.txt");
  1548. */
  1549. unsafeWindow.saveAs = saveAs;
  1550.  
  1551.  
  1552. /* mousetrap v1.6.2 craig.is/killing/mice */
  1553. /*global define:false */
  1554. /**
  1555. * Copyright 2012-2017 Craig Campbell
  1556. *
  1557. * Licensed under the Apache License, Version 2.0 (the "License");
  1558. * you may not use this file except in compliance with the License.
  1559. * You may obtain a copy of the License at
  1560. *
  1561. * http://www.apache.org/licenses/LICENSE-2.0
  1562. *
  1563. * Unless required by applicable law or agreed to in writing, software
  1564. * distributed under the License is distributed on an "AS IS" BASIS,
  1565. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  1566. * See the License for the specific language governing permissions and
  1567. * limitations under the License.
  1568. *
  1569. * Mousetrap is a simple keyboard shortcut library for Javascript with
  1570. * no external dependencies
  1571. *
  1572. * @version 1.6.2
  1573. * @url craig.is/killing/mice
  1574. */
  1575. (function (window, document, undefined) {
  1576.  
  1577. // Check if mousetrap is used inside browser, if not, return
  1578. if (!window) {
  1579. return;
  1580. }
  1581.  
  1582. /**
  1583. * mapping of special keycodes to their corresponding keys
  1584. *
  1585. * everything in this dictionary cannot use keypress events
  1586. * so it has to be here to map to the correct keycodes for
  1587. * keyup/keydown events
  1588. *
  1589. * @type {Object}
  1590. */
  1591. var _MAP = {
  1592. 8: 'backspace',
  1593. 9: 'tab',
  1594. 13: 'enter',
  1595. 16: 'shift',
  1596. 17: 'ctrl',
  1597. 18: 'alt',
  1598. 20: 'capslock',
  1599. 27: 'esc',
  1600. 32: 'space',
  1601. 33: 'pageup',
  1602. 34: 'pagedown',
  1603. 35: 'end',
  1604. 36: 'home',
  1605. 37: 'left',
  1606. 38: 'up',
  1607. 39: 'right',
  1608. 40: 'down',
  1609. 45: 'ins',
  1610. 46: 'del',
  1611. 91: 'meta',
  1612. 93: 'meta',
  1613. 224: 'meta'
  1614. };
  1615.  
  1616. /**
  1617. * mapping for special characters so they can support
  1618. *
  1619. * this dictionary is only used incase you want to bind a
  1620. * keyup or keydown event to one of these keys
  1621. *
  1622. * @type {Object}
  1623. */
  1624. var _KEYCODE_MAP = {
  1625. 106: '*',
  1626. 107: '+',
  1627. 109: '-',
  1628. 110: '.',
  1629. 111: '/',
  1630. 186: ';',
  1631. 187: '=',
  1632. 188: ',',
  1633. 189: '-',
  1634. 190: '.',
  1635. 191: '/',
  1636. 192: '`',
  1637. 219: '[',
  1638. 220: '\\',
  1639. 221: ']',
  1640. 222: '\''
  1641. };
  1642.  
  1643. /**
  1644. * this is a mapping of keys that require shift on a US keypad
  1645. * back to the non shift equivelents
  1646. *
  1647. * this is so you can use keyup events with these keys
  1648. *
  1649. * note that this will only work reliably on US keyboards
  1650. *
  1651. * @type {Object}
  1652. */
  1653. var _SHIFT_MAP = {
  1654. '~': '`',
  1655. '!': '1',
  1656. '@': '2',
  1657. '#': '3',
  1658. '$': '4',
  1659. '%': '5',
  1660. '^': '6',
  1661. '&': '7',
  1662. '*': '8',
  1663. '(': '9',
  1664. ')': '0',
  1665. '_': '-',
  1666. '+': '=',
  1667. ':': ';',
  1668. '\"': '\'',
  1669. '<': ',',
  1670. '>': '.',
  1671. '?': '/',
  1672. '|': '\\'
  1673. };
  1674.  
  1675. /**
  1676. * this is a list of special strings you can use to map
  1677. * to modifier keys when you specify your keyboard shortcuts
  1678. *
  1679. * @type {Object}
  1680. */
  1681. var _SPECIAL_ALIASES = {
  1682. 'option': 'alt',
  1683. 'command': 'meta',
  1684. 'return': 'enter',
  1685. 'escape': 'esc',
  1686. 'plus': '+',
  1687. 'mod': /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl'
  1688. };
  1689.  
  1690. /**
  1691. * variable to store the flipped version of _MAP from above
  1692. * needed to check if we should use keypress or not when no action
  1693. * is specified
  1694. *
  1695. * @type {Object|undefined}
  1696. */
  1697. var _REVERSE_MAP;
  1698.  
  1699. /**
  1700. * loop through the f keys, f1 to f19 and add them to the map
  1701. * programatically
  1702. */
  1703. for (var i = 1; i < 20; ++i) {
  1704. _MAP[111 + i] = 'f' + i;
  1705. }
  1706.  
  1707. /**
  1708. * loop through to map numbers on the numeric keypad
  1709. */
  1710. for (i = 0; i <= 9; ++i) {
  1711.  
  1712. // This needs to use a string cause otherwise since 0 is falsey
  1713. // mousetrap will never fire for numpad 0 pressed as part of a keydown
  1714. // event.
  1715. //
  1716. // @see https://github.com/ccampbell/mousetrap/pull/258
  1717. _MAP[i + 96] = i.toString();
  1718. }
  1719.  
  1720. /**
  1721. * cross browser add event method
  1722. *
  1723. * @param {Element|HTMLDocument} object
  1724. * @param {string} type
  1725. * @param {Function} callback
  1726. * @returns void
  1727. */
  1728. function _addEvent(object, type, callback) {
  1729. if (object.addEventListener) {
  1730. object.addEventListener(type, callback, false);
  1731. return;
  1732. }
  1733.  
  1734. try {
  1735. object.attachEvent('on' + type, callback);
  1736. } catch (e) {
  1737. console.error(e);
  1738. }
  1739. }
  1740.  
  1741. /**
  1742. * takes the event and returns the key character
  1743. *
  1744. * @param {Event} e
  1745. * @return {string}
  1746. */
  1747. function _characterFromEvent(e) {
  1748.  
  1749. // for keypress events we should return the character as is
  1750. if (e.type == 'keypress') {
  1751. var character = String.fromCharCode(e.which);
  1752.  
  1753. // if the shift key is not pressed then it is safe to assume
  1754. // that we want the character to be lowercase. this means if
  1755. // you accidentally have caps lock on then your key bindings
  1756. // will continue to work
  1757. //
  1758. // the only side effect that might not be desired is if you
  1759. // bind something like 'A' cause you want to trigger an
  1760. // event when capital A is pressed caps lock will no longer
  1761. // trigger the event. shift+a will though.
  1762. if (!e.shiftKey) {
  1763. character = character.toLowerCase();
  1764. }
  1765.  
  1766. return character;
  1767. }
  1768.  
  1769. // for non keypress events the special maps are needed
  1770. if (_MAP[e.which]) {
  1771. return _MAP[e.which];
  1772. }
  1773.  
  1774. if (_KEYCODE_MAP[e.which]) {
  1775. return _KEYCODE_MAP[e.which];
  1776. }
  1777.  
  1778. // if it is not in the special map
  1779.  
  1780. // with keydown and keyup events the character seems to always
  1781. // come in as an uppercase character whether you are pressing shift
  1782. // or not. we should make sure it is always lowercase for comparisons
  1783. return String.fromCharCode(e.which).toLowerCase();
  1784. }
  1785.  
  1786. /**
  1787. * checks if two arrays are equal
  1788. *
  1789. * @param {Array} modifiers1
  1790. * @param {Array} modifiers2
  1791. * @returns {boolean}
  1792. */
  1793. function _modifiersMatch(modifiers1, modifiers2) {
  1794. return modifiers1.sort().join(',') === modifiers2.sort().join(',');
  1795. }
  1796.  
  1797. /**
  1798. * takes a key event and figures out what the modifiers are
  1799. *
  1800. * @param {Event} e
  1801. * @returns {Array}
  1802. */
  1803. function _eventModifiers(e) {
  1804. var modifiers = [];
  1805.  
  1806. if (e.shiftKey) {
  1807. modifiers.push('shift');
  1808. }
  1809.  
  1810. if (e.altKey) {
  1811. modifiers.push('alt');
  1812. }
  1813.  
  1814. if (e.ctrlKey) {
  1815. modifiers.push('ctrl');
  1816. }
  1817.  
  1818. if (e.metaKey) {
  1819. modifiers.push('meta');
  1820. }
  1821.  
  1822. return modifiers;
  1823. }
  1824.  
  1825. /**
  1826. * prevents default for this event
  1827. *
  1828. * @param {Event} e
  1829. * @returns void
  1830. */
  1831. function _preventDefault(e) {
  1832. if (e.preventDefault) {
  1833. e.preventDefault();
  1834. return;
  1835. }
  1836.  
  1837. e.returnValue = false;
  1838. }
  1839.  
  1840. /**
  1841. * stops propogation for this event
  1842. *
  1843. * @param {Event} e
  1844. * @returns void
  1845. */
  1846. function _stopPropagation(e) {
  1847. if (e.stopPropagation) {
  1848. e.stopPropagation();
  1849. return;
  1850. }
  1851.  
  1852. e.cancelBubble = true;
  1853. }
  1854.  
  1855. /**
  1856. * determines if the keycode specified is a modifier key or not
  1857. *
  1858. * @param {string} key
  1859. * @returns {boolean}
  1860. */
  1861. function _isModifier(key) {
  1862. return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
  1863. }
  1864.  
  1865. /**
  1866. * reverses the map lookup so that we can look for specific keys
  1867. * to see what can and can't use keypress
  1868. *
  1869. * @return {Object}
  1870. */
  1871. function _getReverseMap() {
  1872. if (!_REVERSE_MAP) {
  1873. _REVERSE_MAP = {};
  1874. for (var key in _MAP) {
  1875.  
  1876. // pull out the numeric keypad from here cause keypress should
  1877. // be able to detect the keys from the character
  1878. if (key > 95 && key < 112) {
  1879. continue;
  1880. }
  1881.  
  1882. if (_MAP.hasOwnProperty(key)) {
  1883. _REVERSE_MAP[_MAP[key]] = key;
  1884. }
  1885. }
  1886. }
  1887. return _REVERSE_MAP;
  1888. }
  1889.  
  1890. /**
  1891. * picks the best action based on the key combination
  1892. *
  1893. * @param {string} key - character for key
  1894. * @param {Array} modifiers
  1895. * @param {string=} action passed in
  1896. */
  1897. function _pickBestAction(key, modifiers, action) {
  1898.  
  1899. // if no action was picked in we should try to pick the one
  1900. // that we think would work best for this key
  1901. if (!action) {
  1902. action = _getReverseMap()[key] ? 'keydown' : 'keypress';
  1903. }
  1904.  
  1905. // modifier keys don't work as expected with keypress,
  1906. // switch to keydown
  1907. if (action == 'keypress' && modifiers.length) {
  1908. action = 'keydown';
  1909. }
  1910.  
  1911. return action;
  1912. }
  1913.  
  1914. /**
  1915. * Converts from a string key combination to an array
  1916. *
  1917. * @param {string} combination like "command+shift+l"
  1918. * @return {Array}
  1919. */
  1920. function _keysFromString(combination) {
  1921. if (combination === '+') {
  1922. return ['+'];
  1923. }
  1924.  
  1925. combination = combination.replace(/\+{2}/g, '+plus');
  1926. return combination.split('+');
  1927. }
  1928.  
  1929. /**
  1930. * Gets info for a specific key combination
  1931. *
  1932. * @param {string} combination key combination ("command+s" or "a" or "*")
  1933. * @param {string=} action
  1934. * @returns {Object}
  1935. */
  1936. function _getKeyInfo(combination, action) {
  1937. var keys;
  1938. var key;
  1939. var i;
  1940. var modifiers = [];
  1941.  
  1942. // take the keys from this pattern and figure out what the actual
  1943. // pattern is all about
  1944. keys = _keysFromString(combination);
  1945.  
  1946. for (i = 0; i < keys.length; ++i) {
  1947. key = keys[i];
  1948.  
  1949. // normalize key names
  1950. if (_SPECIAL_ALIASES[key]) {
  1951. key = _SPECIAL_ALIASES[key];
  1952. }
  1953.  
  1954. // if this is not a keypress event then we should
  1955. // be smart about using shift keys
  1956. // this will only work for US keyboards however
  1957. if (action && action != 'keypress' && _SHIFT_MAP[key]) {
  1958. key = _SHIFT_MAP[key];
  1959. modifiers.push('shift');
  1960. }
  1961.  
  1962. // if this key is a modifier then add it to the list of modifiers
  1963. if (_isModifier(key)) {
  1964. modifiers.push(key);
  1965. }
  1966. }
  1967.  
  1968. // depending on what the key combination is
  1969. // we will try to pick the best event for it
  1970. action = _pickBestAction(key, modifiers, action);
  1971.  
  1972. return {
  1973. key: key,
  1974. modifiers: modifiers,
  1975. action: action
  1976. };
  1977. }
  1978.  
  1979. function _belongsTo(element, ancestor) {
  1980. if (element === null || element === document) {
  1981. return false;
  1982. }
  1983.  
  1984. if (element === ancestor) {
  1985. return true;
  1986. }
  1987.  
  1988. return _belongsTo(element.parentNode, ancestor);
  1989. }
  1990.  
  1991. function Mousetrap(targetElement) {
  1992. var self = this;
  1993.  
  1994. targetElement = targetElement || document;
  1995.  
  1996. if (!(self instanceof Mousetrap)) {
  1997. return new Mousetrap(targetElement);
  1998. }
  1999.  
  2000. /**
  2001. * element to attach key events to
  2002. *
  2003. * @type {Element}
  2004. */
  2005. self.target = targetElement;
  2006.  
  2007. /**
  2008. * a list of all the callbacks setup via Mousetrap.bind()
  2009. *
  2010. * @type {Object}
  2011. */
  2012. self._callbacks = {};
  2013.  
  2014. /**
  2015. * direct map of string combinations to callbacks used for trigger()
  2016. *
  2017. * @type {Object}
  2018. */
  2019. self._directMap = {};
  2020.  
  2021. /**
  2022. * keeps track of what level each sequence is at since multiple
  2023. * sequences can start out with the same sequence
  2024. *
  2025. * @type {Object}
  2026. */
  2027. var _sequenceLevels = {};
  2028.  
  2029. /**
  2030. * variable to store the setTimeout call
  2031. *
  2032. * @type {null|number}
  2033. */
  2034. var _resetTimer;
  2035.  
  2036. /**
  2037. * temporary state where we will ignore the next keyup
  2038. *
  2039. * @type {boolean|string}
  2040. */
  2041. var _ignoreNextKeyup = false;
  2042.  
  2043. /**
  2044. * temporary state where we will ignore the next keypress
  2045. *
  2046. * @type {boolean}
  2047. */
  2048. var _ignoreNextKeypress = false;
  2049.  
  2050. /**
  2051. * are we currently inside of a sequence?
  2052. * type of action ("keyup" or "keydown" or "keypress") or false
  2053. *
  2054. * @type {boolean|string}
  2055. */
  2056. var _nextExpectedAction = false;
  2057.  
  2058. /**
  2059. * resets all sequence counters except for the ones passed in
  2060. *
  2061. * @param {Object} doNotReset
  2062. * @returns void
  2063. */
  2064. function _resetSequences(doNotReset) {
  2065. doNotReset = doNotReset || {};
  2066.  
  2067. var activeSequences = false,
  2068. key;
  2069.  
  2070. for (key in _sequenceLevels) {
  2071. if (doNotReset[key]) {
  2072. activeSequences = true;
  2073. continue;
  2074. }
  2075. _sequenceLevels[key] = 0;
  2076. }
  2077.  
  2078. if (!activeSequences) {
  2079. _nextExpectedAction = false;
  2080. }
  2081. }
  2082.  
  2083. /**
  2084. * finds all callbacks that match based on the keycode, modifiers,
  2085. * and action
  2086. *
  2087. * @param {string} character
  2088. * @param {Array} modifiers
  2089. * @param {Event|Object} e
  2090. * @param {string=} sequenceName - name of the sequence we are looking for
  2091. * @param {string=} combination
  2092. * @param {number=} level
  2093. * @returns {Array}
  2094. */
  2095. function _getMatches(character, modifiers, e, sequenceName, combination, level) {
  2096. var i;
  2097. var callback;
  2098. var matches = [];
  2099. var action = e.type;
  2100.  
  2101. // if there are no events related to this keycode
  2102. if (!self._callbacks[character]) {
  2103. return [];
  2104. }
  2105.  
  2106. // if a modifier key is coming up on its own we should allow it
  2107. if (action == 'keyup' && _isModifier(character)) {
  2108. modifiers = [character];
  2109. }
  2110.  
  2111. // loop through all callbacks for the key that was pressed
  2112. // and see if any of them match
  2113. for (i = 0; i < self._callbacks[character].length; ++i) {
  2114. callback = self._callbacks[character][i];
  2115.  
  2116. // if a sequence name is not specified, but this is a sequence at
  2117. // the wrong level then move onto the next match
  2118. if (!sequenceName && callback.seq && _sequenceLevels[callback.seq] != callback.level) {
  2119. continue;
  2120. }
  2121.  
  2122. // if the action we are looking for doesn't match the action we got
  2123. // then we should keep going
  2124. if (action != callback.action) {
  2125. continue;
  2126. }
  2127.  
  2128. // if this is a keypress event and the meta key and control key
  2129. // are not pressed that means that we need to only look at the
  2130. // character, otherwise check the modifiers as well
  2131. //
  2132. // chrome will not fire a keypress if meta or control is down
  2133. // safari will fire a keypress if meta or meta+shift is down
  2134. // firefox will fire a keypress if meta or control is down
  2135. if ((action == 'keypress' && !e.metaKey && !e.ctrlKey) || _modifiersMatch(modifiers, callback.modifiers)) {
  2136.  
  2137. // when you bind a combination or sequence a second time it
  2138. // should overwrite the first one. if a sequenceName or
  2139. // combination is specified in this call it does just that
  2140. //
  2141. // @todo make deleting its own method?
  2142. var deleteCombo = !sequenceName && callback.combo == combination;
  2143. var deleteSequence = sequenceName && callback.seq == sequenceName && callback.level == level;
  2144. if (deleteCombo || deleteSequence) {
  2145. self._callbacks[character].splice(i, 1);
  2146. }
  2147.  
  2148. matches.push(callback);
  2149. }
  2150. }
  2151.  
  2152. return matches;
  2153. }
  2154.  
  2155. /**
  2156. * actually calls the callback function
  2157. *
  2158. * if your callback function returns false this will use the jquery
  2159. * convention - prevent default and stop propogation on the event
  2160. *
  2161. * @param {Function} callback
  2162. * @param {Event} e
  2163. * @returns void
  2164. */
  2165. function _fireCallback(callback, e, combo, sequence) {
  2166.  
  2167. // if this event should not happen stop here
  2168. if (self.stopCallback(e, e.target || e.srcElement, combo, sequence)) {
  2169. return;
  2170. }
  2171.  
  2172. if (callback(e, combo) === false) {
  2173. _preventDefault(e);
  2174. _stopPropagation(e);
  2175. }
  2176. }
  2177.  
  2178. /**
  2179. * handles a character key event
  2180. *
  2181. * @param {string} character
  2182. * @param {Array} modifiers
  2183. * @param {Event} e
  2184. * @returns void
  2185. */
  2186. self._handleKey = function (character, modifiers, e) {
  2187. var callbacks = _getMatches(character, modifiers, e);
  2188. var i;
  2189. var doNotReset = {};
  2190. var maxLevel = 0;
  2191. var processedSequenceCallback = false;
  2192.  
  2193. // Calculate the maxLevel for sequences so we can only execute the longest callback sequence
  2194. for (i = 0; i < callbacks.length; ++i) {
  2195. if (callbacks[i].seq) {
  2196. maxLevel = Math.max(maxLevel, callbacks[i].level);
  2197. }
  2198. }
  2199.  
  2200. // loop through matching callbacks for this key event
  2201. for (i = 0; i < callbacks.length; ++i) {
  2202.  
  2203. // fire for all sequence callbacks
  2204. // this is because if for example you have multiple sequences
  2205. // bound such as "g i" and "g t" they both need to fire the
  2206. // callback for matching g cause otherwise you can only ever
  2207. // match the first one
  2208. if (callbacks[i].seq) {
  2209.  
  2210. // only fire callbacks for the maxLevel to prevent
  2211. // subsequences from also firing
  2212. //
  2213. // for example 'a option b' should not cause 'option b' to fire
  2214. // even though 'option b' is part of the other sequence
  2215. //
  2216. // any sequences that do not match here will be discarded
  2217. // below by the _resetSequences call
  2218. if (callbacks[i].level != maxLevel) {
  2219. continue;
  2220. }
  2221.  
  2222. processedSequenceCallback = true;
  2223.  
  2224. // keep a list of which sequences were matches for later
  2225. doNotReset[callbacks[i].seq] = 1;
  2226. _fireCallback(callbacks[i].callback, e, callbacks[i].combo, callbacks[i].seq);
  2227. continue;
  2228. }
  2229.  
  2230. // if there were no sequence matches but we are still here
  2231. // that means this is a regular match so we should fire that
  2232. if (!processedSequenceCallback) {
  2233. _fireCallback(callbacks[i].callback, e, callbacks[i].combo);
  2234. }
  2235. }
  2236.  
  2237. // if the key you pressed matches the type of sequence without
  2238. // being a modifier (ie "keyup" or "keypress") then we should
  2239. // reset all sequences that were not matched by this event
  2240. //
  2241. // this is so, for example, if you have the sequence "h a t" and you
  2242. // type "h e a r t" it does not match. in this case the "e" will
  2243. // cause the sequence to reset
  2244. //
  2245. // modifier keys are ignored because you can have a sequence
  2246. // that contains modifiers such as "enter ctrl+space" and in most
  2247. // cases the modifier key will be pressed before the next key
  2248. //
  2249. // also if you have a sequence such as "ctrl+b a" then pressing the
  2250. // "b" key will trigger a "keypress" and a "keydown"
  2251. //
  2252. // the "keydown" is expected when there is a modifier, but the
  2253. // "keypress" ends up matching the _nextExpectedAction since it occurs
  2254. // after and that causes the sequence to reset
  2255. //
  2256. // we ignore keypresses in a sequence that directly follow a keydown
  2257. // for the same character
  2258. var ignoreThisKeypress = e.type == 'keypress' && _ignoreNextKeypress;
  2259. if (e.type == _nextExpectedAction && !_isModifier(character) && !ignoreThisKeypress) {
  2260. _resetSequences(doNotReset);
  2261. }
  2262.  
  2263. _ignoreNextKeypress = processedSequenceCallback && e.type == 'keydown';
  2264. };
  2265.  
  2266. /**
  2267. * handles a keydown event
  2268. *
  2269. * @param {Event} e
  2270. * @returns void
  2271. */
  2272. function _handleKeyEvent(e) {
  2273.  
  2274. // normalize e.which for key events
  2275. // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
  2276. if (typeof e.which !== 'number') {
  2277. e.which = e.keyCode;
  2278. }
  2279.  
  2280. var character = _characterFromEvent(e);
  2281.  
  2282. // no character found then stop
  2283. if (!character) {
  2284. return;
  2285. }
  2286.  
  2287. // need to use === for the character check because the character can be 0
  2288. if (e.type == 'keyup' && _ignoreNextKeyup === character) {
  2289. _ignoreNextKeyup = false;
  2290. return;
  2291. }
  2292.  
  2293. self.handleKey(character, _eventModifiers(e), e);
  2294. }
  2295.  
  2296. /**
  2297. * called to set a 1 second timeout on the specified sequence
  2298. *
  2299. * this is so after each key press in the sequence you have 1 second
  2300. * to press the next key before you have to start over
  2301. *
  2302. * @returns void
  2303. */
  2304. function _resetSequenceTimer() {
  2305. clearTimeout(_resetTimer);
  2306. _resetTimer = setTimeout(_resetSequences, 1000);
  2307. }
  2308.  
  2309. /**
  2310. * binds a key sequence to an event
  2311. *
  2312. * @param {string} combo - combo specified in bind call
  2313. * @param {Array} keys
  2314. * @param {Function} callback
  2315. * @param {string=} action
  2316. * @returns void
  2317. */
  2318. function _bindSequence(combo, keys, callback, action) {
  2319.  
  2320. // start off by adding a sequence level record for this combination
  2321. // and setting the level to 0
  2322. _sequenceLevels[combo] = 0;
  2323.  
  2324. /**
  2325. * callback to increase the sequence level for this sequence and reset
  2326. * all other sequences that were active
  2327. *
  2328. * @param {string} nextAction
  2329. * @returns {Function}
  2330. */
  2331. function _increaseSequence(nextAction) {
  2332. return function () {
  2333. _nextExpectedAction = nextAction;
  2334. ++_sequenceLevels[combo];
  2335. _resetSequenceTimer();
  2336. };
  2337. }
  2338.  
  2339. /**
  2340. * wraps the specified callback inside of another function in order
  2341. * to reset all sequence counters as soon as this sequence is done
  2342. *
  2343. * @param {Event} e
  2344. * @returns void
  2345. */
  2346. function _callbackAndReset(e) {
  2347. _fireCallback(callback, e, combo);
  2348.  
  2349. // we should ignore the next key up if the action is key down
  2350. // or keypress. this is so if you finish a sequence and
  2351. // release the key the final key will not trigger a keyup
  2352. if (action !== 'keyup') {
  2353. _ignoreNextKeyup = _characterFromEvent(e);
  2354. }
  2355.  
  2356. // weird race condition if a sequence ends with the key
  2357. // another sequence begins with
  2358. setTimeout(_resetSequences, 10);
  2359. }
  2360.  
  2361. // loop through keys one at a time and bind the appropriate callback
  2362. // function. for any key leading up to the final one it should
  2363. // increase the sequence. after the final, it should reset all sequences
  2364. //
  2365. // if an action is specified in the original bind call then that will
  2366. // be used throughout. otherwise we will pass the action that the
  2367. // next key in the sequence should match. this allows a sequence
  2368. // to mix and match keypress and keydown events depending on which
  2369. // ones are better suited to the key provided
  2370. for (var i = 0; i < keys.length; ++i) {
  2371. var isFinal = i + 1 === keys.length;
  2372. var wrappedCallback = isFinal ? _callbackAndReset : _increaseSequence(action || _getKeyInfo(keys[i + 1]).action);
  2373. _bindSingle(keys[i], wrappedCallback, action, combo, i);
  2374. }
  2375. }
  2376.  
  2377. /**
  2378. * binds a single keyboard combination
  2379. *
  2380. * @param {string} combination
  2381. * @param {Function} callback
  2382. * @param {string=} action
  2383. * @param {string=} sequenceName - name of sequence if part of sequence
  2384. * @param {number=} level - what part of the sequence the command is
  2385. * @returns void
  2386. */
  2387. function _bindSingle(combination, callback, action, sequenceName, level) {
  2388.  
  2389. // store a direct mapped reference for use with Mousetrap.trigger
  2390. self._directMap[combination + ':' + action] = callback;
  2391.  
  2392. // make sure multiple spaces in a row become a single space
  2393. combination = combination.replace(/\s+/g, ' ');
  2394.  
  2395. var sequence = combination.split(' ');
  2396. var info;
  2397.  
  2398. // if this pattern is a sequence of keys then run through this method
  2399. // to reprocess each pattern one key at a time
  2400. if (sequence.length > 1) {
  2401. _bindSequence(combination, sequence, callback, action);
  2402. return;
  2403. }
  2404.  
  2405. info = _getKeyInfo(combination, action);
  2406.  
  2407. // make sure to initialize array if this is the first time
  2408. // a callback is added for this key
  2409. self._callbacks[info.key] = self._callbacks[info.key] || [];
  2410.  
  2411. // remove an existing match if there is one
  2412. _getMatches(info.key, info.modifiers, {type: info.action}, sequenceName, combination, level);
  2413.  
  2414. // add this call back to the array
  2415. // if it is a sequence put it at the beginning
  2416. // if not put it at the end
  2417. //
  2418. // this is important because the way these are processed expects
  2419. // the sequence ones to come first
  2420. self._callbacks[info.key][sequenceName ? 'unshift' : 'push']({
  2421. callback: callback,
  2422. modifiers: info.modifiers,
  2423. action: info.action,
  2424. seq: sequenceName,
  2425. level: level,
  2426. combo: combination
  2427. });
  2428. }
  2429.  
  2430. /**
  2431. * binds multiple combinations to the same callback
  2432. *
  2433. * @param {Array} combinations
  2434. * @param {Function} callback
  2435. * @param {string|undefined} action
  2436. * @returns void
  2437. */
  2438. self._bindMultiple = function (combinations, callback, action) {
  2439. for (var i = 0; i < combinations.length; ++i) {
  2440. _bindSingle(combinations[i], callback, action);
  2441. }
  2442. };
  2443.  
  2444. // start!
  2445. _addEvent(targetElement, 'keypress', _handleKeyEvent);
  2446. _addEvent(targetElement, 'keydown', _handleKeyEvent);
  2447. _addEvent(targetElement, 'keyup', _handleKeyEvent);
  2448. }
  2449.  
  2450. /**
  2451. * binds an event to mousetrap
  2452. *
  2453. * can be a single key, a combination of keys separated with +,
  2454. * an array of keys, or a sequence of keys separated by spaces
  2455. *
  2456. * be sure to list the modifier keys first to make sure that the
  2457. * correct key ends up getting bound (the last key in the pattern)
  2458. *
  2459. * @param {string|Array} keys
  2460. * @param {Function} callback
  2461. * @param {string=} action - 'keypress', 'keydown', or 'keyup'
  2462. * @returns void
  2463. */
  2464. Mousetrap.prototype.bind = function (keys, callback, action) {
  2465. var self = this;
  2466. keys = keys instanceof Array ? keys : [keys];
  2467. self._bindMultiple.call(self, keys, callback, action);
  2468. return self;
  2469. };
  2470.  
  2471. /**
  2472. * unbinds an event to mousetrap
  2473. *
  2474. * the unbinding sets the callback function of the specified key combo
  2475. * to an empty function and deletes the corresponding key in the
  2476. * _directMap dict.
  2477. *
  2478. * TODO: actually remove this from the _callbacks dictionary instead
  2479. * of binding an empty function
  2480. *
  2481. * the keycombo+action has to be exactly the same as
  2482. * it was defined in the bind method
  2483. *
  2484. * @param {string|Array} keys
  2485. * @param {string} action
  2486. * @returns void
  2487. */
  2488. Mousetrap.prototype.unbind = function (keys, action) {
  2489. var self = this;
  2490. return self.bind.call(self, keys, function () {
  2491. }, action);
  2492. };
  2493.  
  2494. /**
  2495. * triggers an event that has already been bound
  2496. *
  2497. * @param {string} keys
  2498. * @param {string=} action
  2499. * @returns void
  2500. */
  2501. Mousetrap.prototype.trigger = function (keys, action) {
  2502. var self = this;
  2503. if (self._directMap[keys + ':' + action]) {
  2504. self._directMap[keys + ':' + action]({}, keys);
  2505. }
  2506. return self;
  2507. };
  2508.  
  2509. /**
  2510. * resets the library back to its initial state. this is useful
  2511. * if you want to clear out the current keyboard shortcuts and bind
  2512. * new ones - for example if you switch to another page
  2513. *
  2514. * @returns void
  2515. */
  2516. Mousetrap.prototype.reset = function () {
  2517. var self = this;
  2518. self._callbacks = {};
  2519. self._directMap = {};
  2520. return self;
  2521. };
  2522.  
  2523. /**
  2524. * should we stop this event before firing off callbacks
  2525. *
  2526. * @param {Event} e
  2527. * @param {Element} element
  2528. * @return {boolean}
  2529. */
  2530. Mousetrap.prototype.stopCallback = function (e, element) {
  2531. var self = this;
  2532.  
  2533. // if the element has the class "mousetrap" then no need to stop
  2534. if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
  2535. return false;
  2536. }
  2537.  
  2538. if (_belongsTo(element, self.target)) {
  2539. return false;
  2540. }
  2541.  
  2542. // stop for input, select, and textarea
  2543. return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable;
  2544. };
  2545.  
  2546. /**
  2547. * exposes _handleKey publicly so it can be overwritten by extensions
  2548. */
  2549. Mousetrap.prototype.handleKey = function () {
  2550. var self = this;
  2551. return self._handleKey.apply(self, arguments);
  2552. };
  2553.  
  2554. /**
  2555. * allow custom key mappings
  2556. */
  2557. Mousetrap.addKeycodes = function (object) {
  2558. for (var key in object) {
  2559. if (object.hasOwnProperty(key)) {
  2560. _MAP[key] = object[key];
  2561. }
  2562. }
  2563. _REVERSE_MAP = null;
  2564. };
  2565.  
  2566. /**
  2567. * Init the global mousetrap functions
  2568. *
  2569. * This method is needed to allow the global mousetrap functions to work
  2570. * now that mousetrap is a constructor function.
  2571. */
  2572. Mousetrap.init = function () {
  2573. var documentMousetrap = Mousetrap(document);
  2574. for (var method in documentMousetrap) {
  2575. if (method.charAt(0) !== '_') {
  2576. Mousetrap[method] = (function (method) {
  2577. return function () {
  2578. return documentMousetrap[method].apply(documentMousetrap, arguments);
  2579. };
  2580. }(method));
  2581. }
  2582. }
  2583. };
  2584.  
  2585. Mousetrap.init();
  2586.  
  2587. // expose mousetrap to the global object
  2588. window.Mousetrap = Mousetrap;
  2589.  
  2590. // expose as a common js module
  2591. if (typeof module !== 'undefined' && module.exports) {
  2592. module.exports = Mousetrap;
  2593. }
  2594.  
  2595. // expose mousetrap as an AMD module
  2596. if (typeof define === 'function' && define.amd) {
  2597. define(function () {
  2598. return Mousetrap;
  2599. });
  2600. }
  2601. })(typeof window !== 'undefined' ? window : null, typeof window !== 'undefined' ? document : null);
  2602. unsafeWindow.Mousetrap = Mousetrap;
  2603.  
  2604. unsafeWindow.fetchSimilarHeaders = fetchSimilarHeaders;
  2605. function fetchSimilarHeaders(callback) {
  2606. var request = new XMLHttpRequest();
  2607. request.onreadystatechange = function () {
  2608. if (request.readyState === 4) {
  2609. //
  2610. // The following headers may often be similar
  2611. // to those of the original page request...
  2612. //
  2613. if (callback && typeof callback === 'function') {
  2614. callback(request.getAllResponseHeaders());
  2615. }
  2616. }
  2617. };
  2618.  
  2619. //
  2620. // Re-request the same page (document.location)
  2621. // We hope to get the same or similar response headers to those which
  2622. // came with the current page, but we have no guarantee.
  2623. // Since we are only after the headers, a HEAD request may be sufficient.
  2624. //
  2625. request.open('HEAD', document.location, true);
  2626. request.send(null);
  2627. }
  2628.  
  2629. String.prototype.escapeSpecialChars = function () {
  2630. return this.replace(/\\n/g, "\\n")
  2631. .replace(/\\'/g, "\\'")
  2632. .replace(/\\"/g, '\\"')
  2633. .replace(/\\&/g, "\\&")
  2634. .replace(/\\r/g, "\\r")
  2635. .replace(/\\t/g, "\\t")
  2636. .replace(/\\b/g, "\\b")
  2637. .replace(/\\f/g, "\\f");
  2638. };
  2639. function headers2Object(headers) {
  2640. if (!headers) return {};
  2641. const jsonParseEscape = function (str) {
  2642. return str.replace(/\n/g, "\\n")
  2643. .replace(/\'/g, "\\'")
  2644. .replace(/\"/g, '\\"')
  2645. .replace(/\&/g, "\\&")
  2646. .replace(/\r/g, "\\r")
  2647. .replace(/\t/g, "\\t")
  2648. .replace(/\f/g, "\\f");
  2649. };
  2650. var jsonStr = '{\n' +
  2651. headers.trim().split("\n").filter(line => line.length > 2)
  2652. .map(
  2653. line => " " + [line.slice(0, line.indexOf(':')), line.slice(line.indexOf(':') + 1)]
  2654. .map(part => '"' + jsonParseEscape(part.trim()) + '"').join(':')
  2655. )
  2656. .join(",\n") +
  2657. '\n}';
  2658. console.log('jsonStr:', jsonStr);
  2659. return JSON.parse(jsonStr);
  2660. }
  2661.  
  2662.  
  2663. unsafeWindow.fetchUsingProxy = fetchUsingProxy;
  2664. /**
  2665. * @param url
  2666. * Found from: https://stackoverflow.com/questions/43871637/no-access-control-allow-origin-header-is-present-on-the-requested-resource-whe
  2667. * @param callback
  2668. * @see https://cors-anywhere.herokuapp.com/
  2669. */
  2670. function fetchUsingProxy(url, callback) {
  2671. const proxyurl = "https://cors-anywhere.herokuapp.com/";
  2672. callback = callback || (contents => console.log(contents));
  2673. fetch(proxyurl + url) // https://cors-anywhere.herokuapp.com/https://example.com
  2674. .then(response => response.text())
  2675. .then(callback)
  2676. .catch(() => console.error(`Cant access ${url} response. Blocked by browser?`))
  2677. }
  2678.  
  2679. unsafeWindow.getModKeys = getModifierKeys;
  2680. unsafeWindow.KeyEvent = {
  2681. DOM_VK_BACKSPACE: 8,
  2682. DOM_VK_TAB: 9,
  2683. DOM_VK_ENTER: 13,
  2684. DOM_VK_SHIFT: 16,
  2685. DOM_VK_CTRL: 17,
  2686. DOM_VK_ALT: 18,
  2687. DOM_VK_PAUSE_BREAK: 19,
  2688. DOM_VK_CAPS_LOCK: 20,
  2689. DOM_VK_ESCAPE: 27,
  2690. DOM_VK_PGUP: 33, DOM_VK_PAGE_UP: 33,
  2691. DOM_VK_PGDN: 34, DOM_VK_PAGE_DOWN: 34,
  2692. DOM_VK_END: 35,
  2693. DOM_VK_HOME: 36,
  2694. DOM_VK_LEFT: 37, DOM_VK_LEFT_ARROW: 37,
  2695. DOM_VK_UP: 38, DOM_VK_UP_ARROW: 38,
  2696. DOM_VK_RIGHT: 39, DOM_VK_RIGHT_ARROW: 39,
  2697. DOM_VK_DOWN: 40, DOM_VK_DOWN_ARROW: 40,
  2698. DOM_VK_INSERT: 45,
  2699. DOM_VK_DEL: 46, DOM_VK_DELETE: 46,
  2700. DOM_VK_0: 48, DOM_VK_ALPHA0: 48,
  2701. DOM_VK_1: 49, DOM_VK_ALPHA1: 49,
  2702. DOM_VK_2: 50, DOM_VK_ALPHA2: 50,
  2703. DOM_VK_3: 51, DOM_VK_ALPHA3: 51,
  2704. DOM_VK_4: 52, DOM_VK_ALPHA4: 52,
  2705. DOM_VK_5: 53, DOM_VK_ALPHA5: 53,
  2706. DOM_VK_6: 54, DOM_VK_ALPHA6: 54,
  2707. DOM_VK_7: 55, DOM_VK_ALPHA7: 55,
  2708. DOM_VK_8: 56, DOM_VK_ALPHA8: 56,
  2709. DOM_VK_9: 57, DOM_VK_ALPHA9: 57,
  2710. DOM_VK_A: 65,
  2711. DOM_VK_B: 66,
  2712. DOM_VK_C: 67,
  2713. DOM_VK_D: 68,
  2714. DOM_VK_E: 69,
  2715. DOM_VK_F: 70,
  2716. DOM_VK_G: 71,
  2717. DOM_VK_H: 72,
  2718. DOM_VK_I: 73,
  2719. DOM_VK_J: 74,
  2720. DOM_VK_K: 75,
  2721. DOM_VK_L: 76,
  2722. DOM_VK_M: 77,
  2723. DOM_VK_N: 78,
  2724. DOM_VK_O: 79,
  2725. DOM_VK_P: 80,
  2726. DOM_VK_Q: 81,
  2727. DOM_VK_R: 82,
  2728. DOM_VK_S: 83,
  2729. DOM_VK_T: 84,
  2730. DOM_VK_U: 85,
  2731. DOM_VK_V: 86,
  2732. DOM_VK_W: 87,
  2733. DOM_VK_X: 88,
  2734. DOM_VK_Y: 89,
  2735. DOM_VK_Z: 90,
  2736. DOM_VK_LWIN: 91, DOM_VK_LEFT_WINDOW: 91,
  2737. DOM_VK_RWIN: 92, DOM_VK_RIGHT_WINDOW: 92,
  2738. DOM_VK_SELECT: 93,
  2739.  
  2740. DOM_VK_NUMPAD0: 96,
  2741. DOM_VK_NUMPAD1: 97,
  2742. DOM_VK_NUMPAD2: 98,
  2743. DOM_VK_NUMPAD3: 99,
  2744. DOM_VK_NUMPAD4: 100,
  2745. DOM_VK_NUMPAD5: 101,
  2746. DOM_VK_NUMPAD6: 102,
  2747. DOM_VK_NUMPAD7: 103,
  2748. DOM_VK_NUMPAD8: 104,
  2749. DOM_VK_NUMPAD9: 105,
  2750. DOM_VK_MULTIPLY: 106,
  2751.  
  2752. DOM_VK_ADD: 107,
  2753. DOM_VK_SUBTRACT: 109,
  2754. DOM_VK_DECIMAL_POINT: 110,
  2755. DOM_VK_DIVIDE: 111,
  2756. DOM_VK_F1: 112,
  2757. DOM_VK_F2: 113,
  2758. DOM_VK_F3: 114,
  2759. DOM_VK_F4: 115,
  2760. DOM_VK_F5: 116,
  2761. DOM_VK_F6: 117,
  2762. DOM_VK_F7: 118,
  2763. DOM_VK_F8: 119,
  2764. DOM_VK_F9: 120,
  2765. DOM_VK_F10: 121,
  2766. DOM_VK_F11: 122,
  2767. DOM_VK_F12: 123,
  2768. DOM_VK_NUM_LOCK: 144,
  2769. DOM_VK_SCROLL_LOCK: 145,
  2770. DOM_VK_SEMICOLON: 186,
  2771. DOM_VK_EQUALS: 187, DOM_VK_EQUAL_SIGN: 187,
  2772. DOM_VK_COMMA: 188,
  2773. DOM_VK_DASH: 189,
  2774. DOM_VK_PERIOD: 190,
  2775. DOM_VK_FORWARD_SLASH: 191,
  2776. DOM_VK_GRAVE_ACCENT: 192,
  2777. DOM_VK_OPEN_BRACKET: 219,
  2778. DOM_VK_BACK_SLASH: 220,
  2779. DOM_VK_CLOSE_BRACKET: 221,
  2780. DOM_VK_SINGLE_QUOTE: 222
  2781. };
  2782. /**
  2783. * Order of key strokes in naming convention: Ctrl > Shift > Alt > Meta
  2784. * @param keyEvent
  2785. * @returns {{CTRL_ONLY: boolean, SHIFT_ONLY: boolean, ALT_ONLY: boolean, META_ONLY: boolean, NONE: boolean}}
  2786. */
  2787. function getModifierKeys(keyEvent) {
  2788. /** @type {{CTRL_ONLY: boolean, SHIFT_ONLY: boolean, ALT_ONLY: boolean, NONE: boolean}} */
  2789. return {
  2790. CTRL_SHIFT: keyEvent.ctrlKey && !keyEvent.altKey && keyEvent.shiftKey && !keyEvent.metaKey,
  2791. CTRL_ALT: keyEvent.ctrlKey && keyEvent.altKey && !keyEvent.shiftKey && !keyEvent.metaKey,
  2792. ALT_SHIFT: !keyEvent.ctrlKey && keyEvent.altKey && keyEvent.shiftKey && !keyEvent.metaKey,
  2793. CTRL_ONLY: keyEvent.ctrlKey && !keyEvent.altKey && !keyEvent.shiftKey && !keyEvent.metaKey,
  2794. CTRL_ALT_SHIFT: keyEvent.ctrlKey && keyEvent.altKey && keyEvent.shiftKey && !keyEvent.metaKey,
  2795.  
  2796. SHIFT_ONLY: !keyEvent.ctrlKey && !keyEvent.altKey && keyEvent.shiftKey && !keyEvent.metaKey,
  2797. ALT_ONLY: !keyEvent.ctrlKey && keyEvent.altKey && !keyEvent.shiftKey && !keyEvent.metaKey,
  2798. META_ONLY: !keyEvent.ctrlKey && !keyEvent.altKey && !keyEvent.shiftKey && keyEvent.metaKey,
  2799.  
  2800. NONE: !keyEvent.ctrlKey && !keyEvent.shiftKey && !keyEvent.altKey && !keyEvent.metaKey,
  2801.  
  2802. targetIsInput: (function targetIsInput() {
  2803. const ignores = document.getElementsByTagName('input');
  2804. const target = keyEvent.target;
  2805. for (let ignore of ignores)
  2806. if (target === ignore || ignore.contains(target)) {
  2807. // console.log('The target recieving the keycode is of type "input", so it will not recieve your keystroke', target);
  2808. return true;
  2809. }
  2810. return false;
  2811. })()
  2812. };
  2813. }
  2814.  
  2815. function publicizeSymbols(...parameters) {
  2816. for (const parameter of parameters) {
  2817. unsafeWindow[parameter] = parameter;
  2818. }
  2819. }
  2820.  
  2821. function mapObject(o) {
  2822. var map = new Map();
  2823. for (const key in (o)) {
  2824. if (o.hasOwnProperty(key))
  2825. map.set(key, o[key]);
  2826. }
  2827. return map;
  2828. }
  2829. function getObjOfType(targetInstance, parentObj) {
  2830. var list = [];
  2831. for (const o in parentObj) if (o instanceof targetInstance) {
  2832. return o;
  2833. }
  2834. return list;
  2835. }
  2836. function getNestedMembers(parentObject, targetType, list) {
  2837. if (!parentObject) {
  2838. console.error("parentObject is not defined:", parent);
  2839. return;
  2840. }
  2841. list = list || [];
  2842. for (const member in parentObject) {
  2843.  
  2844. const typeofObj = typeof member;
  2845.  
  2846. if (typeofObj === "object") {
  2847. getNestedMembers(member, targetType, list);
  2848. } else if (typeofObj !== 'undefined') {
  2849. if (targetType && typeofObj !== targetType)
  2850. continue;
  2851. list.push(member);
  2852. }
  2853. }
  2854. return list;
  2855. }