Double-click hyphenated word selection

In selecting a word by double click, it remove break separation by hyphen character (-). So that it will be considered as one word.

  1. // ==UserScript==
  2. // @name Double-click hyphenated word selection
  3. // @namespace https://greasyfork.org/en/users/1261421-colemeg
  4. // @version v1.1
  5. // @description In selecting a word by double click, it remove break separation by hyphen character (-). So that it will be considered as one word.
  6. // @author colemeg, based on code by @lexogram (https://github.com/lexogram) - (https://github.com/lexogram/select-with-hyphens/blob/main/selectWordsWithHyphens.js)
  7. // @match *://*/*
  8. // @icon none
  9. // @grant none
  10. // @license UNLICENSE - For more information, please refer to <https://unlicense.org>
  11. // ==/UserScript==
  12.  
  13. // Credit goes to @lexogram (https://github.com/lexogram)
  14. // Based on repo "selectWordsWithHyphens" (https://github.com/lexogram/select-with-hyphens/blob/main/selectWordsWithHyphens.js)
  15. //
  16. // Tweak to make a double-click select words with hyphens or
  17. // apostrophes.
  18. //
  19. // NOTE 1: It is not trivial to distinguish between a final
  20. // apostrophe, which is an integral part of a word, that is used
  21. // to indicate possession)...
  22. //
  23. // She said, "Those books are Jodi's, but these are my kids'".
  24. //
  25. // ... from a closing single quote:
  26. //
  27. // He said, "She said, 'Meet Jo and Di. These are my kids'".
  28. //
  29. // For simplicity, this script ignores both cases. As of 2023-04-12,
  30. // all major browsers behave in exactly the same way.
  31.  
  32. "use strict"
  33.  
  34. document.body.addEventListener('dblclick', selectWord); // double-click events on the document body and triggers the selectWord function.
  35.  
  36. function selectWord(event) {
  37.  
  38. var selection = window.getSelection();
  39. var range = selection.getRangeAt(0); // Gets the range object for the current selection.
  40.  
  41. if (event.target.isContentEditable || event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') { // Checks if the clicked element is editable (like an input or textarea).
  42.  
  43. // Handling editable text
  44. var inputString = event.target.value; // Gets the value of the editable element.
  45. var cursorPos = event.target.selectionStart; // Retrieves the cursor position within the editable element.
  46. var start = cursorPos; // Sets the start position of the selection to the cursor position.
  47. var endPos = cursorPos; // Sets the end position of the selection to the cursor position.
  48.  
  49. // Check for the word to the left of the cursor
  50. while (start > 0 && /[\w‑'’-]/g.test(inputString[start - 1])) { // Loops backward to find the beginning of a word.
  51. start--; // Decrements the start position
  52. }
  53. // Check for the word to the right of the cursor
  54. while (endPos < inputString.length && /[\w‑'’-]/g.test(inputString[endPos])) { // Loops forward to find the end of a word.
  55. endPos++; // Increments the end position
  56. }
  57.  
  58. var ignoreRegexEditable = /^[\u00AD‑'’-]{2,}$/; // Regex to detect if the selection is just a series of join characters.
  59. var startRegexEditable = /(\w+[\u00AD‑'’-]?)+$/g; // Regex to find a word+join before the selected word. Examples: ad-|lib| seven-o'|clock|
  60. var endRegexEditable = /^([\u00AD‑'’-]?\w+)+/; // Regex to find a join character after the selected word.
  61. var edgeRegexEditable = /\w|-|‑|'|’|\u00AD/; // Edge case: check if the selection contains no word characters or - or '.
  62.  
  63. var chunkEditable = inputString.substring(start, endPos); // Retrieves the selected portion of text.
  64. var ignoreEditable = ignoreRegexEditable.test(chunkEditable) || !edgeRegexEditable.test(chunkEditable); // Checks if the selection should be ignored.
  65.  
  66. if (ignoreEditable) { // If the selection should be ignored:
  67. return; // Exits the function.
  68. }
  69.  
  70. event.target.setSelectionRange(start, endPos); // Sets the selection range to the determined start and end positions.
  71.  
  72. } else {
  73.  
  74. // Handling non-editable text
  75. var container = range.endContainer; // Retrieves the end container of the selection.
  76. var endOffset = range.endOffset; // Retrieves the end offset of the selection.
  77. var lastSelectedCharIsSpace = (container.textContent.substring(endOffset - 1, endOffset) === " "); // Checks if the last selected character is a space.
  78. endOffset -= lastSelectedCharIsSpace; // true → 1, false → 0
  79.  
  80. if (!endOffset) { // If the end offset is zero:
  81. container = range.startContainer; // Sets range to the start container.
  82. endOffset = container.length; // Sets the end offset to the length of the container.
  83. }
  84.  
  85. var string = container.textContent; // Retrieves the text content of the container.
  86. var startOffset = (container === range.startContainer) ? range.startOffset : 0; // Retrieves the start offset of the selection.
  87.  
  88. // Regex definitions for non-editable text
  89. var ignoreRegexNonEditable = /^[\u00AD‑'’-]{2,}$/;
  90. var startRegexNonEditable = /(\w+[\u00AD‑'’-]?)+$/g;
  91. var endRegexNonEditable = /^([\u00AD‑'’-]?\w+)+/;
  92. var edgeRegexNonEditable = /\w|-|‑|'|’|\u00AD/;
  93. var chunkNonEditable = string.substring(startOffset, endOffset); // Retrieves the selected portion of text.
  94. var ignoreNonEditable = ignoreRegexNonEditable.test(chunkNonEditable) || !edgeRegexNonEditable.test(chunkNonEditable); // Checks if the selection should be ignored.
  95.  
  96. if (ignoreNonEditable) { // If the selection should be ignored:
  97. return; // Exits the function.
  98. }
  99. extendSelectionBackBeforeWord(string, startOffset); // Extends the selection backward before the word.
  100. extendSelectionForwardAfterWord(string, endOffset); // Extends the selection forward after the word.
  101. selection.removeAllRanges(); // Removes all existing ranges from the selection.
  102. selection.addRange(range); // Adds the updated range to the selection.
  103. }
  104.  
  105. // Function to extend the selection backward before the word
  106. function extendSelectionBackBeforeWord(string, offset) {
  107. var lastIndex = 0;
  108. var result, index;
  109. string = string.substring(0, offset);
  110. while ((result = startRegexNonEditable.exec(string))) {
  111. index = result.index;
  112. lastIndex = startRegexNonEditable.lastIndex;
  113. }
  114. if (lastIndex === offset) {
  115. range.setStart(container, index);
  116. }
  117. }
  118.  
  119. // Function to extend the selection forward after the word
  120. function extendSelectionForwardAfterWord(string, offset) {
  121. if (!offset) {
  122. return;
  123. }
  124. string = string.substring(offset);
  125. var result = endRegexNonEditable.exec(string);
  126. if (result) {
  127. endOffset = offset + result[0].length;
  128. range.setEnd(container, endOffset);
  129. }
  130. }
  131.  
  132. }