selections.js (2734B)
1 /** 2 * Replaces the current selection (if any) with a new range, after 3 * it’s configured by the given function. 4 * 5 * See also: selectNodeContents 6 * Example: 7 * 8 * selectRangeWith(range => { 9 * range.selectNodeContents(foo); 10 * range.setStart(foo.childNodes[0], 3); 11 * range.setEnd(foo.childNodes[0], 5); 12 * }); 13 */ 14 function selectRangeWith(fun) { 15 const selection = getSelection(); 16 17 // Deselect any ranges that happen to be selected, to prevent the 18 // Selection#addRange call from ignoring our new range (see 19 // <https://www.chromestatus.com/feature/6680566019653632> for 20 // more details). 21 selection.removeAllRanges(); 22 23 // Create and configure a range. 24 const range = document.createRange(); 25 fun(range); 26 27 // Select our new range. 28 selection.addRange(range); 29 } 30 31 /** 32 * Replaces the current selection (if any) with a new range, spanning 33 * the contents of the given node. 34 */ 35 function selectNodeContents(node) { 36 const previousActive = document.activeElement; 37 38 selectRangeWith(range => range.selectNodeContents(node)); 39 40 // If the selection update causes the node or an ancestor to be 41 // focused (Chromium 80+), unfocus it, to avoid any focus-related 42 // styling such as outlines. 43 if (document.activeElement != previousActive) { 44 document.activeElement.blur(); 45 } 46 } 47 48 /** 49 * Tries to convince a UA with lazy spellcheck to check and highlight 50 * the contents of the given nodes (form fields or @contenteditables). 51 * 52 * Each node is focused then immediately unfocused. Both focus and 53 * selection can be used for this purpose, but only focus works for 54 * @contenteditables. 55 */ 56 function trySpellcheck(...nodes) { 57 // This is inherently a flaky test risk, but Chromium (as of 87) 58 // seems to cancel spellcheck on a node if it wasn’t the last one 59 // focused for “long enough” (though immediate unfocus is ok). 60 // Using requestAnimationFrame or setInterval(0) are usually not 61 // long enough (see <https://bucket.daz.cat/work/igalia/0/0.html> 62 // under “trySpellcheck strategy” for an example). 63 const interval = setInterval(() => { 64 if (nodes.length > 0) { 65 const node = nodes.shift(); 66 node.focus(); 67 node.blur(); 68 } else { 69 clearInterval(interval); 70 } 71 }, 250); 72 } 73 74 function createRangeForTextOnly(element, start, end) { 75 const textNode = element.firstChild; 76 if (element.childNodes.length != 1 || textNode.nodeName != '#text') { 77 throw new Error('element must contain a single #text node only'); 78 } 79 const range = document.createRange(); 80 range.setStart(textNode, start); 81 range.setEnd(textNode, end); 82 return range; 83 }