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