Google pagedown AngularJS directive

2.3k views Asked by At

See bottom of question for an improved solution to this problem

I have been trying for some time now to get a directive for the pagedown working. This is the exact same editor used by stackoverflow. Stackoverflow make this code available here:

https://code.google.com/p/pagedown/

There are some versions out there on the internet but none work well. What I need is one that will appear with all the editor buttons just like stackoverflow both when coded inline and also when it's inline as part of an ngRepeat.

I would like to make this directive work when it's coded inline and also inside an ng-repeat using Angular version 1.2.7. What's needed is that when the model data changes the directive needs to update the pagedown views to show the new question and answers. When the user changes the pagedown edit area the directive needs to be able to update the model. When the user clicks [save] the model data needs to be saved to the database (or at least to another object to confirm it worked).

The directive needs to be able to respond to changes in the model and also save it's raw data to the model on keyup or when the 'change' button is pressed in the editing paned. Here is what I have so far. Note that this version does not have the $wmdInput.on('change' but it's a start for what is needed.

Most important I would like to have this working with version 1.2.7 of Angular and jQuery 2.0.3 Please note that I found differences with my non-working code between versions 1.2.2 and 1.2.7. I think it's best if any solution works for the latest (1.2.7) release.

Update

I now this directive which is simpler and solves some recent problems I had with the content not showing. I would highly recommend using this directive which is based on the answer accepted plus a few improvements: https://github.com/kennyki/angular-pagedown

4

There are 4 answers

10
Ilan Frumer On BEST ANSWER

Here is a working link:

http://cssdeck.com/labs/qebukp9k

UPDATE

  • I made some optimizations.
  • I use ngModel.$formatters ! no need for another $watch.
  • I use $timeout and then scope.$apply to avoid $digest in progress errors.

Angular.js & Performance

  • If you hit performance maybe your application is using too many $watch / $on.
  • In my experience, using 3rd-party libraries can cause all sort of non efficient / memory leaking behavior, mostly because it was not implemented with angular / SPA in mind.
  • I was able to do some smart integration for some libraries but some just don't fit well to angular's world.
  • If your application must show 1000+ questions you should probably start with writing your custom repeater, and prefer dynamic DOM insertions.
  • Angular.js will not perform well with tons of data bindings unless you are willing to write some smart lower level stuff (It's actually fun when you know how!).
  • Again, prefer pagination! As Misko Hevery says: "You can't really show more than about 2000 pieces of information to a human on a single page. Anything more than that is really bad UI, and humans can't process this anyway".
  • Read this: How does data binding work in AngularJS?
  • I'm more than happy to help you, but First let me show the code (contact me)..

Solution:

var app = angular.module('App', []);

app.directive('pagedownAdmin', function ($compile, $timeout) {
    var nextId = 0;
    var converter = Markdown.getSanitizingConverter();
    converter.hooks.chain("preBlockGamut", function (text, rbg) {
        return text.replace(/^ {0,3}""" *\n((?:.*?\n)+?) {0,3}""" *$/gm, function (whole, inner) {
            return "<blockquote>" + rbg(inner) + "</blockquote>\n";
        });
    });

    return {
        require: 'ngModel',
        replace: true,
        template: '<div class="pagedown-bootstrap-editor"></div>',
        link: function (scope, iElement, attrs, ngModel) {

            var editorUniqueId;

            if (attrs.id == null) {
                editorUniqueId = nextId++;
            } else {
                editorUniqueId = attrs.id;
            }

            var newElement = $compile(
                '<div>' +
                   '<div class="wmd-panel">' +
                      '<div id="wmd-button-bar-' + editorUniqueId + '"></div>' +
                      '<textarea class="wmd-input" id="wmd-input-' + editorUniqueId + '">' +
                      '</textarea>' +
                   '</div>' +
                   '<div id="wmd-preview-' + editorUniqueId + '" class="pagedown-preview wmd-panel wmd-preview"></div>' +
                '</div>')(scope);

            iElement.html(newElement);

            var help = function () {
                alert("There is no help");
            }

            var editor = new Markdown.Editor(converter, "-" + editorUniqueId, {
                handler: help
            });

            var $wmdInput = iElement.find('#wmd-input-' + editorUniqueId);

            var init = false;

            editor.hooks.chain("onPreviewRefresh", function () {
              var val = $wmdInput.val();
              if (init && val !== ngModel.$modelValue ) {
                $timeout(function(){
                  scope.$apply(function(){
                    ngModel.$setViewValue(val);
                    ngModel.$render();
                  });
                });
              }              
            });

            ngModel.$formatters.push(function(value){
              init = true;
              $wmdInput.val(value);
              editor.refreshPreview();
              return value;
            });

            editor.run();
        }
    }
});
7
Dalorzo On

You can change this:

scope.$watch(attrs.ngModel, function () {
var val = scope.$eval(attrs.ngModel);

For this:

scope.$watch(attrs.ngModel, function(newValue, oldValue) {
  var val = newValue;
});

Additionally can try commenting this code out:

if (val !== undefined) {
    $wmdInput.val(val);
    ...    

}

I think it may be associated with the odd behavior.

3
Daiwei On

Demo: http://plnkr.co/edit/FyywJS?p=preview

Summary

  1. I removed keyup and added a hook on onPreviewRefresh to ensure clicking on toolbar will properly update ng-model.

  2. Functions on $rootScope will demonstrate the ability to update ng-model from outside of pagedown.

  3. save functionality purely depends on your choice, since you can access ng-model anywhere now.

2
allenhwkim On

It might not be the answer, but all the problem occurs when you start using Markdown.Editor which does not gives you a lot of benefits.

Of course, you need to use it for markdown editor beginners, but when use markdown, they are already not beginners anyway(I might be wrong).

What I approached to this problem was to make fully working version of this without using editor.

It has preview also.

It's also very simple.

https://github.com/allenhwkim/wiki

---- edit ----
removed
---- edit ----
removed
---- edit ----

To provide a fully working editor, after few hours of trial and asking questions, the following is the simplest I can get. This does require any $watch nor $formatters. It just wraps the given element with all attributes given by the textarea.

http://plnkr.co/edit/jeZ5EdLwOfwo6HzcTAOR?p=preview

app.directive('pagedownEditor', function($compile, $timeout) {
  var num=0;
  return {
    priority: 1001, //higher than ng-repeat, 1000
    link: function(scope, el, attrs) {
      var uniqNum = scope.$index || num++;
      var wmdPanel = document.createElement('div');
      wmdPanel.className = "wmd-panel";
      var wmdButtonBar = document.createElement('div');
      wmdButtonBar.id = 'wmd-button-bar-'+uniqNum;
      wmdPanel.appendChild(wmdButtonBar);
      el.wrap(wmdPanel); // el is ng-repeat comment, it takes tim

      var converter = Markdown.getSanitizingConverter();
      var editor = new Markdown.Editor(converter, "-"+uniqNum);
      $timeout(function() {
        wmdPanel.querySelector('textarea').id = 'wmd-input-'+uniqNum;
        wmdPanel.querySelector('textarea').className += ' wmd-input';
        wmdPanel.insertAdjacentHTML('afterend', '<div id="wmd-preview-'+uniqNum+'" '
          +'class="pagedown-preview wmd-panel wmd-preview">');
        editor.run()
      }, 50);
    }
  };