Google Docs Shortcuts

Adds shortcuts for text colors (Alt+1 for black, Alt+2 for #00a797, Alt+3 for #006057, Alt+4 for #ff3333), opening heading outline (Alt+5), opening text color menu (Alt+7), restoring scroll position before document close (Alt+8), and switch to last selected document tab (Alt+W), and Borders and Shading (Alt+G)

目前为 2025-02-26 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Google Docs Shortcuts
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.3
  5. // @license MIT
  6. // @description Adds shortcuts for text colors (Alt+1 for black, Alt+2 for #00a797, Alt+3 for #006057, Alt+4 for #ff3333), opening heading outline (Alt+5), opening text color menu (Alt+7), restoring scroll position before document close (Alt+8), and switch to last selected document tab (Alt+W), and Borders and Shading (Alt+G)
  7. // @match https://docs.google.com/*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function () {
  12. 'use strict';
  13.  
  14. // Key combinations for color shortcuts
  15. const COLOR_SHORTCUTS = {
  16. '4': { rgb: 'rgb(255, 51, 51)', hex: '#ff3333' },
  17. '3': { rgb: 'rgb(0, 96, 87)', hex: '#006057' },
  18. '2': { rgb: 'rgb(0, 167, 151)', hex: '#00a797' },
  19. '1': { rgb: 'rgb(0, 0, 0)', hex: '#000000' }
  20. };
  21.  
  22. // Key combinations for other shortcuts
  23. const TAB_SHORTCUT_KEY = '5'; // Alt + 5
  24. const COLOR_SHORTCUT_KEY = '7'; // Alt + 7
  25. const SCROLL_SHORTCUT_KEY = '8'; // Alt + 8
  26. const TAB_SWITCH_KEY_CODE = 87; // KeyCode for 'W' (Alt + W)
  27. const BORDER_SHADING_KEY_CODE = 71; // KeyCode for 'G' (Alt + G)
  28.  
  29. // Constants for Tab Switching
  30. const TAB_SWITCH_REFACTORY_PERIOD = 500; // Time in milliseconds
  31. let lastSelectedTab = null;
  32. let currentSelectedTab = null;
  33. let isTabSwitchInProgress = false; // Variable to track whether a tab switch is in progress
  34.  
  35. // Handle keydown events for various shortcuts
  36. function handleKeydown(event) {
  37. if (event.altKey && !event.ctrlKey) {
  38. switch (event.key) {
  39. case TAB_SHORTCUT_KEY: // Alt + 5
  40. event.preventDefault();
  41. event.stopImmediatePropagation();
  42. clickSelectedTab();
  43. break;
  44. case COLOR_SHORTCUT_KEY: // Alt + 7
  45. event.preventDefault();
  46. event.stopImmediatePropagation();
  47. clickTextColorButton();
  48. break;
  49. case SCROLL_SHORTCUT_KEY: // Alt + 8
  50. event.preventDefault();
  51. event.stopImmediatePropagation();
  52. restoreScrollPosition();
  53. break;
  54. case '4': // Shortcut for #ff3333
  55. case '3': // Shortcut for #006057
  56. case '2': // Shortcut for #00a797
  57. case '1': // Shortcut for #000000
  58. event.preventDefault();
  59. event.stopPropagation();
  60. event.stopImmediatePropagation();
  61. const { rgb, hex } = COLOR_SHORTCUTS[event.key];
  62. setTimeout(() => clickColor(rgb, hex), 50);
  63. break;
  64. }
  65. }
  66. }
  67.  
  68. // Handle Alt + W separately (for tab switching)
  69. function handleAltWKey(event) {
  70. if (event.altKey && event.keyCode === TAB_SWITCH_KEY_CODE) { // Alt + W
  71. event.preventDefault();
  72. event.stopImmediatePropagation();
  73. clickLastSelectedTab();
  74. }
  75. }
  76.  
  77. // Handle Alt + G (Borders and Shading)
  78. function handleAltGKey(event) {
  79. if (event.altKey && event.keyCode === BORDER_SHADING_KEY_CODE) { // Alt + G
  80. event.preventDefault();
  81. event.stopImmediatePropagation();
  82. clickBordersAndShading();
  83. }
  84. }
  85.  
  86. // Attach the keydown event listener to both the top window and iframe
  87. function attachKeyListener() {
  88. const iframe = document.querySelector('iframe.docs-texteventtarget-iframe');
  89. if (iframe) {
  90. const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
  91. iframeDoc.addEventListener('keydown', handleKeydown, true);
  92. iframeDoc.addEventListener('keydown', handleAltWKey, true); // Attach Alt+W listener to iframe
  93. iframeDoc.addEventListener('keydown', handleAltGKey, true); // Attach Alt+G listener to iframe
  94. console.log('Key listener attached to iframe.');
  95. } else {
  96. console.log('Iframe not found. Retrying...');
  97. setTimeout(attachKeyListener, 1000); // Retry after 1 second
  98. }
  99.  
  100. window.addEventListener('keydown', handleKeydown, true); // Attach to top window
  101. window.addEventListener('keydown', handleAltWKey, true); // Attach Alt+W listener to top window
  102. window.addEventListener('keydown', handleAltGKey, true); // Attach Alt+G listener to top window
  103. console.log('Key listener attached to top window.');
  104. }
  105.  
  106. // Common Functions
  107.  
  108. // Function to simulate a click event (for tab, color button, and color pickers)
  109. function clickElement(element) {
  110. const mouseDown = new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window });
  111. const mouseUp = new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window });
  112. const clickEvt = new MouseEvent('click', { bubbles: true, cancelable: true, view: window });
  113.  
  114. element.dispatchEvent(mouseDown);
  115. element.dispatchEvent(mouseUp);
  116. element.dispatchEvent(clickEvt);
  117.  
  118. console.log('Simulated real mouse event sequence on', element);
  119. }
  120.  
  121. // Click on the selected tab (Alt + 5) - reverted to the original working selector
  122. function clickSelectedTab() {
  123. const tabElement = document.querySelector('.chapter-item-label-and-buttons-container[aria-selected="true"]');
  124. if (tabElement) {
  125. clickElement(tabElement);
  126. console.log('Tab clicked');
  127. } else {
  128. console.log('Tab element not found.');
  129. }
  130. }
  131.  
  132. // Click the text color button (Alt + 7)
  133. function clickTextColorButton() {
  134. const textColorButton = document.querySelector('div[aria-label="Text color"]'); // Updated selector
  135. if (textColorButton) {
  136. clickElement(textColorButton);
  137. console.log('Text color button clicked');
  138. } else {
  139. console.log('Text color button not found.');
  140. }
  141. }
  142.  
  143. // Simulate click on a color in the color picker (Alt+1, Alt+2, Alt+3, Alt+4)
  144. function clickColor(rgb, hex) {
  145. let colorElement;
  146. if (hex === '#000000') {
  147. colorElement = document.querySelector('td[aria-label="black"]'); // Special handling for black
  148. } else {
  149. colorElement = document.querySelector(`div[style*="${rgb}"]`); // Custom color selector
  150. }
  151.  
  152. if (colorElement) {
  153. clickElement(colorElement);
  154. console.log(`Simulated click on color ${hex}`);
  155. } else {
  156. console.log(`Color element for ${hex} not found.`);
  157. }
  158. }
  159.  
  160. // Function to click the "Borders and Shading" menu
  161. function clickBordersAndShading() {
  162. const bordersAndShadingButton = document.querySelector('span[aria-label="Borders and shading b'); // Updated selector
  163. if (bordersAndShadingButton) {
  164. clickElement(bordersAndShadingButton);
  165. console.log('Borders and shading menu clicked');
  166. } else {
  167. console.log('Borders and shading menu item not found.');
  168. }
  169. }
  170.  
  171. // Function to get the current document's unique identifier (URL)
  172. function getDocumentId() {
  173. const url = new URL(window.location.href); // Create a URL object from the current URL
  174. url.hash = ''; // Remove any fragment identifier (e.g., #heading=h.c7jmgehkx73h)
  175. return url.toString(); // Return the cleaned URL, including query parameters
  176. }
  177.  
  178. // Function to save the scroll position
  179. function saveScrollPosition() {
  180. const documentId = getDocumentId();
  181. const scrollableElement = document.querySelector('.kix-appview-editor');
  182. if (scrollableElement) {
  183. const scrollPosition = scrollableElement.scrollTop;
  184. const scrollData = JSON.parse(localStorage.getItem('googleDocsScrollData') || '{}');
  185. scrollData[documentId] = scrollPosition;
  186. localStorage.setItem('googleDocsScrollData', JSON.stringify(scrollData));
  187. console.log('Scroll position saved for document:', documentId, scrollPosition);
  188. }
  189. }
  190.  
  191. // Function to restore the scroll position
  192. function restoreScrollPosition() {
  193. const documentId = getDocumentId();
  194. const scrollData = JSON.parse(localStorage.getItem('googleDocsScrollData') || '{}');
  195. const scrollPosition = scrollData[documentId];
  196. const scrollableElement = document.querySelector('.kix-appview-editor');
  197. if (scrollableElement && scrollPosition !== undefined) {
  198. scrollableElement.scrollTo(0, parseInt(scrollPosition, 10));
  199. console.log('Scroll position restored for document:', documentId, scrollPosition);
  200. } else {
  201. console.log('No scroll position saved for this document.');
  202. }
  203. }
  204.  
  205. // Functions Related to Tab Selection
  206.  
  207. // Function to get all document tabs and subtabs
  208. function getTabsAndSubtabs() {
  209. const treeItems = document.querySelectorAll('[role="treeitem"]');
  210. return Array.from(treeItems).filter(item => {
  211. const ariaLabel = item.getAttribute('aria-label');
  212. return ariaLabel && !ariaLabel.toLowerCase().includes('level'); // Filter out headings
  213. });
  214. }
  215.  
  216. // Function to detect and update the current selected tab
  217. function getLastSelectedTab() {
  218. const selectedTab = document.querySelector('.chapter-item-label-and-buttons-container[aria-selected="true"]');
  219. if (selectedTab) {
  220. if (currentSelectedTab !== selectedTab) {
  221. lastSelectedTab = currentSelectedTab;
  222. }
  223. currentSelectedTab = selectedTab; // Update current selected tab
  224. console.log('Current selected tab:', selectedTab.getAttribute('aria-label')); // Debugging log
  225. } else {
  226. console.log('No tab is currently selected.');
  227. }
  228. }
  229.  
  230. // Function to simulate a click on the last selected tab
  231. function clickLastSelectedTab() {
  232. if (isTabSwitchInProgress) return; // Prevent switching if a switch is in progress (refractory period)
  233.  
  234. if (lastSelectedTab && lastSelectedTab !== currentSelectedTab) {
  235. console.log('Clicking on last selected tab:', lastSelectedTab.getAttribute('aria-label')); // Debugging log
  236. isTabSwitchInProgress = true; // Mark tab switch as in progress
  237. clickElement(lastSelectedTab); // Using the clickElement function from the first script
  238.  
  239. // Ensure focus is inside the iframe and the caret is active
  240. const iframe = document.querySelector('iframe.docs-texteventtarget-iframe');
  241. if (iframe) {
  242. const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
  243. iframe.focus(); // Ensure focus is inside the iframe
  244. iframeDoc.body.click(); // Simulate clicking on the body to ensure caret is active
  245. console.log('Focus set inside the document and caret activated!');
  246. }
  247.  
  248. // After the refractory period, allow the next tab switch
  249. setTimeout(() => {
  250. isTabSwitchInProgress = false;
  251. }, TAB_SWITCH_REFACTORY_PERIOD); // 500ms refractory period
  252. } else {
  253. console.log('No valid last selected tab found.');
  254. }
  255. }
  256.  
  257. // Initialization
  258.  
  259. // Function to initialize listeners and start updating the last selected tab
  260. function initialize() {
  261. console.log('Userscript loaded. Ready to detect shortcuts.');
  262.  
  263. // Update the last selected tab whenever the tab changes
  264. setInterval(getLastSelectedTab, 1000); // Update every 1 second
  265.  
  266. // Attach the key listener to detect Alt+W and Alt+G
  267. attachKeyListener();
  268. }
  269.  
  270. // Save scroll position before the page unloads
  271. window.addEventListener('beforeunload', saveScrollPosition);
  272.  
  273. // Start attaching listeners after the window loads
  274. window.addEventListener('load', initialize);
  275.  
  276. })();