7

I have a DIV with ContentEditable set to TRUE. Inside it there are several spans with ContentEditable set to FALSE.

I trap the BackSpace key so that if the element under cursor is <span> I can delete it.

The problem is that it works alternately with odd spans only.

So, for example, with the html code below, put the cursor at the end of text in DIV, and press backspace all the way till the beginning of div. Observe that it will select/delete first span, then leave the second, then select/delete the third span, then leave the fourth and so on.

This behavior is only on Internet Explorer. It works exactly as expected on Firefox.

How should I make the behavior consistant in Internet Explorer?

Following html code can be used to reproduce the behavior:

var EditableDiv = document.getElementById('EditableDiv');

EditableDiv.onkeydown = function(event) {
  var ignoreKey;
  var key = event.keyCode || event.charCode;
  if (!window.getSelection) return;
  var selection = window.getSelection();
  var focusNode = selection.focusNode,
    anchorNode = selection.anchorNode;

  if (key == 8) { //backspace
    if (!selection.isCollapsed) {
      if (focusNode.nodeName == 'SPAN' || anchorNode.nodeName == 'SPAN') {
        anchorNode.parentNode.removeChild(anchorNode);
        ignoreKey = true;
      }
    } else if (anchorNode.previousSibling && anchorNode.previousSibling.nodeName == 'SPAN' && selection.anchorOffset <= 1) {
      SelectText(event, anchorNode.previousSibling);
      ignoreKey = true;
    }
  }
  if (ignoreKey) {
    var evt = event || window.event;
    if (evt.stopPropagation) evt.stopPropagation();
    evt.preventDefault();
    return false;
  }
}

function SelectText(event, element) {
  var range, selection;
  EditableDiv.focus();
  if (document.body.createTextRange && element.nodeName == 'SPAN') {
    range = document.body.createTextRange();
    range.moveToElementText(element);
    range.select();
  } else if (window.getSelection) {
    selection = window.getSelection();
    range = document.createRange();
    range.selectNodeContents(element);
    selection.removeAllRanges();
    selection.addRange(range);
  }
  var evt = (event) ? event : window.event;
  if (evt.stopPropagation) evt.stopPropagation();
  if (evt.cancelBubble != null) evt.cancelBubble = true;
  return false;
}
#EditableDiv {
  height: 75px;
  width: 500px;
  font-family: Consolas;
  font-size: 10pt;
  font-weight: normal;
  letter-spacing: 1px;
  background-color: white;
  overflow-y: scroll;
  overflow-x: hidden;
  border: 1px solid black;
  padding: 5px;
}
#EditableDiv span {
  color: brown;
  font-family: Verdana;
  font-size: 8.5pt;
  min-width: 10px;
  _width: 10px;
}
#EditableDiv p,
#EditableDiv br {
  display: inline;
}
<div id="EditableDiv" contenteditable="true">
  &nbsp;(<span contenteditable='false' onclick='SelectText(event, this);' unselectable='on'>Field1</span> < 500) <span contenteditable='false' onclick='SelectText(event, this);' unselectable='on'>OR</span> (<span contenteditable='false' onclick='SelectText(event, this);' unselectable='on'>Field2</span> > 100 <span contenteditable='false' onclick='SelectText(event, this);' unselectable='on'>AND</span> (<span contenteditable='false' onclick='SelectText(event, this);'
    unselectable='on'>Field3</span> <=200) ) 
</div>

EDIT

Just FYI. I have asked this question in MSDN Forum as well.

Pradeep Kumar
  • 6,836
  • 4
  • 21
  • 47
  • @ user4749485, Thanks so much for your efforts. Tried the demo. It works good if you start backspacing from the last but removes characters from the right when starting somewhere from in between. Can you please look into it. I believe it can be resolved. :) – Pradeep Kumar Jul 16 '15 at 07:55
  • Had you refered to http://stackoverflow.com/questions/14615551/html-contenteditable-with-non-editable-islands .. Please let me know – Jaffer Wilson Jul 17 '15 at 05:32
  • @JafferWilson, yes I had searched a lot on this influencing that link you posted. It didn't help. Is there anything specific there you want me to look into? – Pradeep Kumar Jul 17 '15 at 12:24
  • @user4749485, i can't see a difference between your earlier demo and the new one! the content after cursor position still gets deleted. e.g. place the cursor just before `100` and start backspacing. Notice that as soon as `Field2` is deleted, `100` also gets deleted. – Pradeep Kumar Jul 17 '15 at 12:27
  • I tried to dwell deeper into the problem and the findings are here: [MSDN Forums Thread](https://social.msdn.microsoft.com/Forums/ie/en-US/56a938be-2398-4791-9281-2410178aabb9/selection-and-deletion-problems-with-noneditable-span-inside-contenteditable-div?forum=iewebdevelopment). This might help. – Pradeep Kumar Jul 17 '15 at 12:29
  • yes, I know about the bug. That's why you see the check for `nodeName` everywhere in my code which makes is all the more messier. :) The demo is working better now but still with one problem. If I put the cursor just before `200` and start backspacing, the first `AND` is left out. Can you please look into that? – Pradeep Kumar Jul 17 '15 at 13:52
  • wow! this seems to be working prefect. Please post this as an answer and I will award you the bounty. (cant award bounty in comments). Thanks. :) – Pradeep Kumar Jul 20 '15 at 10:42

