链接助手 - 三击自动转换

三击将文本URL转换为可点击链接

目前为 2025-04-04 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Link Helper - Triple Click Text to Link
  3. // @name:zh-CN 链接助手 - 三击自动转换
  4. // @namespace http://tampermonkey.net/
  5. // @version 1.0
  6. // @description Convert text URLs to links on triple click
  7. // @description:zh-CN 三击将文本URL转换为可点击链接
  8. // @author Alex3236
  9. // @match *://*/*
  10. // @grant none
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. let clicks = [];
  18. const CLICK_TIMEOUT = 1000;
  19. const CLICK_THRESHOLD = 10;
  20.  
  21. document.addEventListener('click', function(e) {
  22. const now = Date.now();
  23. clicks.push({
  24. time: now,
  25. x: e.clientX,
  26. y: e.clientY
  27. });
  28.  
  29. if (clicks.length > 3) clicks.shift();
  30.  
  31. if (clicks.length === 3) {
  32. const [first, second, third] = clicks;
  33.  
  34. if (third.time - first.time < CLICK_TIMEOUT &&
  35. distance(first, second) < CLICK_THRESHOLD &&
  36. distance(second, third) < CLICK_THRESHOLD) {
  37.  
  38. processTripleClick(e.clientX, e.clientY);
  39. clicks = []; // 重置点击记录
  40. }
  41. }
  42. });
  43.  
  44. function distance(a, b) {
  45. return Math.hypot(a.x - b.x, a.y - b.y);
  46. }
  47.  
  48. function processTripleClick(x, y) {
  49. const textNode = getTextNodeFromPoint(x, y);
  50. if (!textNode || isInsideLink(textNode)) return;
  51.  
  52. const text = textNode.nodeValue;
  53. const matches = findUrls(text);
  54.  
  55. if (matches.length > 0) {
  56. replaceTextWithLinks(textNode, matches);
  57. }
  58. }
  59.  
  60. function getTextNodeFromPoint(x, y) {
  61. let range;
  62. if (document.caretRangeFromPoint) {
  63. range = document.caretRangeFromPoint(x, y);
  64. } else if (document.caretPositionFromPoint) {
  65. const pos = document.caretPositionFromPoint(x, y);
  66. if (!pos) return null;
  67. range = document.createRange();
  68. range.setStart(pos.offsetNode, pos.offset);
  69. range.collapse(true);
  70. }
  71. return range?.startContainer?.nodeType === Node.TEXT_NODE ? range.startContainer : null;
  72. }
  73.  
  74. function isInsideLink(node) {
  75. let parent = node.parentNode;
  76. while (parent) {
  77. if (parent.tagName === 'A') return true;
  78. parent = parent.parentNode;
  79. }
  80. return false;
  81. }
  82.  
  83. function findUrls(text) {
  84. const urlRegex = /(https?:\/\/[^\s<]+|www\.[^\s<]+\.[^\s<]{2,})/gi;
  85. const matches = [];
  86. let match;
  87.  
  88. while ((match = urlRegex.exec(text)) !== null) {
  89. matches.push({
  90. start: match.index,
  91. end: match.index + match[0].length,
  92. url: match[0]
  93. });
  94. }
  95. return matches;
  96. }
  97.  
  98. function replaceTextWithLinks(textNode, matches) {
  99. const parent = textNode.parentNode;
  100. const docFrag = document.createDocumentFragment();
  101. let lastIndex = 0;
  102.  
  103. matches.forEach(match => {
  104. if (match.start > lastIndex) {
  105. docFrag.appendChild(document.createTextNode(
  106. textNode.nodeValue.slice(lastIndex, match.start)
  107. ));
  108. }
  109.  
  110. const a = document.createElement('a');
  111. a.style = "color: #66ccff; background: #163E64";
  112. a.href = match.url.startsWith('http') ? match.url : `http://${match.url}`;
  113. a.textContent = match.url;
  114. a.target = '_blank';
  115. docFrag.appendChild(a);
  116.  
  117. lastIndex = match.end;
  118. });
  119.  
  120. if (lastIndex < textNode.nodeValue.length) {
  121. docFrag.appendChild(document.createTextNode(
  122. textNode.nodeValue.slice(lastIndex)
  123. ));
  124. }
  125.  
  126. parent.replaceChild(docFrag, textNode);
  127. }
  128. })();