Copy Toggl report totals to clipboard latest version (currently v1.0.1)

← Back to User Scripts

Script Content

// ==UserScript==
// @name         Toggl: Copy Report Totals
// @namespace    https://www.timhilton.xyz/user-scripts
// @version      1.0.1
// @description  Adds copy buttons to each metric container in Toggl reports to copy the totals
// @author       Tim Hilton using Copilot
// @match        https://track.toggl.com/reports/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const LOG_PREFIX = '[Toggl: Copy Report Totals]';

    const COPY_ICON_SVG = `

`;

    const TICK_ICON_SVG = `

    

`;

    const injectStyles = () => {
        console.debug(`${LOG_PREFIX} Injecting styles`);
        
        const style = document.createElement('style');
        style.textContent = `
            [class*="MetricContainer"] {
                position: relative;
            }
            .toggl-copy-icon {
                border-radius: 4px;
                transition: background-color 0.25s ease-in-out;
                cursor: pointer;
                display: inline-flex;
                align-items: center;
                justify-content: center;
                width: 24px;
                height: 24px;
                position: absolute;
                right: 8px;
                top: 50%;
                transform: translateY(-50%);
            }
            .toggl-copy-icon:hover {
                background-color: rgba(0, 0, 0, 0.1);
            }
            .toggl-copy-icon svg path {
                fill: currentColor;
            }
        `;
        document.head.appendChild(style);
    };

    const handleCopyClick = (icon, text) => {
        console.debug(`${LOG_PREFIX} Copy button clicked for: ${text}`);
        
        navigator.clipboard.writeText(text).then(() => {
            const originalContent = icon.innerHTML;
            icon.innerHTML = TICK_ICON_SVG;
            console.log(`${LOG_PREFIX} ✅ Text copied: ${text}`);
            setTimeout(() => {
                if (document.body.contains(icon)) {
                    icon.innerHTML = originalContent;
                }
            }, 1000);
        }).catch(err => {
            console.log(`${LOG_PREFIX} ❌ Failed to copy text: ${err}`);
        });
    };

    const createCopyButton = (text) => {
        const button = document.createElement('span');
        button.innerHTML = COPY_ICON_SVG;
        button.classList.add('toggl-copy-icon');
        button.setAttribute('title', 'Copy to clipboard');
        button.addEventListener('click', () => handleCopyClick(button, text));
        return button;
    };

    const setupCopyButtons = () => {
        console.debug(`${LOG_PREFIX} Setting up copy buttons`);
        
        // Find all MetricContainer elements by looking for elements whose class contains "MetricContainer"
        const metricContainers = Array.from(document.querySelectorAll('[class*="MetricContainer"]'));
        
        console.debug(`${LOG_PREFIX} Found ${metricContainers.length} metric containers`);
        
        let addedCount = 0;
        metricContainers.forEach(container => {
            // Skip if we've already added a button to this container
            if (container.querySelector('.toggl-copy-icon')) {
                return;
            }

            // Find the ValueContainer element within the MetricContainer
            const valueContainer = container.querySelector('[class*="ValueContainer"]');
            if (!valueContainer) {
                return;
            }

            // Get the text content, handling cases where there might be nested elements
            const text = valueContainer.textContent.trim();
            if (!text) {
                return;
            }

            // Create and append the copy button to the container (not the valueContainer)
            const copyButton = createCopyButton(text);
            container.appendChild(copyButton);
            addedCount++;
        });
        
        if (addedCount > 0) {
            console.log(`${LOG_PREFIX} ✅ Added ${addedCount} copy buttons`);
        }
    };

    // Inject styles
    injectStyles();

    // Initial setup
    console.debug(`${LOG_PREFIX} Initializing script`);
    setupCopyButtons();

    // Use MutationObserver to handle dynamic content
    let observerFireCount = 0;
    const observer = new MutationObserver(() => {
        observerFireCount++;
        console.debug(`${LOG_PREFIX} MutationObserver fired (count: ${observerFireCount})`);
        setupCopyButtons();
    });

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

    console.log(`${LOG_PREFIX} 🎉 Script initialized successfully`);
})();