1 Answers1

8

The challenge to this is to get IE11 to backspace from the right directly against the <span>.  Then the next backspace will select and highlight it.  This seems like such a simple objective, but IE11 just won't cooperate.  There should be a quick easy patch, right?  And so the bugs begin.

The approach I came up with is to walk the tree backwards to the first previous non-empty node, clearing the empty nodes between to appease IE, and then evaluate a few conditions.  If the caret should end up at the right side of the <span>, then do it manually (because IE won't) by creating a new range obj with the selection there at the end of the <span>.

online demo


I added an additional kludge for IE in the case that two spans are dragged against eachother.  For example, Field2Field3.  When you then backspace from the right onto Field3, then backspace once again to delete it, IE would jump the caret leftward over Field2.  Skip right over Field2.  grrr.  The workaround is to intercept that and insert a space between the pair of spans.  I wasn't confident you'd be happy with that.  But, you know, it's a workaround.  Anyway, that turned-up yet another bug, where IE changes the inserted space into two empty textnodes.  more grrr.  And so a workaround for the workaround.  See the non-isCollapsed code. 

CODE SNIPPET

var EditableDiv = document.getElementById('EditableDiv');

       EditableDiv.onkeydown = function(event) {
         var ignoreKey;
         var key = event.keyCode || event.charCode;
         if (!window.getSelection) return;
         var selection = window.getSelection();
         var focusNode = selection.focusNode,
           anchorNode = selection.anchorNode;

         var anchorOffset = selection.anchorOffset;

         if (!anchorNode) return

         if (anchorNode.nodeName.toLowerCase() != '#text') {
           if (anchorOffset < anchorNode.childNodes.length)
             anchorNode = anchorNode.childNodes[anchorOffset]
           else {
             while (!anchorNode.nextSibling) anchorNode = anchorNode.parentNode // this might step out of EditableDiv to "justincase" comment node
             anchorNode = anchorNode.nextSibling
           }
           anchorOffset = 0
         }

         function backseek() {

           while ((anchorOffset == 0) && (anchorNode != EditableDiv)) {

             if (anchorNode.previousSibling) {
               if (anchorNode.previousSibling.nodeName.toLowerCase() == '#text') {
                 if (anchorNode.previousSibling.nodeValue.length == 0)
                   anchorNode.parentNode.removeChild(anchorNode.previousSibling)
                 else {
                   anchorNode = anchorNode.previousSibling
                   anchorOffset = anchorNode.nodeValue.length
                 }
               } else if ((anchorNode.previousSibling.offsetWidth == 0) && (anchorNode.previousSibling.offsetHeight == 0))
                 anchorNode.parentNode.removeChild(anchorNode.previousSibling)

               else {
                 anchorNode = anchorNode.previousSibling

                 while ((anchorNode.lastChild) && (anchorNode.nodeName.toUpperCase() != 'SPAN')) {

                   if ((anchorNode.lastChild.offsetWidth == 0) && (anchorNode.lastChild.offsetHeight == 0))
                     anchorNode.removeChild(anchorNode.lastChild)

                   else if (anchorNode.lastChild.nodeName.toLowerCase() != '#text')
                     anchorNode = anchorNode.lastChild

                   else if (anchorNode.lastChild.nodeValue.length == 0)
                     anchorNode.removeChild(anchorNode.lastChild)

                   else {
                     anchorNode = anchorNode.lastChild
                     anchorOffset = anchorNode.nodeValue.length
                       //break       //don't need to break, textnode has no children
                   }
                 }
                 break
               }
             } else
               while (((anchorNode = anchorNode.parentNode) != EditableDiv) && !anchorNode.previousSibling) {}
           }
         }

         if (key == 8) { //backspace
           if (!selection.isCollapsed) {

             try {
               document.createElement("select").size = -1
             } catch (e) { //kludge for IE when 2+ SPANs are back-to-back adjacent

               if (anchorNode.nodeName.toUpperCase() == 'SPAN') {
                 backseek()
                 if (anchorNode.nodeName.toUpperCase() == 'SPAN') {
                   var k = document.createTextNode(" ") // doesn't work here between two spans.  IE makes TWO EMPTY textnodes instead !
                   anchorNode.parentNode.insertBefore(k, anchorNode) // this works
                   anchorNode.parentNode.insertBefore(anchorNode, k) // simulate "insertAfter"
                 }
               }
             }


           } else {
             backseek()

             if (anchorNode == EditableDiv)
               ignoreKey = true

             else if (anchorNode.nodeName.toUpperCase() == 'SPAN') {
               SelectText(event, anchorNode)
               ignoreKey = true
             } else if ((anchorNode.nodeName.toLowerCase() == '#text') && (anchorOffset <= 1)) {

               var prev, anchorNodeSave = anchorNode,
                 anchorOffsetSave = anchorOffset
               anchorOffset = 0
               backseek()
               if (anchorNode.nodeName.toUpperCase() == 'SPAN') prev = anchorNode
               anchorNode = anchorNodeSave
               anchorOffset = anchorOffsetSave

               if (prev) {
                 if (anchorOffset == 0)
                   SelectEvent(prev)

                 else {
                   var r = document.createRange()
                   selection.removeAllRanges()

                   if (anchorNode.nodeValue.length > 1) {
                     r.setStart(anchorNode, 0)
                     selection.addRange(r)
                     anchorNode.deleteData(0, 1) 
                   } 
                   else {
                     for (var i = 0, p = prev.parentNode; true; i++)
                       if (p.childNodes[i] == prev) break
                     r.setStart(p, ++i)
                     selection.addRange(r)
                     anchorNode.parentNode.removeChild(anchorNode)
                   }
                 }
                 ignoreKey = true
               }
             }
           }
         }
         if (ignoreKey) {
           var evt = event || window.event;
           if (evt.stopPropagation) evt.stopPropagation();
           evt.preventDefault();
           return false;
         }
       }

       function SelectText(event, element) {
         var range, selection;
         EditableDiv.focus();
         if (window.getSelection) {
           selection = window.getSelection();
           range = document.createRange();
           range.selectNode(element)
           selection.removeAllRanges();
           selection.addRange(range);
         } else {
           range = document.body.createTextRange();
           range.moveToElementText(element);
           range.select();
         }
         var evt = (event) ? event : window.event;
         if (evt.stopPropagation) evt.stopPropagation();
         if (evt.cancelBubble != null) evt.cancelBubble = true;
         return false;
       }
#EditableDiv {
          height: 75px;
          width: 500px;
          font-family: Consolas;
          font-size: 10pt;
          font-weight: normal;
          letter-spacing: 1px;
          background-color: white;
          overflow-y: scroll;
          overflow-x: hidden;
          border: 1px solid black;
          padding: 5px;
        }
        #EditableDiv span {
          color: brown;
          font-family: Verdana;
          font-size: 8.5pt;
          min-width: 10px;
          /*_width: 10px;*/
          /* what is this? */
        }
        #EditableDiv p,
        #EditableDiv br {
          display: inline;
        }
