您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在安卓移动端(包括 Via 浏览器)上,选中文字后显示自定义浮动菜单,只提供标记功能,解决原生菜单冲突。
// ==UserScript== // @name 阅读位置标记 // @namespace your.namespace // @version 2.4 // @description 在安卓移动端(包括 Via 浏览器)上,选中文字后显示自定义浮动菜单,只提供标记功能,解决原生菜单冲突。 // @match *://*/* // @grant none // @run-at document-idle // ==/UserScript== (function() { 'use strict'; // --- CSS 样式注入 --- const highlightClass = 'reading-highlight-red-bg'; const customMenuId = 'reading-custom-selection-menu'; const styleId = 'reading-highlight-style-v1_4'; // 更新样式ID以避免缓存问题 if (!document.getElementById(styleId)) { const styleElement = document.createElement('style'); styleElement.id = styleId; styleElement.innerHTML = ` /* 高亮样式 */ .${highlightClass} { background-color: rgba(255, 0, 0, 0.5); /* 红色半透明背景 */ cursor: pointer; box-decoration-break: clone; /* 针对跨行选择时背景连续性 */ -webkit-box-decoration-break: clone; /* 兼容Webkit内核浏览器 */ } /* 自定义选择菜单样式 */ #${customMenuId} { position: absolute; background-color: #333; color: white; padding: 5px 10px; border-radius: 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.3); z-index: 99999; display: none; /* 默认隐藏 */ white-space: nowrap; /* 确保按钮不换行 */ font-family: sans-serif; font-size: 14px; } #${customMenuId} button { background-color: transparent; color: white; border: none; padding: 5px 10px; margin: 0; /* 调整 margin,因为只有一个按钮 */ cursor: pointer; font-size: 14px; border-radius: 3px; min-width: 60px; /* 确保按钮有足够的宽度 */ text-align: center; } #${customMenuId} button:hover { background-color: #555; } #${customMenuId} button:active { background-color: #666; } `; document.head.appendChild(styleElement); } // --- DOM 元素创建 --- let customMenu = document.getElementById(customMenuId); if (!customMenu) { customMenu = document.createElement('div'); customMenu.id = customMenuId; customMenu.innerHTML = ` <button id="${customMenuId}-mark">标记</button> `; document.body.appendChild(customMenu); } const markButton = document.getElementById(`${customMenuId}-mark`); let lastHighlightedElements = []; // 用于存储上次高亮的DOM元素 // --- 辅助函数 --- /** * 将选中的文本包裹在一个带有高亮类的span标签中 * @param {Selection} selection 选区对象 */ function applyHighlight(selection) { if (!selection || selection.rangeCount === 0 || selection.isCollapsed) { console.warn("没有有效选区,无法高亮。"); return; } removeLastHighlight(); // 移除上次高亮 const range = selection.getRangeAt(0); const nodesToHighlight = []; const container = range.commonAncestorContainer; const iterator = document.createNodeIterator( container, NodeFilter.SHOW_TEXT, { acceptNode: function(node) { const nodeRange = document.createRange(); nodeRange.selectNodeContents(node); return range.intersectsNode(node) && node.nodeValue.trim().length > 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; } } ); let node; while ((node = iterator.nextNode())) { const textContent = node.nodeValue; if (textContent.trim().length === 0) continue; const parent = node.parentNode; let startOffset = 0; let endOffset = textContent.length; if (node === range.startContainer) { startOffset = range.startOffset; } if (node === range.endContainer) { endOffset = range.endOffset; } // 分割文本节点并插入高亮span const preText = document.createTextNode(textContent.substring(0, startOffset)); const highlightedText = document.createElement('span'); highlightedText.classList.add(highlightClass); highlightedText.textContent = textContent.substring(startOffset, endOffset); nodesToHighlight.push(highlightedText); const postText = document.createTextNode(textContent.substring(endOffset)); // 安全替换节点 if (parent) { // 先插入新节点,再移除旧节点 if (node.nextSibling) { parent.insertBefore(postText, node.nextSibling); parent.insertBefore(highlightedText, postText); parent.insertBefore(preText, highlightedText); } else { parent.appendChild(preText); parent.appendChild(highlightedText); parent.appendChild(postText); } parent.removeChild(node); parent.normalize(); // 合并相邻的文本节点 } } lastHighlightedElements = nodesToHighlight; console.log("高亮已应用。"); } /** * 移除上次的高亮 */ function removeLastHighlight() { lastHighlightedElements.forEach(span => { const parent = span.parentNode; if (parent) { while (span.firstChild) { parent.insertBefore(span.firstChild, span); } parent.removeChild(span); parent.normalize(); } }); lastHighlightedElements = []; console.log("上次高亮已移除。"); } /** * 保存阅读位置到localStorage * @param {Selection} selection 选区对象 */ function saveReadingPosition(selection) { if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return; const range = selection.getRangeAt(0); const selectionData = { anchorNodePath: getXPath(range.startContainer), anchorOffset: range.startOffset, focusNodePath: getXPath(range.endContainer), focusOffset: range.endOffset }; localStorage.setItem('readingPosition_' + window.location.href, JSON.stringify(selectionData)); console.log("阅读位置已保存:", selectionData); } /** * 从localStorage加载并恢复阅读位置 */ function loadReadingPosition() { const savedPosition = localStorage.getItem('readingPosition_' + window.location.href); if (savedPosition) { try { const selectionData = JSON.parse(savedPosition); const startNode = getElementByXPath(selectionData.anchorNodePath); const endNode = getElementByXPath(selectionData.focusNodePath); if (startNode && endNode) { const selection = window.getSelection(); const range = document.createRange(); try { range.setStart(startNode, selectionData.anchorOffset); range.setEnd(endNode, selectionData.focusOffset); selection.removeAllRanges(); selection.addRange(range); // 自动滚动到高亮位置 const rect = range.getBoundingClientRect(); window.scrollBy({ top: rect.top - (window.innerHeight / 3), behavior: 'smooth' }); applyHighlight(selection); console.log("阅读位置已加载并恢复。"); } catch (e) { console.error("无法恢复选区:", e); } } else { console.warn("无法找到保存的节点,阅读位置可能已失效。"); } } catch (e) { console.error("解析保存的阅读位置失败:", e); } } } /** * 获取一个DOM节点的XPath (改进版,处理文本节点) * @param {Node} node 目标节点 * @returns {string} 节点的XPath */ function getXPath(node) { if (!node || node.nodeType === Node.DOCUMENT_NODE) { return ''; } if (node.nodeType === Node.TEXT_NODE) { let index = 1; let sibling = node; while (sibling.previousSibling) { sibling = sibling.previousSibling; if (sibling.nodeType === Node.TEXT_NODE) { index++; } } return getXPath(node.parentNode) + `/text()[${index}]`; } const parts = []; let currentNode = node; while (currentNode && currentNode.nodeType !== Node.DOCUMENT_NODE) { let selector = currentNode.nodeName.toLowerCase(); if (currentNode.id) { selector += `[@id="${currentNode.id}"]`; } else { let sibling = currentNode; let nth = 1; while (sibling.previousSibling) { sibling = sibling.previousSibling; if (sibling.nodeName.toLowerCase() === selector) { nth++; } } if (nth > 1) { selector += `[${nth}]`; } } parts.unshift(selector); currentNode = currentNode.parentNode; } return parts.length ? '/' + parts.join('/') : ''; } /** * 根据XPath获取一个DOM节点 (改进版,处理文本节点) * @param {string} path 节点的XPath * @returns {Node|null} 找到的节点或者null */ function getElementByXPath(path) { if (!path) return null; try { if (path.includes('/text()')) { const parts = path.split('/text()'); const elementPath = parts[0]; const textIndexMatch = parts[1].match(/\[(\d+)\]/); const textIndex = textIndexMatch ? parseInt(textIndexMatch[1]) : 1; const element = getElementByXPath(elementPath); if (element) { let textNodeCount = 0; for (let i = 0; i < element.childNodes.length; i++) { const child = element.childNodes[i]; if (child.nodeType === Node.TEXT_NODE && child.nodeValue.trim().length > 0) { textNodeCount++; if (textNodeCount === textIndex) { return child; } } } } return null; } else { const result = document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); return result.singleNodeValue; } } catch (e) { console.error("无效的XPath:", path, e); return null; } } // --- 事件监听 --- let currentSelectionTimeout; document.addEventListener('selectionchange', () => { clearTimeout(currentSelectionTimeout); // 清除之前的延迟 const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { // 有效选区,延迟显示自定义菜单 currentSelectionTimeout = setTimeout(() => { if (!window.getSelection().isCollapsed) { // 再次确认选区仍然存在 showCustomMenu(selection); } }, 200); // 200ms 延迟,给用户操作原生菜单的时间 } else { // 没有有效选区或选区被清除,隐藏自定义菜单 hideCustomMenu(); } }); // 监听标记按钮点击 markButton.addEventListener('click', () => { const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { applyHighlight(selection); saveReadingPosition(selection); } else { alert('请先选择要标记的文本。'); } hideCustomMenu(); // 标记后清除选区,防止再次误触发 window.getSelection().removeAllRanges(); }); // 点击文档其他地方时隐藏菜单 document.addEventListener('mousedown', (event) => { // 如果点击的是菜单或菜单里的按钮,不隐藏 if (customMenu.contains(event.target)) { return; } // 如果有选区,且点击在选区内,也不隐藏(为了让用户有机会再次点击选区以显示菜单) const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { const range = selection.getRangeAt(0); const rect = range.getBoundingClientRect(); // 检查点击位置是否在选区矩形内 if (event.clientX >= rect.left && event.clientX <= rect.right && event.clientY >= rect.top && event.clientY <= rect.bottom) { return; } } hideCustomMenu(); }); // --- 菜单显示/隐藏逻辑 --- /** * 显示自定义菜单并定位 * @param {Selection} selection 当前选区 */ function showCustomMenu(selection) { if (selection.rangeCount === 0 || selection.isCollapsed) { hideCustomMenu(); return; } const range = selection.getRangeAt(0); const rect = range.getBoundingClientRect(); // 获取选区位置信息 // 计算菜单位置 // 尝试放在选区上方,如果空间不够则放在下方 let top = rect.top + window.scrollY - customMenu.offsetHeight - 10; // 10px 上方偏移 let left = rect.left + window.scrollX + (rect.width / 2) - (customMenu.offsetWidth / 2); // 边界检查,防止菜单超出屏幕 if (top < window.scrollY) { // 如果菜单在屏幕顶部之上 top = rect.bottom + window.scrollY + 10; // 放到选区下方 } if (left < window.scrollX) { // 菜单左侧超出屏幕 left = window.scrollX + 10; } if (left + customMenu.offsetWidth > window.innerWidth + window.scrollX) { // 菜单右侧超出屏幕 left = window.innerWidth + window.scrollX - customMenu.offsetWidth - 10; } customMenu.style.top = `${top}px`; customMenu.style.left = `${left}px`; customMenu.style.display = 'block'; } /** * 隐藏自定义菜单 */ function hideCustomMenu() { customMenu.style.display = 'none'; } // --- 页面加载完成时加载阅读位置 --- window.addEventListener('load', loadReadingPosition); })();