25

Scenario

I have a contenteditable <div> area, and within this area I may have some <span contenteditable="false"></span> containing some text. The idea is, these span elements will represent styled text that can not be edited, but may be deleted from the <div contenteditable="true"></div> area by pressing the backspace key.

Issue

The cursor placement is the big issue here. if you delete one of these <span> elements, the cursor jumps to the end of the <div>. More interesting, if you type some text while the cursor is "at the end," the text placement is just fine... Then, if you delete the newly typed text, the cursor jumps back!

I have prepared a fiddle which will demonstrate this. I need this to work only in Chrome, and other browsers are either of non-concern for now or have workarounds in place. (Also note the prepared Fiddle is crafted to demonstrate this in Chrome only).

Fiddle


Fiddle Instruction: Chrome Version 39.0.2171.95 m (64-bit) reproduced in 32-bit as well

  1. Click into <div> area
  2. Type "123"
  3. Backspace "3" Backspace "2" Backspace "1" enter image description here

Related Details

Researching this extensively, I have come across various SO question that are similar, but borrowing the associated solutions has not proved to be the silver bullet I am after. I have also found issues for the Chrome project which seem to target (perhaps not in the exact manner) the issue described above, and can be viewed below.

  1. Issue 384357: Caret position inside contenteditable region with uneditable nodes
  2. Issue 385003: Insert caret style is wrong when reaching the end of a line in a contenteditable element
  3. Issue 71598: Caret in wrong position after non-editable element at the end of contentEditable

The closest SO solution I have found can be here. The idea in this solution is to place &zwnj; characters after the <span> elements, but if I want to now delete my <span>, I instead delete the &zwnj;... forcing my cursor to jump to the end, offering a weird UI experience by not deleting my <span> on my "initial delete key stroke."

Question

Has anyone experienced this issue and found a work around? I welcome any possible solution, even as JS hacky as they come. I've come to learn that leveraging contenteditable comes with a laundry list of struggle, but this seems to be the last remaining difficulty I currently have.

vsync
  • 118,978
  • 58
  • 307
  • 400
scniro
  • 16,844
  • 8
  • 62
  • 106
  • What version of Chrome are you using? I'm using Chrome 39 and cannot reproduce the issue. When I delete any of the non-contenteditable `span`s, the caret position remains the same. – Alvaro Montoro Jan 07 '15 at 16:43
  • @AlvaroMontoro I have added some detailed fiddle instruction with my Chrome version. The issue is for text following the ``. Please let me know if these details help – scniro Jan 07 '15 at 16:54
  • Chrome 39.0.2171.95 (Linux 64-bit) - can't see any such glitch – MightyPork Jan 07 '15 at 16:58
  • @MightyPork are you able to run the example on a Windows Machine? – scniro Jan 07 '15 at 16:59
  • Sorry, I don't have Windows. – MightyPork Jan 07 '15 at 17:00
  • After following that new example, I can see it too. If you check the developer tools, it keeps adding `""`, instead of "writing in the same string", it looks like it creates a new one every time the user writes. It's weird. – Alvaro Montoro Jan 07 '15 at 17:03
  • @AlvaroMontoro Right!! It's weird too, if you move around with left and right and keep account of where you "should" be, and type.. it's in the right spot, but cursor seems to "lag a move behind" in this case. I have been working on a hacky workaround I will post later in case you are interested to see a possible solution for this – scniro Jan 07 '15 at 17:06
  • Are you trying to recreate the effect that Twitter has when you @ someone in the tweet? – bitten Jan 13 '15 at 20:13
  • @AaronHarding Something very similar. I have the solution working how I wish, except for this minor issue in Chrome. Have you done any similar work? – scniro Jan 13 '15 at 20:15
  • Yeah, I tried to recreate it myself and found a lot of inconsistencies. Nothing like yours though. Have you had a read through this: https://github.com/guardian/scribe/blob/master/BROWSERINCONSISTENCIES.md ? Perhaps this may help you. – bitten Jan 13 '15 at 20:34
  • Still not an answer, just an interesting behaviour. If you add a pseudo element to the spans with a space as it's content, you see that the last span doesn't get assigned one until there are some characters in front of it. http://jsfiddle.net/41mo9xxd/11/ perhaps not useful.. just interesting? – bitten Jan 14 '15 at 21:54
  • Seems **not reproducible** any more, although I get **this bug** with a *slightly similar* setup (Mac/Chrome 81) – vsync Sep 16 '20 at 16:50

