// ==UserScript==
// @name 极简划词工具栏
// @description 超小尺寸悬浮窗,支持划词搜索(百度/头条/知乎/小红书)、复制、打开链接;拖拽链接/图片直接新窗口打开,拖拽选中文本则执行搜索或打开域名;极致性能优化,大幅降低CPU占用
// @icon https://www.baidu.com/favicon.ico
// @namespace http://tampermonkey.net/
// @version 2.0.2
// @author ddrwin
// @license MIT
// @match *://*/*
// @grant none
// @note 2026.2.7 V1.0 初始版本,支持复制和百度搜索
// @note 2026.2.8 V1.1 增加打开按钮,优化样式,添加知乎、头条
// @note 2026.2.9 V1.2 增加拖拽搜索功能,修复状态残留bug
// @note 2026.2.10 V1.3 修复点击复制按钮误触拖拽搜索的问题
// @note 2026.2.11 V1.4 增加链接拖拽打开功能
// @note 2026.2.12 V1.5 增加图片拖拽打开功能
// @note 2026.2.13 V1.6 增加小红书搜索引擎
// @note 2026.2.14 V1.7 修复操作后未清除文本选中的问题
// @note 2026.2.15 V1.8 性能优化:缓存元素检测结果,减少DOM操作,调整搜索引擎顺序
// @note 2026.2.16 V1.9 性能大优化:使用Element.closest、raf节流、文档片段构建、快速空选返回
// @note 2026.2.17 V2.0 修复拖拽搜索误触及按钮点击错乱:仅当鼠标按下在选中区域内才触发拖拽,按钮事件改用直接绑定
// @note 2026.2.21 V2.0.2 优化URL识别逻辑,兼容不带协议的域名路径,增强特殊字符支持(基于v2.0.2beta1改进)
// @downloadURL none
// ==/UserScript==
(function() {
'use strict';
// ==================== 图标 ====================
const ICONS = {
copy: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/PjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+PHN2ZyB0PSIxNjU1Nzc5ODc4NDY4IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjE0MTciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCI+PGRlZnM+PHN0eWxlIHR5cGU9InRleHQvY3NzIj5AZm9udC1mYWNlIHsgZm9udC1mYW1pbHk6IGZlZWRiYWNrLWljb25mb250OyBzcmM6IHVybCgiLy9hdC5hbGljZG4uY29tL3QvZm9udF8xMDMxMTU4X3U2OXc4eWh4ZHUud29mZjI/dD0xNjMwMDMzNzU5OTQ0IikgZm9ybWF0KCJ3b2ZmMiIpLCB1cmwoIi8vYXQuYWxpY2RuLmNvbS90L2ZvbnRfMTAzMTE1OF91Njl3OHloeGR1LndvZmY/dD0xNjMwMDMzNzU5OTQ0IikgZm9ybWF0KCJ3b2ZmIiksIHVybCgiLy9hdC5hbGljZG4uY29tL3QvZm9udF8xMDMxMTU4X3U2OXc4eWh4ZHUudHRmP3Q9MTYzMDAzMzc1OTk0NCIpIGZvcm1hdCgidHJ1ZXR5cGUiKTsgfQ0KPC9zdHlsZT48L2RlZnM+PHBhdGggZD0iTTM3NyA0MzJoMzQ5YTggOCAwIDAgMSA4IDh2NDhhOCA4IDAgMCAxLTggOEgzNzdhOCA4IDAgMCAxLTgtOHYtNDhhOCA4IDAgMCAxIDgtOHogbTAgMTYwaDI1OGE4IDggMCAwIDEgOCA4djQ4YTggOCAwIDAgMS04IDhIMzc3YTggOCAwIDAgMS04LTh2LTQ4YTggOCAwIDAgMSA4LTh6IG0tNjUtMjgwdjU3Nmg0ODBWMzEySDMxMnogbS00MC03Mmg1NjBjMTcuNjczIDAgMzIgMTQuMzI3IDMyIDMydjY1NmMwIDE3LjY3My0xNC4zMjcgMzItMzIgMzJIMjcyYy0xNy42NzMgMC0zMi0xNC4zMjctMzItMzJWMjcyYzAtMTcuNjczIDE0LjMyNy0zMiAzMi0zMnogbS04OC01NnY2NjRhOCA4IDAgMCAxLTggOGgtNTZhOCA4IDAgMCAxLTgtOFYxNDRjMC0xNy42NzMgMTQuMzI3LTMyIDMyLTMyaDYzMmE4IDggMCAwIDEgOCA4djU2YTggOCAwIDAgMS04IDhIMTg0eiIgZmlsbD0iIzMzMzMzMyIgcC1pZD0iMTQxOCI+PC9wYXRoPjwvc3ZnPg==',
search: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/PjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+PHN2ZyB0PSIxNjUxNTY3NDk1OTczIiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjIwNzciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCI+PGRlZnM+PHN0eWxlIHR5cGU9InRleHQvY3NzIj48L3N0eWxlPjwvZGVmcz48cGF0aCBkPSJNNDQ2LjExMjMyMyAxNzcuNTQ1MDUxYzEzNy41Njc2NzcgMC4yMTk3OTggMjUyLjYxMjUyNSAxMDQuNTk3OTggMjY2LjE2MjQyNCAyNDEuNDkzMzMzIDEzLjU2MjgyOCAxMzYuODk1MzU0LTc4Ljc3ODE4MiAyNjEuODE4MTgyLTIxMy42MTc3NzcgMjg5LjAwODQ4NS0xMzQuODUyNTI1IDI3LjIwMzIzMi0yNjguMzg2MjYzLTUyLjE1Njc2OC0zMDguOTQ1NDU1LTE4My42MDg4ODlzMjUuMDE4MTgyLTI3Mi4yNTIxMjEgMTUxLjczODE4Mi0zMjUuNzc5Mzk0QTI2Ny4yMzU1NTYgMjY3LjIzNTU1NiAwIDAgMSA0NDYuMTEyMzIzIDE3Ny41NDUwNTFtMC02Mi4wNjA2MDdjLTE4Mi43OTQzNDMgMC0zMzAuOTg5ODk5IDE0OC4xOTU1NTYtMzMwLjk4OTg5OSAzMzAuOTg5ODk5czE0OC4xOTU1NTYgMzMwLjk4OTg5OSAzMzAuOTg5ODk5IDMzMC45ODk4OTkgMzMwLjk4OTg5OS0xNDguMTk1NTU2IDMzMC45ODk4OTktMzMwLjk4OTg5OS0xNDguMTk1NTU2LTMzMC45ODk4OTktMzMwLjk4OTg5OS0zMzAuOTg5ODk5eiBtNDMxLjMyMTIxMiA3OTMuMzQxNDE1YTMwLjg0OTI5MyAzMC44NDkyOTMgMCAwIDEtMjEuOTQxMDEtOS4xMDIyMjNsLTE1Ny4yMjAyMDItMTU3LjIyMDIwMmMtMTEuNzUyNzI3LTEyLjE3OTM5NC0xMS41ODQ2NDYtMzEuNTM0NTQ1IDAuMzc0OTUtNDMuNTA3MDcgMTEuOTcyNTI1LTExLjk3MjUyNSAzMS4zMjc2NzctMTIuMTQwNjA2IDQzLjQ5NDE0MS0wLjM3NDk1bDE1Ny4yMjAyMDIgMTU3LjIyMDIwMmEzMS4wMzY3NjggMzEuMDM2NzY4IDAgMCAxIDYuNzIzMjMyIDMzLjgxMDEwMSAzMS4wMDQ0NDQgMzEuMDA0NDQ0IDAgMCAxLTI4LjY1MTMxMyAxOS4xNzQxNDJ6IG0wIDAiIHAtaWQ9IjIwNzgiIGZpbGw9IiMzMzMzMzMiPjwvcGF0aD48L3N2Zz4=',
openLink: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/PjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+PHN2ZyB0PSIxNjUxNTgwNDU1NTcwIiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9Ijk0MiIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNODMyIDEyOEg2NDB2NjRoMTQ2Ljc1Mkw1MjEuMzc2IDQ1Ny4zNzZsNDUuMjQ4IDQ1LjI0OEw4MzIgMjM3LjI0OFYzODRoNjRWMTI4eiIgZmlsbD0iIzMzMzMzMyIgcC1pZD0iOTQzIj48L3BhdGg+PHBhdGggZD0iTTc2OCA4MzJIMTkyVjI1NmgzNTJ2LTY0SDE2MGEzMiAzMiAwIDAgMC0zMiAzMnY2NDBhMzIgMzIgMCAwIDAgMzIgMzJoNjQwYTMyIDMyIDAgMCAwIDMyLTMzVjQ4MGgtNjR2MzUyeiIgZmlsbD0iIzMzMzMzMyIgcC1pZD0iOTQ0Ij48L3BhdGg+PC9zdmc+'
};
// ==================== 搜索引擎列表 ====================
const ALL_ENGINES = [
{ name: '百度', icon: ICONS.search, url: 'https://www.baidu.com/s?wd=%s' },
{ name: '头条', icon: ICONS.search, url: 'https://www.toutiao.com/search/?keyword=%s' },
{ name: '知乎', icon: ICONS.search, url: 'https://www.zhihu.com/search?type=content&q=%s' },
{ name: '小红书', icon: ICONS.search, url: 'https://www.xiaohongshu.com/search_result?keyword=%s' }
];
// ==================== 域名/URL判断(优化版)====================
function isBaiduDomain() {
return location.hostname.includes('baidu.com');
}
function getEnginesForCurrentSite() {
return isBaiduDomain() ? ALL_ENGINES.filter(e => e.name !== '百度') : ALL_ENGINES;
}
function isDomain(text) {
if (!text || text.includes(' ')) return false;
// 规则1:匹配完整URL(兼容http/https开头,包含路径/特殊字符)
const urlRegex = /^https?:\/\/[a-zA-Z0-9\-._~:/?#[\]@!$&'()*+,;=%]+$/;
if (urlRegex.test(text)) return true;
// 规则2:匹配类似域名(可能带路径/查询),但无协议
const domainLikeRegex = /^[a-zA-Z0-9\-._~:/?#[\]@!$&'()*+,;=%]+$/;
return domainLikeRegex.test(text) &&
text.includes('.') &&
text.split('.').pop().length >= 2;
}
function makeUrl(domain) {
// 优化:完整URL直接返回,否则补全https
return domain.startsWith('http') ? domain : 'https://' + domain;
}
// ==================== 快速判断是否在可编辑元素内(使用 closest)====================
function isInsideEditable(el) {
return el && el.closest('input, textarea, [contenteditable="true"], [contenteditable=""]') !== null;
}
// ==================== 判断链接/图片(缓存 + closest)====================
const linkCache = new WeakMap();
function isLinkElement(el) {
if (!el) return false;
if (linkCache.has(el)) return linkCache.get(el);
const a = el.closest('a[href]');
const result = !!a;
linkCache.set(el, result);
return result;
}
function getLinkUrl(el) {
const a = el.closest('a[href]');
return a ? a.href : null;
}
function isImageElement(el) {
return el && el.tagName === 'IMG';
}
function getImageUrl(el) {
return el && el.tagName === 'IMG' ? el.src : null;
}
// ==================== 样式 ====================
const TOOLBAR_STYLE = `
position: fixed; z-index: 999999; background: #fff; border-radius: 8px;
padding: 2px 4px; box-shadow: 0 2px 6px rgba(0,0,0,0.1); display: flex;
gap: 2px; font-size: 12px; color: #333; pointer-events: auto;
border: 1px solid #e0e0e0; align-items: center; line-height: 1.2;
flex-wrap: nowrap; white-space: nowrap;
`;
const BUTTON_STYLE = `
background: transparent; border: none; color: #333; cursor: pointer;
padding: 4px 4px; border-radius: 6px; font-size: 12px; display: flex;
align-items: center; gap: 3px; transition: background 0.1s, color 0.1s;
white-space: nowrap;
`;
const ICON_STYLE = `width: 13px; height: 13px; display: inline-block; vertical-align: middle; transition: filter 0.1s;`;
const SEPARATOR_STYLE = `width: 1px; height: 18px; background: #ddd; margin: 0 2px;`;
const GREEN_FILTER = 'invert(48%) sepia(79%) saturate(2476%) hue-rotate(86deg) brightness(118%) contrast(119%)';
// ==================== 工具栏管理(使用文档片段)====================
let toolbar = null;
let hideTimeout = null;
function initToolbar() {
toolbar = document.createElement('div');
toolbar.id = 'custom-search-toolbar';
toolbar.style.cssText = TOOLBAR_STYLE;
toolbar.addEventListener('mousedown', e => e.preventDefault());
toolbar.addEventListener('mouseup', e => e.stopPropagation());
// 事件委托:直接调用按钮上存储的 handler
toolbar.addEventListener('click', e => {
const btn = e.target.closest('button');
if (btn && btn._handler) btn._handler();
});
document.body.appendChild(toolbar);
}
function buildToolbar(selectedText) {
if (!toolbar) initToolbar();
toolbar.innerHTML = '';
const fragment = document.createDocumentFragment();
const buttons = [];
if (isDomain(selectedText)) {
buttons.push({
icon: ICONS.openLink,
text: '打开',
handler: () => {
window.open(makeUrl(selectedText), '_blank');
window.getSelection().empty();
hideToolbar();
}
});
}
buttons.push({
icon: ICONS.copy,
text: '复制',
handler: () => {
copyText(selectedText);
window.getSelection().empty();
hideToolbar();
}
});
getEnginesForCurrentSite().forEach(engine => {
buttons.push({
icon: engine.icon,
text: engine.name,
handler: () => {
const url = engine.url.replace('%s', encodeURIComponent(selectedText));
window.open(url, '_blank');
window.getSelection().empty();
hideToolbar();
}
});
});
buttons.forEach((btn, i) => {
if (i > 0) {
const sep = document.createElement('span');
sep.style.cssText = SEPARATOR_STYLE;
fragment.appendChild(sep);
}
const btnEl = document.createElement('button');
btnEl.style.cssText = BUTTON_STYLE;
btnEl.innerHTML = `
${btn.text}`;
// 将 handler 直接存储在按钮元素上
btnEl._handler = btn.handler;
// 悬停效果
btnEl.addEventListener('mouseenter', e => {
e.currentTarget.style.background = '#f0f0f0';
e.currentTarget.style.color = '#5FE382';
e.currentTarget.querySelector('img').style.filter = GREEN_FILTER;
});
btnEl.addEventListener('mouseleave', e => {
e.currentTarget.style.background = 'transparent';
e.currentTarget.style.color = '#333';
e.currentTarget.querySelector('img').style.filter = 'none';
});
fragment.appendChild(btnEl);
});
toolbar.appendChild(fragment);
}
function copyText(text) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text).catch(() => fallbackCopy(text));
} else {
fallbackCopy(text);
}
}
function fallbackCopy(text) {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.cssText = 'position:fixed;opacity:0;';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
function showToolbar(x, y, selectedText) {
if (hideTimeout) clearTimeout(hideTimeout);
// 使用 requestAnimationFrame 批量处理,避免布局抖动
requestAnimationFrame(() => {
buildToolbar(selectedText);
requestAnimationFrame(() => {
const winW = innerWidth, winH = innerHeight;
const toolW = toolbar.offsetWidth, toolH = toolbar.offsetHeight;
let left = x + 6, top = y + 10;
if (left + toolW > winW) left = winW - toolW - 6;
if (top + toolH > winH) top = y - toolH - 6;
left = Math.max(2, left);
top = Math.max(2, top);
toolbar.style.left = left + 'px';
toolbar.style.top = top + 'px';
toolbar.style.display = 'flex';
});
});
}
function hideToolbar() {
if (toolbar) toolbar.style.display = 'none';
}
function scheduleHide() {
if (hideTimeout) clearTimeout(hideTimeout);
hideTimeout = setTimeout(hideToolbar, 500);
}
// ==================== 拖拽相关(raf节流 + 选区位置检测)====================
const DRAG_THRESHOLD = 5;
let dragStartX = 0, dragStartY = 0;
let dragListening = false;
let dragTriggered = false;
let dragMode = null;
let dragUrl = null;
let rafId = null;
function startDragListening(e) {
if (e.button !== 0 || (toolbar && toolbar.contains(e.target)) || isInsideEditable(e.target)) return;
// 检测链接/图片/选中文本
if (isLinkElement(e.target)) {
dragMode = 'link';
dragUrl = getLinkUrl(e.target);
if (!dragUrl) return;
} else if (isImageElement(e.target)) {
dragMode = 'image';
dragUrl = getImageUrl(e.target);
if (!dragUrl) return;
} else {
const selection = window.getSelection();
const selectedText = selection.toString().trim();
if (!selectedText) return;
// 关键修复:检查鼠标按下位置是否在选中区域内,避免误触
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
// 如果鼠标不在选区的矩形范围内,不启动拖拽监听(可能是开始新选择)
if (e.clientX < rect.left || e.clientX > rect.right || e.clientY < rect.top || e.clientY > rect.bottom) {
return;
}
}
dragMode = 'search';
dragUrl = null;
}
dragStartX = e.clientX;
dragStartY = e.clientY;
dragListening = true;
dragTriggered = false;
document.addEventListener('mousemove', onDragMoveThrottled, { passive: true });
document.addEventListener('mouseup', onDragEnd);
}
// 节流的 mousemove 处理
function onDragMoveThrottled(e) {
if (!dragListening || dragTriggered) return;
if (rafId) cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(() => {
const dx = e.clientX - dragStartX;
const dy = e.clientY - dragStartY;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist >= DRAG_THRESHOLD) {
e.preventDefault?.();
e.stopPropagation?.();
dragTriggered = true;
dragListening = false;
cleanupDragListeners();
hideToolbar();
if (dragMode === 'link' && dragUrl) {
window.open(dragUrl, '_blank');
} else if (dragMode === 'image' && dragUrl) {
window.open(dragUrl, '_blank');
} else if (dragMode === 'search') {
const sel = window.getSelection().toString().trim();
if (sel) {
window.open(isDomain(sel) ? makeUrl(sel) : 'https://www.baidu.com/s?wd=' + encodeURIComponent(sel), '_blank');
window.getSelection().empty();
}
}
setTimeout(() => {
dragTriggered = false;
dragMode = null;
dragUrl = null;
}, 100);
}
rafId = null;
});
}
function onDragEnd() {
cleanupDragListeners();
dragTriggered = false;
dragMode = null;
dragUrl = null;
}
function cleanupDragListeners() {
dragListening = false;
document.removeEventListener('mousemove', onDragMoveThrottled);
document.removeEventListener('mouseup', onDragEnd);
if (rafId) cancelAnimationFrame(rafId);
}
// ==================== 事件监听(使用 passive 优化)====================
document.addEventListener('mousedown', startDragListening, { passive: true });
document.addEventListener('mouseup', (e) => {
if (dragTriggered) {
dragTriggered = false;
dragMode = null;
dragUrl = null;
return;
}
// 快速判断:无选中文本则直接返回,不执行任何后续逻辑
const sel = window.getSelection();
const text = sel.toString().trim();
if (!text) {
hideToolbar();
return;
}
if (toolbar && toolbar.contains(e.target)) return;
if (isInsideEditable(e.target)) {
hideToolbar();
return;
}
showToolbar(e.clientX, e.clientY, text);
});
document.addEventListener('mousedown', (e) => {
if (toolbar && !toolbar.contains(e.target)) hideToolbar();
}, { passive: true });
window.addEventListener('scroll', hideToolbar, { passive: true });
document.addEventListener('selectionchange', () => {
if (!window.getSelection().toString().trim()) {
setTimeout(() => {
if (!window.getSelection().toString().trim()) hideToolbar();
}, 100);
}
});
if (toolbar) {
toolbar.addEventListener('mouseenter', () => clearTimeout(hideTimeout));
toolbar.addEventListener('mouseleave', scheduleHide);
}
})();