Faris Handy Webdev JavaScript functions

A bunch of useful JavaScript functions

目前為 2019-04-25 提交的版本,檢視 最新版本

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