/** * Replaces the current selection (if any) with a new range, after * it’s configured by the given function. * * See also: selectNodeContents * Example: * * selectRangeWith(range => { * range.selectNodeContents(foo); * range.setStart(foo.childNodes[0], 3); * range.setEnd(foo.childNodes[0], 5); * }); */ function selectRangeWith(fun) { const selection = getSelection(); // Deselect any ranges that happen to be selected, to prevent the // Selection#addRange call from ignoring our new range (see // for // more details). selection.removeAllRanges(); // Create and configure a range. const range = document.createRange(); fun(range); // Select our new range. selection.addRange(range); } /** * Replaces the current selection (if any) with a new range, spanning * the contents of the given node. */ function selectNodeContents(node) { const previousActive = document.activeElement; selectRangeWith(range => range.selectNodeContents(node)); // If the selection update causes the node or an ancestor to be // focused (Chromium 80+), unfocus it, to avoid any focus-related // styling such as outlines. if (document.activeElement != previousActive) { document.activeElement.blur(); } } /** * Tries to convince a UA with lazy spellcheck to check and highlight * the contents of the given nodes (form fields or @contenteditables). * * Each node is focused then immediately unfocused. Both focus and * selection can be used for this purpose, but only focus works for * @contenteditables. */ function trySpellcheck(...nodes) { // This is inherently a flaky test risk, but Chromium (as of 87) // seems to cancel spellcheck on a node if it wasn’t the last one // focused for “long enough” (though immediate unfocus is ok). // Using requestAnimationFrame or setInterval(0) are usually not // long enough (see // under “trySpellcheck strategy” for an example). const interval = setInterval(() => { if (nodes.length > 0) { const node = nodes.shift(); node.focus(); node.blur(); } else { clearInterval(interval); } }, 250); }