Faris Handy Webdev JavaScript functions

A bunch of useful JavaScript functions

目前為 2018-11-20 提交的版本,檢視 最新版本

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