Search code examples
javascriptcsscontenteditable

How to prevent creating new text nodes after 'Enter' press in contenteditable element?


Is it possible using some CSS properties or HTML attributes or JS solution prevent splitting text node?

I don't want to call div.normalize() after each 'Enter' press And please don't suggest using textarea

🌀 I need to get a caret position relative to the start of text, but when I press Enter, browser creates each time a new text node and after that the Selection.anchorOffset returns the offset in the new created node. But I want to have the ( ONLY ONE TEXT NODE ) and easy get the caret position

<!DOCTYPE html>Caret:<a id=Caret>0</a> Nodes:<a id=Nodes>1</a>
<div id=div contenteditable='plaintext-only'>Prevent Creating new textNodes</div>
<style>
  #Caret { color: red  }
  #Nodes { color: blue }
  div {
    padding: 10px;
    height: 5lh;
    outline: 1px solid;
    overflow-y: auto;
    overscroll-behavior: contain
  }
</style>
<script>  'use strict';
const sel=getSelection();

div.onkeyup=div.onpointerup=()=>{
  Caret.text=sel.anchorOffset
  Nodes.text=div.childNodes.length
}

</script>

Also I don't want to manually process 'Enter' press in keydown event via event.preventDefault()

I am search a simple solution

UPDATE:

I am going to highlight via CSS Custom Highlight API CSS Highlight. One div, one textNode ( -> easy get caret and create highlight ranges ). Wrapping to SPAN or something is not needed already. Just One textnode and ranges to highlight

-> My Question: PREVENT CREATING NEW TEXTNODES with ENTER to have ONLY ONE TEXTNODE

Thank You!


Solution

  • This solution may not be perfectly match with your very narrow requirements where you stated please don't suggest using textarea and Also I don't want to manually process 'Enter' press in keydown event via event.preventDefault().

    But you also said: I am search a simple solution and I can't see any easier solution than this...

    Using a pre element insteads of div so that the newline will be treated as is and intercepting the Enter key hit preventing the browser's default behaviour that would add a new child text node instead of appending the new content to the unique node.

    This solution will just show a content editable element with a fixed height given by its style. If you type new content in there you won't see the node count increase even when you add new lines.

    'use strict';
    
    //here I'm fetching the elements explicitely instead of using window scope
    const div = document.getElementById('div');
    const Caret = document.getElementById('Caret');
    const Nodes = document.getElementById('Nodes');
    
    div.addEventListener('keydown', function(event) {
      if (event.key === 'Enter') {
    
        //prevent the default behaviour (where the browser would add a new text node)
        event.preventDefault();
    
        //determine current caret position and current target content
        const selection = window.getSelection();
        const caretPos = selection.anchorOffset;
        const textContent = div.textContent;
    
        //check if caret is at the end of the content..
        //this is needed because at the end of the content it requires a special behaviour for newline to show up and this condition will be checked when adding the \n
        const atEnd = caretPos === textContent.length;
    
        //replace the content adding a new line at the caret position
        const newTextContent =
          textContent.slice(0, caretPos)
          + '\n' + (atEnd ? '\n' : '') + textContent.slice(caretPos);
        div.textContent = newTextContent;
    
        //move the caret after the newline
        setCaretPosition(div, caretPos + 1);
    
        updateInfo();
      }
    });
    
    //move the caret at the new position
    function setCaretPosition(elem, pos) {
      const textNode = elem.firstChild;
      const range = document.createRange();
      const sel = window.getSelection();
      range.setStart(textNode, pos);
      range.collapse(true);
      sel.removeAllRanges();
      sel.addRange(range);
    }
    
    function updateInfo() {
      setTimeout(() => {
        const selection = window.getSelection();
        Caret.textContent = selection.anchorOffset;
        Nodes.textContent = div.childNodes.length;
      }, 0);
    }
    
    div.addEventListener('keyup', updateInfo);
    div.addEventListener('click', updateInfo);
    div.addEventListener('input', updateInfo);
    
    div.focus();
    #Caret {
      color: red
    }
    
    #Nodes {
      color: blue
    }
    
    #div {
      padding: 10px;
       height: 5lh;
      outline: 1px solid;
      overflow-y: auto;  
    }
    <!DOCTYPE html>
    Caret:<a id="Caret">0</a> Nodes:<a id="Nodes">1</a>
    <pre id="div" contenteditable="true">Prevent Creating new textNodes</pre>