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 提交的版本,檢視 最新版本

// ==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);

})();