Subversion Repositories Integrator Subversion

Rev

Details | Last modification | View Log | RSS feed

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