Select text inside a link like Opera

Disable link dragging and select text.

  1. // ==UserScript==
  2. // @name Select text inside a link like Opera
  3. // @version 6.0.0
  4. // @description Disable link dragging and select text.
  5. // @homepageURL https://github.com/eight04/select-text-inside-a-link-like-opera#readme
  6. // @supportURL https://github.com/eight04/select-text-inside-a-link-like-opera/issues
  7. // @license MIT
  8. // @author eight <eight04@gmail.com> (http://eight04.blogspot.tw)
  9. // @namespace eight04.blogspot.com
  10. // @include *
  11. // @grant GM_addStyle
  12. // @run-at document-start
  13. // ==/UserScript==
  14.  
  15. // const IS_FIREFOX = typeof InstallTrigger !== 'undefined';
  16. // const tracker = IS_FIREFOX && createMovementTracker();
  17. const tracker = createMovementTracker();
  18. const selection = window.getSelection();
  19. // waiting -> starting -> started -> ending -> waiting
  20. let state = "WAITING";
  21. let preState;
  22. let mousemoves = 0;
  23. let linkTarget;
  24. const initPos = [0, 0];
  25. let selectType;
  26.  
  27. const EVENTS = {
  28. mousedown: e => {
  29. if (state === "WAITING") {
  30. if (e.altKey || e.button) {
  31. return;
  32. }
  33. if (/img/i.test(e.target.nodeName)) {
  34. return;
  35. }
  36. const target = findLinkTarget(e.target);
  37. if (!target || !target.href) {
  38. return;
  39. }
  40. selectType = e.ctrlKey ? "add" :
  41. e.shiftKey ? "extend" : "new";
  42. initPos[0] = e.pageX;
  43. initPos[1] = e.pageY;
  44. if (selectType === "new") {
  45. if (!selection.isCollapsed && inSelect(getInitPos(), selection)) {
  46. return;
  47. }
  48. }
  49. mousemoves = 0;
  50. state = "STARTING";
  51. linkTarget = target;
  52. linkTarget.classList.add("select-text-inside-a-link");
  53. }
  54. },
  55. mousemove: e => {
  56. if (state === "STARTING") {
  57. mousemoves++;
  58. // dragstart event may not fire all the time
  59. // https://github.com/eight04/select-text-inside-a-link-like-opera/issues/9
  60. if (mousemoves >= 3) {
  61. startSelecting(e);
  62. }
  63. }
  64. if (state === "STARTED") {
  65. const caretPos = caretPositionFromPoint(
  66. e.pageX - window.scrollX,
  67. e.pageY - window.scrollY
  68. );
  69. selection.extend(caretPos.offsetNode, caretPos.offset);
  70. }
  71. },
  72. mouseup: () => {
  73. if (state !== "WAITING") {
  74. preState = state;
  75. state = "ENDING";
  76. // delay uninit to cancel click event
  77. setTimeout(startWaiting);
  78. }
  79. },
  80. click: e => {
  81. if (state === "ENDING") {
  82. if (preState === "STARTED") {
  83. // fix browser clicking issue. Cancel click event if we have selected
  84. // something.
  85. const clickedTarget = findLinkTarget(e.target);
  86. if (clickedTarget === linkTarget) {
  87. e.preventDefault();
  88. e.stopImmediatePropagation();
  89. }
  90. }
  91. startWaiting();
  92. }
  93. },
  94. dragstart: e => {
  95. if (state === "STARTED") {
  96. e.preventDefault();
  97. return;
  98. }
  99. if (state === "STARTING") {
  100. startSelecting(e);
  101. }
  102. }
  103. };
  104.  
  105. for (const key in EVENTS) {
  106. document.addEventListener(key, EVENTS[key], true);
  107. }
  108.  
  109. if (!document.contentType || !document.contentType.endsWith("/xml")) {
  110. document.addEventListener("DOMContentLoaded", function(){
  111. GM_addStyle(".select-text-inside-a-link{ -moz-user-select: text!important; }");
  112. });
  113. }
  114.  
  115. function startSelecting(e) {
  116. if (!shouldStart(e)) {
  117. startWaiting();
  118. return;
  119. }
  120. if (e.type === "dragstart") {
  121. e.preventDefault();
  122. }
  123. if (selectType === "new") {
  124. const pos = getInitPos();
  125. selection.collapse(pos.offsetNode, pos.offset);
  126. } else if (selectType === "add") {
  127. const range = new Range;
  128. const pos = getInitPos();
  129. range.setStart(pos.offsetNode, pos.offset);
  130. selection.addRange(range);
  131. }
  132. state = "STARTED";
  133. }
  134.  
  135. function getInitPos() {
  136. return caretPositionFromPoint(initPos[0] - window.scrollX, initPos[1] - window.scrollY);
  137. }
  138.  
  139. function shouldStart(e) {
  140. const delta = tracker ? tracker() :
  141. [Math.abs(e.pageX - initPos[0]), Math.abs(e.pageY - initPos[1])];
  142. return delta[0] >= delta[1];
  143. }
  144.  
  145. function startWaiting() {
  146. if (linkTarget) {
  147. linkTarget.classList.remove("select-text-inside-a-link");
  148. }
  149. state = "WAITING";
  150. linkTarget = null;
  151. }
  152.  
  153. function createMovementTracker() {
  154. // we always have to track mouse movement so we can use the delta on dragstart
  155. // event.
  156. // it is possible to calculate the movement between mousedown and dragstart
  157. // events in Chrome. In Firefox, the two events are fired at the same time.
  158. const moves = [[0, 0], [0, 0], [0, 0]];
  159. let index = 0;
  160. document.addEventListener("mousemove", e => {
  161. moves[index][0] = e.pageX;
  162. moves[index][1] = e.pageY;
  163. index = (index + 1) % 3;
  164. });
  165. return () => {
  166. const output = [];
  167. for (let i = 0; i < 2; i++) {
  168. // FIXME: should we assume that the array contains initial values [0, 0]?
  169. output.push(
  170. Math.abs(moves[index][i] - moves[(index + 1) % 3][i]) +
  171. Math.abs(moves[(index + 1) % 3][i] - moves[(index + 2) % 3][i])
  172. );
  173. }
  174. return output;
  175. };
  176. }
  177.  
  178. function caretPositionFromPoint(x, y) {
  179. if (document.caretPositionFromPoint) {
  180. return document.caretPositionFromPoint(x, y);
  181. }
  182. var r = document.caretRangeFromPoint(x, y);
  183. return {
  184. offsetNode: r.startContainer,
  185. offset: r.startOffset
  186. };
  187. }
  188.  
  189. function inSelect(caretPos, selection){
  190. var i, len = selection.rangeCount, range;
  191. for (i = 0; i < len; i++) {
  192. range = selection.getRangeAt(i);
  193. if (range.isPointInRange(caretPos.offsetNode, caretPos.offset)) {
  194. return true;
  195. }
  196. }
  197. return false;
  198. }
  199.  
  200. function findLinkTarget(target) {
  201. while (target && target.nodeName !== "A" && target.nodeName !== "a") {
  202. target = target.parentNode;
  203. }
  204. return target;
  205. }