hjg
2024-10-30 8cf23534166c07e711aac2a25911ada317ba01f0
提交 | 用户 | 时间
58d006 1 /* ===================================================
A 2  * bootstrap-markdown.js v2.9.0
3  * http://github.com/toopay/bootstrap-markdown
4  * ===================================================
5  * Copyright 2013-2015 Taufan Aditya
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  * http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ========================================================== */
19
20 !function ($) {
21
22   "use strict"; // jshint ;_;
23
24   /* MARKDOWN CLASS DEFINITION
25    * ========================== */
26
27   var Markdown = function (element, options) {
28     // @TODO : remove this BC on next major release
29     // @see : https://github.com/toopay/bootstrap-markdown/issues/109
30     var opts = ['autofocus', 'savable', 'hideable', 'width', 
31       'height', 'resize', 'iconlibrary', 'language', 
32       'footer', 'fullscreen', 'hiddenButtons', 'disabledButtons'];
33     $.each(opts,function(_, opt){
34       if (typeof $(element).data(opt) !== 'undefined') {
35         options = typeof options == 'object' ? options : {}
36         options[opt] = $(element).data(opt)
37       }
38     });
39     // End BC
40
41     // Class Properties
42     this.$ns           = 'bootstrap-markdown';
43     this.$element      = $(element);
44     this.$editable     = {el:null, type:null,attrKeys:[], attrValues:[], content:null};
45     this.$options      = $.extend(true, {}, $.fn.markdown.defaults, options, this.$element.data('options'));
46     this.$oldContent   = null;
47     this.$isPreview    = false;
48     this.$isFullscreen = false;
49     this.$editor       = null;
50     this.$textarea     = null;
51     this.$handler      = [];
52     this.$callback     = [];
53     this.$nextTab      = [];
54
55     this.showEditor();
56   };
57
58   Markdown.prototype = {
59
60     constructor: Markdown
61
62   , __alterButtons: function(name,alter) {
63       var handler = this.$handler, isAll = (name == 'all'),that = this;
64
65       $.each(handler,function(k,v) {
66         var halt = true;
67         if (isAll) {
68           halt = false;
69         } else {
70           halt = v.indexOf(name) < 0;
71         }
72
73         if (halt === false) {
74           alter(that.$editor.find('button[data-handler="'+v+'"]'));
75         }
76       });
77     }
78
79   , __buildButtons: function(buttonsArray, container) {
80       var i,
81           ns = this.$ns,
82           handler = this.$handler,
83           callback = this.$callback;
84
85       for (i=0;i<buttonsArray.length;i++) {
86         // Build each group container
87         var y, btnGroups = buttonsArray[i];
88         for (y=0;y<btnGroups.length;y++) {
89           // Build each button group
90           var z,
91               buttons = btnGroups[y].data,
92               btnGroupContainer = $('<div/>', {
93                                     'class': 'btn-group'
94                                   });
95
96           for (z=0;z<buttons.length;z++) {
97             var button = buttons[z],
98                 buttonContainer, buttonIconContainer,
99                 buttonHandler = ns+'-'+button.name,
100                 buttonIcon = this.__getIcon(button.icon),
101                 btnText = button.btnText ? button.btnText : '',
102                 btnClass = button.btnClass ? button.btnClass : 'btn',
103                 tabIndex = button.tabIndex ? button.tabIndex : '-1',
104                 hotkey = typeof button.hotkey !== 'undefined' ? button.hotkey : '',
105                 hotkeyCaption = typeof jQuery.hotkeys !== 'undefined' && hotkey !== '' ? ' ('+hotkey+')' : '';
106
107             // Construct the button object
108             buttonContainer = $('<button></button>');
109             buttonContainer.text(' ' + this.__localize(btnText)).addClass('btn-default btn-sm').addClass(btnClass);
110             if(btnClass.match(/btn\-(primary|success|info|warning|danger|link)/)){
111                 buttonContainer.removeClass('btn-default');
112             }
113             buttonContainer.attr({
114                 'type': 'button',
115                 'title': this.__localize(button.title) + hotkeyCaption,
116                 'tabindex': tabIndex,
117                 'data-provider': ns,
118                 'data-handler': buttonHandler,
119                 'data-hotkey': hotkey
120             });
121             if (button.toggle === true){
122               buttonContainer.attr('data-toggle', 'button');
123             }
124             buttonIconContainer = $('<span/>');
125             buttonIconContainer.addClass(buttonIcon);
126             buttonIconContainer.prependTo(buttonContainer);
127
128             // Attach the button object
129             btnGroupContainer.append(buttonContainer);
130
131             // Register handler and callback
132             handler.push(buttonHandler);
133             callback.push(button.callback);
134           }
135
136           // Attach the button group into container dom
137           container.append(btnGroupContainer);
138         }
139       }
140
141       return container;
142     }
143   , __setListener: function() {
144       // Set size and resizable Properties
145       var hasRows = typeof this.$textarea.attr('rows') !== 'undefined',
146           maxRows = this.$textarea.val().split("\n").length > 5 ? this.$textarea.val().split("\n").length : '5',
147           rowsVal = hasRows ? this.$textarea.attr('rows') : maxRows;
148
149       this.$textarea.attr('rows',rowsVal);
150       if (this.$options.resize) {
151         this.$textarea.css('resize',this.$options.resize);
152       }
153
154       this.$textarea
155         .on('focus',    $.proxy(this.focus, this))
156         .on('keypress', $.proxy(this.keypress, this))
157         .on('keyup',    $.proxy(this.keyup, this))
158         .on('change',   $.proxy(this.change, this))
159         .on('select',   $.proxy(this.select, this));
160
161       if (this.eventSupported('keydown')) {
162         this.$textarea.on('keydown', $.proxy(this.keydown, this));
163       }
164
165       // Re-attach markdown data
166       this.$textarea.data('markdown',this);
167     }
168
169   , __handle: function(e) {
170       var target = $(e.currentTarget),
171           handler = this.$handler,
172           callback = this.$callback,
173           handlerName = target.attr('data-handler'),
174           callbackIndex = handler.indexOf(handlerName),
175           callbackHandler = callback[callbackIndex];
176
177       // Trigger the focusin
178       $(e.currentTarget).focus();
179
180       callbackHandler(this);
181
182       // Trigger onChange for each button handle
183       this.change(this);
184
185       // Unless it was the save handler,
186       // focusin the textarea
187       if (handlerName.indexOf('cmdSave') < 0) {
188         this.$textarea.focus();
189       }
190
191       e.preventDefault();
192     }
193
194   , __localize: function(string) {
195       var messages = $.fn.markdown.messages,
196           language = this.$options.language;
197       if (
198         typeof messages !== 'undefined' &&
199         typeof messages[language] !== 'undefined' &&
200         typeof messages[language][string] !== 'undefined'
201       ) {
202         return messages[language][string];
203       }
204       return string;
205     }
206
207   , __getIcon: function(src) {
208     return typeof src == 'object' ? src[this.$options.iconlibrary] : src;
209   }
210
211   , setFullscreen: function(mode) {
212     var $editor = this.$editor,
213         $textarea = this.$textarea;
214
215     if (mode === true) {
216       $editor.addClass('md-fullscreen-mode');
217       $('body').addClass('md-nooverflow');
218       this.$options.onFullscreen(this);
219     } else {
220       $editor.removeClass('md-fullscreen-mode');
221       $('body').removeClass('md-nooverflow');
222
223       if (this.$isPreview == true) this.hidePreview().showPreview()
224     }
225
226     this.$isFullscreen = mode;
227     $textarea.focus();
228   }
229
230   , showEditor: function() {
231       var instance = this,
232           textarea,
233           ns = this.$ns,
234           container = this.$element,
235           originalHeigth = container.css('height'),
236           originalWidth = container.css('width'),
237           editable = this.$editable,
238           handler = this.$handler,
239           callback = this.$callback,
240           options = this.$options,
241           editor = $( '<div/>', {
242                       'class': 'md-editor',
243                       click: function() {
244                         instance.focus();
245                       }
246                     });
247
248       // Prepare the editor
249       if (this.$editor === null) {
250         // Create the panel
251         var editorHeader = $('<div/>', {
252                             'class': 'md-header btn-toolbar'
253                             });
254
255         // Merge the main & additional button groups together
256         var allBtnGroups = [];
257         if (options.buttons.length > 0) allBtnGroups = allBtnGroups.concat(options.buttons[0]);
258         if (options.additionalButtons.length > 0) allBtnGroups = allBtnGroups.concat(options.additionalButtons[0]);
259
260         // Reduce and/or reorder the button groups
261         if (options.reorderButtonGroups.length > 0) {
262           allBtnGroups = allBtnGroups
263               .filter(function(btnGroup) {
264                 return options.reorderButtonGroups.indexOf(btnGroup.name) > -1;
265               })
266               .sort(function(a, b) {
267                 if (options.reorderButtonGroups.indexOf(a.name) < options.reorderButtonGroups.indexOf(b.name)) return -1;
268                 if (options.reorderButtonGroups.indexOf(a.name) > options.reorderButtonGroups.indexOf(b.name)) return 1;
269                 return 0;
270               });
271         }
272
273         // Build the buttons
274         if (allBtnGroups.length > 0) {
275           editorHeader = this.__buildButtons([allBtnGroups], editorHeader);
276         }
277
278         if (options.fullscreen.enable) {
279           editorHeader.append('<div class="md-controls"><a class="md-control md-control-fullscreen" href="#"><span class="'+this.__getIcon(options.fullscreen.icons.fullscreenOn)+'"></span></a></div>').on('click', '.md-control-fullscreen', function(e) {
280               e.preventDefault();
281               instance.setFullscreen(true);
282           });
283         }
284
285         editor.append(editorHeader);
286
287         // Wrap the textarea
288         if (container.is('textarea')) {
289           container.before(editor);
290           textarea = container;
291           textarea.addClass('md-input');
292           editor.append(textarea);
293         } else {
294           var rawContent = (typeof toMarkdown == 'function') ? toMarkdown(container.html()) : container.html(),
295               currentContent = $.trim(rawContent);
296
297           // This is some arbitrary content that could be edited
298           textarea = $('<textarea/>', {
299                        'class': 'md-input',
300                        'val' : currentContent
301                       });
302
303           editor.append(textarea);
304
305           // Save the editable
306           editable.el = container;
307           editable.type = container.prop('tagName').toLowerCase();
308           editable.content = container.html();
309
310           $(container[0].attributes).each(function(){
311             editable.attrKeys.push(this.nodeName);
312             editable.attrValues.push(this.nodeValue);
313           });
314
315           // Set editor to blocked the original container
316           container.replaceWith(editor);
317         }
318
319         var editorFooter = $('<div/>', {
320                            'class': 'md-footer'
321                          }),
322             createFooter = false,
323             footer = '';
324         // Create the footer if savable
325         if (options.savable) {
326           createFooter = true;
327           var saveHandler = 'cmdSave';
328
329           // Register handler and callback
330           handler.push(saveHandler);
331           callback.push(options.onSave);
332
333           editorFooter.append('<button class="btn btn-success" data-provider="'
334                               + ns
335                               + '" data-handler="'
336                               + saveHandler
337                               + '"><i class="icon icon-white icon-ok"></i> '
338                               + this.__localize('Save')
339                               + '</button>');
340
341
342         }
343
344         footer = typeof options.footer === 'function' ? options.footer(this) : options.footer;
345
346         if ($.trim(footer) !== '') {
347           createFooter = true;
348           editorFooter.append(footer);
349         }
350
351         if (createFooter) editor.append(editorFooter);
352
353         // Set width
354         if (options.width && options.width !== 'inherit') {
355           if (jQuery.isNumeric(options.width)) {
356             editor.css('display', 'table');
357             textarea.css('width', options.width + 'px');
358           } else {
359             editor.addClass(options.width);
360           }
361         }
362
363         // Set height
364         if (options.height && options.height !== 'inherit') {
365           if (jQuery.isNumeric(options.height)) {
366             var height = options.height;
367             if (editorHeader) height = Math.max(0, height - editorHeader.outerHeight());
368             if (editorFooter) height = Math.max(0, height - editorFooter.outerHeight());
369             textarea.css('height', height + 'px');
370           } else {
371             editor.addClass(options.height);
372           }
373         }
374
375         // Reference
376         this.$editor     = editor;
377         this.$textarea   = textarea;
378         this.$editable   = editable;
379         this.$oldContent = this.getContent();
380
381         this.__setListener();
382
383         // Set editor attributes, data short-hand API and listener
384         this.$editor.attr('id',(new Date()).getTime());
385         this.$editor.on('click', '[data-provider="bootstrap-markdown"]', $.proxy(this.__handle, this));
386
387         if (this.$element.is(':disabled') || this.$element.is('[readonly]')) {
388           this.$editor.addClass('md-editor-disabled');
389           this.disableButtons('all');
390         }
391
392         if (this.eventSupported('keydown') && typeof jQuery.hotkeys === 'object') {
393           editorHeader.find('[data-provider="bootstrap-markdown"]').each(function() {
394             var $button = $(this),
395                 hotkey = $button.attr('data-hotkey');
396             if (hotkey.toLowerCase() !== '') {
397               textarea.bind('keydown', hotkey, function() {
398                 $button.trigger('click');
399                 return false;
400               });
401             }
402           });
403         }
404
405         if (options.initialstate === 'preview') {
406           this.showPreview();
407         } else if (options.initialstate === 'fullscreen' && options.fullscreen.enable) {
408           this.setFullscreen(true);
409         }
410
411       } else {
412         this.$editor.show();
413       }
414
415       if (options.autofocus) {
416         this.$textarea.focus();
417         this.$editor.addClass('active');
418       }
419
420       if (options.fullscreen.enable && options.fullscreen !== false) {
421         this.$editor.append('<div class="md-fullscreen-controls">'
422                         + '<a href="#" class="exit-fullscreen" title="Exit fullscreen"><span class="' + this.__getIcon(options.fullscreen.icons.fullscreenOff) + '">'
423                         + '</span></a>'
424                         + '</div>');
425         this.$editor.on('click', '.exit-fullscreen', function(e) {
426           e.preventDefault();
427           instance.setFullscreen(false);
428         });
429       }
430
431       // hide hidden buttons from options
432       this.hideButtons(options.hiddenButtons);
433
434       // disable disabled buttons from options
435       this.disableButtons(options.disabledButtons);
436
437       // Trigger the onShow hook
438       options.onShow(this);
439
440       return this;
441     }
442
443   , parseContent: function(val) {
444       var content;
445
446       // parse with supported markdown parser
447       var val = val || this.$textarea.val();
448
449       if (this.$options.parser) {
450         content = this.$options.parser(val);
451       } else if (typeof markdown == 'object') {
452         content = markdown.toHTML(val);
453       } else if (typeof marked == 'function') {
454         content = marked(val);
455       } else {
456         content = val;
457       }
458
459       return content;
460     }
461
462   , showPreview: function() {
463       var options = this.$options,
464           container = this.$textarea,
465           afterContainer = container.next(),
466           replacementContainer = $('<div/>',{'class':'md-preview','data-provider':'markdown-preview'}),
467           content,
468           callbackContent;
469
470       if (this.$isPreview == true) {
471         // Avoid sequenced element creation on missused scenario
472         // @see https://github.com/toopay/bootstrap-markdown/issues/170
473         return this;
474       }
475       
476       // Give flag that tell the editor enter preview mode
477       this.$isPreview = true;
478       // Disable all buttons
479       this.disableButtons('all').enableButtons('cmdPreview');
480
481       // Try to get the content from callback
482       callbackContent = options.onPreview(this);
483       // Set the content based from the callback content if string otherwise parse value from textarea
484       content = typeof callbackContent == 'string' ? callbackContent : this.parseContent();
485
486       // Build preview element
487       replacementContainer.html(content);
488
489       if (afterContainer && afterContainer.attr('class') == 'md-footer') {
490         // If there is footer element, insert the preview container before it
491         replacementContainer.insertBefore(afterContainer);
492       } else {
493         // Otherwise, just append it after textarea
494         container.parent().append(replacementContainer);
495       }
496
497       // Set the preview element dimensions
498       replacementContainer.css({
499         width: container.outerWidth() + 'px',
500         height: container.outerHeight() + 'px'
501       });
502
503       if (this.$options.resize) {
504         replacementContainer.css('resize',this.$options.resize);
505       }
506
507       // Hide the last-active textarea
508       container.hide();
509
510       // Attach the editor instances
511       replacementContainer.data('markdown',this);
512
513       if (this.$element.is(':disabled') || this.$element.is('[readonly]')) {
514         this.$editor.addClass('md-editor-disabled');
515         this.disableButtons('all');
516       }
517
518       return this;
519     }
520
521   , hidePreview: function() {
522       // Give flag that tell the editor quit preview mode
523       this.$isPreview = false;
524
525       // Obtain the preview container
526       var container = this.$editor.find('div[data-provider="markdown-preview"]');
527
528       // Remove the preview container
529       container.remove();
530
531       // Enable all buttons
532       this.enableButtons('all');
533       // Disable configured disabled buttons
534       this.disableButtons(this.$options.disabledButtons);
535
536       // Back to the editor
537       this.$textarea.show();
538       this.__setListener();
539
540       return this;
541     }
542
543   , isDirty: function() {
544       return this.$oldContent != this.getContent();
545     }
546
547   , getContent: function() {
548       return this.$textarea.val();
549     }
550
551   , setContent: function(content) {
552       this.$textarea.val(content);
553
554       return this;
555     }
556
557   , findSelection: function(chunk) {
558     var content = this.getContent(), startChunkPosition;
559
560     if (startChunkPosition = content.indexOf(chunk), startChunkPosition >= 0 && chunk.length > 0) {
561       var oldSelection = this.getSelection(), selection;
562
563       this.setSelection(startChunkPosition,startChunkPosition+chunk.length);
564       selection = this.getSelection();
565
566       this.setSelection(oldSelection.start,oldSelection.end);
567
568       return selection;
569     } else {
570       return null;
571     }
572   }
573
574   , getSelection: function() {
575
576       var e = this.$textarea[0];
577
578       return (
579
580           ('selectionStart' in e && function() {
581               var l = e.selectionEnd - e.selectionStart;
582               return { start: e.selectionStart, end: e.selectionEnd, length: l, text: e.value.substr(e.selectionStart, l) };
583           }) ||
584
585           /* browser not supported */
586           function() {
587             return null;
588           }
589
590       )();
591
592     }
593
594   , setSelection: function(start,end) {
595
596       var e = this.$textarea[0];
597
598       return (
599
600           ('selectionStart' in e && function() {
601               e.selectionStart = start;
602               e.selectionEnd = end;
603               return;
604           }) ||
605
606           /* browser not supported */
607           function() {
608             return null;
609           }
610
611       )();
612
613     }
614
615   , replaceSelection: function(text) {
616
617       var e = this.$textarea[0];
618
619       return (
620
621           ('selectionStart' in e && function() {
622               e.value = e.value.substr(0, e.selectionStart) + text + e.value.substr(e.selectionEnd, e.value.length);
623               // Set cursor to the last replacement end
624               e.selectionStart = e.value.length;
625               return this;
626           }) ||
627
628           /* browser not supported */
629           function() {
630               e.value += text;
631               return jQuery(e);
632           }
633
634       )();
635     }
636
637   , getNextTab: function() {
638       // Shift the nextTab
639       if (this.$nextTab.length === 0) {
640         return null;
641       } else {
642         var nextTab, tab = this.$nextTab.shift();
643
644         if (typeof tab == 'function') {
645           nextTab = tab();
646         } else if (typeof tab == 'object' && tab.length > 0) {
647           nextTab = tab;
648         }
649
650         return nextTab;
651       }
652     }
653
654   , setNextTab: function(start,end) {
655       // Push new selection into nextTab collections
656       if (typeof start == 'string') {
657         var that = this;
658         this.$nextTab.push(function(){
659           return that.findSelection(start);
660         });
661       } else if (typeof start == 'number' && typeof end == 'number') {
662         var oldSelection = this.getSelection();
663
664         this.setSelection(start,end);
665         this.$nextTab.push(this.getSelection());
666
667         this.setSelection(oldSelection.start,oldSelection.end);
668       }
669
670       return;
671     }
672
673   , __parseButtonNameParam: function(nameParam) {
674       var buttons = [];
675
676       if (typeof nameParam == 'string') {
677         buttons = nameParam.split(',')
678       } else {
679         buttons = nameParam;
680       }
681
682       return buttons;
683     }
684
685   , enableButtons: function(name) {
686       var buttons = this.__parseButtonNameParam(name),
687         that = this;
688
689       $.each(buttons, function(i, v) {
690         that.__alterButtons(buttons[i], function (el) {
691           el.removeAttr('disabled');
692         });
693       });
694
695       return this;
696     }
697
698   , disableButtons: function(name) {
699       var buttons = this.__parseButtonNameParam(name),
700         that = this;
701
702       $.each(buttons, function(i, v) {
703         that.__alterButtons(buttons[i], function (el) {
704           el.attr('disabled','disabled');
705         });
706       });
707
708       return this;
709     }
710
711   , hideButtons: function(name) {
712       var buttons = this.__parseButtonNameParam(name),
713         that = this;
714
715       $.each(buttons, function(i, v) {
716         that.__alterButtons(buttons[i], function (el) {
717           el.addClass('hidden');
718         });
719       });
720
721       return this;
722     }
723
724   , showButtons: function(name) {
725       var buttons = this.__parseButtonNameParam(name),
726         that = this;
727
728       $.each(buttons, function(i, v) {
729         that.__alterButtons(buttons[i], function (el) {
730           el.removeClass('hidden');
731         });
732       });
733
734       return this;
735     }
736
737   , eventSupported: function(eventName) {
738       var isSupported = eventName in this.$element;
739       if (!isSupported) {
740         this.$element.setAttribute(eventName, 'return;');
741         isSupported = typeof this.$element[eventName] === 'function';
742       }
743       return isSupported;
744     }
745
746   , keyup: function (e) {
747       var blocked = false;
748       switch(e.keyCode) {
749         case 40: // down arrow
750         case 38: // up arrow
751         case 16: // shift
752         case 17: // ctrl
753         case 18: // alt
754           break;
755
756         case 9: // tab
757           var nextTab;
758           if (nextTab = this.getNextTab(),nextTab !== null) {
759             // Get the nextTab if exists
760             var that = this;
761             setTimeout(function(){
762               that.setSelection(nextTab.start,nextTab.end);
763             },500);
764
765             blocked = true;
766           } else {
767             // The next tab memory contains nothing...
768             // check the cursor position to determine tab action
769             var cursor = this.getSelection();
770
771             if (cursor.start == cursor.end && cursor.end == this.getContent().length) {
772               // The cursor already reach the end of the content
773               blocked = false;
774             } else {
775               // Put the cursor to the end
776               this.setSelection(this.getContent().length,this.getContent().length);
777
778               blocked = true;
779             }
780           }
781
782           break;
783
784         case 13: // enter
785           blocked = false;
786           break;
787         case 27: // escape
788           if (this.$isFullscreen) this.setFullscreen(false);
789           blocked = false;
790           break;
791
792         default:
793           blocked = false;
794       }
795
796       if (blocked) {
797         e.stopPropagation();
798         e.preventDefault();
799       }
800
801       this.$options.onChange(this);
802     }
803
804   , change: function(e) {
805       this.$options.onChange(this);
806       return this;
807     }
808   , select: function (e) {
809       this.$options.onSelect(this);
810       return this;
811     }
812   , focus: function (e) {
813       var options = this.$options,
814           isHideable = options.hideable,
815           editor = this.$editor;
816
817       editor.addClass('active');
818
819       // Blur other markdown(s)
820       $(document).find('.md-editor').each(function(){
821         if ($(this).attr('id') !== editor.attr('id')) {
822           var attachedMarkdown;
823
824           if (attachedMarkdown = $(this).find('textarea').data('markdown'),
825               attachedMarkdown === null) {
826               attachedMarkdown = $(this).find('div[data-provider="markdown-preview"]').data('markdown');
827           }
828
829           if (attachedMarkdown) {
830             attachedMarkdown.blur();
831           }
832         }
833       });
834
835       // Trigger the onFocus hook
836       options.onFocus(this);
837
838       return this;
839     }
840
841   , blur: function (e) {
842       var options = this.$options,
843           isHideable = options.hideable,
844           editor = this.$editor,
845           editable = this.$editable;
846
847       if (editor.hasClass('active') || this.$element.parent().length === 0) {
848         editor.removeClass('active');
849
850         if (isHideable) {
851           // Check for editable elements
852           if (editable.el !== null) {
853             // Build the original element
854             var oldElement = $('<'+editable.type+'/>'),
855                 content = this.getContent(),
856                 currentContent = this.parseContent(content);
857
858             $(editable.attrKeys).each(function(k,v) {
859               oldElement.attr(editable.attrKeys[k],editable.attrValues[k]);
860             });
861
862             // Get the editor content
863             oldElement.html(currentContent);
864
865             editor.replaceWith(oldElement);
866           } else {
867             editor.hide();
868           }
869         }
870
871         // Trigger the onBlur hook
872         options.onBlur(this);
873       }
874
875       return this;
876     }
877
878   };
879
880  /* MARKDOWN PLUGIN DEFINITION
881   * ========================== */
882
883   var old = $.fn.markdown;
884
885   $.fn.markdown = function (option) {
886     return this.each(function () {
887       var $this = $(this)
888         , data = $this.data('markdown')
889         , options = typeof option == 'object' && option;
890       if (!data) $this.data('markdown', (data = new Markdown(this, options)))
891     })
892   };
893
894   $.fn.markdown.messages = {};
895
896   $.fn.markdown.defaults = {
897     /* Editor Properties */
898     autofocus: false,
899     hideable: false,
900     savable: false,
901     width: 'inherit',
902     height: 'inherit',
903     resize: 'none',
904     iconlibrary: 'glyph',
905     language: 'en',
906     initialstate: 'editor',
907     parser: null,
908
909     /* Buttons Properties */
910     buttons: [
911       [{
912         name: 'groupFont',
913         data: [{
914           name: 'cmdBold',
915           hotkey: 'Ctrl+B',
916           title: 'Bold',
917           icon: { glyph: 'glyphicon glyphicon-bold', fa: 'fa fa-bold', 'fa-3': 'icon-bold' },
918           callback: function(e){
919             // Give/remove ** surround the selection
920             var chunk, cursor, selected = e.getSelection(), content = e.getContent();
921
922             if (selected.length === 0) {
923               // Give extra word
924               chunk = e.__localize('strong text');
925             } else {
926               chunk = selected.text;
927             }
928
929             // transform selection and set the cursor into chunked text
930             if (content.substr(selected.start-2,2) === '**'
931                 && content.substr(selected.end,2) === '**' ) {
932               e.setSelection(selected.start-2,selected.end+2);
933               e.replaceSelection(chunk);
934               cursor = selected.start-2;
935             } else {
936               e.replaceSelection('**'+chunk+'**');
937               cursor = selected.start+2;
938             }
939
940             // Set the cursor
941             e.setSelection(cursor,cursor+chunk.length);
942           }
943         },{
944           name: 'cmdItalic',
945           title: 'Italic',
946           hotkey: 'Ctrl+I',
947           icon: { glyph: 'glyphicon glyphicon-italic', fa: 'fa fa-italic', 'fa-3': 'icon-italic' },
948           callback: function(e){
949             // Give/remove * surround the selection
950             var chunk, cursor, selected = e.getSelection(), content = e.getContent();
951
952             if (selected.length === 0) {
953               // Give extra word
954               chunk = e.__localize('emphasized text');
955             } else {
956               chunk = selected.text;
957             }
958
959             // transform selection and set the cursor into chunked text
960             if (content.substr(selected.start-1,1) === '_'
961                 && content.substr(selected.end,1) === '_' ) {
962               e.setSelection(selected.start-1,selected.end+1);
963               e.replaceSelection(chunk);
964               cursor = selected.start-1;
965             } else {
966               e.replaceSelection('_'+chunk+'_');
967               cursor = selected.start+1;
968             }
969
970             // Set the cursor
971             e.setSelection(cursor,cursor+chunk.length);
972           }
973         },{
974           name: 'cmdHeading',
975           title: 'Heading',
976           hotkey: 'Ctrl+H',
977           icon: { glyph: 'glyphicon glyphicon-header', fa: 'fa fa-header', 'fa-3': 'icon-font' },
978           callback: function(e){
979             // Append/remove ### surround the selection
980             var chunk, cursor, selected = e.getSelection(), content = e.getContent(), pointer, prevChar;
981
982             if (selected.length === 0) {
983               // Give extra word
984               chunk = e.__localize('heading text');
985             } else {
986               chunk = selected.text + '\n';
987             }
988
989             // transform selection and set the cursor into chunked text
990             if ((pointer = 4, content.substr(selected.start-pointer,pointer) === '### ')
991                 || (pointer = 3, content.substr(selected.start-pointer,pointer) === '###')) {
992               e.setSelection(selected.start-pointer,selected.end);
993               e.replaceSelection(chunk);
994               cursor = selected.start-pointer;
995             } else if (selected.start > 0 && (prevChar = content.substr(selected.start-1,1), !!prevChar && prevChar != '\n')) {
996               e.replaceSelection('\n\n### '+chunk);
997               cursor = selected.start+6;
998             } else {
999               // Empty string before element
1000               e.replaceSelection('### '+chunk);
1001               cursor = selected.start+4;
1002             }
1003
1004             // Set the cursor
1005             e.setSelection(cursor,cursor+chunk.length);
1006           }
1007         }]
1008       },{
1009         name: 'groupLink',
1010         data: [{
1011           name: 'cmdUrl',
1012           title: 'URL/Link',
1013           hotkey: 'Ctrl+L',
1014           icon: { glyph: 'glyphicon glyphicon-link', fa: 'fa fa-link', 'fa-3': 'icon-link' },
1015           callback: function(e){
1016             // Give [] surround the selection and prepend the link
1017             var chunk, cursor, selected = e.getSelection(), content = e.getContent(), link;
1018
1019             if (selected.length === 0) {
1020               // Give extra word
1021               chunk = e.__localize('enter link description here');
1022             } else {
1023               chunk = selected.text;
1024             }
1025
1026             //ACE
1027             if('bootbox' in window) {
1028                 bootbox.prompt(e.__localize('Insert Hyperlink'), function(link) {
1029                     if (link != null && link != '' && link != 'http://' && link.substr(0,4) == 'http') {
1030                       var sanitizedLink = $('<div>'+link+'</div>').text()
1031
1032                       // transform selection and set the cursor into chunked text
1033                       e.replaceSelection('['+chunk+']('+sanitizedLink+')')
1034                       cursor = selected.start+1
1035
1036                       // Set the cursor
1037                       e.setSelection(cursor,cursor+chunk.length)
1038                     }
1039                 });
1040             }
1041             else {
1042                 link = prompt(e.__localize('Insert Hyperlink'),'http://')
1043
1044                 if (link != null && link != '' && link != 'http://' && link.substr(0,4) == 'http') {
1045                   var sanitizedLink = $('<div>'+link+'</div>').text()
1046
1047                   // transform selection and set the cursor into chunked text
1048                   e.replaceSelection('['+chunk+']('+sanitizedLink+')')
1049                   cursor = selected.start+1
1050
1051                   // Set the cursor
1052                   e.setSelection(cursor,cursor+chunk.length)
1053                 }
1054             }
1055           }
1056         },{
1057           name: 'cmdImage',
1058           title: 'Image',
1059           hotkey: 'Ctrl+G',
1060           icon: { glyph: 'glyphicon glyphicon-picture', fa: 'fa fa-picture-o', 'fa-3': 'icon-picture' },
1061           callback: function(e){
1062             // Give ![] surround the selection and prepend the image link
1063             var chunk, cursor, selected = e.getSelection(), content = e.getContent(), link;
1064
1065             if (selected.length === 0) {
1066               // Give extra word
1067               chunk = e.__localize('enter image description here');
1068             } else {
1069               chunk = selected.text;
1070             }
1071
1072             //ACE
1073             if('bootbox' in window) {
1074                  bootbox.prompt(e.__localize('Insert Image Hyperlink'), function(link) {
1075                     if (link != null && link != '' && link != 'http://' && link.substr(0,4) == 'http') {
1076                       var sanitizedLink = $('<div>'+link+'</div>').text()
1077                       
1078                       // transform selection and set the cursor into chunked text
1079                       e.replaceSelection('!['+chunk+']('+sanitizedLink+' "'+e.__localize('enter image title here')+'")')
1080                       cursor = selected.start+2
1081
1082                       // Set the next tab
1083                       e.setNextTab(e.__localize('enter image title here'))
1084
1085                       // Set the cursor
1086                       e.setSelection(cursor,cursor+chunk.length)
1087                     }
1088                 });
1089             }
1090             else {
1091                 link = prompt(e.__localize('Insert Image Hyperlink'),'http://')
1092
1093                 if (link != null && link != '' && link != 'http://' && link.substr(0,4) == 'http') {
1094                   var sanitizedLink = $('<div>'+link+'</div>').text()
1095                   
1096                   // transform selection and set the cursor into chunked text
1097                   e.replaceSelection('!['+chunk+']('+sanitizedLink+' "'+e.__localize('enter image title here')+'")')
1098                   cursor = selected.start+2
1099
1100                   // Set the next tab
1101                   e.setNextTab(e.__localize('enter image title here'))
1102
1103                   // Set the cursor
1104                   e.setSelection(cursor,cursor+chunk.length)
1105                 }
1106             }
1107           }
1108         }]
1109       },{
1110         name: 'groupMisc',
1111         data: [{
1112           name: 'cmdList',
1113           hotkey: 'Ctrl+U',
1114           title: 'Unordered List',
1115           icon: { glyph: 'glyphicon glyphicon-list', fa: 'fa fa-list', 'fa-3': 'icon-list-ul' },
1116           callback: function(e){
1117             // Prepend/Give - surround the selection
1118             var chunk, cursor, selected = e.getSelection(), content = e.getContent();
1119
1120             // transform selection and set the cursor into chunked text
1121             if (selected.length === 0) {
1122               // Give extra word
1123               chunk = e.__localize('list text here');
1124
1125               e.replaceSelection('- '+chunk);
1126               // Set the cursor
1127               cursor = selected.start+2;
1128             } else {
1129               if (selected.text.indexOf('\n') < 0) {
1130                 chunk = selected.text;
1131
1132                 e.replaceSelection('- '+chunk);
1133
1134                 // Set the cursor
1135                 cursor = selected.start+2;
1136               } else {
1137                 var list = [];
1138
1139                 list = selected.text.split('\n');
1140                 chunk = list[0];
1141
1142                 $.each(list,function(k,v) {
1143                   list[k] = '- '+v;
1144                 });
1145
1146                 e.replaceSelection('\n\n'+list.join('\n'));
1147
1148                 // Set the cursor
1149                 cursor = selected.start+4;
1150               }
1151             }
1152
1153             // Set the cursor
1154             e.setSelection(cursor,cursor+chunk.length);
1155           }
1156         },
1157         {
1158           name: 'cmdListO',
1159           hotkey: 'Ctrl+O',
1160           title: 'Ordered List',
1161           icon: { glyph: 'glyphicon glyphicon-th-list', fa: 'fa fa-list-ol', 'fa-3': 'icon-list-ol' },
1162           callback: function(e) {
1163
1164             // Prepend/Give - surround the selection
1165             var chunk, cursor, selected = e.getSelection(), content = e.getContent();
1166
1167             // transform selection and set the cursor into chunked text
1168             if (selected.length === 0) {
1169               // Give extra word
1170               chunk = e.__localize('list text here');
1171               e.replaceSelection('1. '+chunk);
1172               // Set the cursor
1173               cursor = selected.start+3;
1174             } else {
1175               if (selected.text.indexOf('\n') < 0) {
1176                 chunk = selected.text;
1177
1178                 e.replaceSelection('1. '+chunk);
1179
1180                 // Set the cursor
1181                 cursor = selected.start+3;
1182               } else {
1183                 var list = [];
1184
1185                 list = selected.text.split('\n');
1186                 chunk = list[0];
1187
1188                 $.each(list,function(k,v) {
1189                   list[k] = '1. '+v;
1190                 });
1191
1192                 e.replaceSelection('\n\n'+list.join('\n'));
1193
1194                 // Set the cursor
1195                 cursor = selected.start+5;
1196               }
1197             }
1198
1199             // Set the cursor
1200             e.setSelection(cursor,cursor+chunk.length);
1201           }
1202         },
1203         {
1204           name: 'cmdCode',
1205           hotkey: 'Ctrl+K',
1206           title: 'Code',
1207           icon: { glyph: 'glyphicon glyphicon-asterisk', fa: 'fa fa-code', 'fa-3': 'icon-code' },
1208           callback: function(e) {
1209             // Give/remove ** surround the selection
1210             var chunk, cursor, selected = e.getSelection(), content = e.getContent();
1211
1212             if (selected.length === 0) {
1213               // Give extra word
1214               chunk = e.__localize('code text here');
1215             } else {
1216               chunk = selected.text;
1217             }
1218
1219             // transform selection and set the cursor into chunked text
1220             if (content.substr(selected.start-4,4) === '```\n'
1221                 && content.substr(selected.end,4) === '\n```') {
1222               e.setSelection(selected.start-4, selected.end+4);
1223               e.replaceSelection(chunk);
1224               cursor = selected.start-4;
1225             } else if (content.substr(selected.start-1,1) === '`'
1226                 && content.substr(selected.end,1) === '`') {
1227               e.setSelection(selected.start-1,selected.end+1);
1228               e.replaceSelection(chunk);
1229               cursor = selected.start-1;
1230             } else if (content.indexOf('\n') > -1) {
1231               e.replaceSelection('```\n'+chunk+'\n```');
1232               cursor = selected.start+4;
1233             } else {
1234               e.replaceSelection('`'+chunk+'`');
1235               cursor = selected.start+1;
1236             }
1237
1238             // Set the cursor
1239             e.setSelection(cursor,cursor+chunk.length);
1240           }
1241         },
1242         {
1243           name: 'cmdQuote',
1244           hotkey: 'Ctrl+Q',
1245           title: 'Quote',
1246           icon: { glyph: 'glyphicon glyphicon-comment', fa: 'fa fa-quote-left', 'fa-3': 'icon-quote-left' },
1247           callback: function(e) {
1248             // Prepend/Give - surround the selection
1249             var chunk, cursor, selected = e.getSelection(), content = e.getContent();
1250
1251             // transform selection and set the cursor into chunked text
1252             if (selected.length === 0) {
1253               // Give extra word
1254               chunk = e.__localize('quote here');
1255
1256               e.replaceSelection('> '+chunk);
1257
1258               // Set the cursor
1259               cursor = selected.start+2;
1260             } else {
1261               if (selected.text.indexOf('\n') < 0) {
1262                 chunk = selected.text;
1263
1264                 e.replaceSelection('> '+chunk);
1265
1266                 // Set the cursor
1267                 cursor = selected.start+2;
1268               } else {
1269                 var list = [];
1270
1271                 list = selected.text.split('\n');
1272                 chunk = list[0];
1273
1274                 $.each(list,function(k,v) {
1275                   list[k] = '> '+v;
1276                 });
1277
1278                 e.replaceSelection('\n\n'+list.join('\n'));
1279
1280                 // Set the cursor
1281                 cursor = selected.start+4;
1282               }
1283             }
1284
1285             // Set the cursor
1286             e.setSelection(cursor,cursor+chunk.length);
1287           }
1288         }]
1289       },{
1290         name: 'groupUtil',
1291         data: [{
1292           name: 'cmdPreview',
1293           toggle: true,
1294           hotkey: 'Ctrl+P',
1295           title: 'Preview',
1296           btnText: 'Preview',
1297           btnClass: 'btn btn-primary btn-sm',
1298           icon: { glyph: 'glyphicon glyphicon-search', fa: 'fa fa-search', 'fa-3': 'icon-search' },
1299           callback: function(e){
1300             // Check the preview mode and toggle based on this flag
1301             var isPreview = e.$isPreview,content;
1302
1303             if (isPreview === false) {
1304               // Give flag that tell the editor enter preview mode
1305               e.showPreview();
1306             } else {
1307               e.hidePreview();
1308             }
1309           }
1310         }]
1311       }]
1312     ],
1313     additionalButtons:[], // Place to hook more buttons by code
1314     reorderButtonGroups:[],
1315     hiddenButtons:[], // Default hidden buttons
1316     disabledButtons:[], // Default disabled buttons
1317     footer: '',
1318     fullscreen: {
1319       enable: true,
1320       icons: {
1321         fullscreenOn: {
1322           fa: 'fa fa-expand',
1323           glyph: 'glyphicon glyphicon-fullscreen',
1324           'fa-3': 'icon-resize-full'
1325         },
1326         fullscreenOff: {
1327           fa: 'fa fa-compress',
1328           glyph: 'glyphicon glyphicon-fullscreen',
1329           'fa-3': 'icon-resize-small'
1330         }
1331       }
1332     },
1333
1334     /* Events hook */
1335     onShow: function (e) {},
1336     onPreview: function (e) {},
1337     onSave: function (e) {},
1338     onBlur: function (e) {},
1339     onFocus: function (e) {},
1340     onChange: function(e) {},
1341     onFullscreen: function(e) {},
1342     onSelect: function (e) {}
1343   };
1344
1345   $.fn.markdown.Constructor = Markdown;
1346
1347
1348  /* MARKDOWN NO CONFLICT
1349   * ==================== */
1350
1351   $.fn.markdown.noConflict = function () {
1352     $.fn.markdown = old;
1353     return this;
1354   };
1355
1356   /* MARKDOWN GLOBAL FUNCTION & DATA-API
1357   * ==================================== */
1358   var initMarkdown = function(el) {
1359     var $this = el;
1360
1361     if ($this.data('markdown')) {
1362       $this.data('markdown').showEditor();
1363       return;
1364     }
1365
1366     $this.markdown()
1367   };
1368
1369   var blurNonFocused = function(e) {
1370     var $activeElement = $(document.activeElement);
1371
1372     // Blur event
1373     $(document).find('.md-editor').each(function(){
1374       var $this            = $(this),
1375           focused          = $activeElement.closest('.md-editor')[0] === this,
1376           attachedMarkdown = $this.find('textarea').data('markdown') ||
1377                              $this.find('div[data-provider="markdown-preview"]').data('markdown');
1378
1379       if (attachedMarkdown && !focused) {
1380         attachedMarkdown.blur();
1381       }
1382     })
1383   };
1384
1385   $(document)
1386     .on('click.markdown.data-api', '[data-provide="markdown-editable"]', function (e) {
1387       initMarkdown($(this));
1388       e.preventDefault();
1389     })
1390     .on('click focusin', function (e) {
1391       blurNonFocused(e);
1392     })
1393     .ready(function(){
1394       $('textarea[data-provide="markdown"]').each(function(){
1395         initMarkdown($(this));
1396       })
1397     });
1398
1399 }(window.jQuery);