Search code examples
javascripthtmlvuejs2contenteditablerangy

Cannot delete HTML element in contentEditable parent if said element is the first child


The problem

I am building a custom boolean search input that should accept the boolean tags under the form of some "boolean tag bubbles" that can be added to the query via a click. Basically, instead of typing {AND} the user can click on the "AND" boolean tag and it is added to the input. Please see the picture attached to understand the layout - https://i.sstatic.net/dFaq8.png

LE:This behaviour appears to happen only on Chrome.

Regarding the problem: if a tag is added first in the input, (like in the layout example picture) and after adding the tag the types characters only to return before the tag and press backspace, the tag is not going to get deleted.

The tag can be removed via backspace only if the user deletes all the characters added - basically moving the caret at the end and then via backspace deleting everything.

What I have already tried

Initially I made a connection between the problem and all the spans that were generated when I was moving between with my "left/right" arrow keys between the characters toward the boolean tag. Therefore I wrote some code that every time I press a key, I scans the contentEditable parent and clears all the spans and brs created. This cleared some strange cases but I am still stuck with not being able to delete the tag if it's the first element and if there are characters or other elements after the boolean tag.

Several hours ago I found this - contenteditable div backspace and deleting text node problems. The function that inserts my boolean tag creates the tag as an element. I tried creating the node element as a as suggested in that post. Even as a button, if my element is the first element and it has characters after, it cannot be deleted.

Some of my code

For my current version with the boolean tags as elements, this is the method that creates, on click, my boolean tags and adds them to the parent element.

            addBooleanTag($event){
                this.$refs.divInput.focus();
                if(this.typed == false & this.input_length == 0){
                    this.$refs.divInput.innerHTML = ''
                    var space = '';
                    this.typed = true
                    this.saveCursorLocation();
                }
                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', () => {
                    this.$refs.divInput.removeChild(node);
                })
                this.insertNode(node);
                this.saveCursorLocation();
            },

This is how the contentEditable parent element looks like

                <div 
                        @keydown.enter.prevent
                        @blur="addPlaceholder"
                        @keyup="saveCursorLocation(); clearHtmlElem($event)"
                        @input="updateBooleanInput($event); clearHtmlElem($event)"
                        @paste="pasted"
                        v-on:click="clearPlaceholder(); saveCursorLocation(); deleteBooleanTag();"
                        class="input__boolean input__boolean--no-focus"
                        ref="divInput"
                        contenteditable="true">Boolean search..</div>

This is the method that clears my contentEditable parent of breaks and spans

            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;
                            }
                        }
                    }
                }
            },

Expected behaviour vs actual results

I expect that when I press backspace with the caret being after the boolean tag (img element) to delete the element. A normal behaviour. Instead, it does nothing. The only way to delete that added element/boolean tag is if the tag is the last thing to be deleted.

Please see this gif that I made while reproducing the problem. https://i.sstatic.net/lAOyV.jpg - when the console "freaks out" it's basically me pressing backspace against that img element/boolean tag and the element refusing to be deleted.


Solution

  • With some help, I managed to figure this out. I am posting this here for anyone who has problems with contentEditable.

    Do not style the contentEditable element. I had some style on the contentEditable div and that was messing a lot of stuff like caret positioning, backspace not working properly etc. Make a wrap around it and move the styling on that wrapper.

    LE: One (and probably the main) reason why the contentEditable element was misbehaving was due to having display: flex attached to it.