bootstrap-markdown.js 43 KB

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