angularjs textarea with colors (with html5 contenteditable)

1.3k views Asked by At

I'm trying to create an editor which does "syntax highlighting", it is rather simple:

yellow -> <span style="color:yellow">yellow</span>

I'm also using <code contenteditable> html5 tag to replace <textarea>, and have color output.

I started from angularjs documentation, and created the following simple directive. It does work, except it do not update the contenteditable area with the generated html. If I use a element.html(htmlTrusted) instead of ngModel.$setViewValue(htmlTrusted), everything works, except the cursor jumps to the beginning at each keypress.

directive:

app.directive("contenteditable", function($sce) {
  return {
    restrict: "A", // only activate on element attribute
    require: "?ngModel", // get ng-model, if not provided in html, then null
    link: function(scope, element, attrs, ngModel) {
      if (!ngModel) {return;} // do nothing if no ng-model

      element.on('blur keyup change', function() {
        console.log('app.directive->contenteditable->link->element.on()');
        //runs at each event inside <div contenteditable>
        scope.$evalAsync(read);
      });

       function read() {
         console.log('app.directive->contenteditable->link->read()');
         var html = element.html();
        // When we clear the content editable the browser leaves a <br> behind
        // If strip-br attribute is provided then we strip this out
        if ( attrs.stripBr && html == '<br>' ) {
          html = '';
        }

        html = html.replace(/&lt;/, '<');
        html = html.replace(/&gt;/, '>');
        html = html.replace(/<span\ style=\"color:\w+\">(.*?)<\/span>/g, "$1");

        html = html.replace('yellow', '<span style="color:yellow">yellow</span>');
        html = html.replace('green', '<span style="color:green">green</span>');
        html = html.replace('purple', '<span style="color:purple">purple</span>');
        html = html.replace('blue', '<span style="color:yellow">blue</span>');

        console.log('read()-> html:', html);
        var htmlTrusted = $sce.trustAsHtml(html);
        ngModel.$setViewValue(htmlTrusted);
      }
      read(); // INITIALIZATION, run read() when initializing
    }
  };
});  

html:

<body ng-app="MyApp">

 <code contenteditable
      name="myWidget" ng-model="userContent"
      strip-br="true"
      required>This <span style="color:purple">text is purple.</span> Change me!</code>
 <hr>
 <pre>{{userContent}}</pre>

</body>

plunkr: demo (type yellow, green or blue into the change me input area)

I tried scope.$apply(), ngModel.$render() but has no effect. I must miss something really obvious...

The links I already read through:

Any help is much appreciated. Please see the plunker demo above.

1

There are 1 answers

0
arcol On

After almost a year, I finally settled to Codemirror, and I was never happier. I'm doing side-by-side markdown source editing with live update (with syntax highlighting, so even a bit more advanced than stackoverflow's editing page.)

I created a simple codeEditor angular directive, which requires codeMirror, and uses it.

For completeness, here is the component sourcecode:

$ cat components/codeEditor/code-editor.html
<div class="code-editor"></div>

$ cat codeEditor.js 
'use strict';

angular.module('myApp')
.directive('codeEditor', function($timeout, TextUtils){
  return {
    restrict: 'E',
    replace: true,
    require: '?ngModel',
    transclude: true,
    scope: {
      syntax: '@',
      theme: '@'
    },
    templateUrl: 'components/codeEditor/code-editor.html',
    link: function(scope, element, attrs, ngModelCtrl, transclude){
      // Initialize Codemirror
      var option = {
        mode: scope.syntax || 'xml',
        theme: scope.theme || 'default',
        lineNumbers: true
      };
      if (option.mode === 'xml') {
        option.htmlMode = true;
      }

      scope.$on('toedit', function () { //event
        //This is required to correctly refresh the codemirror view.
        // otherwise the view stuck with 'Both <code...empty.' initial text.
        $timeout(function() {
          editor.refresh();
        });
      });

      // Require CodeMirror
      if (angular.isUndefined(window.CodeMirror)) {
        throw new Error('codeEditor.js needs CodeMirror to work... (o rly?)');
      }

      var editor = window.CodeMirror(element[0], option);

      // Handle setting the editor when the model changes if ngModel exists
      if(ngModelCtrl) {
        // Timeout is required here to give ngModel a chance to setup. This prevents
        // a value of undefined getting passed as the view is rendered for the first
        // time, which causes CodeMirror to throw an error.
        $timeout(function(){
          ngModelCtrl.$render = function() {
            if (!!ngModelCtrl.$viewValue) {
              // overwrite <code-editor>SOMETHING</code-editor>
              // if the $scope.content.code (ngModelCtrl.$viewValue) is not empty.
              editor.setValue(ngModelCtrl.$viewValue); //THIRD happening
            }
          };
          ngModelCtrl.$render();
        });
      }

      transclude(scope, function(clonedEl){
        var initialText = clonedEl.text();
        if (!!initialText) {
          initialText = TextUtils.normalizeWhitespace(initialText);
        } else {
          initialText = 'Both <code-editor> tag and $scope.content.code is empty.';
        }
        editor.setValue(initialText); // FIRST happening

        // Handle setting the model if ngModel exists
        if(ngModelCtrl){
          // Wrap these initial setting calls in a $timeout to give angular a chance
          // to setup the view and set any initial model values that may be set in the view
          $timeout(function(){
            // Populate the initial ng-model if it exists and is empty.
            // Prioritize the value in ngModel.
            if(initialText && !ngModelCtrl.$viewValue){
              ngModelCtrl.$setViewValue(initialText); //SECOND happening
            }

            // Whenever the editor emits any change events, update the value
            // of the model.
            editor.on('change', function(){
              ngModelCtrl.$setViewValue(editor.getValue());
            });
          });
        }
      });

      // Clean up the CodeMirror change event whenever the directive is destroyed
      scope.$on('$destroy', function(){
        editor.off('change');
      });
    }
  };
});

There is also inside the components/codeEditor/vendor directory the full codemirror sourcecode.

I can highly recommend codeMirror. It is a rocksolid component, works in every browser combination (firefox, firefox for android, chromium).