51

I am using contenteditable div elements in my web application and I am trying to come up with a solution to limit the amount of characters allowed in the area, and once the limit is hit, attempting to enter characters simply does nothing.

This is what I have so far:

var content_id = 'editable_div';

// Binding keyup/down events on the contenteditable div
$('#' + content_id).keyup(function(){ check_charcount(content_id, max); });
$('#' + content_id).keydown(function(){ check_charcount(content_id, max); });

function check_charcount(content_id, max)
{
    if($('#' + content_id).text().length > max)
    {
        $('#' + content_id).text($('#' + content_id).text().substring(0, max));
    }
}

This does limit the number of characters to the number specified by 'max', however once the area's text is set by the jQuery .text() function the cursor resets itself to the beginning of the area.

So if the user keeps on typing, the newly entered characters will be inserted at the beginning of the text and the last character of the text will be removed. So really, I just need some way to keep the cursor at the end of the contenteditable area's text.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Bill Dami
  • 3,205
  • 5
  • 51
  • 70

13 Answers13

56

Pass the event object to your function and call e.preventDefault() if the maximum is reached:

var content_id = 'editable_div';

max = 10;

//binding keyup/down events on the contenteditable div
$('#'+content_id).keyup(function(e){ check_charcount(content_id, max, e); });
$('#'+content_id).keydown(function(e){ check_charcount(content_id, max, e); });

function check_charcount(content_id, max, e)
{
    if(e.which != 8 && $('#'+content_id).text().length > max)
    {
       // $('#'+content_id).text($('#'+content_id).text().substring(0, max));
       e.preventDefault();
    }
}

Although, you may need to do a little more to allow the user to do things like 'delete'.

