
排除了 pdf 界面(因为某些字参差不齐的选区会反复创建卡顿严重)

(function() {
// 检查是否已在运行,避免重复注入
if (window.customSelectionInitialized) {
return;
}
window.customSelectionInitialized = true;
// 1. 动态注入 CSS (与之前类似,但增加了对 .protyle-wysiwyg 的 position 设置)
const css = `
.protyle-wysiwyg {
position: relative;
}
::selection {
background-color: transparent!important;
color: inherit;
}
textarea::selection,
input::selection {
background-color: var(--b3-theme-primary-lightest, rgba(0, 120, 212, 0.2))!important;
color: inherit;
}
.custom-selection-marker {
position: absolute;
width: 2px;
background-color: var(--b3-theme-primary, #0078d4);
z-index: 99;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s ease-out;
}
.custom-selection-background {
position: absolute;
background-color: var(--b3-theme-primary-lightest, rgba(0, 120, 212, 0.2));
z-index: 98; /* 比光标低一层 */
pointer-events: none;
}
`;
const styleElement = document.createElement('style');
styleElement.innerHTML = css;
document.head.appendChild(styleElement);
// 2. 创建元素(但不立即附加到 body)
const startMarker = document.createElement('div');
startMarker.className = 'custom-selection-marker';
const endMarker = document.createElement('div');
endMarker.className = 'custom-selection-marker';
const backgroundContainer = document.createElement('div');
// 3. 全局变量,用于跟踪当前选区和其所在的编辑器
let currentSelection = null;
let currentEditor = null;
// 4 & 5. 清除和隐藏函数 (基本不变)
function clearBackgrounds() {
backgroundContainer.innerHTML = '';
}
function hideAllSelection() {
clearBackgrounds();
startMarker.style.opacity = '0';
endMarker.style.opacity = '0';
setTimeout(() => {
if (startMarker.parentElement) {
startMarker.style.display = 'none';
endMarker.style.display = 'none';
}
}, 150);
}
// 6. 更新自定义选区显示的函数 (核心逻辑调整)
function updateCustomSelection() {
const selection = window.getSelection();
if (!selection || selection.isCollapsed || selection.rangeCount === 0) {
if (currentSelection) {
hideAllSelection();
currentSelection = null;
currentEditor = null;
}
return;
}
const range = selection.getRangeAt(0);
const startNode = range.startContainer;
// --- 核心判断:找到选区所在的编辑器 ---
const editorElement = (startNode.nodeType === Node.TEXT_NODE ? startNode.parentElement : startNode).closest('.protyle-wysiwyg');
// 如果选区不在 .protyle-wysiwyg 内,或者在一些需要原生选区的特殊区域,则隐藏自定义选区
if (!editorElement || (startNode.parentElement && startNode.parentElement.closest('.pdfViewer,.cm-editor,textarea,input'))) {
if (currentSelection) {
hideAllSelection();
currentSelection = null;
currentEditor = null;
}
return;
}
// --- 动态附加/移动元素 ---
if (currentEditor !== editorElement) {
currentEditor = editorElement;
// 将我们的自定义元素附加到新的编辑器中
currentEditor.appendChild(startMarker);
currentEditor.appendChild(endMarker);
currentEditor.appendChild(backgroundContainer);
}
clearBackgrounds();
// 保存当前选区信息
currentSelection = {
startContainer: range.startContainer,
startOffset: range.startOffset,
endContainer: range.endContainer,
endOffset: range.endOffset
};
renderSelection(range, currentEditor);
}
// 7. 渲染选区的函数 (接收 editorElement 作为参数)
function renderSelection(range, editorElement) {
if (!editorElement) return;
const rects = range.getClientRects();
const editorRect = editorElement.getBoundingClientRect();
if (rects.length > 0) {
const startRect = rects[0];
const endRect = rects[rects.length - 1];
startMarker.style.display = 'block';
endMarker.style.display = 'block';
// 坐标计算 (与上个版本相同)
startMarker.style.height = `${startRect.height}px`;
startMarker.style.top = `${startRect.top - editorRect.top + editorElement.scrollTop}px`;
startMarker.style.left = `${startRect.left - editorRect.left + editorElement.scrollLeft}px`;
endMarker.style.height = `${endRect.height}px`;
endMarker.style.top = `${endRect.top - editorRect.top + editorElement.scrollTop}px`;
endMarker.style.left = `${endRect.right - editorRect.left + editorElement.scrollLeft}px`;
startMarker.style.opacity = '1';
endMarker.style.opacity = '1';
for (const rect of rects) {
const bgBlock = document.createElement('div');
bgBlock.className = 'custom-selection-background';
bgBlock.style.top = `${rect.top - editorRect.top + editorElement.scrollTop}px`;
bgBlock.style.left = `${rect.left - editorRect.left + editorElement.scrollLeft}px`;
bgBlock.style.width = `${rect.width}px`;
bgBlock.style.height = `${rect.height}px`;
backgroundContainer.appendChild(bgBlock);
}
}
}
// 8. 重新渲染当前选区的函数
function refreshSelection() {
if (!currentSelection || !currentEditor) return;
try {
const range = document.createRange();
range.setStart(currentSelection.startContainer, currentSelection.startOffset);
range.setEnd(currentSelection.endContainer, currentSelection.endOffset);
clearBackgrounds();
renderSelection(range, currentEditor);
} catch (e) {
console.error('刷新选区失败:', e);
currentSelection = null;
currentEditor = null;
hideAllSelection();
}
}
// 9. 监听选区变化
document.addEventListener('selectionchange', updateCustomSelection);
// 10. 监听滚动和调整大小事件
// 注意:这里无法直接监听 currentEditor 的滚动,因为它是动态变化的。
// 我们采用事件委托的方式,在 document 层面捕获滚动事件。
let scrollTimeout;
function handleScrollOrResize(event) {
if (!currentSelection || !currentEditor) return;
// 检查滚动事件是否发生在当前编辑器或其内部
if (event.type === 'scroll' && event.target !== document && !currentEditor.contains(event.target)) {
// 如果滚动的是其他不相关的元素,则忽略
// return; // 注释掉这行可以简化逻辑,代价是任何滚动都会触发刷新,但性能影响极小
}
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(refreshSelection, 16);
}
// 使用 capture: true 可以在事件到达目标前捕获它
document.addEventListener('scroll', handleScrollOrResize, { passive: true, capture: true });
window.addEventListener('resize', handleScrollOrResize);
// 11. 监听页面点击事件
document.addEventListener('mousedown', () => {
setTimeout(() => {
const selection = window.getSelection();
if (!selection || selection.isCollapsed) {
if (currentSelection) {
currentSelection = null;
currentEditor = null;
hideAllSelection();
}
}
}, 10);
});
})();
文科生暂时还没有测试过多行代码选中的卡顿情况,有需要可以测试下
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于