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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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

})();