Limit the number of character in tinyMCE

Solution 1:

This works in tinyMCE 4.3.12 and also captures pasting:

EDIT: Fixed bugs and extended code to display a character counter under the editor. Possibly not the best way as it relies a bit on the current HTML structure of tinyMCE having the editor div before the hidden textarea.

This version only counts the text length and ignores HTML tag length. To count full HTML length, replace all "innerText" with "innerHTML".

tinymce.init({
    max_chars: 1000, // max. allowed chars
    setup: function (ed) {
        var allowedKeys = [8, 37, 38, 39, 40, 46]; // backspace, delete and cursor keys
        ed.on('keydown', function (e) {
            if (allowedKeys.indexOf(e.keyCode) != -1) return true;
            if (tinymce_getContentLength() + 1 > this.settings.max_chars) {
                e.preventDefault();
                e.stopPropagation();
                return false;
            }
            return true;
        });
        ed.on('keyup', function (e) {
            tinymce_updateCharCounter(this, tinymce_getContentLength());
        });
    },
    init_instance_callback: function () { // initialize counter div
        $('#' + this.id).prev().append('<div class="char_count" style="text-align:right"></div>');
        tinymce_updateCharCounter(this, tinymce_getContentLength());
    },
    paste_preprocess: function (plugin, args) {
        var editor = tinymce.get(tinymce.activeEditor.id);
        var len = editor.contentDocument.body.innerText.length;
        var text = $(args.content).text();
        if (len + text.length > editor.settings.max_chars) {
            alert('Pasting this exceeds the maximum allowed number of ' + editor.settings.max_chars + ' characters.');
            args.content = '';
        } else {
            tinymce_updateCharCounter(editor, len + text.length);
        }
    }
});

function tinymce_updateCharCounter(el, len) {
    $('#' + el.id).prev().find('.char_count').text(len + '/' + el.settings.max_chars);
}

function tinymce_getContentLength() {
    return tinymce.get(tinymce.activeEditor.id).contentDocument.body.innerText.length;
}

Reference: How can I prevent tinyMCE's paste event?

Solution 2:

TinyMCE 4+
+
jQuery

<textarea id="description_edit" name="description_edit"><?=htmlspecialchars($this->company->description);?></textarea>

<div><span>Characters left:</span> <span id="chars_left"></span></div>


<script type="text/javascript" src="/js/tinymce/tinymce.min.js"></script>
<script>
    var max_chars = 200; //max characters
    var max_for_html = 300; //max characters for html tags
    var allowed_keys = [8, 13, 16, 17, 18, 20, 33, 34, 35, 36, 37, 38, 39, 40, 46];
    var chars_without_html = 0;

    function alarmChars() {
        if (chars_without_html > (max_chars - 25)) {
            $('#chars_left').css('color', 'red');
        } else {
            $('#chars_left').css('color', 'gray');
        }
    }

    $(function () {
        tinymce.init({
            selector: "#description_edit",
            theme: "modern",
            width: 320,
            height: 130,
            plugins: [
                "advlist autolink lists charmap print preview hr anchor pagebreak",
                "searchreplace visualblocks visualchars code insertdatetime media nonbreaking",
                "save table contextmenu directionality paste textcolor"
            ],
            image_advtab: true,
            language: 'en',
            menubar: false,
            statusbar: false,

            setup: function (ed) {
                ed.on("KeyDown", function (ed, evt) {
                    chars_without_html = $.trim(tinyMCE.activeEditor.getContent().replace(/(<([^>]+)>)/ig, "")).length;
                    chars_with_html = tinyMCE.activeEditor.getContent().length;
                    var key = ed.keyCode;

                    $('#chars_left').html(max_chars - chars_without_html);

                    if (allowed_keys.indexOf(key) != -1) {
                        alarmChars();
                        return;
                    }

                    if (chars_with_html > (max_chars + max_for_html)) {
                        ed.stopPropagation();
                        ed.preventDefault();
                    } else if (chars_without_html > max_chars - 1 && key != 8 && key != 46) {
                        alert('Characters limit!');
                        ed.stopPropagation();
                        ed.preventDefault();
                    }
                    alarmChars();
                });
            },

            toolbar: "bold italic underline | alignleft aligncenter alignright alignjustify | forecolor backcolor | bullist numlist | charmap",
            style_formats: [
                {title: 'Bold text', inline: 'b'},
                {title: 'Red text', inline: 'span', styles: {color: '#ff0000'}},
                {title: 'Red header', block: 'h1', styles: {color: '#ff0000'}},
                {title: 'Example 1', inline: 'span', classes: 'example1'},
                {title: 'Example 2', inline: 'span', classes: 'example2'},
                {title: 'Table styles'},
                {title: 'Table row 1', selector: 'tr', classes: 'tablerow1'}
            ]
        });

        chars_without_html = $.trim($("#description_edit").text().replace(/(<([^>]+)>)/ig, "")).length;
        $('#chars_left').html(max_chars - chars_without_html);
        alarmChars();
    });
</script>