---
name: 文本高亮块 (UI版)
description: 悬浮面板:在文本编辑模式下,为选中的文字生成指定颜色的底层高亮块。
author: ymjr
version: 1.0.0
license: MIT
usage: 运行此脚本以唤起“背景高亮工具”悬浮面板。进入文本编辑状态并选中部分文字,在面板上使用取色器选择颜色,点击生成按钮,即可在选中的文字底层自动铺设对应颜色的矩形高亮色块。
features:
- 包含悬浮 DOM 面板与系统取色器
- 结合 Canvas `measureText` 实现跨行多段文本高亮块的自动物理位置对齐与生成
dependencies:
- 无依赖
autorun: false
---
/*
```javascript
*/
var locales = {
zh: {
notice_select: "请先在文本编辑状态下选中文字片段!",
ui_title: "🖍️ 背景高亮工具",
ui_apply: "生成高亮块"
},
en: {
notice_select: "Please select text in edit mode first!",
ui_title: "🖍️ Background Highlighter",
ui_apply: "Generate Highlight"
}
};
const { Notice } = ea.obsidian;
const api = ea.getExcalidrawAPI();
function applyHighlight(color) {
const editingTextElement = api.getAppState().editingTextElement;
const editable = ea.targetView.contentEl.querySelector("textarea.excalidraw-wysiwyg");
if (!editingTextElement || !editable || editable.selectionStart === editable.selectionEnd) {
new Notice(t("notice_select"));
return;
}
const canvas = ea.targetView.contentEl.querySelector('canvas.excalidraw__canvas.interactive');
const ctx = canvas.getContext("2d");
ea.style.strokeColor = "transparent";
ea.style.backgroundColor = color;
ea.style.fillStyle = "solid";
ea.style.roughness = 0;
ea.style.roundness = { type: 3 };
ctx.font = `${editingTextElement.fontSize}px ${ea.getFontFamily(editingTextElement.fontFamily)}`;
let text_lines = editingTextElement.text.split("\n");
const lineHeight = editingTextElement.height / text_lines.length;
let currentPosSum = 0;
for (let i = 0; i < text_lines.length; i++) {
const lineLength = text_lines[i].length;
if (editable.selectionStart < currentPosSum + lineLength && editable.selectionEnd > currentPosSum) {
let startRel = Math.max(0, editable.selectionStart - currentPosSum);
let endRel = Math.min(lineLength, editable.selectionEnd - currentPosSum);
let xOffset = startRel > 0 ? ctx.measureText(text_lines[i].substring(0, startRel)).width : 0;
const highlightWidth = ctx.measureText(text_lines[i].substring(startRel, endRel)).width;
ea.addRect(editingTextElement.x + xOffset, editingTextElement.y + (i * lineHeight), highlightWidth, lineHeight);
}
currentPosSum += (lineLength + 1);
if (currentPosSum > editable.selectionEnd) break;
}
ea.addElementsToView(false, false, false);
setTimeout(() => editable.focus(), 50);
}
function createPanel() {
if (document.getElementById("ea-highlight-bg-panel")) return;
const panel = document.createElement('div');
panel.id = "ea-highlight-bg-panel";
panel.style.cssText = `position:fixed; top:200px; right:150px; width:220px; background:var(--background-primary); border:1px solid var(--background-modifier-border); box-shadow:0 8px 24px rgba(0,0,0,0.2); border-radius:8px; z-index:9999; display:flex; flex-direction:column;`;
const header = document.createElement('div');
header.style.cssText = `padding:8px 12px; background:var(--background-secondary); cursor:move; border-bottom:1px solid var(--background-modifier-border); display:flex; justify-content:space-between; align-items:center; user-select:none; border-radius:8px 8px 0 0;`;
header.innerHTML = `${t("ui_title")}`;
panel.appendChild(header);
const content = document.createElement('div');
content.style.cssText = `padding:15px; display:flex; justify-content:space-between; align-items:center; gap:10px;`;
const colorPicker = document.createElement('input');
colorPicker.type = "color";
colorPicker.value = "#f8961e";
colorPicker.style.cssText = "width:35px; height:35px; padding:0; border:none; cursor:pointer; background:none;";
const btnApply = document.createElement('button');
btnApply.innerText = t("ui_apply");
btnApply.style.cssText = "flex:1; padding:6px; background:var(--interactive-accent); color:var(--text-on-accent); border:none; border-radius:4px; cursor:pointer;";
btnApply.onmousedown = (e) => { e.preventDefault(); applyHighlight(colorPicker.value); };
content.appendChild(colorPicker);
content.appendChild(btnApply);
panel.appendChild(content);
document.body.appendChild(panel);
header.querySelector('#close-btn').onclick = () => panel.remove();
let isDragging = false, startX, startY, initLeft, initTop;
header.onmousedown = (e) => {
if(e.target.tagName === 'BUTTON') return;
isDragging = true; startX = e.clientX; startY = e.clientY;
const rect = panel.getBoundingClientRect(); initLeft = rect.left; initTop = rect.top;
e.preventDefault();
document.onmousemove = (e) => {
if(!isDragging) return;
panel.style.left = (initLeft + e.clientX - startX) + 'px';
panel.style.top = (initTop + e.clientY - startY) + 'px';
};
document.onmouseup = () => { isDragging = false; document.onmousemove = null; document.onmouseup = null; };
};
}
createPanel();