Toggl Copy Report Totals v1.0.0
This script adds copy buttons to each metric container in Toggl reports (total hours, billable hours, amount, average daily hours), allowing you to quickly copy the totals to your clipboard with a single click. The script uses robust class name matching that ignores generated hash parts for compatibility with Toggl's dynamic CSS.
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`);
})();