Google Calendar Add "Duplicate" Menu Item

Injects a "Duplicate" option into Google Calendar's right-click popup menu

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Google Calendar Add "Duplicate" Menu Item
// @namespace    http://tampermonkey.net/
// @version      1.3.1
// @description  Injects a "Duplicate" option into Google Calendar's right-click popup menu
// @author       desk195
// @match        https://calendar.google.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

// Note, this was made with Chat-GPT...
// This injects a "Duplicate" list element to the google calendar right click menu
// And opens a new link to the google calendar duplicate functionality
// it still requires the user to manually save the event
// but at least saves a few clicks to get to that menu

(function() {
    'use strict';

    console.log('Google Calendar "Duplicate" Menu Injector loaded.');


    // Fetch and store the current google account user index
    const url = window.location.href;

    // Match "u/<number>" in the URL
    const match = url.match(/\/u\/(\d+)\//);

    if (match && match[1] !== undefined) {
        const currentUserIndex = parseInt(match[1], 10);
        console.log('Current Google account index:', currentUserIndex);
        // store globally
        window.currentGoogleAccountIndex = currentUserIndex;
    } else {
        console.log('No user index found in URL, defaulting to 0');
        window.currentGoogleAccountIndex = 0;
    }

    // --- 1. Capture the event's data-eventid on right-click ---
    document.addEventListener('contextmenu', function(e) {
        const eventElement = e.target.closest('[data-eventid]');
        if (eventElement) {
            window.lastRightClickedEventId = eventElement.getAttribute('data-eventid');
            console.log('📌 Captured event ID:', window.lastRightClickedEventId);
        } else {
            window.lastRightClickedEventId = null;
        }
    }, true);

    // Function to create and insert the new "Duplicate" menu item
    function injectDuplicateItem(ul) {
        // Avoid duplicates if it’s already been injected
        if (ul.querySelector('.tm-duplicate-item')) return;

        // Create a separator element matching Google Calendar’s style
        const separator = document.createElement('li');
        separator.setAttribute('role', 'separator');
        separator.className = 'aqdrmf-clz4Ic aqdrmf-clz4Ic-OWXEXe-Vkfede O68mGe-xl07Ob-clz4Ic ugNmBf';
        ul.insertBefore(separator, ul.firstChild);

        // Create the new "Duplicate" menu item
        const newItem = document.createElement('li');
        newItem.className = 'tm-duplicate-item'; // unique class for our item
        newItem.style.cursor = 'pointer';
        newItem.style.padding = '8px 12px';
        newItem.style.userSelect = 'none';
        newItem.style.borderTop = '0px solid rgba(0,0,0,0.1)';
        newItem.innerText = 'Duplicate';

        // Hover feedback (simple styling to match menu behavior)
        newItem.addEventListener('mouseenter', () => {
            newItem.style.backgroundColor = '#D9D9D9';
        });
        newItem.addEventListener('mouseleave', () => {
            newItem.style.backgroundColor = '';
        });

        // Add a click handler — for now, just logs (you can replace this with your logic)
        newItem.addEventListener('click', async e => {
            e.stopPropagation();
            e.preventDefault();

            // get event ID from a right-clicked element if you stored it
            const eventId = window.lastRightClickedEventId;
            if (!eventId) {
                alert('No event selected to duplicate.');
                return;
            }

            // Open the event’s duplication page using a documented Calendar URL pattern
            // fetch current user account index
            const currentUser = window.currentGoogleAccountIndex;

            const duplicateUrl = `https://calendar.google.com/calendar/u/${currentUser}/r/eventedit/duplicate/${eventId}`;
            window.open(duplicateUrl, '_self');
        });  

        // Insert the new item above the menu
        ul.insertBefore(newItem, ul.firstChild);
        console.log('✅ "Duplicate" menu item injected.');
    }

    // Observe for the popup menu being added to the DOM
    const observer = new MutationObserver(mutations => {
        for (const mutation of mutations) {
            for (const node of mutation.addedNodes) {
                if (!(node instanceof HTMLElement)) continue;

                // Detect the UL menu
                if (node.matches('ul.aqdrmf-rymPhb.pa1Qpd')) {
                    injectDuplicateItem(node);
                } else {
                    const menu = node.querySelector('ul.aqdrmf-rymPhb.pa1Qpd');
                    if (menu) injectDuplicateItem(menu);
                }
            }
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });
})();