5

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
Community
  • 1
  • 1
Darkkz
  • 384
  • 4
  • 21
  • 1
    I wanted to take a look, but without https://stackoverflow.com/help/mcve I can't. – Nickolay Aug 11 '19 at 05:56
  • @Nickolay I will do my best composing a fiddle today and update the initial post. – Darkkz Aug 12 '19 at 08:45
  • 1
    @Nickolay I managed to reproduce the problem in a fiddle. I have updated the intiial post, you can find the link at the end. Thank you – Darkkz Aug 15 '19 at 10:03

1 Answers1

3

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/

Nickolay
  • 31,095
  • 13
  • 107
  • 185
  • This is such a big improvement and in such an innovative way. I cannot thank you enough. I tested on multiple browsers and it works as intended. The only problem I found was on Safari, that I cannot insert a tag in the middle of a word or between two words. Here is a gif - https://i.imgur.com/wvqh29u.mp4. Those tags are actually boolean tags thus it's important that a user can insert between two words. How should I handle this edge case? I was thinking about checking the browser and if it's safari go for the rangy method and if it's chrome/firefox go for the method you provided. – Darkkz Aug 15 '19 at 20:17
  • My Safari fails to open jsfiddle at all, so you'll need to debug this for yourself - what's the `Selection` like in `addBooleanTag`? Does `user-select` work? It should - with the prefix, what's the Safari version? – Nickolay Aug 16 '19 at 05:11
  • Whether I have "user-select: none;" (or "-webkit-user-select: none;") nothing changes in Safari, it behaves the same - when clicking the tag, it does not respect the selection position and adds the tag at the beginning of the input. This leads me to believe that user select none does nothing on Safari. I am using your code/fiddle as a base, so we are on the same page. – Darkkz Aug 16 '19 at 19:06
  • You should look at the `console.log` output in my fiddle - does it show that the selection is "in" the button? user-select is supposed to be supported in Safari with a prefix (which I didn't include in my fiddle)... Given that I can't test in safari, perhaps a separate question would give you more visibility? – Nickolay Aug 16 '19 at 19:17
  • There is no selection in the console in Safari. There is no console log to be more precise, either the line does not run (I doubt) or it has nothing to output (most likely). Apparently, even with the -webkit-, user-select none behaves differently in Safari. – Darkkz Aug 19 '19 at 08:49
  • I'm talking about this: `console.log(getSelection().anchorNode, getSelection().anchorOffset, getSelection().focusNode, getSelection().focusOffset)` - it has to output _something_. – Nickolay Aug 19 '19 at 12:01
  • Safari keeps resetting that console on errors only. Indeed, that log returns something. It's the same in Firefox and Chrome but different, obviously, in Safari. Here's a side by side comparison - https://i.imgur.com/Z9OQemV.png. I changed my console logs a bit, basically, label is up, the value related to the label is below. I typed Foobar, clicked between foo and bar and inserted a tag. – Darkkz Aug 19 '19 at 14:23
  • Ah, sorry: you need to move the log to the top of the handler; it looks like the selection is already changed by the time it runs. – Nickolay Aug 19 '19 at 15:28
  • Yes, moving the log makes it display a different result but that does not fix the problem. As far as things go, getSelection() sees the position/offset at the beginning when the div loses focus... I have made a new question in the meantime - https://stackoverflow.com/questions/57553850/user-select-none-behaves-differently-in-safari – Darkkz Aug 19 '19 at 19:55
  • I was curious about the value logged, as it would clarify whether `user-select` was indeed not working in Safari. If it doesn't I'd see if the selection was available `onblur` and [use rangy's "Text Range" to save/restore it](https://stackoverflow.com/a/57546051/1026). – Nickolay Aug 19 '19 at 20:35
  • But won't rangy's text range get me into the same problems I was trying to avoid on Firefox? A solution that I was thinking, tho' not the most elegant would be.. using a browser identifier and check if it's Safari, keep your solution for chrome and firefox and use my previous rangy implementation only on Safari? – Darkkz Aug 19 '19 at 20:57
  • The "text range" module uses a different method that doesn't modify the DOM, so it won't have the bad effect on Firefox you've observed. – Nickolay Aug 19 '19 at 21:39