Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Caret disappears in Firefox when saving its position with Rangy

This happens only in Firefox.

Important: I am saving the caret's position with rangy.saveSelection():

  • when click the content editable div
  • on keyup
  • when adding an external html element (as a node) to the content editable div

I need the position saved constantly through multiple means to be able to insert html elements on click (I have some tags).

When I click in the contentEditable div and the div is empty (first focus, let's say), I cannot see the caret unless I start typing. If the caret is at the end, I cannot see it either.

Another weird behaviour is that I cannot use the arrows to navigate between the text in the contentEditable div.

If I remove the functions which (constantly) saves the caret's position (on input, click etc.) the caret returns to normal (the caret is visible).

The problem appears when I start saving the position of the caret. Clearly I should be doing some sort of reset or a clear.. but from what I understand, those seem counterproductive (as from my understanding they destroy the saved caret location).

The content editable div

                <div class="input__boolean input__boolean--no-focus">
                    <div 
                            @keydown.enter.prevent
                            @blur="addPlaceholder"
                            @keyup="saveCursorLocation($event); fixDelete(); clearHtmlElem($event);"
                            @input="updateBooleanInput($event); clearHtmlElem($event);"
                            @paste="pasted"
                            v-on:click="clearPlaceholder(); saveCursorLocation($event);"
                            class="input__boolean-content"
                            ref="divInput"
                            contenteditable="true">Cuvinte cheie, cautare booleana..</div>
                </div>

My methods/functions

            inputLength($event){
                this.input_length = $event.target.innerText.length;
                if(this.input_length == 0)
                    this.typed = false;
            },
            addPlaceholder(){
                if(this.input_length == 0 && this.typed == false){
                    this.$refs.divInput.innerHTML = 'Cuvinte cheie, cautare booleana..'
                }
            },
            clearPlaceholder(){
                if(this.input_length == 0 && this.typed == false){
                    this.$refs.divInput.innerHTML = '';
                }
            },
            updateBooleanInput($event){
                this.typed = true;
                this.inputLength($event);
            },
            saveCursorLocation($event){
                if($event.which != 8){
                    if(this.saved_sel)
                        rangy.removeMarkers(this.saved_sel)
                    this.saved_sel = rangy.saveSelection();
                }
                // if(this.input_length == 0 && this.typed == false){
                //  var div = this.$refs.divInput;
                //  var sel = rangy.getSelection();
                //  sel.collapse(div, 0);
                // }
            },
            insertNode: function(node){
                var selection = rangy.getSelection();
                var range = selection.getRangeAt(0);
                range.insertNode(node);
                range.setStartAfter(node);
                range.setEndAfter(node);
                selection.removeAllRanges();
                selection.addRange(range);
            },
            addBooleanTag($event){
                // return this.$refs.ChatInput.insertEmoji($event.img);
                this.$refs.divInput.focus();
                console.log(this.input_length);
                if(this.typed == false & this.input_length == 0){
                    this.$refs.divInput.innerHTML = ''
                    var space = '';
                    this.typed = true
                    this.saveCursorLocation($event);
                }
                rangy.restoreSelection(this.saved_sel);

                var node = document.createElement('img');
                node.src = $event.img;
                node.className = "boolean-button--img boolean-button--no-margin";
                node.addEventListener('click', (event) => {
                    // event.currentTarget.node.setAttribute('contenteditable','false');
                    this.$refs.divInput.removeChild(node);
                })
                this.insertNode(node);
                this.saveCursorLocation($event);
            },
            clearHtmlElem($event){
                var i = 0;
                var temp = $event.target.querySelectorAll("span, br");
                if(temp.length > 0){
                    for(i = 0; i < temp.length; i++){
                        if(!temp[i].classList.contains('rangySelectionBoundary')){
                            if (temp[i].tagName == "br"){
                                temp[i].parentNode.removeChild(temp[i]);
                            } else {
                                temp[i].outerHTML = temp[i].innerHTML;
                            }
                        }
                    }
                }
            },
            pasted($event){
                $event.preventDefault();
                var text = $event.clipboardData.getData('text/plain');
                this.insert(document.createTextNode(text));
                this.inputLength($event);
                this.typed == true;
            },
            insert(node){
                this.$refs.divInput.focus();
                this.insertNode(node);
                this.saveCursorLocation($event);
            },

As you can see in the saveCursorLocation(), I was trying to solve the scenario in which you click in the contentEditable div and there's no caret - which is confusing for the user.

                // if(this.input_length == 0 && this.typed == false){
                //  var div = this.$refs.divInput;
                //  var sel = rangy.getSelection();
                //  sel.collapse(div, 0);
                // }

It was a dead end - most likely because of my poor understanding of Rangy and how should I use those functions.

Expected behaviour vs actual results on Firefox

When I click on the contentEditable div I expect the caret to appear (while in the background to save my position). When typing, I expect the caret to appear after the last typed character while also on keyup to save my caret's position. Also I expect to be able to navigate the text via left/right arrows and see the caret when doing so.

All of these are generated by

v-on:click="..... saveCursorLocation($event);"

and

@keyup="saveCursorLocation($event);....."

If anybody believes that it would be helpful, I can record the content editable div and its behaviour in Firefox.

EDIT: I managed to isolate the problem and reproduce it into a JSFiddle - https://jsfiddle.net/Darkkz/6Landbj5/13.

What to look for?

  • Open the fiddle link in Firefox, then press one of the blue buttons (SI, SAU, NU) and then look at the input, the caret is not displayed.
  • Click the input, the caret is not displayed
  • While typing in the input,the caret is not displayed. Although, if you click in a word/in between content, the caret will be displayed
like image 477
Darkkz Avatar asked Oct 15 '22 12:10

Darkkz


1 Answers

Apparently rangy's Selection Save and Restore module can't be used to keep track of the current selection while the user interacts with a contenteditable, like you want.

I digged into it a bit, and the problem is that rangy inserts hidden <span>s as markers, and updates the selection to be after the marker, instead of keeping it inside the #text node the user's editing:

 <div contenteditable>
    #text [This is something I typed <!-- selection is moved from here -->] 
    <span class="rangySelectionBoundary"/>
    <!-- to here -->
 </div>

Firefox has trouble displaying the caret in this scenario (I haven't found a bug about this specific issue, but here's a similar one where the caret is not displayed when the selection is between two <span>s).

Commenting this code out seems to fix the issue with the disappearing caret. It's unclear to me why that code is needed -- it was added before 1.0 in a large commit with its message saying: "Fixes for save/restore problems with control ranges and multiple range selections. Added demos for save/restore and CSS class applier modules." -- so I'm not comfortable to suggest fixing this in rangy (and since it's unmaintained for a few years, I don't have much hope in getting its author's input on this).

So I tried to figure out why you needed this in the first place, to suggest other solutions not involving rangy.saveSelection (for example, rangy's Text Range module provides getSelection().saveCharacterRanges(containerNode) that works without modifying the DOM.

It appears that you have a <div contenteditable> and some "buttons" (<span>s), clicking on which would insert some HTML at the caret position. The problem you were trying to solve was that when the "buttons" were clicked, the selection moved from the contenteditable into the button, and you were unable to detect the insert position.

Instead of storing and restoring the selection, you can instead make the buttons user-select: none - this will keep the caret in the contenteditable.

To test this, I commented out all references to rangy.saveSelection and rangy.restoreSelection and changed the this.$refs.divInput.focus(); call in the "button"'s onclick handler to run only when the contenteditable wasn't already focused by wrapping it in an if (!this.$refs.divInput.contains(document.activeElement)). See how this works in this updated fiddle:
https://jsfiddle.net/fjxsgvm2/

like image 111
Nickolay Avatar answered Nov 13 '22 01:11

Nickolay