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.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Double-click hyphenated word selection
// @namespace    https://greasyfork.org/en/users/1261421-colemeg
// @version      v1.1
// @description  In selecting a word by double click, it remove break separation by hyphen character (-). So that it will be considered as one word.
// @author       colemeg, based on code by @lexogram (https://github.com/lexogram) - (https://github.com/lexogram/select-with-hyphens/blob/main/selectWordsWithHyphens.js)
// @match        *://*/*
// @icon         none
// @grant        none
// @license      UNLICENSE - For more information, please refer to <https://unlicense.org> 
// ==/UserScript==

// Credit goes to @lexogram (https://github.com/lexogram) 
// Based on repo "selectWordsWithHyphens" (https://github.com/lexogram/select-with-hyphens/blob/main/selectWordsWithHyphens.js)
// 
// Tweak to make a double-click select words with hyphens or
// apostrophes.
//
// NOTE 1: It is not trivial to distinguish between a final
// apostrophe, which is an integral part of a word, that is used
// to indicate possession)...
//
//   She said, "Those books are Jodi's, but these are my kids'".
//
// ... from a closing single quote:
//
//   He said, "She said, 'Meet Jo and Di. These are my kids'".
//
// For simplicity, this script ignores both cases. As of 2023-04-12,
// all major browsers behave in exactly the same way.

"use strict"

document.body.addEventListener('dblclick', selectWord); // double-click events on the document body and triggers the selectWord function.

function selectWord(event) { 

  var selection = window.getSelection(); 
  var range = selection.getRangeAt(0); // Gets the range object for the current selection.

  if (event.target.isContentEditable || event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') { // Checks if the clicked element is editable (like an input or textarea).

    // Handling editable text
    var inputString = event.target.value; // Gets the value of the editable element.
    var cursorPos = event.target.selectionStart; // Retrieves the cursor position within the editable element.
    var start = cursorPos; // Sets the start position of the selection to the cursor position.
    var endPos = cursorPos; // Sets the end position of the selection to the cursor position.

    // Check for the word to the left of the cursor
    while (start > 0 && /[\w‑'’-]/g.test(inputString[start - 1])) { // Loops backward to find the beginning of a word.
        start--; // Decrements the start position 
    }
    // Check for the word to the right of the cursor
    while (endPos < inputString.length && /[\w‑'’-]/g.test(inputString[endPos])) { // Loops forward to find the end of a word.
        endPos++; // Increments the end position 
    }

    var ignoreRegexEditable = /^[\u00AD‑'’-]{2,}$/; // Regex to detect if the selection is just a series of join characters.
    var startRegexEditable = /(\w+[\u00AD‑'’-]?)+$/g; // Regex to find a word+join before the selected word. Examples: ad-|lib|  seven-o'|clock|
    var endRegexEditable = /^([\u00AD‑'’-]?\w+)+/; // Regex to find a join character after the selected word.
    var edgeRegexEditable = /\w|-|‑|'|’|\u00AD/; // Edge case: check if the selection contains no word characters or - or '.

    var chunkEditable = inputString.substring(start, endPos); // Retrieves the selected portion of text.
    var ignoreEditable = ignoreRegexEditable.test(chunkEditable) || !edgeRegexEditable.test(chunkEditable); // Checks if the selection should be ignored.

    if (ignoreEditable) { // If the selection should be ignored:
      return; // Exits the function.
    }

    event.target.setSelectionRange(start, endPos); // Sets the selection range to the determined start and end positions.

  } else {

    // Handling non-editable text
    var container = range.endContainer; // Retrieves the end container of the selection.
    var endOffset = range.endOffset; // Retrieves the end offset of the selection.
    var lastSelectedCharIsSpace = (container.textContent.substring(endOffset - 1, endOffset) === " "); // Checks if the last selected character is a space.
    endOffset -= lastSelectedCharIsSpace; // true → 1, false → 0

    if (!endOffset) { // If the end offset is zero:
      container = range.startContainer; // Sets range to the start container.
      endOffset = container.length; // Sets the end offset to the length of the container.
    }

    var string = container.textContent; // Retrieves the text content of the container.
    var startOffset = (container === range.startContainer) ? range.startOffset : 0; // Retrieves the start offset of the selection.

    // Regex definitions for non-editable text
    var ignoreRegexNonEditable = /^[\u00AD‑'’-]{2,}$/;
    var startRegexNonEditable = /(\w+[\u00AD‑'’-]?)+$/g; 
    var endRegexNonEditable = /^([\u00AD‑'’-]?\w+)+/;
    var edgeRegexNonEditable = /\w|-|‑|'|’|\u00AD/;
    
    var chunkNonEditable = string.substring(startOffset, endOffset); // Retrieves the selected portion of text.
    var ignoreNonEditable = ignoreRegexNonEditable.test(chunkNonEditable) || !edgeRegexNonEditable.test(chunkNonEditable); // Checks if the selection should be ignored.

    if (ignoreNonEditable) { // If the selection should be ignored:
      return; // Exits the function.
    }
    
    extendSelectionBackBeforeWord(string, startOffset); // Extends the selection backward before the word.
    extendSelectionForwardAfterWord(string, endOffset); // Extends the selection forward after the word.
    
    selection.removeAllRanges(); // Removes all existing ranges from the selection.
    selection.addRange(range); // Adds the updated range to the selection.
  }

  // Function to extend the selection backward before the word
  function extendSelectionBackBeforeWord(string, offset) {
    var lastIndex = 0;
    var result, index;
    string = string.substring(0, offset);
    
    while ((result = startRegexNonEditable.exec(string))) {
      index = result.index;
      lastIndex = startRegexNonEditable.lastIndex;
    }
    
    if (lastIndex === offset) {
      range.setStart(container, index);
    }
  }

  // Function to extend the selection forward after the word
  function extendSelectionForwardAfterWord(string, offset) {
    if (!offset) {
      return;
    }
    
    string = string.substring(offset);
    var result = endRegexNonEditable.exec(string);
    
    if (result) {
      endOffset = offset + result[0].length;
      range.setEnd(container, endOffset);
    }
  }

}