Select like opera

Select texts insider links, support firefox and chrome

  1. // ==UserScript==
  2. // @name Select like opera
  3. // @author lkytal
  4. // @namespace Lkytal
  5. // @homepage https://lkytal.github.io/
  6. // @homepageURL https://lkytal.github.io/GM
  7. // @icon https://github.com/lkytal/GM/raw/master/icons/def.ico
  8. // @version 1.2.2
  9. // @description Select texts insider links, support firefox and chrome
  10. // @license AGPL
  11. // @include *
  12. // @grant unsafeWindow
  13. // @grant GM_getValue
  14. // @grant GM_setValue
  15. // @run-at document-end
  16. // @charset UTF-8
  17. // @supportURL https://github.com/lkytal/GM/issues
  18. // ==/UserScript==
  19.  
  20. var selectLikeOpera = function () {
  21. var findHTMLAnchor = function (node) {
  22. if (node.nodeType === 3) node = node.parentNode;
  23. do {
  24. if (node.constructor === HTMLAnchorElement) return node;
  25. } while ((node = node.parentNode) && node !== document.body);
  26. return null;
  27. };
  28.  
  29. var preventEvent = function (e) {
  30. e.preventDefault();
  31. e.stopPropagation();
  32. return false;
  33. };
  34.  
  35. var rangeOperator = (function () {
  36. var notFirefox = navigator.userAgent.indexOf("Firefox") == -1;
  37.  
  38. return {
  39. createRange: function (x, y) {
  40. if (notFirefox) {
  41. return document.caretRangeFromPoint(x, y);
  42. }
  43. else {
  44. var range = document.createRange();
  45. var position = document.caretPositionFromPoint(x, y);
  46. range.setStart(position.offsetNode, position.offset);
  47. return range;
  48. }
  49. },
  50. rangeAttr: (notFirefox ? '-webkit-user-select' : '-moz-user-select')
  51. }
  52. })();
  53.  
  54. var setStyle = (function () {
  55. var styleList = [
  56. {
  57. p: rangeOperator.rangeAttr,
  58. v: 'text'
  59. },
  60. {
  61. p: 'outline-width',
  62. v: 0
  63. }
  64. ];
  65.  
  66. var node;
  67. var s;
  68. return function (_node) {
  69. if (_node) {
  70. node = _node, s = [];
  71. for (var i = styleList.length - 1; i >= 0; i -= 1) {
  72. s.push([node.style.getPropertyValue(styleList[i].p), node.style.getPropertyPriority(styleList[i].p)]);
  73. node.style.setProperty(styleList[i].p, styleList[i].v, 'important');
  74. }
  75. }
  76. else if (node) {
  77. for (var i = styleList.length; i-- > 0;) {
  78. node.style.removeProperty(styleList[i].p);
  79. if (s[i][0] !== null) node.style.setProperty(styleList[i].p, s[i][0], s[i][1]);
  80. }
  81. node = s = null;
  82. }
  83. }
  84. })();
  85.  
  86. var toggleEvent = function (events, bAdd) {
  87. if (bAdd === undefined) bAdd = true;
  88. if (events.constructor !== Array) events = [events];
  89.  
  90. for (var i = 0, len = events.length; i < len; i += 1) {
  91. if (bAdd) {
  92. document.addEventListener(events[i], eventList[events[i]], true);
  93. }
  94. else {
  95. document.removeEventListener(events[i], eventList[events[i]], true);
  96. }
  97. }
  98. };
  99.  
  100. var removeEvent = function (a) {
  101. toggleEvent(a, false);
  102. };
  103.  
  104. var position, q, u, v, z, resetState = function () {
  105. q = v = true;
  106. u = z = false;
  107. };
  108.  
  109. var nodeInfo, selection = document.getSelection();
  110.  
  111. let selectEvent = function (e) {
  112. if (e.which < 2) {
  113. resetState();
  114. var x = e.clientX,
  115. y = e.clientY;
  116. if (selection.rangeCount > 0) {
  117. var selectedRange = selection.getRangeAt(0);
  118. if (!selectedRange.collapsed) {
  119. var newRange = rangeOperator.createRange(x, y);
  120. if (newRange && selectedRange.isPointInRange(newRange.startContainer, newRange.startOffset)) return;
  121. }
  122. }
  123. setStyle();
  124. var target = e.target;
  125. var node = findHTMLAnchor(target);
  126. if (!node) node = target.nodeType !== 3 ? target : target.parentNode;
  127. if (node.constructor === HTMLCanvasElement || node.textContent === '') return;
  128. var range = node.getBoundingClientRect();
  129. nodeInfo = {
  130. n: node,
  131. x: Math.round(range.left),
  132. y: Math.round(range.top),
  133. c: 0
  134. };
  135. position = {
  136. x: x,
  137. y: y
  138. };
  139. toggleEvent(['mousemove', 'mouseup', 'dragend', 'dragstart']);
  140. setStyle(node);
  141. }
  142. };
  143.  
  144. var D = 3, K = 0.8;
  145. var eventList = {
  146. 'mousemove': function (e) {
  147. if (nodeInfo) {
  148. if (nodeInfo.c++ < 12) {
  149. var rect = nodeInfo.n.getBoundingClientRect();
  150. if (Math.round(rect.left) !== nodeInfo.x || Math.round(rect.top) !== nodeInfo.y) {
  151. removeEvent(['mousemove', 'mouseup', 'dragend', 'dragstart', 'click']);
  152. setStyle();
  153. selection.removeAllRanges();
  154. return;
  155. }
  156. }
  157. else {
  158. nodeInfo = null;
  159. }
  160. }
  161. var x = e.clientX,
  162. y = e.clientY;
  163. if (v) {
  164. selection.removeAllRanges();
  165. var offset = x > position.x ? -2 : 2;
  166. var newRange = rangeOperator.createRange(x + offset, y);
  167. if (newRange) {
  168. selection.addRange(newRange);
  169. v = false;
  170. }
  171. }
  172. if (q) {
  173. var c = Math.abs(position.x - x),
  174. d = Math.abs(position.y - y);
  175. u = d === 0 || c / d > K;
  176. if (c > D || d > D) {
  177. q = false;
  178. z = true;
  179. toggleEvent('click');
  180. }
  181. }
  182. if (u) {
  183. var newRange = rangeOperator.createRange(x, y);
  184. if (newRange) selection.extend(newRange.startContainer, newRange.startOffset);
  185. }
  186. },
  187. 'dragstart': function (e) {
  188. removeEvent('dragstart');
  189. if (u) return preventEvent(e);
  190. },
  191. 'mouseup': function (e) {
  192. removeEvent(['mousemove', 'mouseup', 'dragstart', 'dragend']);
  193. if (!u && z) z = false;
  194. setTimeout(function () {
  195. removeEvent('click');
  196. }, 25);
  197. },
  198. 'dragend': function (e) {
  199. removeEvent(['dragend', 'mousemove', 'mouseup']);
  200. },
  201. 'click': function (e) {
  202. removeEvent('click');
  203. if (z) return preventEvent(e);
  204. }
  205. };
  206.  
  207. document.addEventListener('mousedown', selectEvent, true);
  208. };
  209.  
  210. setTimeout(selectLikeOpera, 100);