Jump to Text

Adds single-key hotkeys that jump to specific text (or anchors) on a page.

  1. // ==UserScript==
  2. // @name Jump to Text
  3. // @namespace https://github.com/theborg3of5/Userscripts/
  4. // @version 1.8
  5. // @description Adds single-key hotkeys that jump to specific text (or anchors) on a page.
  6. // @author Gavin Borg
  7. // @require https://greasyfork.org/scripts/28536-gm-config/code/GM_config.js?version=184529
  8. // @match https://greasyfork.org/en/scripts/395551-jump-to-text
  9. // @grant GM_registerMenuCommand
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // ==/UserScript==
  13.  
  14. var configOpen = false; // Whether the config window is open, for our close-config hotkey (Escape).
  15. var config = GM_config;
  16. var maxNumHotkeys = 15; // How many hotkeys are configurable per site.
  17. var contextMenuOpen = false;
  18. var contextTimeout = 2000; // 2 seconds - how long to wait before assuming the context menu was closed.
  19.  
  20. (function() {
  21. 'use strict';
  22.  
  23. initConfig(getMatchingSite());
  24.  
  25. document.oncontextmenu = contextMenuOpened;
  26. document.onkeyup = keyPressed;
  27. })();
  28.  
  29. function initConfig(site) {
  30. var siteClean = cleanSite(site);
  31.  
  32. // Build the fields for each of the available hotkeys
  33. var fields = {};
  34. for (var i = 1; i <= maxNumHotkeys; i++) {
  35. fields[keyField(i)] = {
  36. label: "Key(s) to press:",
  37. title: "The single key(s) to press to jump to this anchor/text. To have multiple keys jump to the same place, separate keys with a space (i.e. \"a r\" for both \"a\" and \"r\" keys ).",
  38. type: "text",
  39. labelPos: "above"
  40. };
  41. fields[anchorNameField(i)] = {
  42. label: "Anchor name:",
  43. title: "The name or id of the anchor to jump to",
  44. type: "text"
  45. };
  46. fields[textField(i)] = {
  47. label: "Text to jump to:",
  48. title: "We'll jump to the first instance of this text on the page. Ignored if anchor name is specified.",
  49. type: "text"
  50. };
  51. }
  52.  
  53. config.init({
  54. id: 'JumpToTextConfig' + siteClean,
  55. title: "Jump to Text Config for: " + site,
  56. fields: fields,
  57. events: {
  58. 'open': function() { configOpen = true; },
  59. 'close': function() { configOpen = false; },
  60. 'save': function() { config.close(); }
  61. }
  62. });
  63.  
  64. // Add a menu item to the menu to launch the config
  65. GM_registerMenuCommand('Configure hotkeys for this site', () => {
  66. config.open();
  67. })
  68. }
  69.  
  70. function cleanSite(site) {
  71. return site.replace(/[\*/:\?\.]/g, ""); // Drop */:?. characters from site for use in ID
  72. }
  73.  
  74. function keyField(index) {
  75. return "Keys_" + index;
  76. }
  77. function anchorNameField(index) {
  78. return "AnchorName_" + index;
  79. }
  80. function textField(index) {
  81. return "Text_" + index;
  82. }
  83.  
  84. function contextMenuOpened() {
  85. contextMenuOpen = true;
  86.  
  87. // There's not a "oncontextmenuclosed" event, so we just have to take a guess and wait that long.
  88. setTimeout(function() { contextMenuOpen = false; }, contextTimeout);
  89. }
  90.  
  91. function keyPressed(e) {
  92. //console.log("Key caught: " + e.key);
  93.  
  94. // Ignore keys while the context menu is open
  95. if(contextMenuOpen) {
  96. return;
  97. }
  98. console.log("Context menu open: " + contextMenuOpen);
  99.  
  100. // Special cases: Ctrl+Comma (,) triggers config, Escape closes it
  101. if (e.ctrlKey && e.key === ",") {
  102. configOpen = true;
  103. config.open();
  104. }
  105. if (e.key === "Escape" && configOpen) {
  106. config.close();
  107. }
  108.  
  109. // Otherwise, only single-button hotkeys are supported
  110. if (e.shiftKey || e.ctrlKey || e.altKey || e.metaKey) { return; }
  111.  
  112. // If there's a matching anchor name, jump to that anchor by updating the URL hash.
  113. var anchorName = getAnchorNameForKey(e.key);
  114. //console.log("Anchor name found: " + anchorName);
  115. if(anchorName !== "") {
  116. // Make sure the anchor name starts with a hash (because that's how it's formatted in window.location.hash)
  117. if (!anchorName.startsWith("#")) {
  118. anchorName = "#" + anchorName;
  119. }
  120.  
  121. // If the URL is already pointed to the spot we're interested in, remove it so we can re-add it and jump there again.
  122. if (window.location.hash == anchorName) {
  123. window.location.hash = "";
  124. }
  125.  
  126. window.location.hash = anchorName;
  127. return;
  128. }
  129.  
  130. // Otherwise try to find the first instance of the configured text
  131. var text = getTextForKey(e.key);
  132. //console.log("Text found: " + text);
  133. if(text !== "") {
  134. var firstElement = document.evaluate("//*[contains(text(), '" + text + "')]", document).iterateNext();
  135. if(firstElement) {
  136. firstElement.scrollIntoView();
  137. return;
  138. }
  139. }
  140. }
  141.  
  142. function getMatchingSite() {
  143. // Get sites that user has chosen to include or match (because that's what hotkeys are keyed to, not direct URLs)
  144. var sites = GM_info.script.options.override.use_matches;
  145. sites.concat(GM_info.script.options.override.use_includes);
  146.  
  147. // Find matching site
  148. var currentURL = window.location.href;
  149. for (var site of sites) {
  150. // Use a RegExp to determine which of the user's includes/matches is currently open, since we allow different hotkeys/anchors per each of those.
  151. var siteRegex = new RegExp(site.replace(/\*/g, "[^ ]*")); // Replace * wildcards with regex-style [^ ]* wildcards
  152. if (siteRegex.test(currentURL)) {
  153. return site; // First match always wins
  154. }
  155. }
  156. }
  157.  
  158. function getAnchorNameForKey(key) {
  159. for (var i = 1; i <= maxNumHotkeys; i++) {
  160. var keyAry = config.get(keyField(i).split(" "));
  161. if (keyAry.includes(key)) {
  162. return config.get(anchorNameField(i));
  163. }
  164. }
  165.  
  166. return "";
  167. }
  168. function getTextForKey(key) {
  169. for (var i = 1; i <= maxNumHotkeys; i++) {
  170. var keyAry = config.get(keyField(i).split(" "));
  171. if (keyAry.includes(key)) {
  172. return config.get(textField(i));
  173. }
  174. }
  175.  
  176. return "";
  177. }