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">×</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">×</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">×</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);