Google Calendar Add "Duplicate" Menu Item

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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 });
})();