您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
從 Microsoft To Do 詳細面板擷取標題、所有步驟與附註為純文字一鍵複製,按鈕可拖曳(自動隱藏圖示)
// ==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 {} }); })();