Make the Tab key Insert a tab character in a contentEditable div and not blur
Asked Answered
M

8

28

I'm building a simple text editor by setting contentEditable=true on a div (anyway i think that textarea behaves in the same way) and i have some problems with the tab key.

What i want is that when the user presses the tab key, the focus remain on the div and a tab character is added to the text.

I've solved the first part of the problem by calling preventDefault() on the event object on keydown and now the div doesn't lose the focus, but i don't know why i can't insert the character.

The entity code for the tab char is 	 but if i try to add this string to the innerHTML Firefox replace it with a simple space (not   just a white space). I also tried with \t but the result is the same.

So my question is, how can I insert a tab character in the text?

Molech answered 10/2, 2010 at 14:34 Comment(0)
C
8

The reason why you only see a simple space is because sequences of whitespace characters (except non-breaking spaces) are treated as a single space character inside divs. If you want the tab character to display in the way you expect you'll need to put it inside something like a <pre> element that renders whitespace as it is in the source HTML, or inside an element with white-space: pre set via CSS.

Crosspatch answered 10/2, 2010 at 14:42 Comment(3)
Additional html markup is not very good solution. CSS in the other answer works fine.Inhalation
@ujifgc: A <pre> element is a very good solution for rendering pre-formatted text. white-space: pre obviously works as well.Crosspatch
Sure, it's okay if you replace the div with it. It's not okay to bloat the markup with unnecessary elements if CSS works fine.Inhalation
A
36

I know this is a bit old, but this ended up working the best for me...

function onKeyDown(e) {
    if (e.keyCode === 9) { // tab key
        e.preventDefault();  // this will prevent us from tabbing out of the editor

        // now insert four non-breaking spaces for the tab key
        var editor = document.getElementById("editor");
        var doc = editor.ownerDocument.defaultView;
        var sel = doc.getSelection();
        var range = sel.getRangeAt(0);

        var tabNode = document.createTextNode("\u00a0\u00a0\u00a0\u00a0");
        range.insertNode(tabNode);

        range.setStartAfter(tabNode);
        range.setEndAfter(tabNode); 
        sel.removeAllRanges();
        sel.addRange(range);
    }
}
Alceste answered 20/8, 2015 at 21:9 Comment(4)
this works perfectly fine. but one prob, if i try to , press backspace, instead of deleting the whole tab \u00a0\u00a0\u00a0\u00a0 space it just deletes one \u00a0. how to work on that.Steffens
Thank you! I have spent three bloody hours on finding proper solution. Thanks, Michael!Hexavalent
Thanks Mate, I swapped out document.getElementById('editor') for e.target.Tourane
Better if you replace the tabNode with this ``` var tabNode = document.createElement('span'); tabNode.setAttribute('style', 'white-space:pre'); tabNode.innerText = "\t" // var tabNode = document.createTextNode("&emsp"); ```Jaworski
Y
20

Since the answer that was supposedly correct didn't work for me, I came up with this solution I think would work for more people.

$(document).on('keyup', '.editor', function(e){
  //detect 'tab' key
  if(e.keyCode == 9){
    //add tab
    document.execCommand('insertHTML', false, '&#009');
    //prevent focusing on next element
    e.preventDefault()   
  }
});

That code worked for me, but also make sure to include the white-space:pre-wrap attribute in your css. Hope this helped!

Yardmaster answered 23/3, 2016 at 2:30 Comment(3)
That's the single most elegant one! Great!Aerial
Worked like a charm! Except for I had to bind it on keydown instead of keyupCircumnavigate
Unfortunately does not work in Internet Explorer because inserHTML is not supported therePompei
F
11

tabs and spaces are collapsed in html to one space unless in a <pre> tag or has white-space: pre in its style

Forsooth answered 10/2, 2010 at 14:43 Comment(2)
Or white-space: pre-wrapMonterrey
Technically speaking, if you have "white-space:pre-wrap" written somewhere, it includes "white-space:pre" as a substring so the answer was correct :) Good point tho.Aerial
E
10

I use this solution:

$('[contenteditable]').on('keydown', function(e){
    if(e.keyCode == 9){
        e.preventDefault();
        document.execCommand('insertHTML', false, '&#009');
    }
}).css('white-space', 'pre-wrap');

When working with editable code, a <pre> element does not need the last added css. keyup vs keydown and the position of e.preventDefault() may differ in browsers.

Note: I think binding the key-event or whatever from the whole document is to be avoided. I had strange problems with Safari / iOS desktop browsers with contenteditable elements and bubbling events.

Emalee answered 26/9, 2017 at 3:9 Comment(0)
C
8