8 Answers8

21

I don't know why this happens, but I had the feeling it has something to do with the sizing of the <div>, so I tried playing with the display property. After setting it to inline-block and playing a little with the text I found that the issue is gone after I make some edits to it, specifically adding a new line.

I saw that, for some reason, the <br/> tag is kept in div.main after I delete my new line, but the appearance of the <div> and the way it responds to arrow keys is the same as if there is no new line in it.

So I restarted the fiddle with the CSS change and a <br/> tag in div.main and viola!

So to conclude:

  1. Add display: inline-block to div.main
  2. add a <br/> at the end of div.main

JSFiddle Link

scniro
  • 16,844
  • 8
  • 62
  • 106
dutzi
  • 1,880
  • 1
  • 18
  • 23
  • pressing "right" when cursor is in between elements is completely busted, as is "up/down" when multiline – Lincoln B Feb 27 '18 at 22:48
5

The problem is that your contenteditable element is a div, which by default is display: block. This is what causes your caret position problem. This can be fixed by making the outermost div non-editable and adding a new editable div that will be treated as inline-block.

The new HTML would have a new div just inside the outer one (and the corresponding closing tag at the end):

<div id="main" class="main"><div id="editable" contenteditable="true">...

And add this to your CSS:

div#editable {
    display: inline-block;
}

For the sake of seeing the caret better when it is between span elements, I've also added margin: 2px to the rule for div.main span in the CSS but this is not necessary to prevent the caret jumping issue reported in the question.

Here is a fiddle.

As you've started discovering, contenteditable is handled inconsistently across browsers. A few years back, I started working on a project (in-browser XML editor) where I thought contenteditable would make everything easier. Then as I developed the application, I soon found myself taking over the functions that I thought contenteditable would give me for free. Today, the only thing contenteditable give me is that it turns on keyboard events on elements I want to edit. Everything else, including caret movement and caret display, is managed by custom code.

Louis
  • 146,715
  • 28
  • 274
  • 320
  • While checking out this fiddle, I am still experiencing cursor issue. When I delete all text in front of a `` element, it disappears. Chrome Version `39.0.2171.95 m` – scniro Jan 13 '15 at 20:17
  • Yes, @Louis your caret still disappears. – bitten Jan 13 '15 at 20:31
  • Ok, so I've edited my answer. Chrome still insists on making the caret non-blinking if it is right before a `span` but I don't know of a pure CSS solution for that. – Louis Jan 13 '15 at 20:42
  • 1
    The core concept behind your answer (add `display: inline-block` to the contenteditable div) solves my problem. I spent a lot of time searching for a solution. Thank you so much. – Melo Nov 12 '18 at 07:47
1

So, I've got a slightly cleaner implementation of your approach, which relies on the input event that is triggered when a contenteditable field is updated. This event is not supported by IE, but it looks like contenteditable is pretty wonky in IE anyways.

It basically injects elements around every element marked as contenteditable="false". It could probably be done on initial load rather than just on the input event, but I figured you might have ways of injecting more span's to the region, so input should account for this.

Limitations include some weird behavior surrounding the arrow keys, as you've seen yourself. Mostly what I've seen is that the cursor maintains its proper position when you move backward through the spans, but not when you move forward.

JSFiddle Link

Javascript

