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`);
})();