Google pagedown AngularJS directive
Asked Answered
R

4

24

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

Reinhold answered 3/1, 2014 at 16:55 Comment(5)
Finally I was able to reproduce your bug...Pickerelweed
@llan - Glad to hear it but not sure of a solution. I packaged my question up here with as big a bounty as possible. Hoping to find someone who likes a challenge. There must be many people out there who would like to use pagedown with AngularJS so I hope someone can come up with a solution.Gerge
Do you have to use 300ms delay?Pickerelweed
No I just added that as I thought there may be a performance hit that would slow things down when the user was clicking the keys quickly. By the way if you are coming up with a solution it would be great if you could put this into a plunker or fiddler or something else so I could test it out and we could have something common to work with. My plunker skills are just about zero so I am not sure how to make one that would show a sidebar with a few buttons and a screen area with editing windows.Gerge
It would make more sense to put the description you have written in the bounty description in the question text itself!Peralta
P
27

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();
        }
    }
});
Pickerelweed answered 6/1, 2014 at 19:14 Comment(10)
@llan - Your code is working for me but it's very slow in my application compared with my original code using 1.2.2. When I enter 5-6 words quickly it can take 3-4 seconds before they appear on the screen. I am trying to work out why your example works good but mine is very slow. By the way you said you were able to reproduce my problem. Does that mean you could get it to work in 1.2.2. but not in 1.2.3? I noticed changes between those versions include a change for "fix(input): Support form auto complete on modern browser " I am wondering if the reason my code does not work is related to thisGerge
@llan - Thanks for your help! 2nd version works good. I noticed now that you just made more improvements so I will test soon. Can I ask you add a one word comment at the top of the file such as // 3.1 just so I know when you make a change. I added one small thing to the question and mentioned that my directives are inside a form. On the form submit button I check to see if changes have been made. It seems your code does not change the value of $pristine. Is there a change you could make so it would make the form "dirty" if changes were made?Gerge
It looks like my last version already sets $pristine to false. I use ngModel.$render() so it's probably setting $pristine automatically. You can check it out: cssdeck.com/labs/qebukp9kPickerelweed
@llan - Yes the setting of pristine now works good. I updated the question with an error message I am getting when the page first loads (when there's not yet any data for the directives and when I guess the inputs are undefined). Do you have any idea what could fix this. Can you simulate this in your test and get the same?Gerge
Fixed! I added populate / unpopulate buttons so you can see how it works.Pickerelweed
Tested this out and it looks good so far. I will let you know tomorrow if I find anything more. If not then I will accept the question. If you think of any performance improvements between now and then please let me know. The user who inputs data was commenting it's still a lot slower now even with not so many questions loaded. She was showing me when adding words before a list the cursor sometimes jumps to after the list in the editor window. I don't think it's your code. Maybe something added in the later releases of Angular and some pagedown timing problems.Gerge
I made another change ( The same Markdown.Converter is used within all directives) As for it being slow , I removed all the $watchers and isolated scope bindings, your editors should be fast as far as my code is involved.Pickerelweed
Ilan - I will check out your latest changes in the next 24 hours and expect to accept the question if all is okay. Sorry for the delay but I have not been able to get the time to check out your latest change today. Thanks for all your help.Gerge
Hey I ran into some issues when I was trying to implement this and append editors to the page dynamically. could you possibly take a look? #21388297Transmogrify
I've posted a question about modifying this to support tabset item display, here: #21392910Cephalic
U
3

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.

Underbelly answered 3/1, 2014 at 17:15 Comment(7)
I believe this was in one of the other pagedown directives. I will try it now and report back. ThanksGerge
I made the change as you suggested but it still does not work. The result is just the same.Gerge
I put this in an apply as another poster before said it should be inside of a $apply. Maybe someone else can comment on that. ThanksGerge
Can you please comment this code out an try again if (val !== undefined) { $wmdInput.val(val); editor.refreshPreview(); }Underbelly
With that commented out it works. BUT it does not get populated with the initial value of modal.data.text. When the window opens it is blank. When I add letters they get correctly added. Note that the order is that the directive changes the DOM and then later my HTTP call returns to populate the data. I did try forcing the date in with this in my controller: $("#wmd-input-question-text").val ($scope.modal.data.text); However I think this is not a recommended way to do it and it's very messy as I would have to do that change everywhere.Gerge
I would recommend you to think in validating through "the model" instead of the control it self. In case it helps there is directive called ngInit can be used to initialize values as well.Underbelly
I cannot use ngInit as the value that goes into the directive comes in after the directive has started up. Then maybe one hour later I might select another question and the modal.data.text would change again. That's why I must use a $watch. As you mentioned yes I certainly want to do it through the model. I don't want to set the value using jQuery. I was just mentioning this as a not good fix that I don't want to use. ThanksGerge
K
3

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);
    }
  };
Kistner answered 6/1, 2014 at 17:50 Comment(2)
Thanks for the link but I really need a solution with an editor just like stackoverflow as the users like to be able to click on the buttons for code etc.Gerge
This can be more simplfied using element.wrap(), which removes all codes related ng-model.Kistner
S
1

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.

Swagman answered 6/1, 2014 at 19:37 Comment(3)
I had problems trying to get your code to work in my application. You have used a templateUrl and maybe that's part of my problem. I did add the template to my page but I get various errors and the editing buttons don't display. Can you try changing your code to look as much as possible like my original so it will be easy to plug in and try. Also on your example plnkr can you try putting in more than one instance of pagedown inside an ng-repeat. This way it's just like I have. Thanks very much for your help so far.Gerge
@SamanthaJ Thanks for your info. I have updated my code and removed templateUrl. Also added ng-repeat to the demo. But if you couldn't see the editor bar button icons, you need to check your css for .wmd-button > span { background-image: url(../../wmd-buttons.png); } as toolbar buttons' icons are not controlled by JS. Let me know any further development of your issue.Swagman
@SamanthaJ I included <link rel="stylesheet" href="http://pagedown.googlecode.com/hg/demo/browser/demo.css"> to make sure the icons are displayed.Swagman

© 2022 - 2024 — McMap. All rights reserved.