--- name: Checkbox 核心引擎 description: 后台引擎:提供 Shift+悬停 时的手型指针、浮动提示,以及 Shift+点击 的状态切换。 author: ymjr version: 1.0.0 license: MIT usage: 后台常驻引擎,负责寻找并操作带有 customData.isCheckbox 标记的文本元素。 features: - 拦截 handleCanvasPointerMove 实现 Shift+鼠标悬停 自动变手型并展示 HTML 浮动提示 - 拦截 handleCanvasPointerDown (基于 move 缓存的元素) 实现 Shift+点击 自动切换 dependencies: - 配合 Action-Toggle-Checkbox 动作脚本使用 autorun: true --- /* ```javascript */ var locales = { zh: { tooltip_tip: "✨ Shift + 点击 切换状态", log_unmounted: "[{id}] 🔌 已卸载", log_mounted: "[{id}] 🚀 挂载完毕" }, en: { tooltip_tip: "✨ Shift + Click to toggle state", log_unmounted: "[{id}] 🔌 Unmounted", log_mounted: "[{id}] 🚀 Mounted successfully" } }; const SCRIPT_ID = "ymjr.feature.checkbox-engine"; // 安全获取或初始化插件级别的状态空间 (不污染 window) const getState = (plugin) => { if (!plugin._ymjr_checkbox_state) { plugin._ymjr_checkbox_state = { hoveredElement: null, ui: {} }; } return plugin._ymjr_checkbox_state; }; // 动态创建并管理现代化的鼠标跟随 Tooltip const getOrCreateTooltip = (state) => { if (!state.ui.tooltip) { const tooltip = document.createElement("div"); tooltip.style.cssText = ` position: fixed; pointer-events: none; background: rgba(0, 0, 0, 0.75); color: #fff; padding: 6px 12px; border-radius: 6px; font-size: 12px; font-family: sans-serif; box-shadow: 0 4px 12px rgba(0,0,0,0.15); backdrop-filter: blur(4px); z-index: 99999; opacity: 0; transition: opacity 0.2s ease; white-space: nowrap; `; document.body.appendChild(tooltip); state.ui.tooltip = tooltip; } return state.ui.tooltip; }; // Hook:处理鼠标悬停 UX (改变光标、浮动提示,并缓存命中元素) const handlePointerMove = (context) => { const { ea, api, App, hitElement, event } = context; if (!ea || !api || !App) return false; const state = getState(ExcalidrawAutomate.plugin); // 关键修复:将 move 时捕获的元素缓存起来,供 pointerDown 使用 state.hoveredElement = hitElement; const tooltip = getOrCreateTooltip(state); if (event.shiftKey && hitElement?.type === "text" && hitElement.customData?.isCheckbox) { if (App.interactiveCanvas) { App.interactiveCanvas.style.cursor = "pointer"; } tooltip.innerHTML = t("tooltip_tip"); tooltip.style.left = `${event.clientX + 15}px`; tooltip.style.top = `${event.clientY + 15}px`; tooltip.style.opacity = "1"; } else { tooltip.style.opacity = "0"; } return false; }; // Hook:处理 Shift+点击 的状态切换逻辑 const handlePointerDown = (context) => { const { ea, api, event } = context; if (!ea || !api) return false; const state = getState(ExcalidrawAutomate.plugin); // 关键修复:从 state 缓存中拿 hitElement const hitElement = state.hoveredElement; if (event.shiftKey && hitElement?.type === "text" && hitElement.customData?.isCheckbox) { let text = hitElement.rawText; let newText = text; if (text.startsWith("- [ ]")) { newText = text.replace("- [ ]", "- [x]"); } else if (text.startsWith("- [x]")) { newText = text.replace("- [x]", "- [ ]"); } else { return false; } ea.copyViewElementsToEAforEditing([hitElement]); const editedEl = ea.getElements()[0]; editedEl.text = newText; editedEl.rawText = newText; ea.addElementsToView(false, false, false); return false; } return false; }; async function mountFeature() { if (!window.EA_Core) return console.warn("[Checkbox Engine] EA_Core 未运行"); if (typeof ExcalidrawAutomate.plugin[`disable_${SCRIPT_ID}`] === "function") { ExcalidrawAutomate.plugin[`disable_${SCRIPT_ID}`](); } window.EA_Core.registerHook(SCRIPT_ID, 'handleCanvasPointerMove', handlePointerMove, 60); window.EA_Core.registerHook(SCRIPT_ID, 'handleCanvasPointerDown', handlePointerDown, 60); ExcalidrawAutomate.plugin[`disable_${SCRIPT_ID}`] = () => { if (window.EA_Core) { window.EA_Core.unregisterHook(SCRIPT_ID); } const state = ExcalidrawAutomate.plugin._ymjr_checkbox_state; if (state && state.ui && state.ui.tooltip && state.ui.tooltip.parentNode) { state.ui.tooltip.parentNode.removeChild(state.ui.tooltip); delete state.ui.tooltip; } // 清理缓存以防内存泄露 ExcalidrawAutomate.plugin._ymjr_checkbox_state = null; // 替换字符串变量模拟模板语法 console.log(t("log_unmounted").replace("{id}", SCRIPT_ID)); }; console.log(t("log_mounted").replace("{id}", SCRIPT_ID)); } mountFeature();