Firefox double click selected text with next space. How to remove end space on screen with javascript. Thank you so much.
Here's solution that does remove trailing whitespace character selected on doubleclick in Windows browsers, somewhat emulating Firefox about:config setting layout.word_select.eat_space_to_next_word being set to "false".
Does it "On screen, not in variable".
Tested in Chrome 60.0.3112.113, Firefox 55.0.3, IE8 and IE Edge(14). It's still sometimes visible as selection shrinks from that extra space but nothing can be done about that.
Works for both arbitrary text and input/textareas(those need completely different approaches).
Uses jQuery
(function() {
var lastSelEvent = null;
var lastSelInputEvent = null;
var lastDblClickEvent = null;
var selchangeModTs = null;
$(document).on('selectstart selectionchange', function (e) //not input/textarea case
{
fixEventTS(e);
lastSelEvent = e;
if (( selchangeModTs != null) && (new Date().getTime() - selchangeModTs < 50)) //without this we get infinite loop in IE11+ as changing selection programmatically apparently generates event itself...
return;
handleSelEvent(e);
});
$(document).on('select', function (e) //input/textarea
{
fixEventTS(e);
lastSelInputEvent = e;
handleSelEvent(e);
});
$(document).on('dblclick',function(e)
{
fixEventTS(e);
lastDblClickEvent = e;
handleSelEvent(e);
});
function fixEventTS(e)
{
if (typeof e.timeStamp == 'undefined') //IE 8 no timestamps for events...
{
e.timeStamp = new Date().getTime();
}
}
function handleSelEvent(e)
{
if (lastDblClickEvent===null)
return;
if ( ((e.type==='selectstart') || (e.type==='selectionchange') || (e.type==='dblclick')) && (lastSelEvent != null) && (Math.abs(lastDblClickEvent.timeStamp - lastSelEvent.timeStamp) < 1000) ) // different browsers have different event order so take abs to be safe...
{
switch (lastSelEvent.type)
{
case 'selectstart':
setTimeout(handleSelChange,50); //IE8 etc fix, selectionchange is actually only change, not selection "creation"
break;
case 'selectionchange':
handleSelChange();
break;
}
}
if ( ((e.type==='select') || (e.type==='dblclick')) && (lastSelInputEvent != null) && (Math.abs(lastDblClickEvent.timeStamp - lastSelInputEvent.timeStamp) < 1000) ){
handleSel(lastSelInputEvent);
}
}
function handleSel(e)
{
//right whitespace
while (/\s$/.test(e.target.value.substr(e.target.selectionEnd - 1, 1)) && e.target.selectionEnd > 0) {
e.target.selectionEnd -= 1;
}
//left whitespace
while (/\s$/.test(e.target.value.substr(e.target.selectionStart, 1)) && e.target.selectionStart > 0) {
e.target.selectionStart += 1;
}
}
function handleSelChange() {
var sel = null;
if (typeof window.getSelection == 'function') // modern browsers
sel = window.getSelection();
else if (typeof document.getSelection == 'function')
sel = document.getSelection();
if (sel && !sel.isCollapsed) {
var range = sel.getRangeAt(0); //have to use range instead of more direct selection.expand/selection.modify as otherwise have to consider selection's direction in non-doubleclick cases
if (range.endOffset > 0 && (range.endContainer.nodeType===3) && (range.endContainer.textContent != ''/*otherwise rightside pictures get deleted*/))
{
//right whitespaces
while ( /[\s\S]+\s$/.test(range.endContainer.textContent.substr(0,range.endOffset)) ) { //have to use instead of range.toString() for IE11+ as it auto-trims whitespaces there and in selection.toString()
selchangeModTs = new Date().getTime();
range.setEnd(range.endContainer, range.endOffset - 1);
}
}
if ((range.startContainer.nodeType===3) && (range.startContainer.textContent != '') && (range.startOffset < range.startContainer.textContent.length)) {
//left whitespaces
while (/^\s[\s\S]+/.test(range.startContainer.textContent.substr(range.startOffset))) {
selchangeModTs = new Date().getTime();
range.setStart(range.startContainer, range.startOffset + 1);
}
}
selchangeModTs = new Date().getTime();
sel.removeAllRanges(); //IE11+ fix, in Firefox/Chrome changes to range apply to selection automatically
sel.addRange(range);
}
else if (typeof document.selection != 'undefined') //IE 10 and lower case
{
var range = document.selection.createRange();
if (range && range.text && range.text.toString()) {
while ((range.text != '') && /[\s\S]+\s$/.test(range.text.toString())) {
selchangeModTs = new Date().getTime();
range.moveEnd('character', -1);
range.select();
}
while ((range.text != '') && /^\s[\s\S]+/.test(range.text.toString())) {
selchangeModTs = new Date().getTime();
range.moveStart('character', 1);
range.select();
}
}
}
}
})();
If you don't need old IE support & limitint to doubleclick-only removal it can be greatly simplified and only needs to handle selectchange and select events to respective functions.(but then you actually can't select stuff with keyboard)
Edit: Using Rangy lib(https://github.com/timdown/rangy) Core + TextRange for non-input elements selection trimming is way better than my basic attempt that only works if endContainer happens to be a text node. Basic action of selection trimming is
rangy.getSelection().trim()
Minimal includes are
Rangy implementation additionally supporting triple-click whole-line selection trimming
(function() {
var lastSelEvent = null;
var lastSelInputEvent = null;
var lastDblClickEvent = null;
var selchangeModTs = null;
var tripleclickFixBound = false;
var selStartTimout = null;
$(document).on('selectstart selectionchange', function (e) //non-inputs
{
fixEventTS(e);
lastSelEvent = e;
if ( ( selchangeModTs != null) && (new Date().getTime() - selchangeModTs < 50) ) //ie11+ fix otherwise get self-loop with our selection changes generating this event
return;
handleSelEvent(e);
});
if ('onselect' in document.documentElement) {
$(document).on('select', function (e) //input/textarea
{
fixEventTS(e);
lastSelInputEvent = e;
handleSelEvent(e);
});
}
$(document).on('click',function(e){
if (typeof e.originalEvent.detail !== 'undefined')
{
multiclickHandlerfunction(e);
}
else
{
fixEventTS(e);
if (!tripleclickFixBound) {
$(document).on('dblclick', function (e) {
fixEventTS(e);
selchangeModTs = null;
lastDblClickEvent = e;
handleSelEvent(e);
});
tripleclickFixBound=true;
}
if ( (lastDblClickEvent != null) && (e.timeStamp - lastDblClickEvent.timeStamp < 300))
{
lastDblClickEvent = e;
selchangeModTs = null;
handleSelEvent(e);
}
}
});
function multiclickHandlerfunction(e) {
if (e.originalEvent.detail === 2 || e.originalEvent.detail === 3) {
fixEventTS(e);
selchangeModTs = null;
lastDblClickEvent = e;
handleSelEvent(e);
}
}
function fixEventTS(e)
{
if (typeof e.timeStamp == 'undefined') //IE 8
{
e.timeStamp = new Date().getTime();
}
}
function handleSelEvent(e)
{
if (lastDblClickEvent===null)
return;
if ( ((e.type==='selectstart') || (e.type==='selectionchange') || (e.type==='dblclick') || (e.type==='click')) && (lastSelEvent != null) && (Math.abs(lastDblClickEvent.timeStamp - lastSelEvent.timeStamp) < 1000) ) // different order of events in different browsers...
{
switch (lastSelEvent.type)
{
case 'selectstart':
case 'selectionchange':
clearTimeout(selStartTimout);
selStartTimout = setTimeout(handleSelChange,(/msie\s|trident\/|edge\//i.test(window.navigator.userAgent)?150:0));
break;
}
}
if ( ((e.type==='select') || (e.type==='dblclick') || (e.type==='click')) && (lastSelInputEvent != null) && (Math.abs(lastDblClickEvent.timeStamp - lastSelInputEvent.timeStamp) < 1000) )
{
handleSel(lastSelInputEvent);
}
}
function handleSel(e)
{
if (typeof(e.target.selectionEnd) != 'undefined') {
//left whitespace
while (/\s$/.test(e.target.value.substr(e.target.selectionEnd - 1, 1)) && e.target.selectionEnd > 0) {
e.target.selectionEnd -= 1;
}
//right whitespace
while (/^s/.test(e.target.value.substr(e.target.selectionStart - 1, 1)) && e.target.selectionStart > 0) {
e.target.selectionStart += 1;
}
}
}
function handleSelChange() {
var sel = rangy.getSelection();
if (sel && !sel.isCollapsed) {
selchangeModTs = new Date().getTime();
sel.trim();
}
else if (typeof document.selection != 'undefined') //IE 10- input/textArea case
{
var range = document.selection.createRange();
if (range && range.text && range.text.toString()) {
while ((range.text != '') && /[\s\S]+\s$/.test(range.text.toString())) {
selchangeModTs = new Date().getTime();
range.moveEnd('character', -1);
range.select();
}
while ((range.text != '') && /^\s[\s\S]+/.test(range.text.toString())) {
selchangeModTs = new Date().getTime();
range.moveStart('character', 1);
range.select();
}
}
}
}
})($,window);
CodePen demo
One thing I tried was to use range.setEnd()
using the selection focus and anchor node and offset but -1.
Please excuse the lack of checks for browser compatibility.
var selection = window.getSelection();
var range = selection.getRangeAt(0);
var selected = range.toString();
if (!selection.isCollapsed) {
if (/\s+$/.test(selected)) {
if (selection.focusOffset > selection.anchorOffset) {
range.setEnd(selection.focusNode, selection.focusOffset - 1);
} else {
range.setEnd(selection.anchorNode, selection.anchorOffset - 1);
}
}
}
I am unsure if it works in IE/Edge but Chrome seems happy with it.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With