// gather all text control related selection data and put it
// into an object based (here the element reference) storage.
function putTextSelectionState(textControl) {
const { value, selectionStart, selectionEnd } = textControl;
const isSelected = (selectionEnd - selectionStart >= 1);
const selection = value.substring(selectionStart, selectionEnd);
textSelectionStorage.set(textControl, {
isSelected,
selection,
selectionStart,
selectionEnd,
value,
});
}
// the object based storage.
const textSelectionStorage = new WeakMap;
// detect whether custom 'deselect' event handling has to take place.
function handleDeselectText(evt) {
const textControl = evt.currentTarget;
const { selectionStart, selectionEnd } = textControl;
// console.log({ type: evt.type, selectionStart, selectionEnd });
const recentState = { ...textSelectionStorage.get(textControl) };
putTextSelectionState(textControl);
const currentState = { ...textSelectionStorage.get(textControl) };
if (
(selectionEnd - selectionStart === 0)
&& recentState.isSelected
) {
// console.log('deselect');
// a custom event will be created and dispatched in case ...
// - there is nothing currently selected ... and ...
// - there was a recent slection right before.
textControl
.dispatchEvent(
new CustomEvent('deselect', {
detail: {
recentState,
currentState,
},
})
);
}
}
// update text control related selection data.
function handleSelectText({ currentTarget }) {
putTextSelectionState(currentTarget);
}
// enable text related, custom 'deselect' event handling.
function initializeHandleDeselectText(textControl) {
const nodeName = textControl.nodeName.toLowerCase();
const textType = ((nodeName === 'textarea') && 'text')
|| ((nodeName === 'input') && textControl.type)
|| null;
if (
(textType === 'text') ||
(textType === 'search') // ... or some more ...
) {
putTextSelectionState(textControl);
textControl.addEventListener('select', handleSelectText);
textControl.addEventListener('input', handleDeselectText);
textControl.addEventListener('keyup', handleDeselectText);
textControl.addEventListener('mouseup', handleDeselectText);
textControl.addEventListener('focusout', handleDeselectText);
}
}
// custom 'deselect' event handling.
function logTextDeselection(evt) {
const { type, detail, currentTarget } = evt;
console.log({ type, detail, currentTarget });
}
// native 'select' event handling.
function logTextSelection(evt) {
const textControl = evt.currentTarget;
const { value, selectionStart, selectionEnd } = textControl;
const selection = value.substring(selectionStart, selectionEnd);
console.log({ type: evt.type, selection });
}
function main() {
const textControl = document.querySelector('#browser1');
// enable text related, custom 'deselect' event handling.
initializeHandleDeselectText(textControl);
// native 'select' event handling.
textControl.addEventListener('select', logTextSelection);
// custom 'deselect' event handling.
textControl.addEventListener('deselect', logTextDeselection);
// default selected initial value.
textControl.focus();
textControl.value = 'The quick brown fox jumps over the lazy dog.';
textControl.selectionStart = 12;
textControl.selectionEnd = 19;
// handle display of all available text selection states.
document
.querySelector('button')
.addEventListener('click', () => console.log([
...document
.querySelectorAll('textarea, input[type="text"], input[type="search"]')
].map(elmNode => ({
elmNode,
isSelected: textSelectionStorage.get(elmNode).isSelected
}))));
}
main();
body { margin: 0; }
button { width: 27%; }
[type="search"] { width: 72%; }
.as-console-wrapper { min-height: 85%; }
<input id="browser1" type="search" placeholder="Search something ..."/>
<button>show select states</button>