bootstrap-markdown.js 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407
  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.push(nameParam)
  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-font', '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. //ACE
  815. if('bootbox' in window) {
  816. bootbox.prompt(e.__localize('Insert Hyperlink'), function(link) {
  817. if (link != null && link != '' && link != 'http://' && link.substr(0,4) == 'http') {
  818. var sanitizedLink = $('<div>'+link+'</div>').text()
  819. // transform selection and set the cursor into chunked text
  820. e.replaceSelection('['+chunk+']('+sanitizedLink+')')
  821. cursor = selected.start+1
  822. // Set the cursor
  823. e.setSelection(cursor,cursor+chunk.length)
  824. }
  825. });
  826. }
  827. else {
  828. link = prompt(e.__localize('Insert Hyperlink'),'http://')
  829. if (link != null && link != '' && link != 'http://' && link.substr(0,4) == 'http') {
  830. var sanitizedLink = $('<div>'+link+'</div>').text()
  831. // transform selection and set the cursor into chunked text
  832. e.replaceSelection('['+chunk+']('+sanitizedLink+')')
  833. cursor = selected.start+1
  834. // Set the cursor
  835. e.setSelection(cursor,cursor+chunk.length)
  836. }
  837. }
  838. }
  839. },{
  840. name: 'cmdImage',
  841. title: 'Image',
  842. hotkey: 'Ctrl+G',
  843. icon: { glyph: 'glyphicon glyphicon-picture', fa: 'fa fa-picture-o', 'fa-3': 'icon-picture' },
  844. callback: function(e){
  845. // Give ![] surround the selection and prepend the image link
  846. var chunk, cursor, selected = e.getSelection(), content = e.getContent(), link
  847. if (selected.length == 0) {
  848. // Give extra word
  849. chunk = e.__localize('enter image description here')
  850. } else {
  851. chunk = selected.text
  852. }
  853. //ACE
  854. if('bootbox' in window) {
  855. bootbox.prompt(e.__localize('Insert Image Hyperlink'), function(link) {
  856. if (link != null && link != '' && link != 'http://' && link.substr(0,4) == 'http') {
  857. var sanitizedLink = $('<div>'+link+'</div>').text()
  858. // transform selection and set the cursor into chunked text
  859. e.replaceSelection('!['+chunk+']('+sanitizedLink+' "'+e.__localize('enter image title here')+'")')
  860. cursor = selected.start+2
  861. // Set the next tab
  862. e.setNextTab(e.__localize('enter image title here'))
  863. // Set the cursor
  864. e.setSelection(cursor,cursor+chunk.length)
  865. }
  866. });
  867. }
  868. else {
  869. link = prompt(e.__localize('Insert Image Hyperlink'),'http://')
  870. if (link != null && link != '' && link != 'http://' && link.substr(0,4) == 'http') {
  871. var sanitizedLink = $('<div>'+link+'</div>').text()
  872. // transform selection and set the cursor into chunked text
  873. e.replaceSelection('!['+chunk+']('+sanitizedLink+' "'+e.__localize('enter image title here')+'")')
  874. cursor = selected.start+2
  875. // Set the next tab
  876. e.setNextTab(e.__localize('enter image title here'))
  877. // Set the cursor
  878. e.setSelection(cursor,cursor+chunk.length)
  879. }
  880. }
  881. }
  882. }]
  883. },{
  884. name: 'groupMisc',
  885. data: [{
  886. name: 'cmdList',
  887. hotkey: 'Ctrl+U',
  888. title: 'Unordered List',
  889. icon: { glyph: 'glyphicon glyphicon-list', fa: 'fa fa-list', 'fa-3': 'icon-list-ul' },
  890. callback: function(e){
  891. // Prepend/Give - surround the selection
  892. var chunk, cursor, selected = e.getSelection(), content = e.getContent()
  893. // transform selection and set the cursor into chunked text
  894. if (selected.length == 0) {
  895. // Give extra word
  896. chunk = e.__localize('list text here')
  897. e.replaceSelection('- '+chunk)
  898. // Set the cursor
  899. cursor = selected.start+2
  900. } else {
  901. if (selected.text.indexOf('\n') < 0) {
  902. chunk = selected.text
  903. e.replaceSelection('- '+chunk)
  904. // Set the cursor
  905. cursor = selected.start+2
  906. } else {
  907. var list = []
  908. list = selected.text.split('\n')
  909. chunk = list[0]
  910. $.each(list,function(k,v) {
  911. list[k] = '- '+v
  912. })
  913. e.replaceSelection('\n\n'+list.join('\n'))
  914. // Set the cursor
  915. cursor = selected.start+4
  916. }
  917. }
  918. // Set the cursor
  919. e.setSelection(cursor,cursor+chunk.length)
  920. }
  921. },
  922. {
  923. name: 'cmdListO',
  924. hotkey: 'Ctrl+O',
  925. title: 'Ordered List',
  926. icon: { glyph: 'glyphicon glyphicon-th-list', fa: 'fa fa-list-ol', 'fa-3': 'icon-list-ol' },
  927. callback: function(e) {
  928. // Prepend/Give - surround the selection
  929. var chunk, cursor, selected = e.getSelection(), content = e.getContent()
  930. // transform selection and set the cursor into chunked text
  931. if (selected.length == 0) {
  932. // Give extra word
  933. chunk = e.__localize('list text here')
  934. e.replaceSelection('1. '+chunk)
  935. // Set the cursor
  936. cursor = selected.start+3
  937. } else {
  938. if (selected.text.indexOf('\n') < 0) {
  939. chunk = selected.text
  940. e.replaceSelection('1. '+chunk)
  941. // Set the cursor
  942. cursor = selected.start+3
  943. } else {
  944. var list = []
  945. list = selected.text.split('\n')
  946. chunk = list[0]
  947. $.each(list,function(k,v) {
  948. list[k] = '1. '+v
  949. })
  950. e.replaceSelection('\n\n'+list.join('\n'))
  951. // Set the cursor
  952. cursor = selected.start+5
  953. }
  954. }
  955. // Set the cursor
  956. e.setSelection(cursor,cursor+chunk.length)
  957. }
  958. },
  959. {
  960. name: 'cmdCode',
  961. hotkey: 'Ctrl+K',
  962. title: 'Code',
  963. icon: { glyph: 'glyphicon glyphicon-asterisk', fa: 'fa fa-code', 'fa-3': 'icon-code' },
  964. callback: function(e) {
  965. // Give/remove ** surround the selection
  966. var chunk, cursor, selected = e.getSelection(), content = e.getContent()
  967. if (selected.length == 0) {
  968. // Give extra word
  969. chunk = e.__localize('code text here')
  970. } else {
  971. chunk = selected.text
  972. }
  973. // transform selection and set the cursor into chunked text
  974. if (content.substr(selected.start-1,1) == '`'
  975. && content.substr(selected.end,1) == '`' ) {
  976. e.setSelection(selected.start-1,selected.end+1)
  977. e.replaceSelection(chunk)
  978. cursor = selected.start-1
  979. } else {
  980. e.replaceSelection('`'+chunk+'`')
  981. cursor = selected.start+1
  982. }
  983. // Set the cursor
  984. e.setSelection(cursor,cursor+chunk.length)
  985. }
  986. },
  987. {
  988. name: 'cmdQuote',
  989. hotkey: 'Ctrl+Q',
  990. title: 'Quote',
  991. icon: { glyph: 'glyphicon glyphicon-comment', fa: 'fa fa-quote-left', 'fa-3': 'icon-quote-left' },
  992. callback: function(e) {
  993. // Prepend/Give - surround the selection
  994. var chunk, cursor, selected = e.getSelection(), content = e.getContent()
  995. // transform selection and set the cursor into chunked text
  996. if (selected.length == 0) {
  997. // Give extra word
  998. chunk = e.__localize('quote here')
  999. e.replaceSelection('> '+chunk)
  1000. // Set the cursor
  1001. cursor = selected.start+2
  1002. } else {
  1003. if (selected.text.indexOf('\n') < 0) {
  1004. chunk = selected.text
  1005. e.replaceSelection('> '+chunk)
  1006. // Set the cursor
  1007. cursor = selected.start+2
  1008. } else {
  1009. var list = []
  1010. list = selected.text.split('\n')
  1011. chunk = list[0]
  1012. $.each(list,function(k,v) {
  1013. list[k] = '> '+v
  1014. })
  1015. e.replaceSelection('\n\n'+list.join('\n'))
  1016. // Set the cursor
  1017. cursor = selected.start+4
  1018. }
  1019. }
  1020. // Set the cursor
  1021. e.setSelection(cursor,cursor+chunk.length)
  1022. }
  1023. }]
  1024. },{
  1025. name: 'groupUtil',
  1026. data: [{
  1027. name: 'cmdPreview',
  1028. toggle: true,
  1029. hotkey: 'Ctrl+P',
  1030. title: 'Preview',
  1031. btnText: 'Preview',
  1032. btnClass: 'btn btn-primary btn-sm',
  1033. icon: { glyph: 'glyphicon glyphicon-search', fa: 'fa fa-search', 'fa-3': 'icon-search' },
  1034. callback: function(e){
  1035. // Check the preview mode and toggle based on this flag
  1036. var isPreview = e.$isPreview,content
  1037. if (isPreview == false) {
  1038. // Give flag that tell the editor enter preview mode
  1039. e.showPreview()
  1040. } else {
  1041. e.hidePreview()
  1042. }
  1043. }
  1044. }]
  1045. }]
  1046. ],
  1047. additionalButtons:[], // Place to hook more buttons by code
  1048. reorderButtonGroups:[],
  1049. hiddenButtons:[], // Default hidden buttons
  1050. disabledButtons:[], // Default disabled buttons
  1051. footer: '',
  1052. fullscreen: {
  1053. enable: true,
  1054. icons: {
  1055. fullscreenOn: {
  1056. fa: 'fa fa-expand',
  1057. glyph: 'glyphicon glyphicon-fullscreen',
  1058. 'fa-3': 'icon-resize-full'
  1059. },
  1060. fullscreenOff: {
  1061. fa: 'fa fa-compress',
  1062. glyph: 'glyphicon glyphicon-fullscreen',
  1063. 'fa-3': 'icon-resize-small'
  1064. }
  1065. }
  1066. },
  1067. /* Events hook */
  1068. onShow: function (e) {},
  1069. onPreview: function (e) {},
  1070. onSave: function (e) {},
  1071. onBlur: function (e) {},
  1072. onFocus: function (e) {},
  1073. onChange: function(e) {},
  1074. onFullscreen: function(e) {}
  1075. }
  1076. $.fn.markdown.Constructor = Markdown
  1077. /* MARKDOWN NO CONFLICT
  1078. * ==================== */
  1079. $.fn.markdown.noConflict = function () {
  1080. $.fn.markdown = old
  1081. return this
  1082. }
  1083. /* MARKDOWN GLOBAL FUNCTION & DATA-API
  1084. * ==================================== */
  1085. var initMarkdown = function(el) {
  1086. var $this = el
  1087. if ($this.data('markdown')) {
  1088. $this.data('markdown').showEditor()
  1089. return
  1090. }
  1091. $this.markdown()
  1092. }
  1093. var analyzeMarkdown = function(e) {
  1094. var blurred = false,
  1095. el,
  1096. $docEditor = $(e.currentTarget)
  1097. // Check whether it was editor childs or not
  1098. if ((e.type == 'focusin' || e.type == 'click') && $docEditor.length == 1 && typeof $docEditor[0] == 'object'){
  1099. el = $docEditor[0].activeElement
  1100. if ( ! $(el).data('markdown')) {
  1101. if (typeof $(el).parent().parent().parent().attr('class') == "undefined"
  1102. || $(el).parent().parent().parent().attr('class').indexOf('md-editor') < 0) {
  1103. if ( typeof $(el).parent().parent().attr('class') == "undefined"
  1104. || $(el).parent().parent().attr('class').indexOf('md-editor') < 0) {
  1105. blurred = true
  1106. }
  1107. } else {
  1108. blurred = false
  1109. }
  1110. }
  1111. if (blurred) {
  1112. // Blur event
  1113. $(document).find('.md-editor').each(function(){
  1114. var parentMd = $(el).parent()
  1115. if ($(this).attr('id') != parentMd.attr('id')) {
  1116. var attachedMarkdown
  1117. if (attachedMarkdown = $(this).find('textarea').data('markdown'),
  1118. attachedMarkdown == null) {
  1119. attachedMarkdown = $(this).find('div[data-provider="markdown-preview"]').data('markdown')
  1120. }
  1121. if (attachedMarkdown) {
  1122. attachedMarkdown.blur()
  1123. }
  1124. }
  1125. })
  1126. }
  1127. e.stopPropagation()
  1128. }
  1129. }
  1130. $(document)
  1131. .on('click.markdown.data-api', '[data-provide="markdown-editable"]', function (e) {
  1132. initMarkdown($(this))
  1133. e.preventDefault()
  1134. })
  1135. .on('click', function (e) {
  1136. analyzeMarkdown(e)
  1137. })
  1138. .on('focusin', function (e) {
  1139. analyzeMarkdown(e)
  1140. })
  1141. .ready(function(){
  1142. $('textarea[data-provide="markdown"]').each(function(){
  1143. initMarkdown($(this))
  1144. })
  1145. })
  1146. }(window.jQuery);