$('#main').on('input', function() {
    var first = true;
    $(this).contents().each(function(){
        if ($(this).is('[contenteditable=false]')) {
            $("<i class='wrapper'/>").insertAfter(this);
            if (first) {
                $("<i class='wrapper'/>").insertBefore(this);
                first = false   
            }
        }
    });
}).trigger('input');

CSS

div.main {
    width: 400px;
    height: 250px;
    border: solid 1px black;
}

div.main span {
    width:40px;
    background-color: red;
    border-radius: 5px;
    color: white;
    cursor: pointer;
}

i.wrapper {
    font-style: normal;
}
cmptrgeekken
  • 8,052
  • 3
  • 29
  • 35
  • Thanks for taking the time to take a crack at this!! Few things, I am not sure which you suggest works best between your code snippet or the fiddle link (fiddle has different JS code and I do not notice any fiddle updates). I also tried the wrapping of ``'s entirely, but it's weird.. only the last one seems to matter. If you run my fiddle with just the `` on the end, then go in between some of the other ``'s and try entering/deleting text, all is fine. For your code snippet, I ran the fiddle and pasted that in, and the wrapping is reoccurring on each `input` – scniro Jan 08 '15 at 16:32
  • However, +1 for making an improvement on the arrow keys. Mine were messed up going back and forth, at least you nailed going back. If only going forward could work – scniro Jan 08 '15 at 16:33
1

In my case also, when I was trying to edit content editable span placed inside div, It was giving the same problem ,But when appended \n after content still problem occurred if we tried to erase last digit so I appended "\n" before and after content like and now it is working fine,thanks to @Mati Horovitz.

var html = `<div id="main" contenteditable="true">' \n ${content} \n </div>`

For Angular people, you can do

<span  (input)="saveContent($event.target.innerHTML)" [attr.contenteditable]="isEditable">{{'\n'+content+'\n'}}</span >

enter image description here

Pranav HS
  • 53
  • 7
1

I know this might be an old thread, but I've faced the same issue and unfortunately none of the above solutions proved to be ideal.

What I did is utilising the mutation observer to listen whenever an intermediate text node (space separating the spans) gets deleted, once it does I check the previous sibling, if it's a span, removing it. By doing this the cursor doesn't reaches the span so there will be no focus or weird cursor movements to the far right:

const observer = new MutationObserver((mutationsList) => {
    for (const mutation of mutationsList) {
      const {
        previousSibling,
        removedNodes,
        type
      } = mutation;

      if (type === 'childList') {
        if (removedNodes.length) {
            const [removed] = removedNodes;

            if (removed) {
              if (previousSibling.nodeName === 'SPAN') {
                previousSibling.remove();
              }
            }
        }
      }
    }
  });
  observer.observe(this.input, {childList: true, subtree: true});
Edmond Tamas
  • 3,148
  • 9
  • 44
  • 89
0

I have found a hacky way to solve the initial problem of the cursor misbehaving at the end of the <div> with some JQuery. I am basically inserting an empty <i> for the cursor to "latch" on to at the end of the <div> contents. This tag works for me because there is no UI difference to the end user when backspacing these from normal text due to overriding font-style: normal (see fiddle), and I also needed something different than <span> to insert

I am not particularly thrilled with this workaround, especially choosing an <i> just because, but I have not come across a better alternative, and I still welcome better solutions- if any exist!. I plan to keep working at this to hopefully figure out the arrow keys as well, but luckily I am only bothered by this glitch in the Fiddle and not my code.


Fiddle (Chrome only)

<div id="main" contenteditable="true">
    <span contenteditable="false">item</span>
    <span contenteditable="false">item</span>
    <span contenteditable="false">item</span>
</div>

function hackCursor() {    
    return $('#main :last-child').get(0).tagName === 'SPAN' ? 
        $('#main span:last-child').after('<i style="font-style: normal"></i>') : 
        false;
}

$(function(){

    hackCursor();

    $('#main').bind({
        'keyup': function(e) {
            hackCursor();
        }
    })
});
scniro
  • 16,844
  • 8
  • 62
  • 106
0

