github.com/insionng/yougam@v0.0.0-20170714101924-2bc18d833463/public/libs/ibootstrap-markdown/markdown-editor.js (about)

     1  /*
     2  * markdown-editor.js
     3  * 
     4  * Markdown Editor plugin for jQuery.
     5  */
     6  
     7  function markdown2html(text) {
     8      return markdown.toHTML(text);
     9  }
    10  
    11  !function($) {
    12      var Markdown = function(element, options, commands) {
    13          this.options = options;
    14          this.$textarea = $(element);
    15          if (! this.$textarea.is('textarea')) {
    16              alert('only textarea can change to markdown!');
    17              return;
    18          }
    19          this.buildMarkdown(commands);
    20      };
    21  
    22      var TextAreaDelegate = function(the_toolbar, the_textarea, the_preview, options) {
    23          this.$toolbar = the_toolbar;
    24          this.$textarea = the_textarea;
    25          this.$container = the_textarea.parent();
    26          this.$dom = the_textarea.get(0);
    27          this.$preview = the_preview;
    28          this.$options = options;
    29      };
    30  
    31      TextAreaDelegate.prototype = {
    32  
    33          constructor: TextAreaDelegate,
    34  
    35          enableAllButtons: function(enabled) {
    36              var btns = this.$toolbar.find('button[data-cmd]');
    37              if (enabled) {
    38                  btns.removeAttr('disabled');
    39              }
    40              else {
    41                  btns.attr('disabled', 'disabled');
    42              }
    43          },
    44  
    45          enableButton: function(key, enabled) {
    46              var btn = this.$toolbar.find('button[data-cmd="' + key + '"]');
    47              if (enabled) {
    48                  btn.removeAttr('disabled');
    49              }
    50              else {
    51                  btn.attr('disabled', 'disabled');
    52              }
    53          },
    54  
    55          getText: function() {
    56              return this.$textarea.val();
    57          },
    58  
    59          getOption: function(key) {
    60              return this.$options[key];
    61          },
    62  
    63          paste: function(s) {
    64              this.$dom.setRangeText(s);
    65          },
    66  
    67          getSelection: function() {
    68              return this.$dom.value.substring(this.$dom.selectionStart, this.$dom.selectionEnd);
    69          },
    70  
    71          selectCurrentLine: function() {
    72              var pos = this.getCaretPosition();
    73              var ss = this.$dom.value.split('\n');
    74              var start = 0;
    75              var end = 0;
    76              for (var i=0; i<ss.length; i++) {
    77                  var s = ss[i];
    78                  if ((start + s.length + 1) > pos) {
    79                      end = start + s.length;
    80                      break;
    81                  }
    82                  start += (s.length + 1);
    83              }
    84              this.setSelection(start, end);
    85              return this.getSelection();
    86          },
    87  
    88          getCaretPosition: function() {
    89              return this.$dom.selectionStart;
    90          },
    91  
    92          unselect: function() {
    93              var p = this.getCaretPosition();
    94              this.$dom.setSelectionRange(p, p);
    95          },
    96  
    97          setSelection: function(start, end) {
    98              this.$dom.setSelectionRange(start, end);
    99          },
   100  
   101          setCaretPosition: function(pos) {
   102              this.$dom.setSelectionRange(pos, pos);
   103          },
   104      };
   105  
   106      Markdown.prototype = {
   107          constructor: Markdown,
   108  
   109          applyCss: function() {
   110              var css = {
   111                  'resize': 'none',
   112                  'font-family': 'Monaco, Menlo, Consolas, "Courier New", monospace',
   113              };
   114              $that = this;
   115              $.map(css, function(v, k) {
   116                  $that.$textarea.css(k, v);
   117              });
   118          },
   119  
   120          executeCommand: function(cmd) {
   121              console.log('Exec: ' + cmd);
   122              var fn = this.$commands[cmd];
   123              fn && fn(this.$delegate);
   124          },
   125  
   126          buildMarkdown: function(commands) {
   127              $that = this;
   128              var L = ['<div class="btn-toolbar markdown-toolbar"><div class="btn-group">'];
   129              $.each(this.options.buttons, function(index, ele) {
   130                  if (ele=='|') {
   131                      L.push('</div><div class="btn-group">');
   132                  }
   133                  else {
   134                      $icon = $that.options.icons[ele] || 'icon-star';
   135                      $tooltip = $that.options.tooltips[ele] || '';
   136                      if (ele=='heading') {
   137                          L.push('<button class="btn dropdown-toggle" data-toggle="dropdown" data-cmd="heading" title="' + $tooltip + '"><i class="' + $icon + '"></i> <span class="caret"></span></button>');
   138                          L.push('<ul class="dropdown-menu">');
   139                          L.push('<li><a href="javascript:void(0)" data-type="md" data-cmd="heading1"># Heading 1</a></li>');
   140                          L.push('<li><a href="javascript:void(0)" data-type="md" data-cmd="heading2">## Heading 2</a></li>');
   141                          L.push('<li><a href="javascript:void(0)" data-type="md" data-cmd="heading3">### Heading 3</a></li>');
   142                          L.push('<li><a href="javascript:void(0)" data-type="md" data-cmd="heading4">#### Heading 4</a></li>');
   143                          L.push('<li><a href="javascript:void(0)" data-type="md" data-cmd="heading5">##### Heading 5</a></li>');
   144                          L.push('<li><a href="javascript:void(0)" data-type="md" data-cmd="heading6">###### Heading 6</a></li>');
   145                          L.push('</ul>');
   146                      }
   147                      else {
   148                          L.push('<button type="button" data-type="md" data-cmd="' + ele + '" title="' + $tooltip + '" class="btn' + ($icon.indexOf('icon-white')>=0 ? ' btn-info' : '') + '"><i class="' + $icon + '"></i></button>');
   149                      }
   150                  }
   151              });
   152              var tw = this.$textarea.outerWidth() - 2;
   153              var th = this.$textarea.outerHeight() - 2;
   154              L.push('</div></div><div class="markdown-preview" style="display:none;padding:0;margin:0;width:' + tw + 'px;height:' + th + 'px;overflow:scroll;background-color:white;border:1px solid #ccc;border-radius:4px"></div>');
   155              this.$commands = commands;
   156              this.$textarea.before(L.join(''));
   157              this.$toolbar = this.$textarea.parent().find('div.markdown-toolbar');
   158              this.$preview = this.$textarea.parent().find('div.markdown-preview');
   159              this.$delegate = new TextAreaDelegate(this.$toolbar, this.$textarea, this.$preview, this.options);
   160              this.$toolbar.find('*[data-type=md]').each(function() {
   161                  $btn = $(this);
   162                  var cmd = $btn.attr('data-cmd');
   163                  $btn.click(function() {
   164                      $that.executeCommand(cmd);
   165                  });
   166                  try {
   167                      //$btn.tooltip();
   168                  }
   169                  catch (e) { /* ignore if tooltip.js not exist */}
   170              });
   171              this.applyCss();
   172          },
   173  
   174          showBackdrop: function() {
   175              if (! this.$backdrop) {
   176                  this.$backdrop = $('<div class="modal-backdrop" />').appendTo(document.body);
   177              }
   178          },
   179  
   180          hideBackdrop: function() {
   181              this.$backdrop && this.$backdrop.remove();
   182              this.$backdrop = null;
   183          },
   184      };
   185  
   186      function setHeading(s, heading) {
   187          var re = new RegExp('^#{1,6}\\s');
   188          var h = re.exec(s);
   189          if (h!=null) {
   190              s = s.substring(h[0].length);
   191          }
   192          return heading + s;
   193      }
   194  
   195      var commands = {
   196  
   197          heading1: function(delegate) {
   198              var line = delegate.selectCurrentLine();
   199              delegate.paste(setHeading(line, '# '));
   200          },
   201  
   202          heading2: function(delegate) {
   203              var line = delegate.selectCurrentLine();
   204              delegate.paste(setHeading(line, '## '));
   205          },
   206  
   207          heading3: function(delegate) {
   208              var line = delegate.selectCurrentLine();
   209              delegate.paste(setHeading(line, '### '));
   210          },
   211  
   212          heading4: function(delegate) {
   213              var line = delegate.selectCurrentLine();
   214              delegate.paste(setHeading(line, '#### '));
   215          },
   216  
   217          heading5: function(delegate) {
   218              var line = delegate.selectCurrentLine();
   219              delegate.paste(setHeading(line, '##### '));
   220          },
   221  
   222          heading6: function(delegate) {
   223              var line = delegate.selectCurrentLine();
   224              delegate.paste(setHeading(line, '###### '));
   225          },
   226  
   227          bold: function(delegate) {
   228              var s = delegate.getSelection();
   229              if (s=='') {
   230                  delegate.paste('****');
   231                  // make cursor to: **|**
   232                  delegate.setCaretPosition(delegate.getCaretPosition() + 2);
   233              }
   234              else {
   235                  delegate.paste('**' + s + '**');
   236              }
   237          },
   238  
   239          italic: function(delegate) {
   240              var s = delegate.getSelection();
   241              if (s=='') {
   242                  delegate.paste('**');
   243                  // make cursor to: *|*
   244                  delegate.setCaretPosition(delegate.getCaretPosition() + 1);
   245              }
   246              else {
   247                  delegate.paste('*' + s + '*');
   248              }
   249          },
   250  
   251          link: function(delegate) {
   252              var s = '<div data-backdrop="static" class="modal hide fade"><div class="modal-header"><button type="button" class="close" data-dismiss="modal">&times;</button><h3>Hyper Link</h3></div>'
   253                    + '<div class="modal-body"><form class="form-horizontal"><div class="control-group"><label class="control-label">Text:</label><div class="controls"><input name="text" type="text" value="" /></div></div>'
   254                    + '<div class="control-group"><label class="control-label">Link:</label><div class="controls"><input name="link" type="text" placeholder="http://" value="" /></div></div>'
   255                    + '</form></div><div class="modal-footer"><a href="#" class="btn btn-primary">OK</a><a href="#" class="btn" data-dismiss="modal">Close</a></div></div>';
   256              $('body').prepend(s);
   257              var $modal = $('body').children(':first');
   258              var sel = delegate.getSelection();
   259              if (sel != '') {
   260                  $modal.find('input[name=text]').val(sel);
   261              }
   262              $modal.modal('show');
   263              $modal.find('.btn-primary').click(function() {
   264                  var text = $.trim($modal.find('input[name=text]').val());
   265                  var link = $.trim($modal.find('input[name=link]').val());
   266                  if (link=='') link = 'http://';
   267                  if (text=='') text = link;
   268                  delegate.paste('[' + text + '](' + link + ')');
   269                  $modal.modal('hide');
   270              });
   271              $modal.on('hidden', function() {
   272                  $modal.remove();
   273              });
   274          },
   275  
   276          email: function(delegate) {
   277              var s = '<div data-backdrop="static" class="modal hide fade"><div class="modal-header"><button type="button" class="close" data-dismiss="modal">&times;</button><h3>Email Address</h3></div>'
   278                    + '<div class="modal-body"><form class="form-horizontal"><div class="control-group"><label class="control-label">Name:</label><div class="controls"><input name="text" type="text" value="" /></div></div>'
   279                    + '<div class="control-group"><label class="control-label">Email:</label><div class="controls"><input name="email" type="text" placeholder="email@example.com" value="" /></div></div>'
   280                    + '</form></div><div class="modal-footer"><a href="#" class="btn btn-primary">OK</a><a href="#" class="btn" data-dismiss="modal">Close</a></div></div>';
   281              $('body').prepend(s);
   282              var $modal = $('body').children(':first');
   283              var sel = delegate.getSelection();
   284              if (sel != '') {
   285                  $modal.find('input[name=text]').val(sel);
   286              }
   287              $modal.modal('show');
   288              $modal.find('.btn-primary').click(function() {
   289                  var text = $.trim($modal.find('input[name=text]').val());
   290                  var email = $.trim($modal.find('input[name=email]').val());
   291                  if (email=='') email = 'email@example.com';
   292                  if (text=='') text = email;
   293                  delegate.paste('[' + text + '](' + email + ')');
   294                  $modal.modal('hide');
   295              });
   296              $modal.on('hidden', function() {
   297                  $modal.remove();
   298              });
   299          },
   300  
   301          image: function(delegate) {
   302              var getObjectURL = function(file) {
   303                  var url = '';
   304                  if (window.createObjectURL!=undefined) // basic
   305                      url = window.createObjectURL(file);
   306                  else if (window.URL!=undefined) // mozilla(firefox)
   307                      url = window.URL.createObjectURL(file);
   308                  else if (window.webkitURL!=undefined) // webkit or chrome
   309                      url = window.webkitURL.createObjectURL(file);
   310                  return url;
   311              };
   312              var s = '<div data-backdrop="static" class="modal hide fade"><div class="modal-header"><button type="button" class="close">&times;</button><h3>Insert Image</h3></div>'
   313                    + '<div class="modal-body"><div style="width:530px;"><div class="alert alert-error hide"></div><div class="row">'
   314                    + '<div class="span" style="width:230px"><div>Preview:</div><div class="preview" style="width:200px;height:150px;border:solid 1px #ccc;padding:4px;margin-top:5px;background-repeat:no-repeat;background-position:center center;background-size:cover;"></div></div>'
   315                    + '<div class="span" style="width:300px"><form>'
   316                    + '<label>Text:</label><input name="text" type="text" value="" />'
   317                    + '<label>File:</label><input name="file" type="file" />'
   318                    + '<label>Progress:</label><div class="progress progress-striped active" style="width:220px; margin-top:6px;margin-bottom:6px"><div class="bar" style="width:0%;"></div></div>'
   319                    + '</form></div>'
   320                    + '</div></div></div><div class="modal-footer"><button class="btn btn-primary">OK</button> <button class="btn btn-cancel">Close</button></div></div>';
   321              $('body').prepend(s);
   322              var $modal = $('body').children(':first');
   323              var $form = $modal.find('form');
   324              var $text = $modal.find('input[name="text"]');
   325              var $file = $modal.find('input[name="file"]');
   326              var $prog = $modal.find('div.bar');
   327              var $preview = $modal.find('div.preview');
   328              var $alert = $modal.find('div.alert');
   329              var $status = { 'uploading': false };
   330              $modal.modal('show');
   331              $file.change(function() {
   332                  // clear error:
   333                  $alert.removeClass('alert-error').hide();
   334                  var f = $file.val();
   335                  if (!f) {
   336                      $preview.css('background-image', '');
   337                      return;
   338                  }
   339                  var lf = $file.get(0).files[0];
   340                  var ft = lf.type;
   341                  if (ft=='image/png' || ft=='image/jpeg' || ft=='image/gif') {
   342                      $preview.css('background-image', 'url(' + getObjectURL(lf) + ')');
   343                      if ($text.val()=='') {
   344                          // extract filename without ext:
   345                          var pos = Math.max(f.lastIndexOf('\\'), f.lastIndexOf('/'));
   346                          if (pos>0) {
   347                              f = f.substring(pos + 1);
   348                          }
   349                          var pos = f.lastIndexOf('.');
   350                          if (pos>0) {
   351                              f = f.substring(0, pos);
   352                          }
   353                          $text.val(f);
   354                      }
   355                  }
   356                  else {
   357                      $preview.css('background-image', '');
   358                      $alert.text('Not a valid web image.').show();
   359                  }
   360              });
   361              var cancel = function() {
   362                  if ($status.uploading) {
   363                      if ( ! confirm('File is uploading, are you sure you want to cancel it?')) {
   364                          return;
   365                      }
   366                      if ($status.uploading) {
   367                          $status.xhr.abort();
   368                      }
   369                  }
   370                  $modal.modal('hide');
   371              };
   372              $modal.find('button.close').click(cancel);
   373              $modal.find('button.btn-cancel').click(cancel);
   374              $modal.find('.btn-primary').click(function() {
   375                  // clear error:
   376                  $alert.removeClass('alert-error').hide();
   377                  // upload file:
   378                  var f = $file.val();
   379                  if (!f) {
   380                      $alert.text('Please select file.').addClass('alert-error').show();
   381                      return;
   382                  }
   383                  var $url = delegate.getOption('upload_image_url');
   384                  if (!$url) {
   385                      $alert.text('upload_image_url not defined.').addClass('alert-error').show();
   386                      return;
   387                  }
   388                  try {
   389                      var text = $text.val();
   390                      var lf = $file.get(0).files[0];
   391                      // send XMLHttpRequest2:
   392                      var fd = null;
   393                      var form = $form.get(0);
   394                      try {
   395                          fd = form.getFormData();
   396                      }
   397                      catch(e) {
   398                          fd = new FormData(form);
   399                      }
   400                      var xhr = new XMLHttpRequest();
   401                      xhr.upload.addEventListener('progress', function(evt) {
   402                          if (evt.lengthComputable) {
   403                              var percent = evt.loaded * 100.0 / evt.total;
   404                              $prog.css('width', percent.toFixed(1) + '%');
   405                          }
   406                      }, false);
   407                      xhr.addEventListener('load', function(evt) {
   408                          var r = $.parseJSON(evt.target.responseText);
   409                          if (r.error) {
   410                              $alert.addClass('alert-error').text(r.message || r.error).show();
   411                              $status.uploading = false;
   412                          }
   413                          else {
   414                              // upload ok!
   415                              delegate.unselect();
   416                              var s = '\n![' + text.replace('[', '').replace(']', '') + '](' + r.url + ')\n';
   417                              delegate.paste(s);
   418                              delegate.setSelection(delegate.getCaretPosition() + 1, delegate.getCaretPosition() + s.length - 1);
   419                              $modal.modal('hide');
   420                          }
   421                      }, false);
   422                      xhr.addEventListener('error', function(evt) {
   423                          $alert.addClass('alert-error').text('Error: upload failed.').show();
   424                          $status.uploading = false;
   425                      }, false);
   426                      xhr.addEventListener('abort', function(evt) {
   427                          $status.uploading = false;
   428                      }, false);
   429                      xhr.open('post', $url);
   430                      xhr.send(fd);
   431                      $status.uploading = true;
   432                      $status.xhr = xhr;
   433                      $file.attr('disabled', 'disabled');
   434                  }
   435                  catch(e) {
   436                      $alert.addClass('alert-error').text('Could not upload.').show();
   437                  }
   438                  $(this).attr('disabled', 'disabled');
   439              });
   440              $modal.on('hidden', function() {
   441                  $modal.remove();
   442              });
   443          },
   444  
   445          preview: function(delegate) {
   446              if ( ! delegate.is_preview) {
   447                  delegate.is_preview = true;
   448                  delegate.enableAllButtons(false);
   449                  delegate.enableButton('preview', true);
   450                  delegate.$textarea.hide();
   451                  delegate.$preview.html('<div style="padding:3px;">' + markdown2html(delegate.$textarea.val()) + '</div>').show();
   452              }
   453              else {
   454                  delegate.is_preview = false;
   455                  delegate.enableAllButtons(true);
   456                  delegate.$preview.html('').hide();
   457                  delegate.$textarea.show();
   458              }
   459          },
   460  
   461          fullscreen: function(delegate) {
   462              if ( ! delegate.is_full_screen) {
   463                  delegate.is_full_screen = true;
   464                  delegate.enableButton('preview', false);
   465                  // z-index=1040, on top of navbar, but on bottom of other modals:
   466                  var s = '<div data-backdrop="false" class="modal hide" style="z-index:1040;top:0;left:0;margin-left:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;">'
   467                        + '<div class="left" style="margin:0;padding:0 2px 2px 2px;float:left;"></div><div class="right" style="float:left;padding:0;margin:0;border-left:solid 1px #ccc;overflow:scroll;"></div>'
   468                        + '</div>';
   469                  $('body').prepend(s);
   470                  var $modal = $('body').children(':first');
   471                  var $left = $modal.find('div.left');
   472                  var $right = $modal.find('div.right');
   473                  $modal.modal('show');
   474                  delegate.$fullscreen = $modal;
   475                  delegate.$toolbar.appendTo($left);
   476                  delegate.$textarea.appendTo($left);
   477                  // store old width and height for textarea:
   478                  delegate.$textarea_old_width = delegate.$textarea.css('width');
   479                  delegate.$textarea_old_height = delegate.$textarea.css('height');
   480                  // bind resize:
   481                  delegate.$fn_resize = function() {
   482                      var w = $(window).width();
   483                      var h = $(window).height();
   484                      if (w<960) { w = 960; }
   485                      if (h<300) { h = 300; }
   486                      console.log('resize to: ' + w + ', ' + h);
   487                      var rw = parseInt(w / 2);
   488                      var $dom = delegate.$fullscreen;
   489                      $dom.css('width', w + 'px');
   490                      $dom.css('height', h + 'px');
   491                      $dom.find('div.right').css('width', (rw - 1) + 'px').css('height', h + 'px');
   492                      $dom.find('div.left').css('width', (w - rw - 4) + 'px').css('height', h + 'px');
   493                      delegate.$textarea.css('width', (w - rw - 18) + 'px').css('height', (h - 64) + 'px');
   494                  };
   495                  $(window).bind('resize', delegate.$fn_resize).trigger('resize');
   496                  $right.html(markdown2html(delegate.getText()));
   497                  // bind text change:
   498                  delegate.$n_wait_for_update = 0;
   499                  delegate.$b_need_update = false;
   500                  delegate.$fn_update_count = function() {
   501                      if (delegate.$b_need_update && delegate.$n_wait_for_update > 10) {
   502                          delegate.$b_need_update = false;
   503                          delegate.$n_wait_for_update = 0;
   504                          $right.html(markdown2html(delegate.getText()));
   505                      }
   506                      else {
   507                          delegate.$n_wait_for_update ++;
   508                      }
   509                  };
   510                  setInterval(delegate.$fn_update_count, 100);
   511                  delegate.$fn_keypress = function() {
   512                      console.log('Keypress...');
   513                      delegate.$b_need_update = true; // should update in N seconds
   514                      delegate.$n_wait_for_update = 0; // reset count from 0
   515                  };
   516                  delegate.$textarea.bind('keypress', delegate.$fn_keypress);
   517              }
   518              else {
   519                  // unbind:
   520                  delegate.$textarea.unbind('keypress', delegate.$fn_keypress);
   521                  $(window).unbind('resize', delegate.$fn_resize);
   522                  delegate.$fn_keypress = null;
   523                  delegate.$fn_resize = null;
   524                  delegate.$fn_update_count = null;
   525  
   526                  delegate.is_full_screen = false;
   527                  delegate.enableButton('preview', true);
   528                  delegate.$toolbar.appendTo(delegate.$container);
   529                  delegate.$preview.appendTo(delegate.$container);
   530                  delegate.$textarea.appendTo(delegate.$container);
   531                  delegate.$fullscreen.modal('hide');
   532                  delegate.$fullscreen.remove();
   533                  // restore width & height:
   534                  delegate.$textarea.css('width', delegate.$textarea_old_width).css('height', delegate.$textarea_old_height);
   535              }
   536          },
   537  
   538      };
   539  
   540      $.fn.markdown = function(option) {
   541          return this.each(function() {
   542              var $this = $(this);
   543              var data = $this.data('markdown');
   544              var options = $.extend({}, $.fn.markdown.defaults, typeof option == 'object' && option);
   545              if (!data) {
   546                  data = new Markdown(this, options, commands);
   547                  $this.data('markdown', data);
   548              }
   549          });
   550      };
   551  
   552      $.fn.markdown.defaults = {
   553          buttons: [
   554              'heading',
   555              '|',
   556              'bold', 'italic', 'ul', 'quote',
   557              '|',
   558              'link', 'email',
   559              '|',
   560              'image', 'video',
   561              '|',
   562              'preview',
   563              '|',
   564              'fullscreen',
   565          ],
   566          tooltips: {
   567              'heading': 'Set Heading',
   568              'bold': 'Bold',
   569              'italic': 'Italic',
   570              'ul': 'Unordered List',
   571              'quote': 'Quote',
   572              'link': 'Insert URL',
   573              'email': 'Insert email address',
   574              'image': 'Insert image',
   575              'video': 'Insert video',
   576              'preview': 'Preview content',
   577              'fullscreen': 'Fullscreen mode',
   578          },
   579          icons: {
   580              'heading': 'icon-font',
   581              'bold': 'icon-bold',
   582              'italic': 'icon-italic',
   583              'ul': 'icon-list',
   584              'quote': 'icon-comment',
   585              'link': 'icon-globe',
   586              'email': 'icon-envelope',
   587              'image': 'icon-picture',
   588              'video': 'icon-facetime-video',
   589              'preview': 'icon-eye-open',
   590              'fullscreen': 'icon-fullscreen icon-white',
   591          },
   592          upload_image_url: '',
   593          upload_file_url: '',
   594      };
   595  
   596      $.fn.markdown.Constructor = Markdown;
   597  
   598  }(window.jQuery);