<div id="EditableDiv" contenteditable="true">
&nbsp;(<span contenteditable='false' onclick='SelectText(event, this);' unselectable='on'>Field1</span> < 500)  <span contenteditable='false' onclick='SelectText(event, this);' unselectable='on'>OR</span> (<span contenteditable='false' onclick='SelectText(event, this);' unselectable='on'>Field2</span> > 100 <span contenteditable='false' onclick='SelectText(event, this);' unselectable='on'>AND</span> (<span contenteditable='false' onclick='SelectText(event, this);' unselectable='on'>Field3</span> <= 200) )
</div>
Pradeep Kumar
  • 6,836
  • 4
  • 21
  • 47
user4749485
  • 1,028
  • 7
  • 10
  • 2
    ++ Also Added another 100 points as bounty :) Good answer@user4749485. Will only be able to award it in the next 23 hours though :) – Siddharth Rout Jul 28 '15 at 10:22
  • Does this code works in mobile devices. I have tried in Android chrome mobile and this does not works there. Trying to buid an app of such type using meteor js . Is there a way to make this work on mobile devices also. – Lucky Sep 20 '17 at 19:41
  • You are a legend – Tim Jun 13 '18 at 09:59
  • I can't see *SelectEvent* function definition !! – kalki Jul 03 '18 at 07:04