- // ==UserScript==
- // @name Google Docs Shortcuts
- // @namespace http://tampermonkey.net/
- // @version 1.3
- // @license MIT
- // @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)
- // @match https://docs.google.com/*
- // @grant none
- // ==/UserScript==
-
- (function () {
- 'use strict';
-
- // Key combinations for color shortcuts
- const COLOR_SHORTCUTS = {
- '4': { rgb: 'rgb(255, 51, 51)', hex: '#ff3333' },
- '3': { rgb: 'rgb(0, 96, 87)', hex: '#006057' },
- '2': { rgb: 'rgb(0, 167, 151)', hex: '#00a797' },
- '1': { rgb: 'rgb(0, 0, 0)', hex: '#000000' }
- };
-
- // Key combinations for other shortcuts
- const TAB_SHORTCUT_KEY = '5'; // Alt + 5
- const COLOR_SHORTCUT_KEY = '7'; // Alt + 7
- const SCROLL_SHORTCUT_KEY = '8'; // Alt + 8
- const TAB_SWITCH_KEY_CODE = 87; // KeyCode for 'W' (Alt + W)
- const BORDER_SHADING_KEY_CODE = 71; // KeyCode for 'G' (Alt + G)
-
- // Constants for Tab Switching
- const TAB_SWITCH_REFACTORY_PERIOD = 500; // Time in milliseconds
- let lastSelectedTab = null;
- let currentSelectedTab = null;
- let isTabSwitchInProgress = false; // Variable to track whether a tab switch is in progress
-
- // Handle keydown events for various shortcuts
- function handleKeydown(event) {
- if (event.altKey && !event.ctrlKey) {
- switch (event.key) {
- case TAB_SHORTCUT_KEY: // Alt + 5
- event.preventDefault();
- event.stopImmediatePropagation();
- clickSelectedTab();
- break;
- case COLOR_SHORTCUT_KEY: // Alt + 7
- event.preventDefault();
- event.stopImmediatePropagation();
- clickTextColorButton();
- break;
- case SCROLL_SHORTCUT_KEY: // Alt + 8
- event.preventDefault();
- event.stopImmediatePropagation();
- restoreScrollPosition();
- break;
- case '4': // Shortcut for #ff3333
- case '3': // Shortcut for #006057
- case '2': // Shortcut for #00a797
- case '1': // Shortcut for #000000
- event.preventDefault();
- event.stopPropagation();
- event.stopImmediatePropagation();
- const { rgb, hex } = COLOR_SHORTCUTS[event.key];
- setTimeout(() => clickColor(rgb, hex), 50);
- break;
- }
- }
- }
-
- // Handle Alt + W separately (for tab switching)
- function handleAltWKey(event) {
- if (event.altKey && event.keyCode === TAB_SWITCH_KEY_CODE) { // Alt + W
- event.preventDefault();
- event.stopImmediatePropagation();
- clickLastSelectedTab();
- }
- }
-
- // Handle Alt + G (Borders and Shading)
- function handleAltGKey(event) {
- if (event.altKey && event.keyCode === BORDER_SHADING_KEY_CODE) { // Alt + G
- event.preventDefault();
- event.stopImmediatePropagation();
- clickBordersAndShading();
- }
- }
-
- // Attach the keydown event listener to both the top window and iframe
- function attachKeyListener() {
- const iframe = document.querySelector('iframe.docs-texteventtarget-iframe');
- if (iframe) {
- const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
- iframeDoc.addEventListener('keydown', handleKeydown, true);
- iframeDoc.addEventListener('keydown', handleAltWKey, true); // Attach Alt+W listener to iframe
- iframeDoc.addEventListener('keydown', handleAltGKey, true); // Attach Alt+G listener to iframe
- console.log('Key listener attached to iframe.');
- } else {
- console.log('Iframe not found. Retrying...');
- setTimeout(attachKeyListener, 1000); // Retry after 1 second
- }
-
- window.addEventListener('keydown', handleKeydown, true); // Attach to top window
- window.addEventListener('keydown', handleAltWKey, true); // Attach Alt+W listener to top window
- window.addEventListener('keydown', handleAltGKey, true); // Attach Alt+G listener to top window
- console.log('Key listener attached to top window.');
- }
-
- // Common Functions
-
- // Function to simulate a click event (for tab, color button, and color pickers)
- function clickElement(element) {
- const mouseDown = new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window });
- const mouseUp = new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window });
- const clickEvt = new MouseEvent('click', { bubbles: true, cancelable: true, view: window });
-
- element.dispatchEvent(mouseDown);
- element.dispatchEvent(mouseUp);
- element.dispatchEvent(clickEvt);
-
- console.log('Simulated real mouse event sequence on', element);
- }
-
- // Click on the selected tab (Alt + 5) - reverted to the original working selector
- function clickSelectedTab() {
- const tabElement = document.querySelector('.chapter-item-label-and-buttons-container[aria-selected="true"]');
- if (tabElement) {
- clickElement(tabElement);
- console.log('Tab clicked');
- } else {
- console.log('Tab element not found.');
- }
- }
-
- // Click the text color button (Alt + 7)
- function clickTextColorButton() {
- const textColorButton = document.querySelector('div[aria-label="Text color"]'); // Updated selector
- if (textColorButton) {
- clickElement(textColorButton);
- console.log('Text color button clicked');
- } else {
- console.log('Text color button not found.');
- }
- }
-
- // Simulate click on a color in the color picker (Alt+1, Alt+2, Alt+3, Alt+4)
- function clickColor(rgb, hex) {
- let colorElement;
- if (hex === '#000000') {
- colorElement = document.querySelector('td[aria-label="black"]'); // Special handling for black
- } else {
- colorElement = document.querySelector(`div[style*="${rgb}"]`); // Custom color selector
- }
-
- if (colorElement) {
- clickElement(colorElement);
- console.log(`Simulated click on color ${hex}`);
- } else {
- console.log(`Color element for ${hex} not found.`);
- }
- }
-
- // Function to click the "Borders and Shading" menu
- function clickBordersAndShading() {
- const bordersAndShadingButton = document.querySelector('span[aria-label="Borders and shading b'); // Updated selector
- if (bordersAndShadingButton) {
- clickElement(bordersAndShadingButton);
- console.log('Borders and shading menu clicked');
- } else {
- console.log('Borders and shading menu item not found.');
- }
- }
-
- // Function to get the current document's unique identifier (URL)
- function getDocumentId() {
- const url = new URL(window.location.href); // Create a URL object from the current URL
- url.hash = ''; // Remove any fragment identifier (e.g., #heading=h.c7jmgehkx73h)
- return url.toString(); // Return the cleaned URL, including query parameters
- }
-
- // Function to save the scroll position
- function saveScrollPosition() {
- const documentId = getDocumentId();
- const scrollableElement = document.querySelector('.kix-appview-editor');
- if (scrollableElement) {
- const scrollPosition = scrollableElement.scrollTop;
- const scrollData = JSON.parse(localStorage.getItem('googleDocsScrollData') || '{}');
- scrollData[documentId] = scrollPosition;
- localStorage.setItem('googleDocsScrollData', JSON.stringify(scrollData));
- console.log('Scroll position saved for document:', documentId, scrollPosition);
- }
- }
-
- // Function to restore the scroll position
- function restoreScrollPosition() {
- const documentId = getDocumentId();
- const scrollData = JSON.parse(localStorage.getItem('googleDocsScrollData') || '{}');
- const scrollPosition = scrollData[documentId];
- const scrollableElement = document.querySelector('.kix-appview-editor');
- if (scrollableElement && scrollPosition !== undefined) {
- scrollableElement.scrollTo(0, parseInt(scrollPosition, 10));
- console.log('Scroll position restored for document:', documentId, scrollPosition);
- } else {
- console.log('No scroll position saved for this document.');
- }
- }
-
- // Functions Related to Tab Selection
-
- // Function to get all document tabs and subtabs
- function getTabsAndSubtabs() {
- const treeItems = document.querySelectorAll('[role="treeitem"]');
- return Array.from(treeItems).filter(item => {
- const ariaLabel = item.getAttribute('aria-label');
- return ariaLabel && !ariaLabel.toLowerCase().includes('level'); // Filter out headings
- });
- }
-
- // Function to detect and update the current selected tab
- function getLastSelectedTab() {
- const selectedTab = document.querySelector('.chapter-item-label-and-buttons-container[aria-selected="true"]');
- if (selectedTab) {
- if (currentSelectedTab !== selectedTab) {
- lastSelectedTab = currentSelectedTab;
- }
- currentSelectedTab = selectedTab; // Update current selected tab
- console.log('Current selected tab:', selectedTab.getAttribute('aria-label')); // Debugging log
- } else {
- console.log('No tab is currently selected.');
- }
- }
-
- // Function to simulate a click on the last selected tab
- function clickLastSelectedTab() {
- if (isTabSwitchInProgress) return; // Prevent switching if a switch is in progress (refractory period)
-
- if (lastSelectedTab && lastSelectedTab !== currentSelectedTab) {
- console.log('Clicking on last selected tab:', lastSelectedTab.getAttribute('aria-label')); // Debugging log
- isTabSwitchInProgress = true; // Mark tab switch as in progress
- clickElement(lastSelectedTab); // Using the clickElement function from the first script
-
- // Ensure focus is inside the iframe and the caret is active
- const iframe = document.querySelector('iframe.docs-texteventtarget-iframe');
- if (iframe) {
- const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
- iframe.focus(); // Ensure focus is inside the iframe
- iframeDoc.body.click(); // Simulate clicking on the body to ensure caret is active
- console.log('Focus set inside the document and caret activated!');
- }
-
- // After the refractory period, allow the next tab switch
- setTimeout(() => {
- isTabSwitchInProgress = false;
- }, TAB_SWITCH_REFACTORY_PERIOD); // 500ms refractory period
- } else {
- console.log('No valid last selected tab found.');
- }
- }
-
- // Initialization
-
- // Function to initialize listeners and start updating the last selected tab
- function initialize() {
- console.log('Userscript loaded. Ready to detect shortcuts.');
-
- // Update the last selected tab whenever the tab changes
- setInterval(getLastSelectedTab, 1000); // Update every 1 second
-
- // Attach the key listener to detect Alt+W and Alt+G
- attachKeyListener();
- }
-
- // Save scroll position before the page unloads
- window.addEventListener('beforeunload', saveScrollPosition);
-
- // Start attaching listeners after the window loads
- window.addEventListener('load', initialize);
-
- })();