Just add a new line char (\n) before closing the contenteditable tag. For example in JavaScript: var html = '<div id="main" contenteditable="true">' + content + "\n" + '</div>'

Mati Horovitz
  • 131
  • 1
  • 5
0

The problem is evident in the below example and can be remedied by changing the contentetiable wrapper to display:block, but that causes a very annoying Chrome bug - <div><br></div> will be added when ENTER key is pressed

[contenteditable="true"]{
  border: 1px dotted red;
  box-sizing: border-box;
  width: 100%;
  padding: 5px;
  outline: none;
  display: inline-block; /* prevents Chrome from adding <div><br></div> when pressing ENTER key. highly undesirable. */
}

[contenteditable="false"]{
  background: lightgreen;
  padding: 3px;
  border-radius: 3px;
}
<div contenteditable="true">
    <span contenteditable="false">item</span>
    <span contenteditable="false">item</span>
    <span contenteditable="false">item</span>
</div>

Lets try to fix it:

I can see the problem with the combination of width being wider than the content and also of display: inline-block.

I also do not want to use javascript if there's any possibility for a CSS fix.

I would very much like to keep display: inline-block to prevent the mentioned above Chrome bug from happening, so it would seem a thing can be done with the width issue, by setting it to auto when the contenteditable gets focused, and wrapping everything with an element that will allow to fake the same width as before using a pseudo-element inside the contenteditable (only when focused)

.wrapper{
  width: 100%;
  position: relative;
}

[contenteditable="true"]{
  border: 1px solid red;
  box-sizing: border-box;
  width: 100%;
  padding: 5px;
  outline: none;
  display: inline-block;
}

[contenteditable="true"]:focus{
  border-color: transparent;
  width: auto;
}

[contenteditable="true"]:focus::before{
  content: '';
  border: 1px solid red; /* same as "real" border */
  position: absolute;
  top:0; right:0; bottom:0; left:0;
}

[contenteditable="false"]{
  background: lightgreen;
  padding: 3px;
  border-radius: 3px;
}
<div class='wrapper'>
  <div contenteditable="true">
      <span contenteditable="false">item</span>
      <span contenteditable="false">item</span>
      <span contenteditable="false">item</span>
  </div>
<div>

The above solution is good for some situations, but not when the lines wrap and on the last line the last element is a node:

.wrapper{
  width: 100%;
  position: relative;
}

[contenteditable="true"]{
  border: 1px solid red;
  box-sizing: border-box;
  width: 100%;
  padding: 5px;
  outline: none;
  display: inline-block;
}

[contenteditable="true"]:focus{
  border-color: transparent;
  width: auto;
}

[contenteditable="true"]:focus::before{
  content: '';
  border: 1px solid red; /* same as "real" border */
  position: absolute;
  top:0; right:0; bottom:0; left:0;
}

[contenteditable="false"]{
  background: lightgreen;
  padding: 3px;
  border-radius: 3px;
}
<div class='wrapper'>
  <div contenteditable="true">
      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
      <span contenteditable="false">item</span>
      <span contenteditable="false">item</span>
      <span contenteditable="false">item</span>
  </div>
<div>

Lets just use javascript and place a br element at the end, if one is not there (it cannot be removed by select-all (CTRLA) and deleting the content):

var elem = document.body.firstElementChild;

if( !elem.lastChild || elem.lastChild.tagName != 'BR' )
    elem.insertAdjacentHTML('beforeend', '<br>')
[contenteditable="true"]{
  border: 1px solid red;
  box-sizing: border-box;
  width: 100%;
  padding: 5px;
  outline: none;
  display: inline-block;
}

[contenteditable="false"]{
  background: lightgreen;
  padding: 3px;
  border-radius: 3px;
}
<div contenteditable="true">
      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
      <span contenteditable="false">item</span>
      <span contenteditable="false">item</span>
      <span contenteditable="false">item</span>
</div>
vsync
  • 118,978
  • 58
  • 307
  • 400