Faris Handy Webdev JavaScript functions

A bunch of useful JavaScript functions

当前为 2019-04-04 提交的版本,查看 最新版本

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