Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to remove end space when firefox double click selected text

Firefox double click selected text with next space. How to remove end space on screen with javascript. Thank you so much.

like image 249
mangovn Avatar asked Sep 03 '25 04:09

mangovn


2 Answers

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

  • https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js
  • https://cdnjs.cloudflare.com/ajax/libs/rangy/1.3.0/rangy-core.min.js
  • https://cdnjs.cloudflare.com/ajax/libs/rangy/1.3.0/rangy-textrange.min.js

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

like image 156
Wrongusername Avatar answered Sep 06 '25 12:09

Wrongusername


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.

like image 37
James Bellaby Avatar answered Sep 06 '25 13:09

James Bellaby