Also, you could probably get rid of the keyup handler. keydown should be enough.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
user113716
  • 318,772
  • 63
  • 451
  • 440
  • Works like a charm! :-) I also added conditionals for arrow keys and backspace/delete, other than that it works perfectly. – Bill Dami May 19 '10 at 16:52
  • 13
    A paste via the context menu or edit menu could still take the user over the limit. – Tim Down May 19 '10 at 16:52
  • @Tim Down - Very true. Unavoidable (to my knowledge) if using a contentEditable div since there's no `change` event. My solution was specific to the question. Your's very aptly covers the general downside of the approach presented. – user113716 May 19 '10 at 17:05
  • @user113716 You can track down paste event like this https://gist.github.com/1787432 (coffescript) – Mikhail Nikalyukin Feb 10 '12 at 07:34
  • 5
    Listening for the [`input` event](http://caniuse.com/#feat=input-event) will allow you to catch keyboard, paste, and drag changes. – josh3736 Oct 29 '15 at 22:40
  • 1
    These keys should be allowed as well: var allowedKeys = { 8: true, // BACKSPACE 35: true, // END 36: true, // HOME 37: true, // LEFT 38: true, // UP 39: true, // RIGHT 40: true, // DOWN 46: true // DEL } – formixian Apr 07 '16 at 13:51
  • Could you do this with pure javascript? – Jared Chu Oct 25 '16 at 16:54
  • You could call check_charcount once more on focusout too to circumvent the copy paste problem. – Hafenkranich Apr 03 '17 at 16:53
34

A simple way to achieve this:

<div onkeypress="return (this.innerText.length <= 256)" contenteditable="true">
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Bala Velayutham
  • 349
  • 3
  • 2
20

Firstly, this kind of thing is irritating for the user: I would suggest instead doing something similar to Stack Overflow's comment field, which allows you type as much or as little as you like, shows you a message telling you how many characters you've typed and whether it's too many or too few, and refuses to let you submit a comment whose length is not valid.

Secondly, if you really have to limit the length of text, replacing the whole content of the <div> on every keystroke if the content is too long is unnecessarily expensive, and will make the editor unresponsive on slower machines. I suggest handling the keypress event and simply preventing the character being inserted using preventDefault() on the event (or in Internet Explorer, setting the event's returnValue to true, assuming you're using attachEvent).

This won't prevent the user from pasting text in, so you'll need to handle the paste event (which doesn't exist in Opera or Firefox < 3, so you'll need some kind of polling-based solution for those). Since you won't be able to access the content being pasted in advance, you'll have no way of knowing if the paste will take you over the character limit, so you'll need to set a timer to check the length again shortly after the paste. All that being the case, the first option seems preferable to me.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Tim Down
  • 318,141
  • 75
  • 454
  • 536
  • 1
    Very good points Tim, I totally forgot to take into a account paste actions or any method of getting text into the editor that won't generate a keypress event for that matter. And yeah, I was never thrilled with the solution of replacing the entire content either, it was just a quick initial stab at the problem. So looking at it now I may just have to go with the solution of allowing an unlimited number of characters initially but warning the user when they are over and not allowing the content to be saved. – Bill Dami May 19 '10 at 17:44
  • 1
    `input` event can handle the `paste` case. – Lewis Aug 18 '15 at 17:41
  • @Tresdin: Indeed, the `input` event (which was not well supported in 2010) definitely does simplify the task, although I'd still prefer to avoid truncating the textarea's content. – Tim Down Aug 18 '15 at 22:20
  • 1
    `input` is not supported for contenteditable in any version of IE, so you can't even rely on that. @TimDown is pointing out the issues with pasting which aren't covered by any of the other solutions in this post. Further, there are actions from calling `document.execCommand()` that will also insert content in a way that you can't detect and prevent and could delete reactively (insertText, insertHTML, paste, insertOrderedList, insertUnorderedList, etc. are all commands that insert text into the editor). More reason to go with solution #1 that @TimDown proposes – Nate Mielnik Feb 11 '17 at 03:00
  • 1
    You can see more conversation around this specific issue in issues [#962](https://github.com/yabwe/medium-editor/issues/962) in medium-editor, where we've been brainstorming ways to implement this feature into the editor. Within medium-editor, we also have the benefit of an `editableInput` custom event which detects all types of text insertion across all browsers (including pasting and document.execCommand()) – Nate Mielnik Feb 11 '17 at 03:02
5

This is the best way to do this in the most generalized form, and it just works great for me!

<div contenteditable="true" name="choice1" class="textfield" max="255"></div>
    $('.textfield').on("keypress paste", function (e) {
        if (this.innerHTML.length >= this.getAttribute("max")) {
            e.preventDefault();
            return false;
        }
    });
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
VizardCrawler
  • 1,343
  • 10
  • 16
5

This is the way I did it by jQuery binding, making it easy for me by just adding a property data-input-length to a content editable element.

Just add the JavaScript code anywhere in your document.

$(document).ready(function(){
    // Excempt keys(arrows, del, backspace, home, end);
    var excempt = [37,38,39,40,46,8,36,35];
    // Loop through every editiable thing
    $("[contenteditable='true']").each(function(index, elem) {
        var $elem = $(elem);
        // Check for a property called data-input-length="value" (<div contenteditiable="true" data-input-length="100">)
        var length = $elem.data('input-length');
        // Validation of value
        if(!isNaN(length)) {
            // Register keydown handler
            $elem.on('keydown', function(evt) {
                // If the key isn't excempt AND the text is longer than length stop the action.
                if(excempt.indexOf(evt.which) === -1 && $elem.text().length > length) {
                   evt.preventDefault();
                   return false;
                }
            });
        }
    });
});
div {
  background-color: #eee;
  border: 1px solid black;
  margin: 5px;
  width: 300px;
  height: 100px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div contenteditable="true" data-input-length="100">
    You can type a 100 characters here
</div>
<div contenteditable="true" data-input-length="150">
    You can type a 150 characters here
</div>
<div contenteditable="true" data-input-length="10">
    You can type a 10 characters here
</div>
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Tschallacka
  • 27,901
  • 14
  • 88
  • 133
4

The following solution also considers the control keys (which correspond to non-printable characters).

//Maximum number of characters
var max = 200;

$('#editable_div').keydown(function(e) {
    var keycode = e.keyCode;

    //List of keycodes of printable characters from:
    //http://stackoverflow.com/questions/12467240/determine-if-javascript-e-keycode-is-a-printable-non-control-character
    var printable = 
        (keycode > 47 && keycode < 58)   || // number keys
        keycode == 32 || keycode == 13   || // spacebar & return key(s) (if you want to allow carriage returns)
        (keycode > 64 && keycode < 91)   || // letter keys
        (keycode > 95 && keycode < 112)  || // numpad keys
        (keycode > 185 && keycode < 193) || // ;=,-./` (in order)
        (keycode > 218 && keycode < 223);   // [\]' (in order)

    if (printable) {
        //Based on the Bala Velayutham's answer
        return $(this).text().length <= max; 
    }
});
Vito Gentile
  • 13,336
  • 9
  • 61
  • 96
3

This is another version of the answer of user113716. It supports the "paste" event, and some text hotkeys, like Ctrl + A.

$('#'+content_id).on('keydown paste', function (e) { maxLimitForContenteditableDiv(e, 140) });

function maxLimitForContenteditableDiv(e, limit) {
    var allowedKeys = false;

    if (e.type === 'keydown') {
        allowedKeys = (
            e.which === 8 ||  /* BACKSPACE */
            e.which === 35 || /* END */
            e.which === 36 || /* HOME */
            e.which === 37 || /* LEFT */
            e.which === 38 || /* UP */
            e.which === 39 || /* RIGHT*/
            e.which === 40 || /* DOWN */
            e.which === 46 || /* DEL*/
            e.ctrlKey === true && e.which === 65 || /* CTRL + A */
            e.ctrlKey === true && e.which === 88 || /* CTRL + X */
            e.ctrlKey === true && e.which === 67 || /* CTRL + C */
            e.ctrlKey === true && e.which === 86 || /* CTRL + V */
            e.ctrlKey === true && e.which === 90    /* CTRL + Z */
        )
    }

    if (e.type === 'paste') {
        setTimeout(function () {
            $(e.target).text($(e.target).text().slice(0, limit));
        });
    }

    if (!allowedKeys && $(e.target).text().length >= limit) {
        e.preventDefault();
    }
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
vodnycheck
  • 39
  • 4
1
$("[contenteditable=true]").keydown(function(e) {
    var max = $(this).attr("maxlength");
    if (e.which != 8 && $(this).text().length > max) {
        e.preventDefault();
    }
});
Yuval Perelman
  • 4,499
  • 1
  • 22
  • 32
0

This is a more generalized version of user113716's answer due to it not working appropriately when multiple contenteditable fields are using the target class. Their answer counts the total number of characters for all of the elements matching the given class, so you can only enter maximum characters total on the page.

This solution allows you to use a general class, and limits the number of characters in each contenteditable field independently.

The HTML:

<div contenteditable="true" name="choice1" class="textfield"></div>

And the JavaScript:

MAX_TEXTINPUT = 10;
TEXTFIELD_CLASS = "textfield"

$(document).ready(function() {

    //binding keyup/down events on the contenteditable div
    $('.'+TEXTFIELD_CLASS).keydown(function(e){ check_charcount(TEXTFIELD_CLASS, MAX_TEXTINPUT, e); });

})

function check_charcount(inputclass, max, e) {
    var focused = $(document.activeElement)

    if(focused.hasClass(inputclass) && e.which != 8 && $(focused).text().length >= max)
    {
        e.preventDefault();
    }
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Wes Modes
  • 2,024
  • 2
  • 22
  • 40
0

If you would like to make it work with classes. In other words, element would be able to share class names and still have their own unique count:

var content_id = '.myclass';  

max = 1;
//binding keyup/down events on the contenteditable div
$(content_id).keyup(function(e){ check_charcount(this, max, e); });
$(content_id).keydown(function(e){ check_charcount(this, max, e); });

function check_charcount(elem, max, e){   
    if(e.which != 8 && $(elem).text().length >= max){       
        e.preventDefault();
    }
}
0
var onKeyPress = function () {
    var keyCode = window.event.keyCode,
        isNumeric = (keyCode > 47 && keyCode < 58),
        isNotEnterKey = !!(window.event.keyCode !== 13);

        (isNotEnterKey) || (this.blur());

        return (isNumeric && this.innerText.length < 3);
};
shawndumas
  • 1,413
  • 15
  • 17
0

You can use a variable to equalize the current text and check if the text is longer than a value. If it is, do not equalize the target textContent to the variable.

var text = null
const textChangeHandler = (e) => {
  if (e.currentTarget.textContent.length > 240) {
    // do nothing
    e.currentTarget.textContent = text
  } else {
    text = e.currentTarget.textContent
    e.currentTarget.textContent = text
  }
}
<p contentEditable={ "true"} style={{cursor: "text"}} onInput={textChangeHandler}>

</p>
burlesquel
  • 35
  • 4
-1
<div id="test" contenteditable="true" tabIndex="-1"></div>
<p><span id="sy">0</span>/120</p> 

var flag = true;
$('#test').on('compositionstart', function() {
    flag = false;
});

$('#test').on('compositionend', function() {
  flag = true;
});

$('#test').on('input', function() {
  setTimeout(function() {
    if (flag) {
      if($('#test').text().length >= 120) {
        setTimeout(() => {
          $('#test').text($('#test').text().substring(0, 120));
          var range = window.getSelection(); // 创建range
          range.selectAllChildren(document.getElementById('test')); // Range 选择obj下所有子内容
          range.collapseToEnd();
          $("#sy").text($('#test').text().length)
        }, 0);
      } else {
        $("#sy").text($('#test').text().length)
      }      
    }
  }, 0);
});
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131