// ==UserScript==
// @name Microsoft To Do 一鍵複製:標題/步驟/附註
// @namespace http://tampermonkey.net/
// @version 1.5.3
// @description 從 Microsoft To Do 詳細面板擷取標題、所有步驟與附註為純文字一鍵複製,按鈕可拖曳(自動隱藏圖示)
// @author shanlan(gpt-5)
// @match https://to-do.office.com/tasks/*
// @grant none
// @run-at document-end
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
const HOST_ID = 'tm-copy-root', POS_KEY = 'tm-copy-root-pos', DONE_KEY = 'tm-copy-done-mark';
let hostEl, shadow, btn, toastEl, switchEl, doneMark = true;
let wrapEl;
function findDetailsRoot() {
return document.getElementById('details') || $('.rightColumn #details') || $('.details') || null;
}
function getCleanTextFromQLEditor(editor) {
const blocks = Array.from(editor.children), out = [];
blocks.forEach(block => {
if (block.childNodes.length === 1 && (block.childNodes[0].nodeName === 'BR' || (block.childNodes[0].nodeType === 3 && block.childNodes[0].textContent.trim() === ''))) {
out.push('');
return;
}
let line = Array.from(block.childNodes)
.map(node => node.nodeType === 3 ? node.textContent : node.nodeType === 1 && node.tagName === 'A' ? node.href : '')
.join('').replace(/\u00A0/g, ' ').trimEnd();
out.push(line);
});
return out.join('\n');
}
function buildExportText(details) {
const titleEl = details.querySelector('.editableContent-display[title]') || details.querySelector('.detailHeader .editableContent-display'),
title = (titleEl?.getAttribute('title') || titleEl?.textContent || '').trim();
const stepTextareas = Array.from(details.querySelectorAll('textarea[aria-label="步驟"]'));
const steps = stepTextareas.map(textarea => {
let row = textarea.closest('.ms-DetailsRow');
let checked = row?.querySelector('[role="checkbox"]')?.getAttribute('aria-checked');
let text = textarea.value != null ? textarea.value : textarea.textContent || '';
text = text.replace(/\r\n/g, '\n').trimEnd();
if (checked === "true" && doneMark) {
text = '[已完成]' + text;
}
return text;
}).filter(s => s.length > 0);
const noteEl = details.querySelector('.detailNote .ql-editor') || details.querySelector('.ql-editor[contenteditable="true"]');
let note = noteEl ? getCleanTextFromQLEditor(noteEl) : '';
const parts = [];
if (title) parts.push(title);
if (title && steps.length) parts.push('');
if (steps.length) parts.push(steps.join('\n'));
let out = parts.join('\n');
if (note) out += (out ? '\n\n' : '') + note;
return out.trim();
}
async function copyToClipboard(text) {
if (navigator.clipboard && window.isSecureContext) return navigator.clipboard.writeText(text);
const ta = document.createElement('textarea');
ta.value = text;
ta.setAttribute('readonly', '');
Object.assign(ta.style, {position: 'fixed', top: '-9999px', left: '-9999px'});
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); } finally { document.body.removeChild(ta); }
}
function applySavedPos(host) {
const s = localStorage.getItem(POS_KEY);
if (!s) return;
try {
const { left, top } = JSON.parse(s) || {};
if (Number.isFinite(left) && Number.isFinite(top)) {
host.style.removeProperty('right');
host.style.removeProperty('bottom');
host.style.setProperty('left', left + 'px', 'important');
host.style.setProperty('top', top + 'px', 'important');
}
} catch {}
}
function ensureUI() {
let host = document.getElementById(HOST_ID);
if (!host) {
host = document.createElement('div');
host.id = HOST_ID;
document.body.appendChild(host);
host.style.setProperty('position', 'fixed', 'important');
host.style.setProperty('right', '20px', 'important');
host.style.setProperty('bottom', '20px', 'important');
host.style.setProperty('z-index', '2147483647', 'important');
host.style.setProperty('display', 'inline-block', 'important');
host.style.setProperty('pointer-events', 'none', 'important');
host.style.setProperty('width', 'auto', 'important');
host.style.setProperty('height', 'auto', 'important');
hostEl = host;
shadow = host.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = `
:host{ all: initial; }
#wrap { all: initial; position: relative; display: inline-flex; align-items: center; pointer-events: auto; touch-action: none; gap: 5px; }
#copyBtn{
all: unset;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: #0078d4;
color: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,.2);
cursor: grab;
font: 600 13px/1.2 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans TC",Arial,"Helvetica Neue",Helvetica,sans-serif;
user-select: none;
white-space: nowrap;
max-width: none;
}
#copyBtn:hover{ background:#106ebe; }
#copyBtn:active{ transform: translateY(1px); }
#copyBtn.dragging{ cursor: grabbing !important; transform: none; }
#switch {
all: unset;
display: inline-flex;
align-items: center;
cursor: pointer;
user-select: none;
margin-left: 2px;
position: relative;
}
#switch-toggle {
width: 32px; height: 18px; border-radius: 9px;
background: #ccc;
position: relative;
transition: background .2s;
margin-right: 3px;
}
#switch-toggle[data-on="1"] { background: #0078d4; }
#switch-knob {
position: absolute;
top: 2px; left: 2px;
width: 14px; height: 14px;
border-radius: 50%;
background: #fff;
box-shadow: 0 1px 4px rgba(0,0,0,.15);
transition: left .2s;
}
#switch-toggle[data-on="1"] #switch-knob { left: 16px; }
#switch-tooltip {
display: none;
position: absolute;
left: 50%; top: -200%;
transform: translateX(-50%);
background: #222;
color: #fff;
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
white-space: nowrap;
z-index: 99;
pointer-events: none;
opacity: 0;
transition: opacity .18s;
}
#switch:hover #switch-tooltip, #switch:focus-within #switch-tooltip {
display: block;
opacity: 1;
}
#toast{
all: unset;
position: absolute;
right: 0;
bottom: 34px;
background: rgba(0,0,0,.85);
color: #fff;
padding: 8px 10px;
border-radius: 6px;
font: 500 12px/1.2 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans TC",Arial,"Helvetica Neue",Helvetica,sans-serif;
opacity: 0;
transition: opacity .2s ease;
pointer-events: none;
}
`;
const wrap = document.createElement('div');
wrap.id = 'wrap';
wrapEl = wrap;
btn = document.createElement('button');
btn.id = 'copyBtn';
btn.innerHTML = '📋複製';
switchEl = document.createElement('span');
switchEl.id = 'switch';
switchEl.innerHTML = `
<span id="switch-toggle" data-on="1">
<span id="switch-knob"></span>
</span>
<span id="switch-tooltip">標註[已完成]</span>
`;
toastEl = document.createElement('div');
toastEl.id = 'toast';
wrap.appendChild(btn);
wrap.appendChild(switchEl);
wrap.appendChild(toastEl);
shadow.appendChild(style);
shadow.appendChild(wrap);
applySavedPos(hostEl);
setupDragAndClick();
setupSwitch();
}
}
function setupSwitch() {
if (!switchEl) return;
doneMark = localStorage.getItem(DONE_KEY) !== '0';
updateSwitchUI();
switchEl.addEventListener('click', e => {
doneMark = !doneMark;
localStorage.setItem(DONE_KEY, doneMark ? '1' : '0');
updateSwitchUI();
e.stopPropagation();
});
}
function updateSwitchUI() {
const toggle = switchEl.querySelector('#switch-toggle');
if (doneMark) {
toggle.setAttribute('data-on', '1');
} else {
toggle.setAttribute('data-on', '0');
}
}
function showToast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.style.opacity = '1';
clearTimeout(showToast._t);
showToast._t = setTimeout(() => { toastEl.style.opacity = '0'; }, 1600);
}
async function runCopy() {
const details = findDetailsRoot();
if (!details) { showToast('未找到 To Do 詳細面板'); return; }
const text = buildExportText(details);
if (!text) { showToast('沒有可複製的內容'); return; }
try {
await copyToClipboard(text);
const lines = text.split('\n').length;
showToast(`已複製 ${lines} 行`);
console.log('[一鍵複製輸出]\n' + text);
} catch (e) {
console.error('複製失敗:', e);
showToast('複製失敗,請檢查主控台');
}
}
let dragging = false, dragMoved = false, dragOffsetX = 0, dragOffsetY = 0, pointerId = null;
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
function setupDragAndClick() {
if (!btn) return;
btn.addEventListener('click', e => {
if (dragMoved) { e.preventDefault(); e.stopImmediatePropagation(); dragMoved = false; return; }
runCopy();
});
btn.addEventListener('pointerdown', onPointerDown);
}
function onPointerDown(e) {
if (e.pointerType === 'mouse' && e.button !== 0) return;
pointerId = e.pointerId != null ? e.pointerId : null;
dragging = true; dragMoved = false;
const rect = hostEl.getBoundingClientRect();
hostEl.style.removeProperty('right');
hostEl.style.removeProperty('bottom');
hostEl.style.setProperty('left', rect.left + 'px', 'important');
hostEl.style.setProperty('top', rect.top + 'px', 'important');
dragOffsetX = e.clientX - rect.left;
dragOffsetY = e.clientY - rect.top;
try { btn.setPointerCapture(pointerId); } catch {}
btn.classList.add('dragging');
document.addEventListener('pointermove', onPointerMove);
document.addEventListener('pointerup', onPointerUp, { once: false });
document.addEventListener('pointercancel', onPointerUp, { once: false });
}
function onPointerMove(e) {
if (!dragging) return;
const w = hostEl.offsetWidth, h = hostEl.offsetHeight;
let x = e.clientX - dragOffsetX, y = e.clientY - dragOffsetY, margin = 6;
x = clamp(x, margin, Math.max(margin, window.innerWidth - w - margin));
y = clamp(y, margin, Math.max(margin, window.innerHeight - h - margin));
hostEl.style.setProperty('left', x + 'px', 'important');
hostEl.style.setProperty('top', y + 'px', 'important');
if (!dragMoved) { const dx = Math.abs(e.movementX || 0), dy = Math.abs(e.movementY || 0); if (dx + dy > 1) dragMoved = true; }
}
function onPointerUp() {
if (!dragging) return;
dragging = false;
try { btn.releasePointerCapture(pointerId); } catch {}
btn.classList.remove('dragging');
document.removeEventListener('pointermove', onPointerMove);
document.removeEventListener('pointerup', onPointerUp);
document.removeEventListener('pointercancel', onPointerUp);
const rect = hostEl.getBoundingClientRect();
try { localStorage.setItem(POS_KEY, JSON.stringify({ left: rect.left, top: rect.top })); } catch {}
}
ensureUI();
function updatePanelVisibility() {
const details = findDetailsRoot();
if (details && details.offsetWidth > 0 && details.offsetHeight > 0) {
wrapEl.style.display = 'inline-flex';
} else {
wrapEl.style.display = 'none';
}
}
setInterval(updatePanelVisibility, 100);
document.addEventListener('keydown', e => {
const key = (e.key || '').toLowerCase();
if ((e.ctrlKey || e.metaKey) && e.shiftKey && key === 'c') { e.preventDefault(); runCopy(); }
});
window.addEventListener('resize', () => {
if (!hostEl) return;
const rect = hostEl.getBoundingClientRect(), w = hostEl.offsetWidth, h = hostEl.offsetHeight, margin = 6;
let x = rect.left, y = rect.top;
x = clamp(x, margin, Math.max(margin, window.innerWidth - w - margin));
y = clamp(y, margin, Math.max(margin, window.innerHeight - h - margin));
hostEl.style.setProperty('left', x + 'px', 'important');
hostEl.style.setProperty('top', y + 'px', 'important');
try { localStorage.setItem(POS_KEY, JSON.stringify({ left: x, top: y })); } catch {}
});
})();