// ==UserScript==
// @name 网页划词高亮工具
// @namespace http://tampermonkey.net/
// @version 0.1.0
// @description 提供网页划词高亮功能
// @author sunny43 & claude-3-7
// @license MIT
// @match *://*/*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// ==/UserScript==
(function () {
'use strict';
// 全局变量
let highlights = [];
let currentPageUrl = window.location.href.split('#')[0];
let currentDomain = window.location.hostname;
let settings = GM_getValue('highlight_settings', {
triggerMode: 'auto', // auto, rightClick, hotkey
minTextLength: 1, // 修改默认最小触发长度为1
colors: ['#ff909c', '#b89fff', '#74b4ff', '#70d382', '#ffcb7e'],
activeColor: '#ff909c',
sidebarPinned: false, // 添加侧边栏固定状态设置
sidebarDescription: '高亮工具', // 添加侧边栏描述文本设置
sidebarWidth: 320, // 添加侧边栏宽度设置,默认320px
showFloatingButton: true // 新增:控制浮动按钮是否显示
});
// 全局变量 - 侧边栏固定状态
let sidebarPinned = settings.sidebarPinned || false;
// 禁用列表
let disabledList = GM_getValue('disabled_list', {
domains: [],
urls: []
});
// 检查当前页面是否禁用高亮功能
let isHighlightDisabled = disabledList.domains.includes(currentDomain) ||
disabledList.urls.includes(currentPageUrl);
// 防抖变量改进
let menuDisplayTimer = null;
let lastMenuDisplayTime = 0;
let menuAnimating = false; // 菜单动画状态标记
let ignoreNextClick = false; // 忽略下一次点击的标志
let isProcessingColorClick = false; // 状态变量来控制点击处理
// 侧边栏状态
let sidebarOpen = false;
GM_addStyle(`
/* ====================
* 1. 高亮菜单样式
* ==================== */
/* 高亮菜单容器 */
.highlight-menu {
position: absolute;
background: #333336;
border: none;
border-radius: 24px;
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
padding: 10px 8px;
z-index: 9999;
display: flex;
flex-direction: row;
align-items: center;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
color: #fff;
opacity: 0; /* 初始隐藏 */
transition: opacity 0.2s ease-in; /* 只保留透明度过渡效果 */
pointer-events: none; /* 隐藏时不响应事件 */
}
.highlight-menu.show {
opacity: 1;
pointer-events: auto; /* 显示时响应事件 */
}
/* 菜单箭头样式 */
.highlight-menu::after {
content: '';
position: absolute;
bottom: -6px;
left: var(--arrow-left, 50%);
width: 12px;
height: 6px;
background-color: #333336;
clip-path: polygon(0 0, 100% 0, 50% 100%);
margin-left: -6px;
}
.highlight-menu.arrow-top::after {
top: -6px; /* 减小间隙 */
bottom: auto;
/* 颠倒三角形方向 */
clip-path: polygon(0 100%, 100% 100%, 50% 0);
}
/* 颜色选择区域 */
.highlight-menu-colors {
display: flex;
flex-direction: row;
align-items: center;
margin: 0 2px; /* 减少外边距让色块更接近菜单边缘 */
flex-wrap: nowrap; /* 确保颜色不会换行 */
flex: 0 0 auto; /* 防止颜色区域被压缩 */
}
/* 颜色选择按钮 */
.highlight-menu-color {
width: 22px;
height: 22px;
border-radius: 50%;
margin: 0 3px;
cursor: pointer;
position: relative;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.15s ease;
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.12);
flex-shrink: 0; /* 防止颜色球被压缩 */
}
.highlight-menu-color:hover {
transform: scale(1.12);
}
.highlight-menu-color.active::after {
content: "";
width: 12px;
height: 12px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23333336' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center;
background-size: contain;
}
/* 菜单按钮通用样式 */
.highlight-menu-action {
height: 22px;
margin: 0 2px; /* 减少外边距让按钮更接近菜单边缘 */
cursor: pointer;
padding: 0 10px; /* 稍微减少按钮的水平内边距 */
border-radius: 12px;
color: #fff;
font-size: 13px;
background: rgba(255,255,255,0.1);
border: none;
transition: all 0.15s ease;
white-space: nowrap;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0; /* 防止按钮被压缩 */
}
.highlight-menu-action:hover {
background: rgba(255,255,255,0.2);
}
/* 删除按钮样式 */
.highlight-action-delete {
color: #f0f0f0;
font-weight: 500;
position: relative;
overflow: hidden;
transition: all 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);
margin-left: 3px; /* 增加与颜色区域的间距 */
}
.highlight-action-delete:hover {
background: rgba(255,82,82,0.12);
color: #ff6b6b;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(255,82,82,0.25);
}
.highlight-action-delete:active {
transform: translateY(0px);
background: rgba(255,82,82,0.2);
}
/* ====================
* 2. 高亮标记样式
* ==================== */
/* 高亮显示的文本 */
.highlight-marked {
position: relative;
cursor: pointer;
border-radius: 2px;
transition: opacity 0.15s ease;
}
.highlight-marked:hover {
opacity: 0.9;
}
/* 闪烁效果用于高亮跳转 */
@keyframes highlightFlash {
0%, 100% { background-color: inherit; }
50% { background-color: rgba(255, 255, 0, 0.5); }
}
.highlight-flash {
animation: highlightFlash 1s ease 2;
}
/* 高亮错误恢复样式 */
@keyframes fadeInOut {
0%, 100% { opacity: 0.3; }
50% { opacity: 1; }
}
.highlight-error-recovery {
animation: fadeInOut 1.5s ease infinite;
border: 2px dashed #ff6b6b !important;
}
/* ====================
* 3. 工具栏按钮样式
* ==================== */
/* 浮动工具栏按钮 */
.highlight-toolbar {
position: fixed;
bottom: 20px;
right: 20px;
background: rgba(51, 51, 54, 0.85);
color: #fff;
border-radius: 50%;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 3px 16px rgba(0,0,0,0.3);
cursor: pointer;
z-index: 9998;
transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s;
user-select: none;
touch-action: none;
backdrop-filter: blur(4px);
}
.highlight-toolbar:hover {
transform: scale(1.05);
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
background: rgba(51, 51, 54, 1);
}
.highlight-toolbar:active {
transform: scale(0.97);
}
/* 拖动时的样式 */
.highlight-toolbar.dragging {
opacity: 0.8;
transform: scale(1.1);
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
cursor: grabbing; /* 只在拖动时显示抓取图标 */
}
/* ====================
* 4. 设置弹窗样式
* ==================== */
/* 设置弹窗容器 */
.highlight-settings {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #fff;
border-radius: 16px;
box-shadow: 0 8px 30px rgba(0,0,0,0.2);
padding: 22px;
z-index: 10000;
display: none;
min-width: 300px;
max-width: 360px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
.highlight-settings h3 {
margin-top: 0;
color: #333;
font-weight: 500;
margin-bottom: 16px;
font-size: 17px;
}
/* 设置表单元素 */
.highlight-settings label {
display: block;
margin: 12px 0 4px;
font-weight: 500;
color: #444;
font-size: 14px;
}
.highlight-settings select,
.highlight-settings input {
width: 100%;
padding: 8px 10px;
border: 1px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
color: #333;
background: #f9f9f9;
}
.highlight-settings select:focus,
.highlight-settings input:focus {
outline: none;
border-color: #90caf9;
box-shadow: 0 0 0 2px rgba(144,202,249,0.2);
}
/* 设置按钮区域 */
.highlight-settings-buttons {
display: flex;
gap: 8px;
margin-top: 20px;
}
.highlight-settings button {
padding: 8px 14px;
border: none;
border-radius: 8px;
background: #2196f3;
color: white;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
font-size: 13px;
flex: 1;
}
.highlight-settings button:hover {
background: #1976d2;
}
.highlight-settings #clearHighlights {
background: #f5f5f5;
color: #e53935;
border: 1px solid #e0e0e0;
}
.highlight-settings #clearHighlights:hover {
background: #ffebee;
border-color: #ffcdd2;
}
.highlight-settings #closeSettings {
background: #f5f5f5;
color: #616161;
border: 1px solid #e0e0e0;
}
.highlight-settings #closeSettings:hover {
background: #eeeeee;
border-color: #bdbdbd;
}
/* 颜色设置区域 */
.highlight-colors-setting {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
.highlight-colors-setting .highlight-menu-color {
width: 24px;
height: 24px;
box-shadow: 0 1px 5px rgba(0,0,0,0.2);
border: 2px solid rgba(255,255,255,0.1);
}
.highlight-colors-setting .highlight-menu-color.active {
box-shadow: 0 0 0 2px #ff5252;
border-color: rgba(255,255,255,0.5);
transform: scale(1.1);
}
/* ====================
* 5. 消息提示样式
* ==================== */
/* 通知提示框 */
.highlight-toast {
position: fixed;
bottom: 80px;
right: 20px;
background: #333;
color: #fff;
padding: 10px 20px;
border-radius: 4px;
z-index: 10001;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 14px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
opacity: 0;
transform: translateY(10px);
transition: opacity 0.3s, transform 0.3s;
}
.highlight-toast.show {
opacity: 1;
transform: translateY(0);
}
.highlight-toast.success {
background: #4caf50;
}
.highlight-toast.error {
background: #f44336;
}
.highlight-toast.warning {
background: #ff9800;
}
/* ====================
* 6. 侧边栏基本结构
* ==================== */
/* 侧边栏容器 */
.highlight-sidebar {
position: fixed;
top: 0;
right: calc(-1 * var(--sidebar-width, 320px));
width: var(--sidebar-width, 320px);
height: 100%;
background: #333336;
color: #e0e0e0;
box-shadow: -2px 0 15px rgba(0,0,0,0.4);
z-index: 10000;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
transition: right 0.3s ease;
display: flex;
flex-direction: column;
overflow: hidden;
border-left: 1px solid rgba(255,255,255,0.05);
min-width: 250px;
max-width: 1600px;
}
.highlight-sidebar.open {
right: 0;
}
/* 侧边栏拖动手柄 */
.sidebar-resizer {
position: absolute;
left: 0;
top: 0;
width: 6px;
height: 100%;
background: transparent;
cursor: ew-resize;
z-index: 10001;
transition: background-color 0.2s, box-shadow 0.2s;
}
.sidebar-resizer:hover,
.sidebar-resizer.dragging {
background-color: rgba(255,82,82,0.3);
}
.sidebar-resizer.min-width,
.sidebar-resizer.max-width {
background-color: rgba(255,82,82,0.5) !important;
box-shadow: 0 0 8px rgba(255,82,82,0.8);
}
/* ====================
* 7. 侧边栏标题栏
* ==================== */
/* 侧边栏标题栏容器 */
.sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 10px;
min-height: 28px;
background: rgba(0,0,0,0.25);
border-bottom: 1px solid rgba(255,255,255,0.05);
}
/* 左侧描述区域 */
.sidebar-description {
/* 文本样式 */
font-size: 12px;
color: #e0e0e0;
font-weight: 500;
letter-spacing: 0.3px;
/* 文本处理 */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 85%; /* 使用最大的占比值 */
/* 编辑相关样式 */
cursor: pointer;
transition: all 0.2s;
border-bottom: 1px dashed transparent;
padding-bottom: 2px;
}
.sidebar-description:hover {
border-bottom-color: rgba(255, 255, 255, 0.3);
}
.sidebar-description::after {
content: '✎';
opacity: 0;
margin-left: 5px;
font-size: 12px;
transition: opacity 0.2s;
}
.sidebar-description:hover::after {
opacity: 0.7;
}
.sidebar-description.editing {
border: none;
background: rgba(255,255,255,0.1);
padding: 2px 8px;
border-radius: 4px;
outline: none;
}
.sidebar-description.editing::after {
content: '';
}
/* 右侧按钮组 */
.sidebar-controls {
display: flex;
gap: 4px; /* 使用最小的间距 */
align-items: center; /* 确保按钮垂直居中 */
}
/* 通用按钮样式 */
.sidebar-btn {
cursor: pointer;
color: #e0e0e0;
width: 22px; /* 使用紧凑型设计尺寸 */
height: 22px;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
background: rgba(255,255,255,0.08);
position: relative;
overflow: hidden;
flex-shrink: 0; /* 防止按钮被压缩 */
padding: 0; /* 移除任何内边距 */
box-sizing: border-box; /* 确保边框计入总尺寸 */
}
.sidebar-btn:hover {
background: rgba(255,255,255,0.15);
color: #fff;
}
.sidebar-btn:active {
transform: none; /* 取消按下的位移效果 */
}
.sidebar-btn svg {
width: 16px;
height: 16px;
transition: color 0.2s ease;
display: block;
margin: 0;
vertical-align: middle;
}
/* 固定按钮样式 */
.sidebar-pin {
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.sidebar-pin svg {
display: block;
margin: 0 auto;
transform-origin: center;
transition: transform 0.3s ease;
}
.sidebar-pin.pinned {
background: rgba(255, 82, 82, 0.15);
color: #ff5252;
}
.sidebar-pin.pinned svg {
transform: none; /* 移除旋转效果 */
filter: drop-shadow(0 1px 1px rgba(0,0,0,0.2));
}
.sidebar-pin.pinned:hover {
background: rgba(255, 82, 82, 0.25);
}
/* 关闭按钮样式 */
.sidebar-close {
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
padding-bottom: 2px;
}
.sidebar-close span {
line-height: 1;
display: block;
}
.sidebar-close:hover {
background: rgba(255,82,82,0.15);
color: #ff5252;
}
.sidebar-close:hover svg {
transform: none; /* 移除旋转动画效果 */
}
.sidebar-close svg {
margin: 0;
position: relative;
top: 0;
}
/* ====================
* 8. 侧边栏内容区
* ==================== */
/* 内容区容器 */
.sidebar-content {
flex: 1;
overflow-y: auto;
padding: 0;
padding-bottom: 15px; /* 确保底部内容不被遮挡 */
}
/* 选项卡导航样式 */
.sidebar-tabs {
display: flex;
background: rgba(0,0,0,0.15);
border-bottom: none;
margin-bottom: 0;
padding: 0;
height: 36px;
align-items: stretch;
}
.sidebar-tab {
padding: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
letter-spacing: 0.3px;
color: #aaa;
font-weight: 500;
height: 36px;
border-bottom: 2px solid transparent;
transition: all 0.2s;
flex-grow: 1;
text-align: center;
cursor: pointer;
}
.sidebar-tab:hover {
color: #fff;
background-color: rgba(255,255,255,0.05);
}
.sidebar-tab.active {
color: #fff;
border-bottom-color: #ff5252;
background-color: rgba(255,82,82,0.1);
font-weight: 600;
}
/* 选项卡内容样式 */
.tab-content {
display: none;
padding: 10px;
}
.tab-content.active {
display: block;
}
/* 特殊的高亮列表tab样式 */
#tab-highlights.active {
display: flex !important;
flex-direction: column;
height: 100%;
padding: 10px 10px 0 10px;
box-sizing: border-box;
}
/* 标签页标题样式 */
.tab-content h3 {
font-size: 13px;
font-weight: 600;
color: #fff;
margin: 12px 0 8px;
padding-bottom: 6px;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.tab-content h3:first-child {
margin-top: 5px;
}
/* ====================
* 9. 设置页面元素
* ==================== */
/* 设置面板的样式调整 */
.sidebar-settings label {
display: block;
margin: 10px 0 4px;
font-weight: 500;
color: #e0e0e0;
font-size: 12px;
}
.sidebar-settings select:focus,
.sidebar-settings input:focus,
.add-disabled-input:focus {
outline: none;
border-color: rgba(255,82,82,0.5);
box-shadow: 0 0 0 2px rgba(255,82,82,0.2);
background: rgba(255,255,255,0.09);
}
/* Chrome/Safari 特定样式 - 使用::-webkit-scrollbar来美化下拉框的滚动条 */
.sidebar-settings select::-webkit-scrollbar {
width: 8px;
}
.sidebar-settings select::-webkit-scrollbar-track {
background: #222224;
}
.sidebar-settings select::-webkit-scrollbar-thumb {
background-color: #666;
border-radius: 4px;
}
/* 下拉选项的样式 - 针对支持的浏览器 */
.sidebar-settings select option {
background-color: #333336;
color: #e0e0e0;
padding: 10px 15px;
}
/* 下拉选项悬停效果 */
.sidebar-settings select option:hover {
background-color: #444448;
}
.sidebar-settings select,
.sidebar-settings input[type="text"],
.sidebar-settings input[type="number"] {
background: rgba(255,255,255,0.06);
color: #e0e0e0;
border: 1px solid rgba(255,255,255,0.1);
border-radius: 4px;
width: 100%;
padding: 7px 10px;
box-sizing: border-box;
font-size: 12px;
}
/* ====================
* 10. 高亮列表样式
* ==================== */
/* 高亮列表容器 */
.highlight-list {
margin-top: 8px;
}
/* 高亮列表tab中特殊样式 */
#tab-highlights .highlight-list {
flex: 1;
overflow-y: auto;
margin-bottom: 0;
padding-right: 4px;
scrollbar-width: thin;
scrollbar-color: rgba(255,255,255,0.1) transparent;
}
/* 自定义滚动条样式 */
#tab-highlights .highlight-list::-webkit-scrollbar {
width: 4px; /* 极细滚动条 */
}
#tab-highlights .highlight-list::-webkit-scrollbar-track {
background: transparent; /* 透明轨道 */
}
#tab-highlights .highlight-list::-webkit-scrollbar-thumb {
background-color: transparent; /* 完全透明滚动条滑块 */
border-radius: 4px;
}
#tab-highlights .highlight-list::-webkit-scrollbar-thumb:hover {
background-color: transparent; /* 悬停时保持透明 */
}
/* 高亮列表项样式 */
.highlight-list-item {
padding: 8px 10px;
margin-bottom: 6px;
border-radius: 6px;
cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.15s ease, background-color 0.15s ease;
position: relative;
border-left: 3px solid; /* 颜色在行内样式中设置 */
background: rgba(255,255,255,0.06);
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
display: flex;
flex-direction: column;
}
.highlight-list-item:hover {
background: rgba(255,255,255,0.09);
transform: translateY(-1px);
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
/* 高亮列表项内容 */
.highlight-list-content {
margin-bottom: 4px;
font-size: 13px;
line-height: 1.3;
-webkit-line-clamp: 2;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
position: relative;
flex: 1;
}
/* 高亮列表元数据 */
.highlight-list-meta {
padding-top: 4px;
font-size: 11px;
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid rgba(255,255,255,0.05);
}
/* 高亮列表时间显示 */
.highlight-list-time {
font-style: italic;
opacity: 0.8;
font-size: 11px; /* 稍微调小字体确保完整显示 */
white-space: nowrap; /* 防止时间换行 */
}
/* 高亮列表为空提示 */
.highlight-list p {
text-align: center;
color: #aaa;
font-style: italic;
background: rgba(255,255,255,0.03);
border: 1px dashed rgba(255,255,255,0.1);
padding: 12px;
font-size: 12px;
border-radius: 4px;
}
/* 高亮列表操作区 */
.highlight-list-actions {
display: flex;
gap: 5px;
align-items: center;
}
/* 高亮列表操作按钮 */
.highlight-list-action {
color: #bbb;
cursor: pointer;
transition: all 0.2s;
font-weight: 500;
padding: 1px 6px;
border-radius: 4px;
font-size: 11px;
}
.highlight-list-action:hover {
color: #fff;
background-color: #ff5252;
}
/* ====================
* 11. 按钮与操作元素
* ==================== */
/* 按钮区域固定在底部 */
#tab-highlights .buttons-row {
position: sticky;
bottom: 0;
background: #333336;
padding: 0; /* 移除左右内边距 */
margin: 0; /* 移除外边距 */
width: 100%; /* 确保全宽 */
z-index: 2;
flex-shrink: 0;
box-shadow: 0 -2px 8px rgba(0,0,0,0.2);
}
/* 按钮组样式 */
.buttons-row {
display: flex;
gap: 6px;
margin-top: 10px;
}
.buttons-row .sidebar-button {
flex: 1;
margin-top: 0;
}
/* 侧边栏通用按钮样式 */
.sidebar-button {
padding: 7px 10px;
border: none;
border-radius: 4px;
background: #ff5252;
color: white;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
font-size: 12px;
margin-top: 5px;
}
.sidebar-button:hover {
background: #e04343;
transform: translateY(-1px);
}
/* 按钮变体样式 */
.sidebar-button.danger {
background: #ff5252;
}
.sidebar-button.danger:hover {
background: #e04343;
}
.sidebar-button.secondary {
background: rgba(255,255,255,0.08);
color: #e0e0e0;
border: 1px solid rgba(255,255,255,0.1);
}
.sidebar-button.secondary:hover {
background: rgba(255,255,255,0.12);
}
/* 删除按钮样式 */
.disabled-item-remove {
color: #ff8080;
margin-left: 8px;
cursor: pointer;
transition: all 0.2s;
font-weight: 500;
padding: 1px 6px;
border-radius: 4px;
font-size: 11px;
}
.disabled-item-remove:hover {
background-color: #ff5252;
color: white;
}
/* ====================
* 12. 表单控件样式
* ==================== */
/* 添加表单样式 */
.add-disabled-form {
display: flex;
margin: 8px 0;
align-items: stretch;
}
/* 输入框样式 */
.add-disabled-input {
flex: 1;
padding: 7px 10px;
border: 1px solid rgba(255,255,255,0.1);
border-radius: 4px 0 0 4px;
font-size: 13px;
color: #e0e0e0;
background: rgba(255,255,255,0.06);
margin: 0;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
transition: all 0.2s;
height: auto;
box-sizing: border-box;
}
/* 添加按钮样式 */
.add-disabled-button {
background: #ff5252;
color: white;
border: none;
border-radius: 0 4px 4px 0;
cursor: pointer;
transition: all 0.2s;
font-weight: 500;
white-space: nowrap;
min-width: 56px;
padding: 0 12px;
font-size: 13px;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.add-disabled-button:hover {
background: #e04343;
}
/* ====================
* 13. 禁用列表样式
* ==================== */
/* 禁用列表容器 */
.disabled-list {
margin-top: 16px;
}
/* 禁用项样式 */
.disabled-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 7px 10px;
border-radius: 4px;
background: rgba(255,255,255,0.06);
margin-bottom: 5px;
border-left: 2px solid rgba(255,255,255,0.2);
}
/* 禁用项文本 */
.disabled-item-text {
font-size: 12px;
color: #ccc;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
`);
// 初始化
// 修改init函数,添加延迟高亮恢复
function init() {
loadHighlights();
// 加载固定状态
sidebarPinned = settings.sidebarPinned || false;
// 确保初始化时侧边栏状态为关闭
sidebarOpen = false;
// 重新检查禁用状态,确保与最新存储同步
disabledList = GM_getValue('disabled_list', {
domains: [],
urls: []
});
isHighlightDisabled = disabledList.domains.includes(currentDomain) ||
disabledList.urls.includes(currentPageUrl);
createSidebar();
registerEvents();
registerMenuCommands();
addToolbarButton();
// 如果不禁用高亮,则应用高亮并设置延迟重试
if (!isHighlightDisabled) {
// 立即尝试应用高亮
applyHighlights();
// 在页面完全加载后再次尝试恢复失败的高亮
if (document.readyState === 'complete') {
// 如果已经加载完成,延迟一小段时间再次尝试
setTimeout(() => retryFailedHighlights(), 1000);
} else {
// 否则等待页面加载完成
window.addEventListener('load', () => {
setTimeout(() => retryFailedHighlights(), 500);
});
}
// 再次延迟尝试,确保捕获延迟加载的内容
setTimeout(() => retryFailedHighlights(), 3000);
}
}
// 添加新函数:重试恢复失败的高亮
function retryFailedHighlights() {
console.log('尝试恢复之前失败的高亮...');
const failedHighlights = highlights.filter(highlight => {
// 检查此高亮是否已经应用(不存在DOM中则为失败的高亮)
return !document.querySelector(`.highlight-marked[data-id="${highlight.id}"]`);
});
if (failedHighlights.length === 0) {
console.log('没有找到需要恢复的失败高亮');
return;
}
console.log(`发现 ${failedHighlights.length} 个失败的高亮需要重试恢复`);
let successCount = 0;
failedHighlights.forEach(highlight => {
// 只尝试文本匹配,因为DOM路径可能已经失效
if (highlight.text && findAndHighlightText(highlight)) {
console.log(`重试成功恢复高亮: ${highlight.id}`);
successCount++;
}
});
console.log(`重试恢复结果: 成功=${successCount}, 剩余失败=${failedHighlights.length - successCount}`);
// 如果还有失败的高亮且侧边栏是打开的,刷新列表确保UI一致性
if (sidebarOpen) {
refreshHighlightsList();
}
}
// 加载高亮数据
function loadHighlights() {
try {
const savedHighlights = GM_getValue('highlights', {});
highlights = savedHighlights[currentPageUrl] || [];
console.log(`加载了${highlights.length}条高亮记录`);
} catch (error) {
console.error('加载高亮数据失败:', error);
highlights = [];
}
}
// 保存高亮数据
function saveHighlights() {
const savedHighlights = GM_getValue('highlights', {});
savedHighlights[currentPageUrl] = highlights;
GM_setValue('highlights', savedHighlights);
}
// 保存禁用列表
function saveDisabledList() {
GM_setValue('disabled_list', disabledList);
}
// 创建侧边栏
function createSidebar() {
const sidebar = document.createElement('div');
sidebar.className = 'highlight-sidebar';
// 设置初始宽度
const initialWidth = settings.sidebarWidth || 320;
sidebar.style.setProperty('--sidebar-width', `${initialWidth}px`);
sidebar.style.width = `${initialWidth}px`;
// 如果初始状态是关闭的,确保设置正确的隐藏位置
if (!sidebarOpen) {
sidebar.style.right = `calc(-1 * ${initialWidth}px)`;
}
sidebar.innerHTML = `
<div class="sidebar-resizer"></div>
<div class="sidebar-header">
<div class="sidebar-description" title="双击修改标题">${settings.sidebarDescription || '网页划词高亮工具'}</div>
<div class="sidebar-controls">
<div class="sidebar-btn sidebar-pin ${sidebarPinned ? 'pinned' : ''}" title="固定侧边栏">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2L12 16"></path>
<path d="M5 12H2"></path>
<path d="M22 12h-3"></path>
<path d="M18 5l-6 7-6-7"></path>
<path d="M18 19l-6-7-6 7"></path>
</svg>
</div>
<div class="sidebar-btn sidebar-close" title="关闭">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</div>
</div>
</div>
<div class="sidebar-tabs">
<div class="sidebar-tab active" data-tab="highlights">高亮列表</div>
<div class="sidebar-tab" data-tab="settings">设置</div>
<div class="sidebar-tab" data-tab="disabled">禁用管理</div>
</div>
<div class="sidebar-content">
<!-- 高亮列表 -->
<div class="tab-content active" id="tab-highlights">
<div class="highlight-list">
${renderHighlightsList()}
</div>
<div class="buttons-row">
<button class="sidebar-button secondary" id="refresh-highlights">刷新列表</button>
<button class="sidebar-button danger" id="clear-all-highlights">清除全部</button>
</div>
</div>
<!-- 设置选项卡 -->
<div class="tab-content" id="tab-settings">
<div class="sidebar-settings">
<label for="triggerMode">触发方式:</label>
<select id="triggerMode">
<option value="auto" ${settings.triggerMode === 'auto' ? 'selected' : ''}>自动触发(选中后立即显示)</option>
<option value="hotkey" ${settings.triggerMode === 'hotkey' ? 'selected' : ''}>快捷键触发 (Ctrl + Alt)</option>
</select>
<label for="minTextLength">最小触发文本长度:</label>
<input type="number" id="minTextLength" min="1" max="50" value="${settings.minTextLength}">
<div>
<label>高亮颜色:<span class="settings-tip">(点击选择默认颜色)</span></label>
<div class="highlight-colors-setting">
${settings.colors.map(color => `
<div class="highlight-menu-color ${color === settings.activeColor ? 'active' : ''}"
style="background-color: ${color};"
data-color="${color}">
</div>
`).join('')}
</div>
</div>
<label for="sidebarDescription">侧边栏描述文字:</label>
<input type="text" id="sidebarDescription" placeholder="自定义侧边栏描述..." value="${settings.sidebarDescription || '高亮工具'}">
<div class="buttons-row">
<button class="sidebar-button" id="save-settings">保存设置</button>
</div>
</div>
</div>
<!-- 禁用管理选项卡 -->
<div class="tab-content" id="tab-disabled">
<div>
<h3>当前页面</h3>
<div class="current-page-status">
${renderCurrentPageStatus()}
</div>
<h3>禁用域名列表</h3>
<div class="disabled-domains-list">
${renderDisabledDomains()}
</div>
<div class="add-disabled-form">
<input type="text" class="add-disabled-input" id="add-domain-input" placeholder="输入域名...">
<button class="add-disabled-button" id="add-domain-btn">添加</button>
</div>
<h3>禁用网址列表</h3>
<div class="disabled-urls-list">
${renderDisabledUrls()}
</div>
<div class="add-disabled-form">
<input type="text" class="add-disabled-input" id="add-url-input" placeholder="输入网址...">
<button class="add-disabled-button" id="add-url-btn">添加</button>
</div>
</div>
</div>
</div>
`;
document.body.appendChild(sidebar);
// 更新描述文字
updateSidebarDescription();
// 绑定侧边栏事件
bindSidebarEvents(sidebar);
// 添加标题双击编辑功能
setupTitleEditing(sidebar);
// 添加侧边栏宽度调整功能
setupSidebarResize(sidebar);
}
// 设置侧边栏宽度调整功能
function setupSidebarResize(sidebar) {
const resizer = sidebar.querySelector('.sidebar-resizer');
if (!resizer) return;
// 初始最大/最小宽度设置
const MIN_WIDTH = 250;
const MAX_WIDTH = 1600;
let isDragging = false;
let initialMouseX = 0;
let initialWidth = 0;
let lastEventTime = 0; // 添加事件时间戳用于去重
// 获取初始宽度(确保有一个有效的起点)
const currentWidth = parseInt(getComputedStyle(sidebar).width) || settings.sidebarWidth || 320;
const safeInitialWidth = Math.max(MIN_WIDTH, Math.min(currentWidth, MAX_WIDTH));
// 立即应用有效的初始宽度
sidebar.style.width = `${safeInitialWidth}px`;
sidebar.style.setProperty('--sidebar-width', `${safeInitialWidth}px`);
// 保存有效宽度到设置
if (settings.sidebarWidth !== safeInitialWidth) {
settings.sidebarWidth = safeInitialWidth;
saveSettings();
}
// 处理鼠标按下事件
function handleMouseDown(e) {
e.preventDefault();
e.stopPropagation();
// 阻止在已经拖动时再次启动拖动
if (isDragging) return;
// 记录初始值
initialMouseX = e.clientX;
initialWidth = parseInt(getComputedStyle(sidebar).width);
// 确保初始宽度有效
if (isNaN(initialWidth) || initialWidth < MIN_WIDTH) {
initialWidth = MIN_WIDTH;
} else if (initialWidth > MAX_WIDTH) {
initialWidth = MAX_WIDTH;
}
isDragging = true;
// 添加拖动类
resizer.classList.add('dragging');
document.body.classList.add('sidebar-resizing');
// 添加临时事件监听
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}
// 处理触摸开始事件 - 修复:完善触摸事件处理
function handleTouchStart(e) {
if (e.touches.length === 1) {
// 阻止默认事件
e.preventDefault();
e.stopPropagation();
// 使用触摸点代替鼠标位置
initialMouseX = e.touches[0].clientX;
initialWidth = parseInt(getComputedStyle(sidebar).width);
// 确保初始宽度有效
if (isNaN(initialWidth) || initialWidth < MIN_WIDTH) {
initialWidth = MIN_WIDTH;
} else if (initialWidth > MAX_WIDTH) {
initialWidth = MAX_WIDTH;
}
isDragging = true;
// 添加拖动类
resizer.classList.add('dragging');
document.body.classList.add('sidebar-resizing');
// 修复:添加临时事件监听
document.addEventListener('touchmove', handleTouchMove, { passive: false });
document.addEventListener('touchend', handleMouseUp);
document.addEventListener('touchcancel', handleMouseUp);
}
}
// 处理鼠标移动事件 - 修复:修正拖动方向逻辑
function handleMouseMove(e) {
if (!isDragging) return;
e.preventDefault();
// 计算拖动距离
// 注意:侧边栏在右侧,向左拖动(正值deltaX)增加宽度,向右拖动(负值deltaX)减少宽度
const deltaX = e.clientX - initialMouseX;
// 由于侧边栏在右侧,拖动计算方向相反
let newWidth = initialWidth - deltaX;
// 应用限制
newWidth = Math.max(MIN_WIDTH, Math.min(newWidth, MAX_WIDTH));
// 修复:增强视觉反馈
resizer.classList.remove('min-width', 'max-width');
if (newWidth <= MIN_WIDTH + 5) { // 接近最小值时添加视觉提示
resizer.classList.add('min-width');
} else if (newWidth >= MAX_WIDTH - 5) { // 接近最大值时添加视觉提示
resizer.classList.add('max-width');
}
// 应用新宽度 - 同时更新实际width和CSS变量
sidebar.style.width = `${newWidth}px`;
sidebar.style.setProperty('--sidebar-width', `${newWidth}px`);
}
// 处理触摸移动事件 - 修复:完善触摸事件处理
function handleTouchMove(e) {
if (!isDragging || e.touches.length !== 1) return;
e.preventDefault();
// 计算拖动距离 - 与鼠标事件保持一致的逻辑
const deltaX = e.touches[0].clientX - initialMouseX;
// 由于侧边栏在右侧,拖动计算方向相反
let newWidth = initialWidth - deltaX;
// 应用限制
newWidth = Math.max(MIN_WIDTH, Math.min(newWidth, MAX_WIDTH));
// 增强视觉反馈
resizer.classList.remove('min-width', 'max-width');
if (newWidth <= MIN_WIDTH + 5) {
resizer.classList.add('min-width');
} else if (newWidth >= MAX_WIDTH - 5) {
resizer.classList.add('max-width');
}
// 应用新宽度
sidebar.style.width = `${newWidth}px`;
sidebar.style.setProperty('--sidebar-width', `${newWidth}px`);
}
// 处理鼠标抬起事件 - 修复:添加事件去重机制
function handleMouseUp(e) {
// 防止触摸事件后的鼠标事件重复触发
const now = Date.now();
if (now - lastEventTime < 100) return;
lastEventTime = now;
if (!isDragging) return;
e.preventDefault();
// 重要:先读取当前宽度,再重置状态
let finalWidth = parseInt(getComputedStyle(sidebar).width);
// 验证最终宽度是否有效
if (isNaN(finalWidth) || finalWidth < MIN_WIDTH) {
finalWidth = MIN_WIDTH;
} else if (finalWidth > MAX_WIDTH) {
finalWidth = MAX_WIDTH;
}
// 重置状态
isDragging = false;
resizer.classList.remove('dragging', 'min-width', 'max-width');
document.body.classList.remove('sidebar-resizing');
// 移除临时事件监听
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
document.removeEventListener('touchmove', handleTouchMove);
document.removeEventListener('touchend', handleMouseUp);
document.removeEventListener('touchcancel', handleMouseUp);
// 确保最终应用的宽度在有效范围内
const safeWidth = Math.max(MIN_WIDTH, Math.min(finalWidth, MAX_WIDTH));
// 更新所有与宽度相关的属性,确保一致性
sidebar.style.width = `${safeWidth}px`;
sidebar.style.setProperty('--sidebar-width', `${safeWidth}px`);
// 如果侧边栏当前是关闭状态,确保隐藏位置也更新
if (!sidebarOpen) {
sidebar.style.right = `calc(-1 * ${safeWidth}px)`;
}
// 保存到设置
settings.sidebarWidth = safeWidth;
saveSettings();
}
// 绑定事件
resizer.addEventListener('mousedown', handleMouseDown);
resizer.addEventListener('touchstart', handleTouchStart, { passive: false });
// 修复:添加窗口大小变化适配
window.addEventListener('resize', function () {
// 如果侧边栏已打开,验证宽度是否合适
if (sidebarOpen) {
const maxAllowedWidth = window.innerWidth * 0.8; // 最大不超过窗口宽度的80%
const currentWidth = parseInt(getComputedStyle(sidebar).width);
if (currentWidth > maxAllowedWidth) {
// 调整为允许的最大宽度
const newWidth = Math.min(maxAllowedWidth, MAX_WIDTH);
sidebar.style.width = `${newWidth}px`;
sidebar.style.setProperty('--sidebar-width', `${newWidth}px`);
settings.sidebarWidth = newWidth;
saveSettings();
}
}
});
}
// 生成高亮列表HTML
function renderHighlightsList() {
if (highlights.length === 0) {
return '<p>当前页面没有高亮内容</p>';
}
// 按时间倒序排列
const sortedHighlights = [...highlights].sort((a, b) => b.timestamp - a.timestamp);
return sortedHighlights.map(highlight => {
// 限制文本长度,避免过长
const displayText = highlight.text.length > 100
? highlight.text.substring(0, 100) + '...'
: highlight.text;
// 格式化时间 - 修改为更精确的格式
const formattedDate = formatDateTime(highlight.timestamp);
return `
<div class="highlight-list-item" data-id="${highlight.id}" style="border-left-color: ${highlight.color}">
<div class="highlight-list-content">${sanitizeHTML(displayText)}</div>
<div class="highlight-list-meta">
<span class="highlight-list-time">${formattedDate}</span>
<div class="highlight-list-actions">
<span class="highlight-list-action highlight-action-jump" data-id="${highlight.id}">跳转</span>
<span class="highlight-list-action highlight-action-remove" data-id="${highlight.id}">删除</span>
</div>
</div>
</div>
`;
}).join('');
}
// 格式化日期时间函数 - YYYY-MM-DD HH:mm:ss
function formatDateTime(timestamp) {
const date = new Date(timestamp);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
// HTML转义函数
function sanitizeHTML(text) {
const element = document.createElement('div');
element.textContent = text;
return element.innerHTML;
}
// 渲染当前页面状态
function renderCurrentPageStatus() {
const isDomainDisabled = disabledList.domains.includes(currentDomain);
const isUrlDisabled = disabledList.urls.includes(currentPageUrl);
if (isDomainDisabled) {
return `
<div class="disabled-item">
<span class="disabled-item-text">此域名 (${currentDomain}) 已禁用高亮</span>
<span class="disabled-item-remove" data-type="domain" data-value="${currentDomain}">启用</span>
</div>
`;
} else if (isUrlDisabled) {
return `
<div class="disabled-item">
<span class="disabled-item-text">此网址已禁用高亮</span>
<span class="disabled-item-remove" data-type="url" data-value="${currentPageUrl}">启用</span>
</div>
`;
} else {
return `
<div class="buttons-row">
<button class="sidebar-button secondary" id="disable-domain">禁用此域名</button>
<button class="sidebar-button secondary" id="disable-url">禁用此网址</button>
</div>
`;
}
}
// 渲染禁用域名列表
function renderDisabledDomains() {
if (disabledList.domains.length === 0) {
return '<p>没有禁用的域名</p>';
}
return disabledList.domains.map(domain => `
<div class="disabled-item">
<span class="disabled-item-text">${domain}</span>
<span class="disabled-item-remove" data-type="domain" data-value="${domain}">删除</span>
</div>
`).join('');
}
// 渲染禁用网址列表
function renderDisabledUrls() {
if (disabledList.urls.length === 0) {
return '<p>没有禁用的网址</p>';
}
return disabledList.urls.map(url => `
<div class="disabled-item">
<span class="disabled-item-text">${url}</span>
<span class="disabled-item-remove" data-type="url" data-value="${url}">删除</span>
</div>
`).join('');
}
// 绑定侧边栏事件 - 修复标签切换
function bindSidebarEvents(sidebar) {
// 关闭侧边栏按钮
sidebar.querySelector('.sidebar-close').addEventListener('click', () => {
toggleSidebar(false); // 显式关闭
});
// 固定侧边栏按钮
sidebar.querySelector('.sidebar-pin').addEventListener('click', (e) => {
const wasPinned = sidebarPinned; // 记录之前的固定状态
sidebarPinned = !sidebarPinned; // 切换固定状态
// 更新按钮样式
e.currentTarget.classList.toggle('pinned', sidebarPinned);
// 保存固定状态
settings.sidebarPinned = sidebarPinned;
saveSettings();
// 显示提示
showToast(sidebarPinned ? '侧边栏已固定' : '侧边栏已取消固定', 'info');
// 如果侧边栏当前是打开状态,处理事件监听器
if (sidebarOpen) {
if (wasPinned && !sidebarPinned) {
// 从固定变为非固定,添加点击外部关闭事件
setTimeout(() => {
document.addEventListener('click', handleOutsideClick);
}, 10);
} else if (!wasPinned && sidebarPinned) {
// 从非固定变为固定,移除点击外部关闭事件
document.removeEventListener('click', handleOutsideClick);
}
}
});
// 选项卡切换 - 修复选择器和事件处理
sidebar.querySelectorAll('.sidebar-tab').forEach(tab => {
tab.addEventListener('click', () => {
console.log('标签点击:', tab.dataset.tab); // 添加调试日志
// 移除所有选项卡的激活状态
sidebar.querySelectorAll('.sidebar-tab').forEach(t => t.classList.remove('active'));
sidebar.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
// 激活当前选项卡
tab.classList.add('active');
// 获取对应的内容元素并显示它
const tabId = `tab-${tab.dataset.tab}`;
const tabContent = document.getElementById(tabId);
if (tabContent) {
tabContent.classList.add('active');
console.log('激活标签内容:', tabId);
} else {
console.error('找不到对应的标签内容元素:', tabId);
}
});
});
// 高亮列表跳转事件
sidebar.querySelectorAll('.highlight-action-jump').forEach(btn => {
btn.addEventListener('click', (e) => {
const id = e.target.dataset.id;
jumpToHighlight(id);
e.stopPropagation();
});
});
// 高亮列表删除事件
sidebar.querySelectorAll('.highlight-action-remove').forEach(btn => {
btn.addEventListener('click', (e) => {
const id = e.target.dataset.id;
removeHighlightById(id);
// 刷新高亮列表
refreshHighlightsList();
e.stopPropagation();
});
});
// 高亮列表项点击事件(跳转)
sidebar.querySelectorAll('.highlight-list-item').forEach(item => {
item.addEventListener('click', () => {
const id = item.dataset.id;
jumpToHighlight(id);
});
});
// 保存设置按钮
sidebar.querySelector('#save-settings').addEventListener('click', () => {
settings.triggerMode = sidebar.querySelector('#triggerMode').value;
settings.minTextLength = parseInt(sidebar.querySelector('#minTextLength').value);
settings.sidebarDescription = sidebar.querySelector('#sidebarDescription').value;
settings.showFloatingButton = sidebar.querySelector('#showFloatingButton').checked;
saveSettings();
updateSidebarDescription();
// 显示保存成功提示
showToast('设置已保存');
});
// 颜色选择器事件
const colorElements = sidebar.querySelectorAll('.highlight-colors-setting .highlight-menu-color');
colorElements.forEach(el => {
el.addEventListener('click', (e) => {
settings.activeColor = e.target.dataset.color;
colorElements.forEach(c => c.classList.remove('active'));
e.target.classList.add('active');
});
});
// 刷新高亮列表
sidebar.querySelector('#refresh-highlights').addEventListener('click', refreshHighlightsList);
// 清除所有高亮
sidebar.querySelector('#clear-all-highlights').addEventListener('click', () => {
if (confirm('确定要清除当前页面的所有高亮吗?')) {
clearAllHighlights();
refreshHighlightsList();
}
});
// 禁用当前域名
const disableDomainBtn = sidebar.querySelector('#disable-domain');
if (disableDomainBtn) {
disableDomainBtn.addEventListener('click', () => {
if (confirm(`确定要禁用域名 ${currentDomain} 上的高亮功能吗?`)) {
disableDomain(currentDomain);
}
});
}
// 禁用当前网址
const disableUrlBtn = sidebar.querySelector('#disable-url');
if (disableUrlBtn) {
disableUrlBtn.addEventListener('click', () => {
if (confirm('确定要禁用当前网址的高亮功能吗?')) {
disableUrl(currentPageUrl);
}
});
}
// 删除禁用项
sidebar.querySelectorAll('.disabled-item-remove').forEach(btn => {
btn.addEventListener('click', (e) => {
const type = e.target.dataset.type;
const value = e.target.dataset.value;
if (type === 'domain') {
enableDomain(value);
} else if (type === 'url') {
enableUrl(value);
}
});
});
// 添加禁用域名
sidebar.querySelector('#add-domain-btn').addEventListener('click', () => {
const input = sidebar.querySelector('#add-domain-input');
const domain = input.value.trim();
if (domain) {
disableDomain(domain);
input.value = '';
}
});
// 添加禁用网址
sidebar.querySelector('#add-url-btn').addEventListener('click', () => {
const input = sidebar.querySelector('#add-url-input');
const url = input.value.trim();
if (url) {
disableUrl(url);
input.value = '';
}
});
}
// 刷新高亮列表
function refreshHighlightsList() {
const listContainer = document.querySelector('.highlight-list');
if (listContainer) {
listContainer.innerHTML = renderHighlightsList();
// 重新绑定事件
const sidebar = document.querySelector('.highlight-sidebar');
if (sidebar) {
// 高亮列表跳转事件
sidebar.querySelectorAll('.highlight-action-jump').forEach(btn => {
btn.addEventListener('click', (e) => {
const id = e.target.dataset.id;
jumpToHighlight(id);
e.stopPropagation();
});
});
// 高亮列表删除事件
sidebar.querySelectorAll('.highlight-action-remove').forEach(btn => {
btn.addEventListener('click', (e) => {
const id = e.target.dataset.id;
removeHighlightById(id);
// 刷新高亮列表
refreshHighlightsList();
e.stopPropagation();
});
});
// 高亮列表项点击事件(跳转)
sidebar.querySelectorAll('.highlight-list-item').forEach(item => {
item.addEventListener('click', () => {
const id = item.dataset.id;
jumpToHighlight(id);
});
});
}
}
}
// 保存设置
function saveSettings() {
GM_setValue('highlight_settings', settings);
}
// 注册菜单命令
function registerMenuCommands() {
GM_registerMenuCommand('设置', function () {
toggleSidebar();
// 模拟点击设置标签页,切换到设置面板
setTimeout(() => {
const settingsTab = document.querySelector('.sidebar-tab[data-tab="settings"]');
if (settingsTab) settingsTab.click();
}, 50); // 短暂延迟确保侧边栏已渲染
return false; // 明确返回false,表示没有异步响应
});
GM_registerMenuCommand('清除所有高亮', function () {
clearAllHighlights();
return false; // 明确返回false,表示没有异步响应
});
GM_registerMenuCommand('显示/隐藏浮动按钮', function () {
toggleFloatingButton();
return false; // 明确返回false,表示没有异步响应
});
}
// 清除所有高亮
function clearAllHighlights() {
document.querySelectorAll('.highlight-marked').forEach(el => {
const parent = el.parentNode;
while (el.firstChild) {
parent.insertBefore(el.firstChild, el);
}
parent.removeChild(el);
});
highlights = [];
saveHighlights();
}
// 通过文本内容查找并高亮
function findAndHighlightText(highlight) {
// 预处理文本,移除多余空格
const searchText = highlight.text.trim().replace(/\s+/g, ' ');
// 获取适合搜索的文本长度
const searchLength = Math.min(300, searchText.length);
const searchFragment = searchText.substring(0, searchLength);
// 文本长度检查 - 修改为更小的阈值,短文本使用精确匹配
const MIN_LENGTH_FOR_NORMAL_SEARCH = 5;
if (searchFragment.length < MIN_LENGTH_FOR_NORMAL_SEARCH) {
console.log(`短文本特殊处理: "${searchFragment}"`);
// 对于短文本,使用更精确的匹配方式
return findShortTextExactMatch(searchFragment, highlight);
}
console.log(`使用文本片段搜索: "${searchFragment.substring(0, 50)}..."`);
// 创建TreeWalker来遍历所有文本节点
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
{
acceptNode: function (node) {
// 跳过不可见元素内的文本
const parent = node.parentElement;
if (!parent) return NodeFilter.FILTER_SKIP;
const style = window.getComputedStyle(parent);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
return NodeFilter.FILTER_REJECT;
}
// 跳过脚本和样式标签内容
const parentTag = parent.tagName.toLowerCase();
if (parentTag === 'script' || parentTag === 'style' || parentTag === 'noscript') {
return NodeFilter.FILTER_REJECT;
}
// 避免处理已经高亮的内容
if (parent.classList && parent.classList.contains('highlight-marked')) {
return NodeFilter.FILTER_REJECT;
}
// 接受有内容的文本节点,降低最小长度要求
return node.textContent.trim().length > 0 ?
NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
}
}
);
// 计算文本在页面中出现的次数,并确定应该高亮第几次出现
const textOccurrenceInfo = findTextOccurrence(searchFragment, highlight.id);
console.log(`文本"${searchFragment.substring(0, 30)}..."在页面中出现${textOccurrenceInfo.totalOccurrences}次,当前应高亮第${textOccurrenceInfo.skipCount + 1}次出现`);
// 尝试精确匹配,传入skipCount参数指示跳过前面几个匹配项
let result = tryExactMatch(walker, searchFragment, highlight, textOccurrenceInfo.skipCount);
if (result) return true;
// 如果精确匹配失败,尝试模糊匹配(只用较短的文本片段)
console.log('精确匹配失败,尝试模糊匹配');
// 使用前80个字符进行模糊匹配
const fuzzySearchText = searchFragment.substring(0, Math.min(80, searchFragment.length));
if (fuzzySearchText.length >= 10) {
// 重置walker
walker.currentNode = document.body;
return tryFuzzyMatch(walker, fuzzySearchText, highlight);
}
return false; // 没有找到匹配
}
// 查找文本在页面中出现的次数和位置信息
function findTextOccurrence(searchText, highlightId) {
// 默认值
let result = {
totalOccurrences: 0,
skipCount: 0
};
// 查找页面中已经存在该文本的高亮元素
const existingHighlights = document.querySelectorAll(`.highlight-marked`);
let matchingNodes = [];
// 第一步:计算总共出现次数
const textFinder = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
{
acceptNode: function (node) {
// 使用与上面相同的过滤逻辑
const parent = node.parentElement;
if (!parent) return NodeFilter.FILTER_SKIP;
const style = window.getComputedStyle(parent);
if (style.display === 'none' || style.visibility === 'hidden') {
return NodeFilter.FILTER_REJECT;
}
const parentTag = parent.tagName.toLowerCase();
if (parentTag === 'script' || parentTag === 'style') {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
}
}
);
// 遍历所有文本节点寻找匹配的文本
let textNode;
while (textNode = textFinder.nextNode()) {
const nodeText = textNode.textContent;
let startIndex = 0;
let index;
// 在当前节点中查找所有匹配项
while ((index = nodeText.indexOf(searchText, startIndex)) !== -1) {
result.totalOccurrences++;
matchingNodes.push({
node: textNode,
index: index
});
startIndex = index + 1; // 继续查找下一个匹配位置
}
}
// 第二步:确定应该跳过的数量
// 查看已有的同文本高亮,确定这次应该高亮第几次出现的文本
for (let i = 0; i < existingHighlights.length; i++) {
const highlightElement = existingHighlights[i];
// 排除当前正在处理的高亮元素
if (highlightElement.dataset.id === highlightId) {
continue;
}
// 检查文本是否匹配
if (highlightElement.textContent === searchText) {
result.skipCount++;
}
}
return result;
}
// 添加新函数: 短文本精确匹配
function findShortTextExactMatch(searchText, highlight) {
// 为短文本创建一个专用的TreeWalker
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
{
acceptNode: function (node) {
// 基本过滤与主函数相同
const parent = node.parentElement;
if (!parent) return NodeFilter.FILTER_SKIP;
const style = window.getComputedStyle(parent);
if (style.display === 'none' || style.visibility === 'hidden') {
return NodeFilter.FILTER_REJECT;
}
// 添加这一行定义parentTag变量
const parentTag = parent.tagName.toLowerCase();
if (parentTag === 'script' || parentTag === 'style') {
return NodeFilter.FILTER_REJECT;
}
if (parent.classList && parent.classList.contains('highlight-marked')) {
return NodeFilter.FILTER_REJECT;
}
// 对短文本,我们需要精确匹配
if (node.textContent.includes(searchText)) {
return NodeFilter.FILTER_ACCEPT;
}
return NodeFilter.FILTER_SKIP;
}
}
);
// 遍历找到包含该短文本的节点
let textNode;
while (textNode = walker.nextNode()) {
const nodeText = textNode.textContent;
const startIndex = nodeText.indexOf(searchText);
if (startIndex >= 0) {
try {
// 创建范围并应用高亮
const range = document.createRange();
range.setStart(textNode, startIndex);
range.setEnd(textNode, startIndex + searchText.length);
if (applyHighlightToRange(range, highlight, true)) {
console.log(`短文本精确匹配成功: "${searchText}"`);
return true;
}
} catch (e) {
console.log('短文本匹配失败:', e);
continue;
}
}
}
console.log(`短文本无法找到匹配: "${searchText}"`);
return false;
}
// 尝试精确匹配
function tryExactMatch(walker, searchText, highlight, skipCount = 0) {
let textNode;
let matchCount = 0; // 用于计数匹配到的次数
while (textNode = walker.nextNode()) {
// 获取节点的完整文本内容
const nodeText = textNode.textContent;
let startSearchPos = 0;
let currentIndex;
// 在当前节点内查找所有匹配项
while ((currentIndex = nodeText.indexOf(searchText, startSearchPos)) >= 0) {
// 找到一个匹配项,检查是否应该跳过
if (matchCount < skipCount) {
// 还需要跳过这个匹配项
matchCount++;
startSearchPos = currentIndex + 1; // 从下一个位置继续搜索
continue;
}
try {
// 找到了需要高亮的匹配项,创建一个范围
const range = document.createRange();
range.setStart(textNode, currentIndex);
range.setEnd(textNode, currentIndex + searchText.length);
// 应用高亮
if (applyHighlightToRange(range, highlight, true)) {
console.log(`精确匹配成功(第${matchCount + 1}次出现): "${searchText.substring(0, 30)}..."`);
return true;
}
} catch (e) {
console.log('精确匹配失败:', e);
}
// 无论是否成功,都移动到下一个位置继续搜索
matchCount++;
startSearchPos = currentIndex + 1;
}
}
return false;
}
// 尝试模糊匹配
function tryFuzzyMatch(walker, searchText, highlight) {
let textNode;
while (textNode = walker.nextNode()) {
// 获取节点的完整文本内容并规范化
const nodeText = textNode.textContent.trim().replace(/\s+/g, ' ');
// 检查是否包含搜索文本的大部分内容(允许少量差异)
if (fuzzyContains(nodeText, searchText)) {
try {
// 找到最佳匹配位置
const bestMatchIndex = getBestMatchPosition(nodeText, searchText);
if (bestMatchIndex >= 0) {
// 创建一个范围,使用模糊匹配的最佳位置
const range = document.createRange();
const endPos = Math.min(bestMatchIndex + searchText.length, nodeText.length);
range.setStart(textNode, bestMatchIndex);
range.setEnd(textNode, endPos);
// 应用高亮
if (applyHighlightToRange(range, highlight, true)) {
console.log(`模糊匹配成功: "${searchText.substring(0, 30)}..."`);
return true;
}
}
} catch (e) {
console.log('模糊匹配失败:', e);
continue; // 继续尝试其他匹配项
}
}
}
return false;
}
// 检查文本是否模糊包含搜索文本
function fuzzyContains(text, search) {
// 简单实现:检查是否包含80%以上的搜索文本
const minLength = Math.floor(search.length * 0.8);
let matchedChars = 0;
// 在text中查找search的字符,允许少量不连续
let lastIndex = -1;
for (let i = 0; i < search.length; i++) {
const char = search[i];
const index = text.indexOf(char, lastIndex + 1);
if (index > -1) {
matchedChars++;
lastIndex = index;
}
}
return matchedChars >= minLength;
}
// 找出最佳匹配位置
function getBestMatchPosition(text, search) {
// 简单实现:寻找最长连续匹配子串的起始位置
let bestLength = 0;
let bestIndex = -1;
for (let i = 0; i <= text.length - search.length; i++) {
let matchLength = 0;
for (let j = 0; j < search.length; j++) {
if (i + j < text.length && text[i + j] === search[j]) {
matchLength++;
} else {
break;
}
}
if (matchLength > bestLength) {
bestLength = matchLength;
bestIndex = i;
}
}
// 要求至少匹配50%的字符才返回位置
return bestLength >= search.length * 0.5 ? bestIndex : -1;
}
// 将高亮应用到指定范围(提取通用逻辑)
function applyHighlightToRange(range, highlight, isRecovered = false) {
try {
const span = document.createElement('span');
span.className = 'highlight-marked';
if (isRecovered) {
span.classList.add('highlight-recovered');
}
span.style.backgroundColor = highlight.color;
span.dataset.id = highlight.id;
try {
range.surroundContents(span);
span.addEventListener('click', (e) => {
e.stopPropagation();
showHighlightContextMenu(e, highlight.id);
});
return true;
} catch (e) {
console.log('无法直接应用高亮,使用备用方法:', e);
try {
// 尝试替代方法
const fragment = range.extractContents();
span.appendChild(fragment);
range.insertNode(span);
span.addEventListener('click', (e) => {
e.stopPropagation();
showHighlightContextMenu(e, highlight.id);
});
return true;
} catch (innerErr) {
console.error('所有方法都无法应用高亮:', innerErr);
return false;
}
}
} catch (e) {
console.error('应用高亮到范围失败:', e);
return false;
}
}
// 改进从路径构建Range对象的函数 - 添加更多错误处理
function constructRangeFromPath(path) {
if (!path || !path.startContainer || !path.endContainer) {
console.log('路径信息不完整');
return null;
}
try {
const range = document.createRange();
const startContainer = getNodeByPath(path.startContainer);
const endContainer = getNodeByPath(path.endContainer);
if (!startContainer || !endContainer) {
console.log('无法找到开始或结束节点');
return null;
}
range.setStart(startContainer, path.startOffset);
range.setEnd(endContainer, path.endOffset);
return range;
} catch (e) {
console.log('构建Range时出错:', e);
return null;
}
}
// 改进根据路径获取节点的函数 - 增加健壮性
function getNodeByPath(path) {
if (!path || !Array.isArray(path) || path.length === 0) {
return null;
}
let node = document.body;
try {
for (let i = 0; i < path.length; i++) {
const index = path[i];
// 检查子节点索引是否有效
if (!node.childNodes || node.childNodes.length <= index) {
return null;
}
node = node.childNodes[index];
}
return node;
} catch (e) {
console.error('根据路径获取节点失败:', e);
return null;
}
}
// 获取节点路径
function getNodePath(node, root = document.body) {
const path = [];
let current = node;
while (current !== root) {
const parent = current.parentNode;
if (!parent) break;
const index = Array.prototype.indexOf.call(parent.childNodes, current);
path.unshift(index);
current = parent;
}
return path;
}
// 创建高亮菜单 - 微信读书风格
function createHighlightMenu(isNewHighlight = true) {
const menu = document.createElement('div');
menu.className = 'highlight-menu';
menu.innerHTML = `
<div class="highlight-menu-colors">
${settings.colors.map(color => `
<div class="highlight-menu-color ${!isNewHighlight && color === settings.activeColor ? 'active' : ''}"
style="background-color: ${color};"
data-color="${color}">
</div>
`).join('')}
</div>
`;
// 添加属性用于跟踪当前高亮ID
menu.dataset.currentHighlightId = '';
document.body.appendChild(menu);
return menu;
}
// 移除高亮菜单 - 平滑淡出
function removeHighlightMenu() {
// 移除可能存在的全局点击事件处理器
document.removeEventListener('click', window.currentMenuCloseHandler);
window.currentMenuCloseHandler = null;
const menu = document.querySelector('.highlight-menu');
if (menu) {
// 平滑移除:触发淡出动画,再移除元素
menu.classList.remove('show');
// 等待动画完成再移除DOM元素
setTimeout(() => {
if (menu.parentNode) menu.remove();
}, 200);
}
}
// 显示高亮菜单 - 修改动画逻辑为直接淡入
function showHighlightMenu(e) {
const selection = window.getSelection();
const selectedText = selection.toString().trim();
if (!selectedText || selectedText.length < settings.minTextLength) {
return;
}
// 如果菜单正在动画中,不再触发新的显示
if (menuAnimating) {
return;
}
// 设置动画状态为true
menuAnimating = true;
// 设置忽略下一次点击,防止双击时第二次点击关闭菜单
ignoreNextClick = true;
// 保存当前选择范围
const savedRange = selection.rangeCount > 0 ? selection.getRangeAt(0).cloneRange() : null;
// 移除现有菜单
removeHighlightMenu();
// 新选中文本时不应该显示已选中颜色图标
const menu = createHighlightMenu(true);
// 获取选择范围的客户端矩形集合 - 这比getBoundingClientRect更精确
const range = selection.getRangeAt(0);
const rects = range.getClientRects();
if (rects.length === 0) {
menuAnimating = false;
return; // 没有矩形,无法定位
}
// 确定最佳的矩形用于定位
// 对于多行文本,使用第一行的矩形
const targetRect = rects[0];
// 预估菜单高度
const menuHeight = 50;
// 计算初始位置 - 放在选中文本上方中心
let initialTop = window.scrollY + targetRect.top - menuHeight - 8;
let showAbove = true;
// 如果上方空间不足,放在下方
if (targetRect.top < menuHeight + 10) {
initialTop = window.scrollY + targetRect.bottom + 8;
showAbove = false;
}
// 设置菜单初始位置
menu.style.top = `${initialTop}px`;
// 等待菜单渲染完成后调整水平位置
setTimeout(() => {
const menuWidth = menu.offsetWidth;
// 计算选中文本中心位置 - 使用实际选中矩形
const textCenterX = targetRect.left + (targetRect.width / 2);
// 确保菜单在视口范围内
let menuLeft;
if (textCenterX - (menuWidth / 2) < 5) {
menuLeft = 5; // 贴近左边界
} else if (textCenterX + (menuWidth / 2) > window.innerWidth - 5) {
menuLeft = window.innerWidth - menuWidth - 5; // 贴近右边界
} else {
menuLeft = textCenterX - (menuWidth / 2); // 居中对齐
}
// 设置菜单位置
menu.style.left = `${menuLeft}px`;
// 清除任何先前的transform
menu.style.transform = 'none';
// 计算并设置箭头位置 - 箭头应该精确指向选中文本中心
const arrowLeft = textCenterX - menuLeft;
// 确保箭头在合理范围内
const minArrowLeft = 12; // 避免太靠近边缘
const maxArrowLeft = menuWidth - 12;
const safeArrowLeft = Math.max(minArrowLeft, Math.min(arrowLeft, maxArrowLeft));
// 设置箭头位置
menu.style.setProperty('--arrow-left', `${safeArrowLeft}px`);
// 设置箭头方向
if (!showAbove) {
menu.classList.add('arrow-top');
} else {
menu.classList.remove('arrow-top');
}
// 添加显示类触发淡入动画
requestAnimationFrame(() => {
menu.classList.add('show');
// 动画完成后重置状态
setTimeout(() => {
menuAnimating = false;
}, 250);
setTimeout(() => {
ignoreNextClick = false;
}, 300);
});
}, 0);
// 绑定颜色选择事件
menu.querySelectorAll('.highlight-menu-color').forEach(el => {
el.addEventListener('click', (e) => {
// 如果正在处理颜色点击,忽略此次点击
if (isProcessingColorClick) {
e.stopPropagation();
return;
}
// 设置处理状态为true
isProcessingColorClick = true;
const color = el.dataset.color;
const isActive = el.classList.contains('active');
const currentHighlightId = menu.dataset.currentHighlightId;
// 如果是激活状态的颜色块,执行删除但保持菜单显示
if (isActive) {
if (currentHighlightId) {
removeHighlightById(currentHighlightId);
// 删除后重置菜单状态,而不是移除菜单
menu.dataset.currentHighlightId = '';
// 重置所有颜色状态为非激活
menu.querySelectorAll('.highlight-menu-color').forEach(colorEl => {
colorEl.classList.remove('active');
});
} else {
// 如果是新选择的文本,只清除选择而不关闭菜单
window.getSelection().removeAllRanges();
// 重置所有颜色状态为非激活
menu.querySelectorAll('.highlight-menu-color').forEach(colorEl => {
colorEl.classList.remove('active');
});
}
} else {
// 点击非激活状态的颜色块 - 应用高亮颜色
settings.activeColor = color;
saveSettings();
if (currentHighlightId) {
// 如果已有高亮ID,直接更新颜色
changeHighlightColor(currentHighlightId, color);
} else {
// 检查选择是否丢失并恢复
const selection = window.getSelection();
if (selection.toString().trim() === '' && savedRange) {
selection.removeAllRanges();
selection.addRange(savedRange);
}
// 应用高亮
const newHighlightId = highlightSelection(color);
// 保存高亮ID以便后续更新
if (newHighlightId) {
menu.dataset.currentHighlightId = newHighlightId;
}
}
// 更新菜单中的活动颜色
menu.querySelectorAll('.highlight-menu-color').forEach(colorEl => {
colorEl.classList.toggle('active', colorEl.dataset.color === color);
});
}
// 在处理完成后重置状态
setTimeout(() => {
isProcessingColorClick = false;
}, 50);
// 阻止事件冒泡,防止触发document的click事件
e.stopPropagation();
});
});
// 存储关闭函数的引用以便后续移除
const closeMenu = function (e) {
// 如果标记为忽略点击,则不关闭菜单
if (ignoreNextClick) {
return;
}
if (!menu.contains(e.target)) {
removeHighlightMenu();
}
};
// 全局存储当前菜单的关闭处理器以确保能正确移除
window.currentMenuCloseHandler = closeMenu;
// 修改:先添加事件监听器,再重置ignoreNextClick
setTimeout(() => {
document.addEventListener('click', window.currentMenuCloseHandler);
// 确保事件监听器添加后再重置忽略标志
setTimeout(() => {
ignoreNextClick = false;
}, 50);
}, 300);
}
// 显示高亮上下文菜单 - 应用相同的动画修改
function showHighlightContextMenu(e, id) {
// 如果菜单正在动画中,不再触发新的显示
if (menuAnimating) {
return;
}
// 设置动画状态为true
menuAnimating = true;
// 设置忽略下一次点击,防止误触关闭菜单
ignoreNextClick = true;
// 移除现有菜单
removeHighlightMenu();
// 获取高亮元素的位置
const highlightElements = document.querySelectorAll(`.highlight-marked[data-id="${id}"]`);
if (!highlightElements.length) {
menuAnimating = false;
return;
}
// 获取触发事件的高亮元素或第一个高亮元素
let targetElement;
if (e && e.target && e.target.classList.contains('highlight-marked')) {
targetElement = e.target; // 使用触发事件的元素
} else {
targetElement = highlightElements[0]; // 使用第一个高亮元素
}
const rect = targetElement.getBoundingClientRect();
// 对已存在高亮的菜单,应该显示已选中的颜色
const menu = createHighlightMenu(false);
menu.dataset.currentHighlightId = id;
// 找到当前高亮的颜色并更新菜单
const highlight = highlights.find(h => h.id === id);
if (highlight) {
menu.querySelectorAll('.highlight-menu-color').forEach(colorEl => {
colorEl.classList.toggle('active', colorEl.dataset.color === highlight.color);
});
}
// 预估菜单高度
const menuHeight = 50;
// 计算初始位置 - 放在高亮元素上方中心
let initialTop = window.scrollY + rect.top - menuHeight - 8;
let showAbove = true;
// 如果上方空间不足,放在下方
if (rect.top < menuHeight + 10) {
initialTop = window.scrollY + rect.bottom + 8;
showAbove = false;
}
// 设置菜单初始位置
menu.style.top = `${initialTop}px`;
// 调整菜单确保在可视区域内
setTimeout(() => {
const menuWidth = menu.offsetWidth;
// 计算高亮元素中心位置
const targetCenterX = rect.left + (rect.width / 2);
// 确保菜单在视口范围内
let menuLeft;
if (targetCenterX - (menuWidth / 2) < 5) {
menuLeft = 5; // 贴近左边界
} else if (targetCenterX + (menuWidth / 2) > window.innerWidth - 5) {
menuLeft = window.innerWidth - menuWidth - 5; // 贴近右边界
} else {
menuLeft = targetCenterX - (menuWidth / 2); // 居中对齐
}
// 设置菜单位置
menu.style.left = `${menuLeft}px`;
// 清除任何先前的transform
menu.style.transform = 'none';
// 计算并设置箭头位置 - 箭头应该指向高亮元素中心
const arrowLeft = targetCenterX - menuLeft;
// 确保箭头在合理范围内
const minArrowLeft = 12; // 避免太靠近边缘
const maxArrowLeft = menuWidth - 12;
const safeArrowLeft = Math.max(minArrowLeft, Math.min(arrowLeft, maxArrowLeft));
// 设置箭头位置
menu.style.setProperty('--arrow-left', `${safeArrowLeft}px`);
// 设置箭头方向
if (!showAbove) {
menu.classList.add('arrow-top');
} else {
menu.classList.remove('arrow-top');
}
// 添加显示类触发淡入动画
requestAnimationFrame(() => {
menu.classList.add('show');
// 动画完成后重置状态
setTimeout(() => {
menuAnimating = false;
}, 250);
setTimeout(() => {
ignoreNextClick = false;
}, 300);
});
}, 0);
// 绑定颜色选择事件
menu.querySelectorAll('.highlight-menu-color').forEach(el => {
el.addEventListener('click', (e) => {
// 如果正在处理颜色点击,忽略此次点击
if (isProcessingColorClick) {
e.stopPropagation();
return;
}
// 设置处理状态为true
isProcessingColorClick = true;
const color = el.dataset.color;
const isActive = el.classList.contains('active');
const currentHighlightId = menu.dataset.currentHighlightId;
// 如果是激活状态的颜色块,执行删除但保持菜单显示
if (isActive) {
if (currentHighlightId) {
removeHighlightById(currentHighlightId);
// 删除后重置菜单状态,而不是移除菜单
menu.dataset.currentHighlightId = '';
// 重置所有颜色状态为非激活
menu.querySelectorAll('.highlight-menu-color').forEach(colorEl => {
colorEl.classList.remove('active');
});
}
} else {
// 点击非激活状态的颜色块 - 更新高亮颜色
changeHighlightColor(currentHighlightId, color);
// 更新菜单中的活动颜色
menu.querySelectorAll('.highlight-menu-color').forEach(colorEl => {
colorEl.classList.toggle('active', colorEl.dataset.color === color);
});
}
// 在处理完成后重置状态
setTimeout(() => {
isProcessingColorClick = false;
}, 50);
// 阻止事件冒泡,防止触发document的click事件
e.stopPropagation();
});
});
// 存储关闭函数的引用以便后续移除
const closeMenu = function (e) {
// 如果标记为忽略点击,则不关闭菜单
if (ignoreNextClick) {
return;
}
if (!menu.contains(e.target)) {
removeHighlightMenu();
}
};
// 全局存储当前菜单的关闭处理器以确保能正确移除
window.currentMenuCloseHandler = closeMenu;
// 修改:先添加事件监听器,再重置ignoreNextClick
setTimeout(() => {
document.addEventListener('click', window.currentMenuCloseHandler);
// 确保事件监听器添加后再重置忽略标志
setTimeout(() => {
ignoreNextClick = false;
}, 50);
}, 300);
}
// 高亮选中文本
function highlightSelection(color) {
const selection = window.getSelection();
if (!selection.toString().trim()) {
console.log('尝试高亮时没有选择文本');
return null;
}
try {
// 检查选择是否仍然有范围
if (selection.rangeCount === 0) {
console.log('选择中没有范围 - 选择可能已经丢失');
return null;
}
const range = selection.getRangeAt(0);
const selectedText = selection.toString().trim();
console.log('正在高亮文本:', selectedText);
// 移除原有的文本去重逻辑,统一创建新的高亮记录
// 生成唯一ID
const id = 'highlight-' + Date.now();
// 创建路径信息
const pathInfo = {
startContainer: getNodePath(range.startContainer),
startOffset: range.startOffset,
endContainer: getNodePath(range.endContainer),
endOffset: range.endOffset
};
// 检查范围内是否包含已有的高亮元素
const containsHighlight = containsHighlightElement(range);
// 使用安全的高亮处理方法
if (containsHighlight) {
// 使用增强的高亮方法处理复杂选择
safelyHighlightComplexRange(range, id, color);
} else {
// 创建高亮元素
const span = document.createElement('span');
span.className = 'highlight-marked';
span.style.backgroundColor = color;
span.dataset.id = id;
try {
range.surroundContents(span);
console.log('成功使用surroundContents应用高亮');
} catch (e) {
console.log('无法直接高亮,尝试替代方法:', e);
safelyHighlightComplexRange(range, id, color);
}
}
// 添加点击事件监听器
const highlightElements = document.querySelectorAll(`[data-id="${id}"]`);
highlightElements.forEach(el => {
el.addEventListener('click', (e) => {
e.stopPropagation();
showHighlightContextMenu(e, id);
});
});
// 保存高亮信息
highlights.push({
id: id,
text: selectedText,
color: color,
timestamp: Date.now(),
path: pathInfo,
isComplex: containsHighlight
});
saveHighlights();
console.log('新高亮已保存,ID:', id);
// 检查侧边栏是否打开,如果打开则刷新高亮列表
if (sidebarOpen) {
refreshHighlightsList();
}
return id;
} catch (e) {
console.error('无法高亮:', e);
alert('无法高亮选定的文本。尝试选择较短的文本段落或避免跨多个元素选择。');
return null;
}
}
// 检查范围内是否包含已有的高亮元素
function containsHighlightElement(range) {
const nodes = getNodesInRange(range);
return nodes.some(node =>
node.nodeType === Node.ELEMENT_NODE &&
node.classList &&
node.classList.contains('highlight-marked')
);
}
// 修改高亮颜色 - 更新为支持多元素复杂高亮
function changeHighlightColor(id, color) {
// 查找所有具有相同ID的高亮元素(适用于复杂情况)
const highlightElements = document.querySelectorAll(`.highlight-marked[data-id="${id}"]`);
if (highlightElements.length > 0) {
highlightElements.forEach(el => {
el.style.backgroundColor = color;
});
// 更新数据
const index = highlights.findIndex(h => h.id === id);
if (index !== -1) {
// 更新颜色和时间戳
highlights[index].color = color;
highlights[index].timestamp = Date.now(); // 更新时间戳,使其在列表中保持靠前
saveHighlights();
// 尝试刷新侧边栏列表,保持UI同步
refreshHighlightsList();
}
}
}
// 获取Range内的所有节点
function getNodesInRange(range) {
const nodes = [];
const iterator = document.createNodeIterator(
range.commonAncestorContainer,
NodeFilter.SHOW_ALL
);
let node;
while (node = iterator.nextNode()) {
if (range.intersectsNode(node)) {
nodes.push(node);
}
}
return nodes;
}
// 注册事件
function registerEvents() {
// 监听鼠标抬起事件
document.addEventListener('mouseup', function (e) {
// 如果页面被禁用,不执行任何高亮操作
if (isHighlightDisabled) {
return;
}
// 防止点击菜单时触发
if (e.target.closest('.highlight-menu') || e.target.closest('.highlight-settings')) {
return;
}
// 检查是否在侧边栏内选择文本
if (e.target.closest('.highlight-sidebar')) {
return;
}
// 检测是否为双击
const isDoubleClick = (e.detail === 2);
// 加强防抖处理,防止双击触发两次
clearTimeout(menuDisplayTimer);
// 如果菜单正在动画中,直接跳过
if (menuAnimating) {
return;
}
// 检查距离上次显示菜单的时间是否大于300ms,防止双击显示两次
const now = Date.now();
if (now - lastMenuDisplayTime < 300) { // 增加时间阈值
return;
}
// 调整延迟时间根据是否为双击
// 双击情况下增加更多延迟,以确保选择完成
const delay = isDoubleClick ? 30 : 10;
menuDisplayTimer = setTimeout(() => {
if (settings.triggerMode === 'auto') {
showHighlightMenu(e);
lastMenuDisplayTime = Date.now(); // 记录本次显示时间
}
// 注意:这里不再检查Shift键,因为我们改用Ctrl+Alt触发
}, delay);
});
// 新增:监听键盘事件,检测Ctrl+Alt组合键
document.addEventListener('keydown', function (e) {
// 如果页面被禁用,不执行任何高亮操作
if (isHighlightDisabled) {
return;
}
// 检查是否为Ctrl+Alt组合键,且triggerMode为hotkey
if (e.ctrlKey && e.altKey && !e.shiftKey && !e.metaKey && settings.triggerMode === 'hotkey') {
const selection = window.getSelection();
const selectedText = selection.toString().trim();
if (selectedText && selectedText.length >= settings.minTextLength) {
// 直接应用高亮,使用当前活动颜色
highlightSelection(settings.activeColor);
e.preventDefault(); // 阻止默认行为
}
}
});
}
// 侧边栏开关功能
function toggleSidebar(forcedState) {
console.log("切换侧边栏, 强制状态:", forcedState);
const sidebar = document.querySelector('.highlight-sidebar');
console.log("侧边栏元素存在:", sidebar ? "是" : "否");
if (!sidebar) {
console.warn("找不到侧边栏元素,尝试重新创建");
createSidebar();
// 再次尝试获取侧边栏
const newSidebar = document.querySelector('.highlight-sidebar');
if (!newSidebar) {
console.error("侧边栏创建失败!");
return;
}
// 如果创建成功,强制设置为打开状态
sidebarOpen = false; // 确保初始状态为关闭
forcedState = true; // 然后强制打开
}
// 记录之前的状态,用于检测是否从关闭变为打开
const wasOpen = sidebarOpen;
// 如果提供了强制状态,使用它;否则切换当前状态
if (forcedState !== undefined) {
sidebarOpen = forcedState;
} else {
sidebarOpen = !sidebarOpen;
}
console.log("侧边栏状态:", sidebarOpen ? "打开" : "关闭");
if (sidebarOpen) {
// 确保CSS变量设置正确
const width = settings.sidebarWidth || 320;
sidebar.style.setProperty('--sidebar-width', `${width}px`);
sidebar.style.width = `${width}px`;
// 强制浏览器重绘,确保过渡效果正常
sidebar.offsetHeight; // 触发重绘
sidebar.classList.add('open');
// 明确设置right为0,确保显示
sidebar.style.right = '0';
// 从关闭状态变为打开状态时,刷新高亮列表
if (!wasOpen) {
refreshHighlightsList();
}
// 如果侧边栏没有被固定,添加点击外部关闭事件
if (!sidebarPinned) {
setTimeout(() => {
document.addEventListener('click', handleOutsideClick);
}, 10);
}
} else {
sidebar.classList.remove('open');
// 确保关闭时隐藏位置与当前宽度匹配
const currentWidth = parseInt(getComputedStyle(sidebar).getPropertyValue('--sidebar-width')) || settings.sidebarWidth || 320;
sidebar.style.setProperty('--sidebar-width', `${currentWidth}px`);
sidebar.style.right = `calc(-1 * ${currentWidth}px)`;
// 移除点击外部关闭事件
document.removeEventListener('click', handleOutsideClick);
}
}
// 处理点击侧边栏外部事件
function handleOutsideClick(e) {
const sidebar = document.querySelector('.highlight-sidebar');
if (!sidebar) return;
// 如果侧边栏已固定,不关闭
if (sidebarPinned) {
document.removeEventListener('click', handleOutsideClick);
return;
}
// 如果点击的是侧边栏内部或高亮工具按钮,不关闭
if (sidebar.contains(e.target) || e.target.closest('.highlight-toolbar')) {
return;
}
// 关闭侧边栏
toggleSidebar(false);
}
// 跳转到高亮位置
function jumpToHighlight(id) {
const highlightElement = document.querySelector(`.highlight-marked[data-id="${id}"]`);
if (highlightElement) {
// 计算元素相对于视口顶部的距离
const elementTop = highlightElement.getBoundingClientRect().top + window.scrollY;
// 添加一个临时闪烁类来突出显示高亮内容
highlightElement.classList.add('highlight-flash');
// 设置一个偏移量,使元素不会紧贴窗口顶部
const offset = window.innerHeight * 0.2;
// 平滑滚动到元素位置
window.scrollTo({
top: elementTop - offset,
behavior: 'smooth'
});
// 滚动后移除闪烁效果
setTimeout(() => {
highlightElement.classList.remove('highlight-flash');
}, 1500);
}
}
// 禁用域名
function disableDomain(domain) {
// 防止重复添加
if (!disabledList.domains.includes(domain)) {
disabledList.domains.push(domain);
saveDisabledList();
// 如果是当前域名,应用禁用效果
if (domain === currentDomain) {
isHighlightDisabled = true;
clearAllHighlights();
// 更新页面状态提示
showToast(`已禁用域名 ${domain} 的高亮功能`);
} else {
showToast(`已添加到禁用域名列表`);
}
// 刷新禁用管理选项卡
refreshDisabledList();
}
}
// 启用域名
function enableDomain(domain) {
disabledList.domains = disabledList.domains.filter(d => d !== domain);
saveDisabledList();
// 如果是当前域名,更新状态
if (domain === currentDomain) {
// 需要检查URL是否也被禁用
isHighlightDisabled = disabledList.urls.includes(currentPageUrl);
if (!isHighlightDisabled) {
loadHighlights();
applyHighlights();
}
showToast(`已启用域名 ${domain} 的高亮功能`);
} else {
showToast(`已从禁用域名列表中移除`);
}
// 刷新禁用管理选项卡
refreshDisabledList();
}
// 禁用URL
function disableUrl(url) {
// 防止重复添加
if (!disabledList.urls.includes(url)) {
disabledList.urls.push(url);
saveDisabledList();
// 如果是当前URL,应用禁用效果
if (url === currentPageUrl) {
isHighlightDisabled = true;
clearAllHighlights();
showToast(`已禁用当前网址的高亮功能`);
} else {
showToast(`已添加到禁用网址列表`);
}
// 刷新禁用管理选项卡
refreshDisabledList();
}
}
// 启用URL
function enableUrl(url) {
disabledList.urls = disabledList.urls.filter(u => u !== url);
saveDisabledList();
// 如果是当前URL,更新状态
if (url === currentPageUrl) {
// 需要检查域名是否也被禁用
isHighlightDisabled = disabledList.domains.includes(currentDomain);
if (!isHighlightDisabled) {
loadHighlights();
applyHighlights();
}
showToast(`已启用当前网址的高亮功能`);
} else {
showToast(`已从禁用网址列表中移除`);
}
// 刷新禁用管理选项卡
refreshDisabledList();
}
// 刷新禁用列表UI
function refreshDisabledList() {
// 更新当前页面状态
const currentPageStatus = document.querySelector('.current-page-status');
if (currentPageStatus) {
currentPageStatus.innerHTML = renderCurrentPageStatus();
}
// 更新禁用域名列表
const domainsListContainer = document.querySelector('.disabled-domains-list');
if (domainsListContainer) {
domainsListContainer.innerHTML = renderDisabledDomains();
}
// 更新禁用网址列表
const urlsListContainer = document.querySelector('.disabled-urls-list');
if (urlsListContainer) {
urlsListContainer.innerHTML = renderDisabledUrls();
}
// 重新绑定事件
bindDisabledListEvents();
}
// 绑定禁用列表事件
function bindDisabledListEvents() {
// 获取侧边栏
const sidebar = document.querySelector('.highlight-sidebar');
if (!sidebar) return;
// 当前页面启用/禁用按钮
const disableDomainBtn = sidebar.querySelector('#disable-domain');
if (disableDomainBtn) {
disableDomainBtn.addEventListener('click', () => {
if (confirm(`确定要禁用域名 ${currentDomain} 上的高亮功能吗?`)) {
disableDomain(currentDomain);
}
});
}
const disableUrlBtn = sidebar.querySelector('#disable-url');
if (disableUrlBtn) {
disableUrlBtn.addEventListener('click', () => {
if (confirm('确定要禁用当前网址的高亮功能吗?')) {
disableUrl(currentPageUrl);
}
});
}
// 删除禁用项按钮
sidebar.querySelectorAll('.disabled-item-remove').forEach(btn => {
btn.addEventListener('click', (e) => {
const type = e.target.dataset.type;
const value = e.target.dataset.value;
if (type === 'domain') {
enableDomain(value);
} else if (type === 'url') {
enableUrl(value);
}
});
});
// 添加禁用域名
const addDomainBtn = sidebar.querySelector('#add-domain-btn');
if (addDomainBtn) {
addDomainBtn.addEventListener('click', () => {
const input = sidebar.querySelector('#add-domain-input');
const domain = input.value.trim();
if (domain) {
disableDomain(domain);
input.value = '';
}
});
}
// 添加禁用网址
const addUrlBtn = sidebar.querySelector('#add-url-btn');
if (addUrlBtn) {
addUrlBtn.addEventListener('click', () => {
const input = sidebar.querySelector('#add-url-input');
const url = input.value.trim();
if (url) {
disableUrl(url);
input.value = '';
}
});
}
}
// 显示消息提示
function showToast(message, type = 'info') {
// 移除可能存在的旧toast
const existingToast = document.querySelector('.highlight-toast');
if (existingToast) {
existingToast.remove();
}
// 创建toast元素
const toast = document.createElement('div');
toast.className = `highlight-toast ${type}`;
toast.textContent = message;
// 添加到页面
document.body.appendChild(toast);
// 短暂显示后自动消失
setTimeout(() => {
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300); // 等待淡出动画完成后移除
}, 2000);
}, 10);
}
// 优化按钮添加函数,实现长按触发拖动
function addToolbarButton() {
// 从存储中读取按钮位置 - 修改为支持百分比值
const savedPosition = GM_getValue('toolbar_position', { right: '20px', bottom: '20px' });
const button = document.createElement('div');
button.className = 'highlight-toolbar';
// 根据设置决定初始显示状态
if (!settings.showFloatingButton) {
button.style.display = 'none';
}
// 使用更小的图标尺寸
button.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
`;
// 应用保存的位置 - 优先使用相对于窗口边缘的定位
if (savedPosition.usePercentage) {
// 如果有百分比值,使用百分比定位
button.style.right = savedPosition.rightPercent;
button.style.bottom = savedPosition.bottomPercent;
button.style.left = 'auto';
button.style.top = 'auto';
} else {
// 否则尝试使用像素定位,但优先使用right/bottom
if (savedPosition.right !== 'auto' && savedPosition.bottom !== 'auto') {
button.style.right = savedPosition.right;
button.style.bottom = savedPosition.bottom;
button.style.left = 'auto';
button.style.top = 'auto';
} else {
button.style.left = savedPosition.left || 'auto';
button.style.top = savedPosition.top || 'auto';
button.style.right = 'auto';
button.style.bottom = 'auto';
}
}
// 拖动相关变量
let isDragging = false;
let offsetX, offsetY;
let originalPosition;
let longPressTimer = null;
let initialX, initialY;
let hasMoved = false;
let mouseDownTime = 0; // 添加按下时间记录
// 添加一个标志来区分点击和拖动
let isClickAllowed = true;
// 修改直接点击事件处理
button.addEventListener('click', function (e) {
console.log('按钮点击事件触发, isClickAllowed:', isClickAllowed, 'isDragging:', isDragging);
// 如果拖动结束后立即触发点击,可能是拖动后的松开,不应执行点击操作
if (isDragging) {
console.log('拖动状态,忽略点击');
e.stopPropagation();
return;
}
// 只有当点击被允许时才处理
if (isClickAllowed) {
console.log('浮动按钮点击,打开侧边栏');
e.preventDefault(); // 阻止默认行为
e.stopPropagation(); // 阻止冒泡
// 确保侧边栏元素存在
const sidebar = document.querySelector('.highlight-sidebar');
if (!sidebar) {
console.warn("找不到侧边栏,尝试重新创建");
createSidebar();
setTimeout(() => toggleSidebar(true), 50); // 延迟切换确保DOM更新,强制设置为打开
} else {
toggleSidebar(true); // 强制打开侧边栏,而不是切换状态
}
}
});
// 修改鼠标按下事件处理
function handleTouchStart(e) {
// 记录按下时间
mouseDownTime = Date.now();
// 每次触摸开始时重置标志
isClickAllowed = true;
// 记录初始接触位置
initialX = e.clientX || (e.touches && e.touches[0].clientX);
initialY = e.clientY || (e.touches && e.touches[0].clientY);
hasMoved = false;
// 保存原始位置
originalPosition = {
left: button.style.left,
top: button.style.top,
right: button.style.right,
bottom: button.style.bottom
};
// 设置长按计时器
longPressTimer = setTimeout(() => {
// 长按后进入拖动模式
isClickAllowed = false; // 禁止点击
startDragging(e);
}, 300); // 长按300ms触发拖动
// 阻止移动设备上的默认行为
if (e.type === 'touchstart') {
e.preventDefault();
}
}
// 启动拖动
function startDragging(e) {
// 清除长按计时器
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
// 一旦开始拖动,就禁止点击
isClickAllowed = false;
// 如果是触摸事件,获取第一个触摸点
const clientX = e.clientX || e.touches[0].clientX;
const clientY = e.clientY || e.touches[0].clientY;
// 计算偏移
const rect = button.getBoundingClientRect();
offsetX = clientX - rect.left;
offsetY = clientY - rect.top;
isDragging = true;
button.classList.add('dragging');
// 阻止默认事件和冒泡
e.preventDefault();
e.stopPropagation();
// 添加临时全局事件监听
document.addEventListener('mousemove', handleDragMove);
document.addEventListener('touchmove', handleDragMove, { passive: false });
document.addEventListener('mouseup', handleDragEnd);
document.addEventListener('touchend', handleDragEnd);
}
// 处理移动
function handleMove(e) {
// 如果长按计时器存在,检查是否移动了足够距离取消长按
if (longPressTimer) {
const clientX = e.clientX || e.touches[0].clientX;
const clientY = e.clientY || e.touches[0].clientY;
// 计算移动距离
const moveX = Math.abs(clientX - initialX);
const moveY = Math.abs(clientY - initialY);
// 如果移动超过阈值,取消长按并禁止点击
if (moveX > 5 || moveY > 5) {
clearTimeout(longPressTimer);
longPressTimer = null;
hasMoved = true;
isClickAllowed = false; // 发生移动时禁止点击
}
}
// 如果是拖动状态,处理拖动逻辑
if (isDragging) {
handleDragMove(e);
}
}
// 拖动过程处理
function handleDragMove(e) {
if (!isDragging) return;
// 如果是触摸事件,获取第一个触摸点
const clientX = e.clientX || e.touches[0].clientX;
const clientY = e.clientY || e.touches[0].clientY;
// 计算新位置
const newLeft = clientX - offsetX;
const newTop = clientY - offsetY;
// 限制在视口内
const buttonWidth = button.offsetWidth;
const buttonHeight = button.offsetHeight;
const maxX = window.innerWidth - buttonWidth;
const maxY = window.innerHeight - buttonHeight;
const boundedLeft = Math.max(0, Math.min(newLeft, maxX));
const boundedTop = Math.max(0, Math.min(newTop, maxY));
// 修改为绝对定位方式,使用left/top而非right/bottom
button.style.left = `${boundedLeft}px`;
button.style.top = `${boundedTop}px`;
button.style.right = 'auto';
button.style.bottom = 'auto';
// 阻止默认事件
e.preventDefault();
}
// 处理触摸或鼠标结束
function handleTouchEnd(e) {
// 清除长按计时器
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
// 如果正在拖动,结束拖动
if (isDragging) {
handleDragEnd(e);
return;
}
}
// 拖动结束处理 - 修改为使用百分比定位
function handleDragEnd(e) {
if (!isDragging) return;
// 计算拖动时间
const dragDuration = Date.now() - mouseDownTime;
// 如果拖动时间很短(小于200ms),可能是用户想点击而不是拖动
if (dragDuration < 200 && !hasMoved) {
isClickAllowed = true;
} else {
// 设置一个短暂的标志,防止拖动结束后的点击被误触发
isClickAllowed = false;
setTimeout(() => { isClickAllowed = true; }, 300);
}
isDragging = false;
button.classList.remove('dragging');
// 移除临时事件监听
document.removeEventListener('mousemove', handleDragMove);
document.removeEventListener('touchmove', handleDragMove);
document.removeEventListener('mouseup', handleDragEnd);
document.removeEventListener('touchend', handleDragEnd);
// 计算相对于窗口边缘的距离(使用百分比)
const rect = button.getBoundingClientRect();
const rightDistance = window.innerWidth - (rect.left + rect.width);
const bottomDistance = window.innerHeight - (rect.top + rect.height);
// 决定使用哪种定位方式 - 如果接近边缘,使用相对于边缘的定位
const EDGE_THRESHOLD = 100; // 100px的边缘阈值
let position;
if (rightDistance <= EDGE_THRESHOLD || bottomDistance <= EDGE_THRESHOLD) {
// 如果接近右边或底部边缘,使用right/bottom定位
const rightPercent = (rightDistance / window.innerWidth * 100).toFixed(2) + '%';
const bottomPercent = (bottomDistance / window.innerHeight * 100).toFixed(2) + '%';
position = {
rightPercent: rightPercent,
bottomPercent: bottomPercent,
right: rightDistance + 'px',
bottom: bottomDistance + 'px',
left: 'auto',
top: 'auto',
usePercentage: true
};
// 立即应用百分比定位,保持按钮位置不变
button.style.right = rightPercent;
button.style.bottom = bottomPercent;
button.style.left = 'auto';
button.style.top = 'auto';
} else {
// 使用left/top定位,但同样记录百分比值
const leftPercent = (rect.left / window.innerWidth * 100).toFixed(2) + '%';
const topPercent = (rect.top / window.innerHeight * 100).toFixed(2) + '%';
position = {
leftPercent: leftPercent,
topPercent: topPercent,
left: rect.left + 'px',
top: rect.top + 'px',
right: 'auto',
bottom: 'auto',
usePercentage: true
};
}
// 保存新位置
GM_setValue('toolbar_position', position);
}
// 绑定事件 - 使用统一的触摸/鼠标事件处理逻辑
button.addEventListener('mousedown', handleTouchStart);
button.addEventListener('touchstart', handleTouchStart, { passive: false });
button.addEventListener('mousemove', handleMove);
button.addEventListener('touchmove', handleMove, { passive: false });
button.addEventListener('mouseup', handleTouchEnd);
button.addEventListener('touchend', handleTouchEnd);
document.body.appendChild(button);
// 添加窗口大小变化监听,确保按钮始终可见
window.addEventListener('resize', function () {
// 获取按钮当前位置
const rect = button.getBoundingClientRect();
// 检查按钮是否在视口内
const isInViewport = (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= window.innerHeight &&
rect.right <= window.innerWidth
);
// 如果按钮不在视口内,重新定位到可见区域
if (!isInViewport) {
console.log('按钮不在视口内,重新定位');
// 获取保存的位置信息
const savedPos = GM_getValue('toolbar_position', { right: '20px', bottom: '20px' });
// 如果使用的是百分比定位,应用百分比
if (savedPos.usePercentage) {
if (savedPos.rightPercent && savedPos.bottomPercent) {
button.style.right = savedPos.rightPercent;
button.style.bottom = savedPos.bottomPercent;
button.style.left = 'auto';
button.style.top = 'auto';
} else if (savedPos.leftPercent && savedPos.topPercent) {
// 检查左/上百分比是否会导致按钮超出视口
const leftPct = parseFloat(savedPos.leftPercent);
const topPct = parseFloat(savedPos.topPercent);
if (leftPct > 95 || topPct > 95) {
// 如果百分比过大,重置到默认位置
button.style.right = '20px';
button.style.bottom = '20px';
button.style.left = 'auto';
button.style.top = 'auto';
} else {
button.style.left = savedPos.leftPercent;
button.style.top = savedPos.topPercent;
button.style.right = 'auto';
button.style.bottom = 'auto';
}
}
} else {
// 回退到安全的默认位置
button.style.right = '20px';
button.style.bottom = '20px';
button.style.left = 'auto';
button.style.top = 'auto';
// 更新保存的位置
const newPosition = {
right: '20px',
bottom: '20px',
rightPercent: '5%',
bottomPercent: '5%',
left: 'auto',
top: 'auto',
usePercentage: true
};
GM_setValue('toolbar_position', newPosition);
}
// 确保按钮位置有效
validateButtonPosition(button);
}
});
// 初始验证按钮位置
validateButtonPosition(button);
}
// 验证按钮位置并确保在视口内
function validateButtonPosition(button) {
// 等待布局完成再检查位置
setTimeout(() => {
const rect = button.getBoundingClientRect();
// 检查按钮是否完全在视口外
if (rect.left > window.innerWidth || rect.top > window.innerHeight ||
rect.right < 0 || rect.bottom < 0) {
console.log('按钮在视口外,重置位置');
// 重置到默认位置
button.style.right = '20px';
button.style.bottom = '20px';
button.style.left = 'auto';
button.style.top = 'auto';
// 更新保存的位置
const newPosition = {
right: '20px',
bottom: '20px',
rightPercent: '5%',
bottomPercent: '5%',
left: 'auto',
top: 'auto',
usePercentage: true
};
GM_setValue('toolbar_position', newPosition);
}
}, 100);
}
// 获取range内的所有文本节点
function getAllTextNodesInRange(range) {
const textNodes = [];
// 创建文本节点迭代器
const walker = document.createTreeWalker(
range.commonAncestorContainer,
NodeFilter.SHOW_TEXT,
{
acceptNode: function (node) {
// 检查节点是否在范围内
if (range.intersectsNode(node)) {
return NodeFilter.FILTER_ACCEPT;
}
return NodeFilter.FILTER_REJECT;
}
}
);
// 收集所有文本节点
let node;
while (node = walker.nextNode()) {
textNodes.push(node);
}
return textNodes;
}
// 检查节点是否已在高亮元素内
function isInsideHighlight(node) {
let parent = node.parentNode;
while (parent) {
if (parent.classList && parent.classList.contains('highlight-marked')) {
return true;
}
parent = parent.parentNode;
}
return false;
}
// 删除高亮
function removeHighlight() {
const selection = window.getSelection();
if (!selection.toString().trim()) {
return;
}
// 尝试查找选中文本对应的已存在高亮
const selectedText = selection.toString().trim();
const existingHighlight = highlights.find(h => h.text.trim() === selectedText);
if (existingHighlight) {
removeHighlightById(existingHighlight.id);
} else {
console.log('没有找到匹配的高亮记录');
}
}
// 通过ID删除高亮
function removeHighlightById(id) {
// 删除DOM中的高亮元素
const highlightElements = document.querySelectorAll(`.highlight-marked[data-id="${id}"]`);
highlightElements.forEach(el => {
const parent = el.parentNode;
// 将所有子元素移回父元素
while (el.firstChild) {
parent.insertBefore(el.firstChild, el);
}
// 移除高亮元素本身
parent.removeChild(el);
});
// 从存储中删除
const index = highlights.findIndex(h => h.id === id);
if (index !== -1) {
highlights.splice(index, 1);
saveHighlights();
refreshHighlightsList();
}
}
// 安全地高亮复杂选择范围 (完整版)
function safelyHighlightComplexRange(range, id, color) {
console.log('使用安全高亮方法处理复杂选择');
// 获取所有在范围内的文本节点
const nodes = getAllTextNodesInRange(range);
// 保存起始和结束信息,以便正确处理部分选择的节点
const startNode = range.startContainer;
const startOffset = range.startOffset;
const endNode = range.endContainer;
const endOffset = range.endOffset;
nodes.forEach(node => {
// 跳过已经在高亮元素内的文本节点
if (isInsideHighlight(node)) {
console.log('跳过已高亮的节点');
return;
}
// 确定需要高亮的文本范围
let nodeStart = 0;
let nodeEnd = node.length;
// 处理起始节点
if (node === startNode) {
nodeStart = startOffset;
}
// 处理结束节点
if (node === endNode) {
nodeEnd = endOffset;
}
// 如果节点需要部分高亮且有内容要高亮
if (nodeStart < nodeEnd) {
try {
// 创建新的范围来高亮当前节点部分
const nodeRange = document.createRange();
nodeRange.setStart(node, nodeStart);
nodeRange.setEnd(node, nodeEnd);
// 创建高亮元素
const span = document.createElement('span');
span.className = 'highlight-marked';
span.style.backgroundColor = color;
span.dataset.id = id;
// 将选中内容替换为高亮元素
try {
nodeRange.surroundContents(span);
// 添加点击事件监听器
span.addEventListener('click', (e) => {
e.stopPropagation();
showHighlightContextMenu(e, id);
});
} catch (e) {
console.error('无法高亮复杂范围的部分节点:', e);
}
} catch (e) {
console.error('处理部分节点时出错:', e);
}
}
});
}
// 整合后的应用高亮函数
function applyHighlights() {
let successCount = 0;
let failedCount = 0;
highlights.forEach(highlight => {
try {
console.log(`尝试恢复高亮ID: ${highlight.id}, 文本: "${highlight.text.substring(0, 30)}..."`);
let restored = false;
// 先尝试通过DOM路径恢复
const range = constructRangeFromPath(highlight.path);
if (range) {
// 如果找到了范围,使用它
if (applyHighlightToRange(range, highlight)) {
console.log(`通过DOM路径成功恢复高亮: ${highlight.id}`);
successCount++;
restored = true;
}
}
// DOM路径失败,尝试文本匹配
if (!restored && highlight.text) {
console.log(`尝试通过文本内容匹配: "${highlight.text.substring(0, 30)}..."`);
// 如果文本长度合适,尝试在页面中查找
if (findAndHighlightText(highlight)) {
console.log(`通过文本匹配成功恢复高亮: ${highlight.id}`);
successCount++;
restored = true;
}
}
// 所有方法都失败了
if (!restored) {
console.warn(`无法恢复高亮 ID: ${highlight.id}`);
failedCount++;
}
} catch (e) {
console.error('应用高亮时出错:', e, highlight);
failedCount++;
}
});
console.log(`高亮恢复统计: 成功=${successCount}, 失败=${failedCount}, 总计=${highlights.length}`);
}
// 添加新函数:设置标题编辑功能
function setupTitleEditing(sidebar) {
const titleElement = sidebar.querySelector('.sidebar-description');
if (!titleElement) return;
// 双击开始编辑
titleElement.addEventListener('dblclick', function (e) {
// 获取当前标题文本 - 直接使用完整标题
const currentText = settings.sidebarDescription || '网页划词高亮工具';
// 切换为编辑模式
titleElement.classList.add('editing');
titleElement.setAttribute('contenteditable', 'true');
titleElement.setAttribute('spellcheck', 'false');
// 直接显示当前文本,不需要分离
titleElement.textContent = currentText;
// 聚焦并全选文本
titleElement.focus();
document.execCommand('selectAll', false, null);
// 阻止事件冒泡
e.stopPropagation();
});
// 保存编辑(失去焦点时)
titleElement.addEventListener('blur', function () {
saveEditedTitle(titleElement);
});
// 按下回车键保存
titleElement.addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault(); // 防止插入换行符
titleElement.blur(); // 触发blur事件保存编辑
} else if (e.key === 'Escape') {
// ESC键取消编辑
titleElement.textContent = settings.sidebarDescription || '网页划词高亮工具';
titleElement.removeAttribute('contenteditable');
titleElement.classList.remove('editing');
e.preventDefault();
}
});
}
// 添加新函数:保存编辑后的标题
function saveEditedTitle(titleElement) {
if (!titleElement.hasAttribute('contenteditable')) return;
// 获取编辑后的文本并清理
let newTitle = titleElement.textContent.trim();
// 如果为空,使用默认值
if (!newTitle) {
newTitle = '网页划词高亮工具';
}
// 更新设置
settings.sidebarDescription = newTitle;
saveSettings();
// 恢复显示格式
titleElement.removeAttribute('contenteditable');
titleElement.classList.remove('editing');
// 更新显示
updateSidebarDescription();
showToast('标题已更新');
}
// 更新侧边栏描述
function updateSidebarDescription() {
const description = document.querySelector('.sidebar-description');
if (description && !description.classList.contains('editing')) {
const customText = settings.sidebarDescription || '网页划词高亮工具';
description.textContent = customText;
}
}
// 切换浮动按钮显示状态的函数
function toggleFloatingButton() {
settings.showFloatingButton = !settings.showFloatingButton;
saveSettings();
// 更新按钮显示状态
const floatingButton = document.querySelector('.highlight-toolbar');
if (floatingButton) {
if (settings.showFloatingButton) {
floatingButton.style.display = 'flex';
showToast('已显示浮动按钮');
} else {
floatingButton.style.display = 'none';
showToast('已隐藏浮动按钮,可通过菜单重新显示');
}
}
}
// 初始化
init();
})();