On the web, hitting Cmd/Ctrl-A (or triggering "Select All" another way) has two distinct modes of operation.
If your focus is inside a <textarea>
, <input>
, or any element marked with contentEditable
—that is, typing keys would enter text there—then Select All will select all of that element's contents. Great! But otherwise...
Problem
🚨 If a user is focused in a non-editable part of your page, they'll instead select all of the page's content.
And for a user on a site which predominantly features a large editor (like a blog authoring interface!), this might be frustrating. The content of a <textarea>
will look 👀 selected... because in a way, it is, as a byproduct of the whole page being selected, but:
- typing (or pasting) won't replace the content
- copy will copy the entire page, including the text of UI elements
Solution
Let's detect when the user triggers Select All, and redirect their selection to an editor of your choice. This solution is already in place on Emojityper. 😂⌨️
If you've read my other posts, you might remember that I'm not a huge fan of hooking into keyboard events directly—there's many ways to trigger Select All, and not all of them are tapping Ctrl/Cmd-A.
So the solution has three steps:
- adding two hidden, but selectable elements to your page
- listening for the
selectionchange
event - calling
.focus()
on your editor.
1. Hidden Elements
Add two hidden elements to your page, which are selectable, but invisible (not display: none
—they need to be on the page), with CSS like:
.extent {
position: fixed;
opacity: 0;
user-select: auto; /* to always allow select-all */
}
.extent::after {
content: '\200b'; /* zero-width space */
}
We then add these at the top and bottom of your page (this could also be done programatically in JS):
<body>
<div class="extent"></div>
<!-- your regular page here -->
<p><a href="https://dev.to/samthor">@samthor</a></p>
<textarea id="input"></textarea>
<div class="extent"></div>
</body>
2. JavaScript listener
By listening to the selectionchange
event, we can detect whether both elements are selected in the same gesture. Since our extent
elements are transparent, fixed, and have no width, a user can't select them by dragging over them.
The code looks like this:
document.addEventListener('selectionchange', (ev) => {
const isExtent = (node) => {
return node instanceof Element && node.classList.contains('extent');
};
// check the selection extends over two extent nodes (top and bottom)
const s = window.getSelection();
if (s.anchorNode === s.focusNode || !isExtent(s.anchorNode) || !isExtent(s.focusNode)) {
return;
}
// clear page's selection (this isn't perfect and a user may see
// a flash of selection anyway- use selectstart + rAF to fix this)
s.removeAllRanges();
// TODO: focus code goes here
});
3. Focus 🔍
Finally, you can focus on something! After the call to s.removeAllRanges()
above, run something like:
const main = document.querySelector('textarea#main'); // or whatever you want to focus
main.focus();
main.setSelectionRange(0, main.value.length);
This programatically selects the entire contents of textarea#main
. You might alternatively want to do something else creative with the Select All gesture! Who knows! 🤔
Thanks!
Here's a CodePen that puts the code together for a demo. 👍
A related concept is user-select: none
, which disables selection on certain parts of your page. While a complex web app with app-like flows could use it, it's not a solution to the "Select All" problem.
This is just one of many gestures that we can leverage: e.g., this is a follow-up to a post called Native Undo & Redo for the Web, which covers how to insert custom events in the undo stack. And, if you'd like to detect opening "Find" in a page, watch this space—the openfind event is coming soon.
4 👋