Search code examples
javascripthtmlcssangularjscontenteditable

Real counter with contenteditable


I'm already looking for a solution for several days to add a character counter associated with my directive contenteditable.

Unfortunately I can not find a solution.

To highlight the problem different browsers have different behaviors with contenteditable.

Here is the directive I'm using :

var app = angular.module("App", []);
app.directive("contenteditable", function($timeout) {
  return {
    restrict: 'A',
    require : ['^?ngModel'],
    link : function(scope, element, attrs, args){
       var ngModel = args[0];
       if (ngModel === null) {
        return null;
       }

       var modelKey = getModelKey();
       opts = {
           onlyText: false,
           convertNewLines: false,
           noLf: false,
       };
        angular.forEach(['onlyText', 'convertNewLines', 'noLf'], function (opt) {
            if (attrs.hasOwnProperty(opt) && attrs[opt] && attrs[opt] !== 'false') {
                opts[opt] = true;
            }
        });

        $timeout(function () {
             return (opts.onlyText && opts.noLf) ? element.text(ngModel.$modelValue) : element.html(ngModel.$modelValue);
        });

        var validate = function (content) {
            var length = content.length;
            refreshFn = function(content) {
                if (content == undefined || content == null) {
                    content = '';
                }
                scope.maxCharacter= attrs.ngMaxlength;
                scope.remaining = (length);
            };
            scope.$watch('ngModel', function(content){
                refreshFn(content);
            }, true);

            if (length > attrs.ngMaxlength) {
                ngModel.$setValidity(modelKey, false);
                return element.addClass('-error');
            }

            if (element.hasClass('-error')) {
                ngModel.$setValidity(modelKey, true);
                return element.removeClass('-error');
            }
         };

        var read = function () {
            var content = '';
            if ((opts.onlyText && opts.noLf)) {
                content = element.text();
             } else {
                content = element.html();
                if (content) {
                    content = parseHtml(content);
                }
             }

             if (content !== '') {
                content = content.replace(/ /g, '');
                content = content.trim();
             }
            ngModel.$setViewValue(content);
            validate(content);
        };

        ngModel.$render = function () {
            if ((opts.onlyText && opts.noLf)) {
                element.text(ngModel.$viewValue || '');
             } else {
                element.html(ngModel.$viewValue || '');
             }
         };

        element.bind('blur keyup change focus', function (event) {
            scope.$apply(read);
            scope.displayCount = true;
            if (event.type === 'blur') {
                scope.$apply(ngModel.$render);
            }
        });

         function getModelKey() {
          if (typeof attrs.ngModel === 'undefined') {
            return null;
          }
          var split = attrs.ngModel.split('.');
          return split[split.length-1];
       }

        function parseHtml(html) {
            html = html.replace(/ /g, '');
            if (opts.convertNewLines || opts.noLf) {
                var lf = '\r\n',
                    rxl = /\r\n$/;

                if (opts.noLf) {
                    lf = ' ';
                    rxl = / $/;
                }

                html = html.replace(/<br(\s*)\/*>/ig, ''); // replace br for newlines
                html = html.replace(/<[div>]+>/ig, lf); // replace div for newlines
                html = html.replace(/<\/[div>]+>/gm, ''); // remove remaining divs
                html = html.replace(/&nbsp;/g, '');
                html = html.replace(/<[p>]+>/ig, lf); // replace p for newlines
                html = html.replace(/<\/[p>]+>/gm, ''); // remove remaining p
                html = html.replace(/<[span>]+>/ig, lf); // replace p for newlines
                html = html.replace(/<\/[span>]+>/gm, ''); // remove remaining p
                html = html.replace(rxl, ''); // remove last newline
                html = _.unescape(html); // replaces &amp;, &lt;, &gt;, &quot;, &#96; and &#x27; with their unescaped counterparts.
            }
            if (opts.onlyText) {
                html = html.replace(/<\S[^><]*>/g, '');
            }

            return html;
        }
    }
    };
 });

and the html file :

     <div class="content-editable" contenteditable="true"                                        
          ng-model="line.value"                                      
          ng-maxlength=255                                       
          only-text="true"               
          convert-new-lines="true"               
          no-lf="false">
     </div>
     <div ng-if="displayCount" class="ctn" ng-if="countChar">
       <span class="numberChar">
         <span class="italic" style="margin-right:5px;"> 
           <span ng-class="remaining>=maxCharacter ? 'errorMsg': ''">
             {{remaining}} 
           </span> / {{maxCharacter}}
         </span>   
         <span class="errorMsg error" ng-if="remaining>=maxCharacter"></span>
       </span>
    </div>                          
</div>

the example on codepen : https://codepen.io/gregand/pen/yLBjWYW

On Chrome and Firefox, when I enter 6 characters (line break included), the counter counts 9 characters

enter image description here

On Edge, the counter is good

enter image description here

I tried to change the contenteditable css of display: block to inline-block, it corrects the problem on Chrome but there are problems on Edge.

I tried to use

document.execCommand('defaultParagraphSeparator', false, 'p');

but without success.

If anyone knows a solution that works on all browsers, I would be interested


Solution

  • I found the solution, in fact the problem came from the next line :

    var lf = '\r\n'
    

    I ask each tag of the replaced by the variable lf

    html = html.replace(/<[div>]+>/ig, lf);
    

    '\r\n' counts two characters, one for '\r' and another for '\n'

    whereas the line break should only have one character.

    For the solution I therefore replace the variable lf

     var lf = '\n'
    

    and it works multi browsers

    https://codepen.io/gregand/pen/yLBjWYW