You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1344 lines
40 KiB

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