Search code examples
javascriptangularjscontenteditable

Calculate the number of characters with contenteditable


I use the following directive to enter editable content on a tag

I modified it to add a character counter.

But when I make line breaks, it counts me more characters.

enter image description here

in the image the counter displays 4 characters whereas visually there are only two.

enter image description here

here the < transforms into 4 characters instead of one and not directly ">"

How to actually calculate the number of characters

Here is the directive used

directives.directive('contenteditable', ['$timeout', function($timeout) {
  return {
    restrict: 'A',
    require: ['^?ngModel'],
    link: function(scope, element, attrs, args) {
      var ngModel = args[0];
      if (ngModel === null) {
        return null;
      }
      // console.log(element);
      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(/&nbsp;/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', function(event) {
        scope.$apply(read);
        scope.displayCount = true;
        if (element.text().length >= attrs.ngMaxlength) {
          event.preventDefault();
          return false;
        }

        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(/&nbsp;/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(/<[p>]+>/ig, lf); // replace p for newlines
          html = html.replace(/<\/[p>]+>/gm, ''); // remove remaining p
          html = html.replace(rxl, ''); // remove last newline
        }
        if (opts.onlyText) {
          html = html.replace(/<\S[^><]*>/g, '');
        }
        return html;
      }
    }
  };
}]);

Solution

  • Because a line feed has 2 characters in Windows and 1 character in Linux. The same way a space is a character altough visually is not a character, but an empty space, the line feed is another character.

    "Visually there are only two" is false, as you can see a line feed, and that's a visual modification (the same way as a space, I repeat).

    If you don't want to count line feeds, just remove them before getting the length. There are two characters for linefeeds: \r and \n, so:

    var length = content.replace(/\r|\n/g, "").length;
    

    In the case that you want to remove all linefeeds, spaces and so on to just count the visual characters, then you just:

    var length = content.replace(/\s+/g, "").length;