├── .gitignore ├── icons ├── icon48.png └── icon96.png ├── options ├── tweaks.css ├── defaults.js ├── options.html └── options.js ├── select.css ├── LICENSE.md ├── manifest.json ├── _locales ├── fa │ └── messages.json ├── en │ └── messages.json └── ru │ └── messages.json ├── README.md └── content.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.zip 2 | *~ 3 | -------------------------------------------------------------------------------- /icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkestrel/dragselectlinktext/HEAD/icons/icon48.png -------------------------------------------------------------------------------- /icons/icon96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkestrel/dragselectlinktext/HEAD/icons/icon96.png -------------------------------------------------------------------------------- /options/tweaks.css: -------------------------------------------------------------------------------- 1 | 2 | .panel-formElements-item input { 3 | margin-right: 4px; 4 | } 5 | 6 | .panel-formElements-item input[type="number"] { 7 | width: 60px; 8 | } -------------------------------------------------------------------------------- /select.css: -------------------------------------------------------------------------------- 1 | 2 | .dragselectlinktext-textcursor { cursor: text !important; } 3 | 4 | .dragselectlinktext-grabcursor { cursor: -moz-grab !important; } 5 | 6 | .dragselectlinktext-selectable { -moz-user-select: text !important; } 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Drag-Select Link Text 2 | Firefox Web Extension 3 | Copyright (C) 2014-2018 Kestrel 4 | 5 | This Source Code Form is subject to the terms of the Mozilla Public 6 | License, v. 2.0. If a copy of the MPL was not distributed with this 7 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 | -------------------------------------------------------------------------------- /options/defaults.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | var defaultOptions = { 4 | "selectGesture": "horizontalSelect", 5 | "holdTimeSec": 0.3, 6 | "holdAllTimeSec": 1, 7 | "changeCursor": true, 8 | "overrideUnselectable": false, 9 | "dragThresholdX": 3, 10 | "dragThresholdY": 3, 11 | }; 12 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "__MSG_extensionName__", 4 | "version": "1.5.5", 5 | "description": "__MSG_extensionDescription__", 6 | "author": "Kestrel", 7 | "homepage_url": "https://github.com/nkestrel/dragselectlinktext", 8 | "default_locale": "en", 9 | 10 | "icons": { 11 | "48": "icons/icon48.png", 12 | "96": "icons/icon96.png" 13 | }, 14 | 15 | "applications": { 16 | "gecko": { 17 | "id": "dragselectlinktext@kestrel", 18 | "strict_min_version": "55.0" 19 | } 20 | }, 21 | 22 | "content_scripts": [ 23 | { 24 | "matches": [""], 25 | "js": [ "options/defaults.js", 26 | "content.js"], 27 | "css": ["select.css"], 28 | "run_at": "document_start", 29 | "all_frames": true, 30 | "match_about_blank": true 31 | } 32 | ], 33 | 34 | "options_ui": { 35 | "page": "options/options.html", 36 | "browser_style": true 37 | }, 38 | 39 | "permissions": ["storage"] 40 | } 41 | -------------------------------------------------------------------------------- /_locales/fa/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "انتخاب متن پیوند با کشیدن", 4 | "description": "Name of the extension." 5 | }, 6 | "extensionDescription": { 7 | "message": "متن پیوند را با حرکات آسان ماوس انتخاب کنید.", 8 | "description": "Description of the extension." 9 | }, 10 | "options_title_manualDragGesture": { 11 | "message": "حرکت کشیدن دستی" 12 | }, 13 | "options_title_holdTime": { 14 | "message": "زمان نگه داشتن" 15 | }, 16 | "options_title_advanced": { 17 | "message": "پیشرفته" 18 | }, 19 | "options_selectGesture_horizontalSelect": { 20 | "message": "افقی - متن را با کشیدن به صورت افقی انتخاب کنید." 21 | }, 22 | "options_selectGesture_holdSelect": { 23 | "message": "نگه داشتن - متن را پس از سپری شدن زمان نگه داشتن بکشید و انتخاب کنید." 24 | }, 25 | "options_selectGesture_immediateSelect": { 26 | "message": "فوری - متن را فورا با کشیدن و بدون سپری شدن زمان نگه داشتن انخاب کنید." 27 | }, 28 | "options_holdTimeSec": { 29 | "message": "انتخاب دستی:" 30 | }, 31 | "options_holdAllTimeSec": { 32 | "message": "انتخاب همه:" 33 | }, 34 | "options_changeCursor": { 35 | "message": "تغییر مکان نما" 36 | }, 37 | "options_overrideUnselectable": { 38 | "message": "لغو متن غیر قابل انتخاب" 39 | }, 40 | "options_seconds": { 41 | "message": "ثانیه" 42 | }, 43 | "options_btndefault": { 44 | "message": "پیش فرض ها" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extensionName": { 4 | "message": "Drag-Select Link Text", 5 | "description": "Name of the extension." 6 | }, 7 | "extensionDescription": { 8 | "message": "Select link text with easy mouse gestures.", 9 | "description": "Description of the extension." 10 | }, 11 | "options_title_manualDragGesture": { 12 | "message": "Manual drag gesture" 13 | }, 14 | "options_title_holdTime": { 15 | "message": "Hold time" 16 | }, 17 | "options_title_advanced": { 18 | "message": "Advanced" 19 | }, 20 | "options_selectGesture_horizontalSelect": { 21 | "message": "Horizontal - Select text by dragging horizontally." 22 | }, 23 | "options_selectGesture_holdSelect": { 24 | "message": "Hold - Select text by waiting for the hold time to elapse before dragging." 25 | }, 26 | "options_selectGesture_immediateSelect": { 27 | "message": "Immediate - Select text by immediately dragging before the hold time elapses." 28 | }, 29 | "options_holdTimeSec": { 30 | "message": "Select manually:" 31 | }, 32 | "options_holdAllTimeSec": { 33 | "message": "Select all:" 34 | }, 35 | "options_changeCursor": { 36 | "message": "Change cursor" 37 | }, 38 | "options_overrideUnselectable": { 39 | "message": "Override unselectable text" 40 | }, 41 | "options_seconds": { 42 | "message": "seconds" 43 | }, 44 | "options_btndefault": { 45 | "message": "Defaults" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /_locales/ru/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Drag-Select Link Text", 4 | "description": "Name of the extension." 5 | }, 6 | "extensionDescription": { 7 | "message": "Выделение текста ссылки простыми жестами мыши.", 8 | "description": "Description of the extension." 9 | }, 10 | "options_title_manualDragGesture": { 11 | "message": "Жест выделения" 12 | }, 13 | "options_title_holdTime": { 14 | "message": "Время удержания" 15 | }, 16 | "options_title_advanced": { 17 | "message": "Дополнительно" 18 | }, 19 | "options_selectGesture_horizontalSelect": { 20 | "message": "Горизонтальное движение – выделение текста горизонтальным перетаскиванием курсора мыши." 21 | }, 22 | "options_selectGesture_holdSelect": { 23 | "message": "Удержание – выделение текста активируется при неподвижном удержании мыши (в течении настраиваемого промежутка времени)." 24 | }, 25 | "options_selectGesture_immediateSelect": { 26 | "message": "Немедленное выделение – выделение текста горизонтальным движением (до истечения настраиваемого промежутка времени)." 27 | }, 28 | "options_holdTimeSec": { 29 | "message": "Выделение части текста:" 30 | }, 31 | "options_holdAllTimeSec": { 32 | "message": "Выделение всей ссылки:" 33 | }, 34 | "options_changeCursor": { 35 | "message": "Менять внешний вид курсора" 36 | }, 37 | "options_overrideUnselectable": { 38 | "message": "Игнорировать запрет на выделение текста" 39 | }, 40 | "options_seconds": { 41 | "message": "сек." 42 | }, 43 | "options_btndefault": { 44 | "message": "Настройки по умолчанию" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Drag-Select Link Text 2 | Firefox Web Extension. 3 | [Available on AMO](https://addons.mozilla.org/en-US/firefox/addon/drag-select-link-text/) 4 | 5 | ![](icons/icon96.png) 6 | 7 | **Select link text with easy mouse gestures.** 8 | 9 | Selecting link text has never been so easy! Don't fumble trying to select 10 | text inside links from the outside edges, using trial and error to get the 11 | right cursor placement and making unwanted selections. Don't struggle with 12 | the Alt key only to click the link by accident or trigger the menubar. Just 13 | do a simple mouse drag gesture on the link exactly where you want selection 14 | to start. 15 | 16 | Three different types of gestures are available: 17 | 18 | * **Horizontal** - Select text by dragging horizontally (similar to Opera 12). 19 | * **Hold** - Select text by waiting for the hold time to elapse before dragging. 20 | * **Immediate** - Select text by immediately dragging before the hold time elapses. 21 | 22 | If selection is not started when leaving the drag threshold then the link 23 | will be dragged instead like it would normally. The length of the hold time 24 | is adjustable (default 0.3 seconds) and when it elapses the cursor changes so 25 | you know when to start dragging. Hold for longer (default 1 second) to 26 | automatically select all of the link's text. Hold Ctrl/Shift/Meta modifier 27 | key to always do selection. 28 | 29 | Full compatibility with Firefox's multi-selection feature. 30 | 31 | Make selections in clickable areas without triggering a click. 32 | 33 | Advanced option available to override the CSS rule that prevents text 34 | selection, note that pseudo-elements still cannot be selected ([Bug 35 | 12460](https://bugzilla.mozilla.org/show_bug.cgi?id=12460)). 36 | 37 | Doesn't work with middle-click paste on Linux due to [Bug 38 | 1015877](https://bugzilla.mozilla.org/show_bug.cgi?id=1015877). 39 | 40 | Official Firefox support for the horizontal selection gesture is tracked by 41 | [Bug 378775](https://bugzilla.mozilla.org/show_bug.cgi?id=378775). 42 | -------------------------------------------------------------------------------- /options/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 |
11 |
12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | 29 |
30 |
31 | 32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | 40 |
41 |
42 | 43 |
44 |
45 | 46 |
47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /options/options.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Drag-Select Link Text 3 | * Firefox Web Extension 4 | * Copyright (C) 2014-2018 Kestrel 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 9 | */ 10 | 11 | function saveOption(e) { 12 | let value; 13 | switch (e.target.type) { 14 | case "number": 15 | if (e.target.validity.valid) { 16 | value = parseFloat(e.target.value); 17 | } 18 | break; 19 | case "checkbox": 20 | value = e.target.checked; 21 | break; 22 | default: 23 | value = e.target.value; 24 | } 25 | if (value != null) { 26 | let name = e.target.name; 27 | if (value === defaultOptions[name]) { 28 | browser.storage.local.remove(name); 29 | } else { 30 | browser.storage.local.set({[name]: value}); 31 | } 32 | } 33 | } 34 | 35 | 36 | function restoreOptions(event) { 37 | browser.storage.local.get(defaultOptions).then((results) => { 38 | for (let key of Object.keys(results)) { 39 | let value = results[key]; 40 | for (let el of document.getElementsByName(key)) { 41 | switch(el.type) { 42 | case "checkbox": 43 | el.checked = value; 44 | break; 45 | case "radio": 46 | el.checked = (el.value == value); 47 | break; 48 | case "number": 49 | el.value = value.toString(); 50 | break; 51 | default: 52 | el.value = value; 53 | } 54 | } 55 | } 56 | }); 57 | } 58 | 59 | 60 | function restoreDefaults() { 61 | browser.storage.local.clear(); 62 | restoreOptions(); 63 | } 64 | 65 | 66 | document.addEventListener("DOMContentLoaded", restoreOptions); 67 | document.getElementById("btndefault").addEventListener("click", restoreDefaults); 68 | 69 | let labels = document.getElementsByTagName("label"); 70 | for (let label of labels) { 71 | let inputId = label.getAttribute("for"); 72 | if (inputId) { 73 | label.textContent = browser.i18n.getMessage("options_" + inputId); 74 | } 75 | } 76 | 77 | let titles = document.getElementsByClassName("text-section-header"); 78 | for (let title of titles) { 79 | title.textContent = browser.i18n.getMessage("options_" + title.id); 80 | } 81 | 82 | let inputs = document.querySelectorAll("input, select"); 83 | for (let input of inputs) { 84 | input.addEventListener("input", saveOption); 85 | if (input.type == "button") { 86 | input.value = browser.i18n.getMessage("options_" + input.id); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /content.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Drag-Select Link Text 3 | * Firefox Web Extension 4 | * Copyright (C) 2014-2018 Kestrel 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 9 | */ 10 | 11 | const 12 | CLASS_SELECTABLE = "dragselectlinktext-selectable", 13 | CLASS_TEXT_CURSOR = "dragselectlinktext-textcursor", 14 | CLASS_GRAB_CURSOR = "dragselectlinktext-grabcursor", 15 | 16 | SELECT_GESTURE = { 17 | HORIZONTAL: "horizontalSelect", 18 | HOLD: "holdSelect", 19 | IMMEDIATE: "immediateSelect", 20 | }, 21 | 22 | GESTURE_CURSOR = { 23 | [SELECT_GESTURE.HOLD]: CLASS_TEXT_CURSOR, 24 | [SELECT_GESTURE.IMMEDIATE]: CLASS_GRAB_CURSOR, 25 | }, 26 | 27 | HOLD_GESTURES = [ 28 | SELECT_GESTURE.HOLD, 29 | SELECT_GESTURE.IMMEDIATE, 30 | ]; 31 | 32 | var 33 | options, 34 | downEvent, 35 | manualSelecting, 36 | selectedAll, 37 | holdTimeout, 38 | holdSelectAllTimeout, 39 | mousemovePoint, 40 | selectionChanged; 41 | 42 | window.addEventListener("mousedown", onMouseDown, true); 43 | 44 | 45 | browser.storage.local.get(defaultOptions).then((items) => { 46 | options = items; 47 | cursorChanger.enabled = options.changeCursor; 48 | selectableChanger.enabled = options.overrideUnselectable; 49 | }); 50 | 51 | 52 | browser.storage.onChanged.addListener(function(changes, areaName) { 53 | let changedItems = Object.keys(changes); 54 | for (item of changedItems) { 55 | let value = changes[item].newValue; 56 | if (typeof value === "undefined") { 57 | value = defaultOptions[item]; 58 | } 59 | options[item] = value; 60 | 61 | if (item == "changeCursor") { 62 | cursorChanger.enabled = value; 63 | } else if (item == "overrideUnselectable") { 64 | selectableChanger.enabled = value; 65 | } 66 | } 67 | }); 68 | 69 | 70 | function onMouseDown(event) { 71 | // Left mouse button only and avoid interfering with XML 72 | if (event.button !== 0 || 73 | !(event.target instanceof HTMLElement || 74 | event.target instanceof SVGElement)) { 75 | return; 76 | } 77 | 78 | window.removeEventListener("click", onLeftClickBlock, true); 79 | selectionChanged = false; 80 | 81 | downEvent = event; 82 | let point = { x: event.clientX, y: event.clientY}; 83 | 84 | // Not inside existing selection 85 | if (inSelection(point)) { 86 | return; 87 | } 88 | 89 | window.addEventListener("mousemove", onMouseMove, true); 90 | window.addEventListener("mouseup", onMouseUp, true); 91 | window.addEventListener("dragend", onDragEnd, true); 92 | window.addEventListener("selectionchange", onSelectionChange, true); 93 | 94 | // Cursor must be over selectable link text for dragstart listener and hold timers. 95 | // Don't interfere when Alt modifier being used (possibly for text selection). 96 | if (isSelectableTextLink(event.target, point) && !event.altKey) { 97 | 98 | window.addEventListener("dragstart", onDragStart, true); 99 | 100 | // Change cursor after hold time to give visual feedback except when modifier 101 | // keys are pressed which force selection. 102 | let selectModifier = hasModifierKey(event); 103 | if (HOLD_GESTURES.includes(options.selectGesture) && !selectModifier) { 104 | holdTimeout = window.setTimeout(function() { 105 | holdTimeout = null; 106 | cursorChanger.set(event.target, GESTURE_CURSOR[options.selectGesture]); 107 | }, options.holdTimeSec * 1000); 108 | } 109 | 110 | // Select all timer 111 | holdSelectAllTimeout = window.setTimeout(function() { 112 | window.clearTimeout(holdTimeout); 113 | cursorChanger.set(event.target, CLASS_GRAB_CURSOR); 114 | selectAll(event.target, selectModifier); 115 | window.addEventListener("click", onLeftClickBlock, true); 116 | window.removeEventListener("mousemove", onMouseMove, true); 117 | }, options.holdAllTimeSec * 1000); 118 | } 119 | } 120 | 121 | 122 | function onMouseUp(event) { 123 | cleanup("mouseup"); 124 | } 125 | 126 | 127 | function onMouseMove(event) { 128 | let selection = window.getSelection(); 129 | 130 | // Do manual selection, don't need to make elements selectable since it 131 | // doesn't recognize selectability. 132 | if (manualSelecting) { 133 | if (!mousemovePoint) { 134 | startManualSelect(downEvent.clientX, 135 | downEvent.clientY, 136 | hasModifierKey(downEvent), 137 | !downEvent.shiftKey); 138 | } else if (selection.rangeCount > 0) { 139 | let range = getCaretRangeFromPoint(event.clientX, event.clientY); 140 | if (range) { 141 | // Extending selection does not consider selectability so intermediate 142 | // unselectable elements may be selected. 143 | selection.extend(range.startContainer, range.startOffset); 144 | } 145 | } 146 | } else { 147 | // Block click when selection changed and threshold exceeded 148 | if (selectionChanged && 149 | (Math.abs(event.screenX - downEvent.screenX) > options.dragThresholdX || 150 | Math.abs(event.screenY - downEvent.screenY) > options.dragThresholdY)) { 151 | window.addEventListener("click", onLeftClickBlock, true); 152 | cursorChanger.set(downEvent.target, CLASS_TEXT_CURSOR); 153 | selectionChanged = false; 154 | 155 | // Remove mousemove listener if no longer needed 156 | if (!options.overrideUnselectable) { 157 | window.removeEventListener("mousemove", onMouseMove, true); 158 | } 159 | } 160 | 161 | if (options.overrideUnselectable) { 162 | // Get element under point as cannot rely on event.target 163 | let el = document.elementFromPoint(event.clientX, event.clientY); 164 | // Only change selectability when necessary 165 | if (!selectableTester.test(el)) { 166 | selectableChanger.set(el, CLASS_SELECTABLE); 167 | selectableTester.update(el, true); 168 | } 169 | } 170 | } 171 | 172 | // Save mousemove point so drag direction can be determined on dragstart 173 | mousemovePoint = {screenX: event.screenX, 174 | screenY: event.screenY}; 175 | } 176 | 177 | 178 | function onSelectionChange(event) { 179 | let selection = window.getSelection(); 180 | // Selection must contain non-newline characters 181 | if (!selection.isCollapsed && selection.toString().match(/[^\r\n]/g) != null) { 182 | window.removeEventListener("selectionchange", onSelectionChange, true); 183 | // Block click when selection changed 184 | if (manualSelecting) { 185 | window.addEventListener("click", onLeftClickBlock, true); 186 | cursorChanger.set(downEvent.target, CLASS_TEXT_CURSOR); 187 | } else { 188 | selectionChanged = true; 189 | } 190 | } 191 | } 192 | 193 | 194 | function onDragStart(event) { 195 | let deltaX, 196 | deltaY; 197 | 198 | // Measure system drag threshold size 199 | if (mousemovePoint) { 200 | deltaX = Math.abs(mousemovePoint.screenX - event.screenX) + 1; 201 | if (deltaX > options.dragThresholdX) { 202 | options.dragThresholdX = deltaX; 203 | browser.storage.local.set({dragThresholdX: deltaX}); 204 | } 205 | deltaY = Math.abs(mousemovePoint.screenY - event.screenY) + 1; 206 | if (deltaY > options.dragThresholdY) { 207 | options.dragThresholdY = deltaY; 208 | browser.storage.local.set({dragThresholdY: deltaY}); 209 | } 210 | } 211 | 212 | if (selectedAll) { 213 | return; 214 | } 215 | 216 | // Modifiers always do selection 217 | let doSelect = hasModifierKey(event); 218 | if (!doSelect) { 219 | switch (options.selectGesture) { 220 | case SELECT_GESTURE.HOLD: 221 | doSelect = holdTimeout == null; 222 | break; 223 | case SELECT_GESTURE.IMMEDIATE: 224 | doSelect = holdTimeout != null; 225 | break; 226 | default: 227 | // SELECT_GESTURE.HORIZONTAL 228 | // dragstart can occur before mousemove for fast movements, 229 | // can't determine direction so assume selecting (more often the case) 230 | doSelect = !mousemovePoint || deltaX > deltaY; 231 | break; 232 | } 233 | } 234 | 235 | if (doSelect) { 236 | // Prevent drag from starting 237 | event.preventDefault(); 238 | event.stopPropagation(); 239 | manualSelecting = true; 240 | mousemovePoint = null; 241 | } 242 | 243 | cleanup("dragstart"); 244 | } 245 | 246 | 247 | function onDragEnd(event) { 248 | cleanup("mouseup"); 249 | } 250 | 251 | 252 | function onLeftClickBlock(event) { 253 | // Block left mouse button 254 | if (event.button !== 0) { 255 | return; 256 | } 257 | event.preventDefault(); 258 | event.stopPropagation(); 259 | window.removeEventListener("click", onLeftClickBlock, true); 260 | } 261 | 262 | 263 | function cleanup(cleanupEvent) { 264 | window.clearTimeout(holdTimeout); 265 | window.clearTimeout(holdSelectAllTimeout); 266 | window.removeEventListener("dragstart", onDragStart, true); 267 | 268 | if (cleanupEvent == "mouseup") { 269 | manualSelecting = false; 270 | selectedAll = false; 271 | window.removeEventListener("mousemove", onMouseMove, true); 272 | window.removeEventListener("mouseup", onMouseUp, true); 273 | window.removeEventListener("dragend", onDragEnd, true); 274 | window.removeEventListener("selectionchange", onSelectionChange, true); 275 | cursorChanger.clear(); 276 | selectableChanger.clear(); 277 | selectableTester.clear(); 278 | downEvent = null; 279 | mousemovePoint = null; 280 | } 281 | } 282 | 283 | 284 | function isSelectableTextLink(element, point) { 285 | // Don't need to override selectability for links to do manual select but 286 | // do for "select all" dragging to work. 287 | // Changing selectability has a performance cost so only do it when necessary. 288 | if (!selectableTester.test(element)) { 289 | if (options.overrideUnselectable) { 290 | selectableChanger.set(element, CLASS_SELECTABLE); 291 | selectableTester.update(element, true); 292 | } else { 293 | return false; 294 | } 295 | } 296 | 297 | let el = element; 298 | while (el) { 299 | // Detect HTML and SVG link anchors 300 | if (el.tagName.toUpperCase() === "A" && 301 | (el.hasAttribute("href") || el.hasAttribute("xlink:href"))) { 302 | return isTextNode(element, point); 303 | } 304 | el = el.parentElement; 305 | } 306 | return false; 307 | } 308 | 309 | 310 | function isTextNode(element, point) { 311 | // Note that calling this for large scrollbars can make them less responsive 312 | let downrect = element.getBoundingClientRect(); 313 | let pointInsideBox = pointInRect(point, downrect); 314 | let walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT); 315 | while (walker.nextNode()) { 316 | if (testNode(walker.currentNode)) { 317 | return true; 318 | } 319 | } 320 | return false; 321 | 322 | function testNode(node) { 323 | // Text nodes must contain non-space characters 324 | if (node.data && node.data.trim().length > 0) { 325 | let range = document.createRange(); 326 | range.selectNode(node); 327 | let rects = range.getClientRects(); 328 | for (let rect of rects) { 329 | if (rect.width > 0 && rect.height > 0) { 330 | if (pointInsideBox) { 331 | // Allow selection to start anywhere inside element box as long 332 | // as a text range is inside 333 | if (intersectRect(rect, downrect)) { 334 | return true; 335 | } 336 | } else { 337 | // For text overflowing the element box, can only start selection 338 | // directly over it 339 | if (pointInRect(point, rect)) { 340 | return true; 341 | } 342 | } 343 | } 344 | } 345 | } 346 | return false; 347 | } 348 | } 349 | 350 | 351 | function inSelection(point) { 352 | let selection = window.getSelection(); 353 | if (!selection.isCollapsed) { 354 | let caretPos = document.caretPositionFromPoint(point.x, point.y); 355 | if (caretPos && caretPos.offsetNode && 356 | caretPos.offset <= caretPos.offsetNode.textContent.length) { 357 | for (let i = 0, iLen = selection.rangeCount; i < iLen; i++) { 358 | let range = selection.getRangeAt(i); 359 | if (!range.collapsed && 360 | range.isPointInRange(caretPos.offsetNode, caretPos.offset)) { 361 | return true; 362 | } 363 | } 364 | } 365 | } 366 | return false; 367 | } 368 | 369 | 370 | function getCaretRangeFromPoint(x, y) { 371 | let el = document.elementFromPoint(x, y); 372 | let range; 373 | if (options.overrideUnselectable || selectableTester.test(el)) { 374 | let caretPos = document.caretPositionFromPoint(x, y); 375 | if (caretPos && caretPos.offsetNode && 376 | caretPos.offset <= caretPos.offsetNode.textContent.length) { 377 | range = document.createRange(); 378 | range.setStart(caretPos.offsetNode, caretPos.offset); 379 | range.collapse(true); 380 | } 381 | } 382 | return range; 383 | } 384 | 385 | 386 | function startManualSelect(x, y, multiSelect, moveAnchor) { 387 | let selection = window.getSelection(); 388 | if (!multiSelect) { 389 | selection.removeAllRanges(); 390 | } 391 | // Don't move anchor for shift selection 392 | if (moveAnchor) { 393 | let range = getCaretRangeFromPoint(x, y); 394 | if (range) { 395 | selection.addRange(range); 396 | } 397 | } 398 | } 399 | 400 | 401 | function selectAll(element, multiSelect) { 402 | let selection = window.getSelection(); 403 | if (!multiSelect) { 404 | selection.removeAllRanges(); 405 | } 406 | let range = document.createRange(); 407 | range.selectNodeContents(element); 408 | selection.addRange(range); 409 | selectedAll = true; 410 | } 411 | 412 | 413 | // Object to add and later remove classes to/from elements 414 | var ClassListChanger = { 415 | enabled: true, 416 | _changed: [], 417 | 418 | set: function(element, value) { 419 | // Exclude