The reason why you only see a simple space is because sequences of whitespace characters (except non-breaking spaces) are treated as a single space character inside divs. If you want the tab character to display in the way you expect you'll need to put it inside something like a <pre> element that renders whitespace as it is in the source HTML, or inside an element with white-space: pre set via CSS.

Crosspatch answered 10/2, 2010 at 14:42 Comment(3)
Additional html markup is not very good solution. CSS in the other answer works fine.Inhalation
@ujifgc: A <pre> element is a very good solution for rendering pre-formatted text. white-space: pre obviously works as well.Crosspatch
Sure, it's okay if you replace the div with it. It's not okay to bloat the markup with unnecessary elements if CSS works fine.Inhalation
B
8

you can insert a span with white-space=pre and content tab. this is better done with ranges like this :

// adapted from https://mcmap.net/q/502920/-is-there-a-way-to-keep-execcommand-quot-inserthtml-quot-from-removing-attributes-in-chrome
function insertTab() {
  if (!window.getSelection) return;
  const sel = window.getSelection();
  if (!sel.rangeCount) return;
  const range = sel.getRangeAt(0);
  range.collapse(true);
  const span = document.createElement('span');
  span.appendChild(document.createTextNode('\t'));
  span.style.whiteSpace = 'pre';
  range.insertNode(span);
  // Move the caret immediately after the inserted span
  range.setStartAfter(span);
  range.collapse(true);
  sel.removeAllRanges();
  sel.addRange(range);
}

$(document).on('keydown', '.editor', function(e) {
  if (e.keyCode == 9) {
    insertTab();
    e.preventDefault()
  }
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div contenteditable=true style="border: solid" class='editor'></div>
Bethea answered 4/4, 2017 at 22:35 Comment(2)
The only solution that allows you to add TAB character anywhere inside the CONTENTEDITABLE and have it aligning texts without any unwanted side effects. Thx.Cindacindee
Just to add on from the answer, if you type between two words, the span tag width will act weirdly and change so in order to prevent it from happening, add span.style.display = 'inline-block'Lahr
B
8

Below is a working example which prohibits the loss of focus when clicking Tab while the contenteditable already has focus, and instead inserts a tab character.

var elm = document.querySelector('[contenteditable]')

// when the contenteditable gets focused, start listening to key presses
elm.addEventListener('focus', () => 
  window.addEventListener('keydown', onKeyDown)
)

// when the contenteditable looses focus, remove "keydown" event listener
elm.addEventListener('blur', () => 
  window.removeEventListener('keydown', onKeyDown)
)

function onKeyDown(e) {
   console.log(e.keyCode)
   if (e.keyCode != 9) return // only allow TAB key
    
    e.preventDefault();  // prevent default behaviour, which is "blur"

    var sel          = document.getSelection(),
        range        = sel.getRangeAt(0),
        tabNodeValue = '\u0009', // tab; with 4 spaces use: Array(4).join('\u00a0')
        tabNode      = document.createTextNode(tabNodeValue);

    range.insertNode(tabNode)
    range.setStartAfter(tabNode)
    range.setEndAfter(tabNode)
}
[contenteditable]{
  min-height: 150px;
  border: 1px solid black;
  padding: 5px;
  white-space: pre;  /* <-- needed in order to render "tab" unicode */
}
<div contenteditable></div>
Baiss answered 7/8, 2020 at 11:38 Comment(0)
M
0

Working with ShadowDOM

Hi all, I just wanted to bring up an issue that I ran into when trying to use this approach with modern vanilla JS webcomponents. Because of the encapsulation of the shadowDOM, the accepted approach will not work. However, this slightly modified method seems to work great! Hope this helps anyone who is stuck in the same place I was!

in your CSS make sure you have pre-wrap for the white-space property

CSS

.input-element {
    white-space: pre-wrap;
}

JS

this.inputElement = this.shadowRoot.querySelector('.input-element');

this.inputElement.addEventListener('keydown', function(event) {
      if (event.key === 'Tab') {
        event.preventDefault(); // Prevent default action of Tab key
    
        var tabNode = this.ownerDocument.createTextNode("\t");
    
        var sel = this.getRootNode().getSelection();
        if (!sel.rangeCount) return false;
        
        var range = sel.getRangeAt(0);
        range.insertNode(tabNode);
    
        var newRange = document.createRange();
        newRange.setStartAfter(tabNode);
        newRange.setEndAfter(tabNode); 
    
        sel.removeAllRanges();
        sel.addRange(newRange);
      }
    });
Musselman answered 15/7, 2023 at 1:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.