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.0
// @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 COPY_ICON_SVG = `
`;
const TICK_ICON_SVG = `
`;
const injectStyles = () => {
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) => {
navigator.clipboard.writeText(text).then(() => {
const originalContent = icon.innerHTML;
icon.innerHTML = TICK_ICON_SVG;
setTimeout(() => {
if (document.body.contains(icon)) {
icon.innerHTML = originalContent;
}
}, 1000);
}).catch(err => {
console.error('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 = () => {
// Find all MetricContainer elements by looking for elements whose class contains "MetricContainer"
const metricContainers = Array.from(document.querySelectorAll('[class*="MetricContainer"]'));
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);
});
};
// Inject styles
injectStyles();
// Initial setup
setupCopyButtons();
// Use MutationObserver to handle dynamic content
const observer = new MutationObserver(() => {
setupCopyButtons();
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
console.log('Toggl Copy Report Totals: Initialized');
})();