bootstrap-markdown.js 38 KB

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