wizard-editor.js.es6 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703
  1. /*global Mousetrap:true */
  2. import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
  3. import { cookAsync } from '../lib/text-lite';
  4. import { getRegister } from 'discourse-common/lib/get-owner';
  5. import { siteDir } from 'discourse/lib/text-direction';
  6. import { determinePostReplaceSelection, clipboardData } from '../lib/utilities-lite';
  7. import toMarkdown from 'discourse/lib/to-markdown';
  8. // Our head can be a static string or a function that returns a string
  9. // based on input (like for numbered lists).
  10. function getHead(head, prev) {
  11. if (typeof head === "string") {
  12. return [head, head.length];
  13. } else {
  14. return getHead(head(prev));
  15. }
  16. }
  17. function getButtonLabel(labelKey, defaultLabel) {
  18. // use the Font Awesome icon if the label matches the default
  19. return I18n.t(labelKey) === defaultLabel ? null : labelKey;
  20. }
  21. const OP = {
  22. NONE: 0,
  23. REMOVED: 1,
  24. ADDED: 2
  25. };
  26. const FOUR_SPACES_INDENT = '4-spaces-indent';
  27. const _createCallbacks = [];
  28. const isInside = (text, regex) => {
  29. const matches = text.match(regex);
  30. return matches && (matches.length % 2);
  31. };
  32. class Toolbar {
  33. constructor(site) {
  34. this.shortcuts = {};
  35. this.groups = [
  36. {group: 'fontStyles', buttons: []},
  37. {group: 'insertions', buttons: []},
  38. {group: 'extras', buttons: []},
  39. {group: 'mobileExtras', buttons: []}
  40. ];
  41. this.addButton({
  42. trimLeading: true,
  43. id: 'bold',
  44. group: 'fontStyles',
  45. icon: 'bold',
  46. label: getButtonLabel('wizard_composer.bold_label', 'B'),
  47. shortcut: 'B',
  48. perform: e => e.applySurround('**', '**', 'bold_text')
  49. });
  50. this.addButton({
  51. trimLeading: true,
  52. id: 'italic',
  53. group: 'fontStyles',
  54. icon: 'italic',
  55. label: getButtonLabel('wizard_composer.italic_label', 'I'),
  56. shortcut: 'I',
  57. perform: e => e.applySurround('_', '_', 'italic_text')
  58. });
  59. this.addButton({id: 'link', group: 'insertions', shortcut: 'K', action: 'showLinkModal'});
  60. this.addButton({
  61. id: 'quote',
  62. group: 'insertions',
  63. icon: 'quote-right',
  64. shortcut: 'Shift+9',
  65. perform: e => e.applyList(
  66. '> ',
  67. 'blockquote_text',
  68. { applyEmptyLines: true, multiline: true }
  69. )
  70. });
  71. this.addButton({id: 'code', group: 'insertions', shortcut: 'Shift+C', action: 'formatCode'});
  72. this.addButton({
  73. id: 'bullet',
  74. group: 'extras',
  75. icon: 'list-ul',
  76. shortcut: 'Shift+8',
  77. title: 'wizard_composer.ulist_title',
  78. perform: e => e.applyList('* ', 'list_item')
  79. });
  80. this.addButton({
  81. id: 'list',
  82. group: 'extras',
  83. icon: 'list-ol',
  84. shortcut: 'Shift+7',
  85. title: 'wizard_composer.olist_title',
  86. perform: e => e.applyList(i => !i ? "1. " : `${parseInt(i) + 1}. `, 'list_item')
  87. });
  88. if (Wizard.SiteSettings.support_mixed_text_direction) {
  89. this.addButton({
  90. id: 'toggle-direction',
  91. group: 'extras',
  92. icon: 'exchange',
  93. shortcut: 'Shift+6',
  94. title: 'wizard_composer.toggle_direction',
  95. perform: e => e.toggleDirection(),
  96. });
  97. }
  98. this.groups[this.groups.length-1].lastGroup = true;
  99. }
  100. addButton(button) {
  101. const g = this.groups.findBy('group', button.group);
  102. if (!g) {
  103. throw `Couldn't find toolbar group ${button.group}`;
  104. }
  105. const createdButton = {
  106. id: button.id,
  107. className: button.className || button.id,
  108. label: button.label,
  109. icon: button.label ? null : button.icon || button.id,
  110. action: button.action || 'toolbarButton',
  111. perform: button.perform || function() { },
  112. trimLeading: button.trimLeading,
  113. popupMenu: button.popupMenu || false
  114. };
  115. if (button.sendAction) {
  116. createdButton.sendAction = button.sendAction;
  117. }
  118. const title = I18n.t(button.title || `wizard_composer.${button.id}_title`);
  119. if (button.shortcut) {
  120. const mac = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
  121. const mod = mac ? 'Meta' : 'Ctrl';
  122. var shortcutTitle = `${mod}+${button.shortcut}`;
  123. // Mac users are used to glyphs for shortcut keys
  124. if (mac) {
  125. shortcutTitle = shortcutTitle
  126. .replace('Shift', "\u21E7")
  127. .replace('Meta', "\u2318")
  128. .replace('Alt', "\u2325")
  129. .replace(/\+/g, '');
  130. } else {
  131. shortcutTitle = shortcutTitle
  132. .replace('Shift', I18n.t('shortcut_modifier_key.shift'))
  133. .replace('Ctrl', I18n.t('shortcut_modifier_key.ctrl'))
  134. .replace('Alt', I18n.t('shortcut_modifier_key.alt'));
  135. }
  136. createdButton.title = `${title} (${shortcutTitle})`;
  137. this.shortcuts[`${mod}+${button.shortcut}`.toLowerCase()] = createdButton;
  138. } else {
  139. createdButton.title = title;
  140. }
  141. if (button.unshift) {
  142. g.buttons.unshift(createdButton);
  143. } else {
  144. g.buttons.push(createdButton);
  145. }
  146. }
  147. }
  148. export default Ember.Component.extend({
  149. classNames: ['d-editor'],
  150. ready: false,
  151. insertLinkHidden: true,
  152. linkUrl: '',
  153. linkText: '',
  154. lastSel: null,
  155. _mouseTrap: null,
  156. showPreview: false,
  157. @computed('placeholder')
  158. placeholderTranslated(placeholder) {
  159. if (placeholder) return I18n.t(placeholder);
  160. return null;
  161. },
  162. _readyNow() {
  163. this.set('ready', true);
  164. if (this.get('autofocus')) {
  165. this.$('textarea').focus();
  166. }
  167. },
  168. init() {
  169. this._super();
  170. this.register = getRegister(this);
  171. },
  172. didInsertElement() {
  173. this._super();
  174. Ember.run.scheduleOnce('afterRender', this, this._readyNow);
  175. const mouseTrap = Mousetrap(this.$('.d-editor-input')[0]);
  176. const shortcuts = this.get('toolbar.shortcuts');
  177. // for some reason I am having trouble bubbling this so hack it in
  178. mouseTrap.bind(['ctrl+alt+f'], (event) =>{
  179. this.appEvents.trigger('header:keyboard-trigger', {type: 'search', event});
  180. return true;
  181. });
  182. Object.keys(shortcuts).forEach(sc => {
  183. const button = shortcuts[sc];
  184. mouseTrap.bind(sc, () => {
  185. this.send(button.action, button);
  186. return false;
  187. });
  188. });
  189. // disable clicking on links in the preview
  190. this.$('.d-editor-preview').on('click.preview', e => {
  191. if ($(e.target).is("a")) {
  192. e.preventDefault();
  193. return false;
  194. }
  195. });
  196. if (this.get('composerEvents')) {
  197. this.appEvents.on('composer:insert-block', text => this._addBlock(this._getSelected(), text));
  198. this.appEvents.on('composer:insert-text', (text, options) => this._addText(this._getSelected(), text, options));
  199. this.appEvents.on('composer:replace-text', (oldVal, newVal) => this._replaceText(oldVal, newVal));
  200. }
  201. this._mouseTrap = mouseTrap;
  202. },
  203. @on('willDestroyElement')
  204. _shutDown() {
  205. if (this.get('composerEvents')) {
  206. this.appEvents.off('composer:insert-block');
  207. this.appEvents.off('composer:insert-text');
  208. this.appEvents.off('composer:replace-text');
  209. }
  210. const mouseTrap = this._mouseTrap;
  211. Object.keys(this.get('toolbar.shortcuts')).forEach(sc => mouseTrap.unbind(sc));
  212. mouseTrap.unbind('ctrl+/','command+/');
  213. this.$('.d-editor-preview').off('click.preview');
  214. },
  215. @computed
  216. toolbar() {
  217. const toolbar = new Toolbar(this.site);
  218. _createCallbacks.forEach(cb => cb(toolbar));
  219. this.sendAction('extraButtons', toolbar);
  220. return toolbar;
  221. },
  222. _updatePreview() {
  223. if (this._state !== "inDOM") { return; }
  224. const value = this.get('value');
  225. const markdownOptions = this.get('markdownOptions') || {};
  226. cookAsync(value, markdownOptions).then(cooked => {
  227. if (this.get('isDestroyed')) { return; }
  228. this.set('preview', cooked);
  229. Ember.run.scheduleOnce('afterRender', () => {
  230. if (this._state !== "inDOM") { return; }
  231. const $preview = this.$('.d-editor-preview');
  232. if ($preview.length === 0) return;
  233. this.sendAction('previewUpdated', $preview);
  234. });
  235. });
  236. },
  237. @observes('ready', 'value')
  238. _watchForChanges() {
  239. if (!this.get('ready')) { return; }
  240. // Debouncing in test mode is complicated
  241. if (Ember.testing) {
  242. this._updatePreview();
  243. } else {
  244. Ember.run.debounce(this, this._updatePreview, 30);
  245. }
  246. },
  247. _getSelected(trimLeading, opts) {
  248. if (!this.get('ready')) { return; }
  249. const textarea = this.$('textarea.d-editor-input')[0];
  250. const value = textarea.value;
  251. let start = textarea.selectionStart;
  252. let end = textarea.selectionEnd;
  253. // trim trailing spaces cause **test ** would be invalid
  254. while (end > start && /\s/.test(value.charAt(end-1))) {
  255. end--;
  256. }
  257. if (trimLeading) {
  258. // trim leading spaces cause ** test** would be invalid
  259. while(end > start && /\s/.test(value.charAt(start))) {
  260. start++;
  261. }
  262. }
  263. const selVal = value.substring(start, end);
  264. const pre = value.slice(0, start);
  265. const post = value.slice(end);
  266. if (opts && opts.lineVal) {
  267. const lineVal = value.split("\n")[value.substr(0, textarea.selectionStart).split("\n").length - 1];
  268. return { start, end, value: selVal, pre, post, lineVal };
  269. } else {
  270. return { start, end, value: selVal, pre, post };
  271. }
  272. },
  273. _selectText(from, length) {
  274. Ember.run.scheduleOnce('afterRender', () => {
  275. const $textarea = this.$('textarea.d-editor-input');
  276. const textarea = $textarea[0];
  277. const oldScrollPos = $textarea.scrollTop();
  278. if (!!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform)) {
  279. $textarea.focus();
  280. }
  281. textarea.selectionStart = from;
  282. textarea.selectionEnd = textarea.selectionStart + length;
  283. $textarea.scrollTop(oldScrollPos);
  284. });
  285. },
  286. // perform the same operation over many lines of text
  287. _getMultilineContents(lines, head, hval, hlen, tail, tlen, opts) {
  288. let operation = OP.NONE;
  289. const applyEmptyLines = opts && opts.applyEmptyLines;
  290. return lines.map(l => {
  291. if (!applyEmptyLines && l.length === 0) {
  292. return l;
  293. }
  294. if (operation !== OP.ADDED &&
  295. (l.slice(0, hlen) === hval && tlen === 0 ||
  296. (tail.length && l.slice(-tlen) === tail))) {
  297. operation = OP.REMOVED;
  298. if (tlen === 0) {
  299. const result = l.slice(hlen);
  300. [hval, hlen] = getHead(head, hval);
  301. return result;
  302. } else if (l.slice(-tlen) === tail) {
  303. const result = l.slice(hlen, -tlen);
  304. [hval, hlen] = getHead(head, hval);
  305. return result;
  306. }
  307. } else if (operation === OP.NONE) {
  308. operation = OP.ADDED;
  309. } else if (operation === OP.REMOVED) {
  310. return l;
  311. }
  312. const result = `${hval}${l}${tail}`;
  313. [hval, hlen] = getHead(head, hval);
  314. return result;
  315. }).join("\n");
  316. },
  317. _applySurround(sel, head, tail, exampleKey, opts) {
  318. const pre = sel.pre;
  319. const post = sel.post;
  320. const tlen = tail.length;
  321. if (sel.start === sel.end) {
  322. if (tlen === 0) { return; }
  323. const [hval, hlen] = getHead(head);
  324. const example = I18n.t(`wizard_composer.${exampleKey}`);
  325. this.set('value', `${pre}${hval}${example}${tail}${post}`);
  326. this._selectText(pre.length + hlen, example.length);
  327. } else if (opts && !opts.multiline) {
  328. const [hval, hlen] = getHead(head);
  329. if (pre.slice(-hlen) === hval && post.slice(0, tail.length) === tail) {
  330. this.set('value', `${pre.slice(0, -hlen)}${sel.value}${post.slice(tail.length)}`);
  331. this._selectText(sel.start - hlen, sel.value.length);
  332. } else {
  333. this.set('value', `${pre}${hval}${sel.value}${tail}${post}`);
  334. this._selectText(sel.start + hlen, sel.value.length);
  335. }
  336. } else {
  337. const lines = sel.value.split("\n");
  338. let [hval, hlen] = getHead(head);
  339. if (lines.length === 1 && pre.slice(-tlen) === tail && post.slice(0, hlen) === hval) {
  340. this.set('value', `${pre.slice(0, -hlen)}${sel.value}${post.slice(tlen)}`);
  341. this._selectText(sel.start - hlen, sel.value.length);
  342. } else {
  343. const contents = this._getMultilineContents(
  344. lines,
  345. head,
  346. hval,
  347. hlen,
  348. tail,
  349. tlen,
  350. opts
  351. );
  352. this.set('value', `${pre}${contents}${post}`);
  353. if (lines.length === 1 && tlen > 0) {
  354. this._selectText(sel.start + hlen, sel.value.length);
  355. } else {
  356. this._selectText(sel.start, contents.length);
  357. }
  358. }
  359. }
  360. },
  361. _applyList(sel, head, exampleKey, opts) {
  362. if (sel.value.indexOf("\n") !== -1) {
  363. this._applySurround(sel, head, '', exampleKey, opts);
  364. } else {
  365. const [hval, hlen] = getHead(head);
  366. if (sel.start === sel.end) {
  367. sel.value = I18n.t(`wizard_composer.${exampleKey}`);
  368. }
  369. const trimmedPre = sel.pre.trim();
  370. const number = (sel.value.indexOf(hval) === 0) ? sel.value.slice(hlen) : `${hval}${sel.value}`;
  371. const preLines = trimmedPre.length ? `${trimmedPre}\n\n` : "";
  372. const trimmedPost = sel.post.trim();
  373. const post = trimmedPost.length ? `\n\n${trimmedPost}` : trimmedPost;
  374. this.set('value', `${preLines}${number}${post}`);
  375. this._selectText(preLines.length, number.length);
  376. }
  377. },
  378. _replaceText(oldVal, newVal) {
  379. const val = this.get('value');
  380. const needleStart = val.indexOf(oldVal);
  381. if (needleStart === -1) {
  382. // Nothing to replace.
  383. return;
  384. }
  385. const textarea = this.$('textarea.d-editor-input')[0];
  386. // Determine post-replace selection.
  387. const newSelection = determinePostReplaceSelection({
  388. selection: { start: textarea.selectionStart, end: textarea.selectionEnd },
  389. needle: { start: needleStart, end: needleStart + oldVal.length },
  390. replacement: { start: needleStart, end: needleStart + newVal.length }
  391. });
  392. // Replace value (side effect: cursor at the end).
  393. this.set('value', val.replace(oldVal, newVal));
  394. // Restore cursor.
  395. this._selectText(newSelection.start, newSelection.end - newSelection.start);
  396. },
  397. _addBlock(sel, text) {
  398. text = (text || '').trim();
  399. if (text.length === 0) {
  400. return;
  401. }
  402. let pre = sel.pre;
  403. let post = sel.value + sel.post;
  404. if (pre.length > 0) {
  405. pre = pre.replace(/\n*$/, "\n\n");
  406. }
  407. if (post.length > 0) {
  408. post = post.replace(/^\n*/, "\n\n");
  409. } else {
  410. post = "\n";
  411. }
  412. const value = pre + text + post;
  413. const $textarea = this.$('textarea.d-editor-input');
  414. this.set('value', value);
  415. $textarea.val(value);
  416. $textarea.prop("selectionStart", (pre+text).length + 2);
  417. $textarea.prop("selectionEnd", (pre+text).length + 2);
  418. Ember.run.scheduleOnce("afterRender", () => $textarea.focus());
  419. },
  420. _addText(sel, text, options) {
  421. const $textarea = this.$('textarea.d-editor-input');
  422. if (options && options.ensureSpace) {
  423. if ((sel.pre + '').length > 0) {
  424. if (!sel.pre.match(/\s$/)) {
  425. text = ' ' + text;
  426. }
  427. }
  428. if ((sel.post + '').length > 0) {
  429. if (!sel.post.match(/^\s/)) {
  430. text = text + ' ';
  431. }
  432. }
  433. }
  434. const insert = `${sel.pre}${text}`;
  435. const value = `${insert}${sel.post}`;
  436. this.set('value', value);
  437. $textarea.val(value);
  438. $textarea.prop("selectionStart", insert.length);
  439. $textarea.prop("selectionEnd", insert.length);
  440. Ember.run.scheduleOnce("afterRender", () => $textarea.focus());
  441. },
  442. _extractTable(text) {
  443. if (text.endsWith("\n")) {
  444. text = text.substring(0, text.length - 1);
  445. }
  446. let rows = text.split("\n");
  447. if (rows.length > 1) {
  448. const columns = rows.map(r => r.split("\t").length);
  449. const isTable = columns.reduce((a, b) => a && columns[0] === b && b > 1) &&
  450. !(columns[0] === 2 && rows[0].split("\t")[0].match(/^•$|^\d+.$/)); // to skip tab delimited lists
  451. if (isTable) {
  452. const splitterRow = [...Array(columns[0])].map(() => "---").join("\t");
  453. rows.splice(1, 0, splitterRow);
  454. return "|" + rows.map(r => r.split("\t").join("|")).join("|\n|") + "|\n";
  455. }
  456. }
  457. return null;
  458. },
  459. _toggleDirection() {
  460. const $textArea = $(".d-editor-input");
  461. let currentDir = $textArea.attr('dir') ? $textArea.attr('dir') : siteDir(),
  462. newDir = currentDir === 'ltr' ? 'rtl' : 'ltr';
  463. $textArea.attr('dir', newDir).focus();
  464. },
  465. paste(e) {
  466. if (!$(".d-editor-input").is(":focus")) {
  467. return;
  468. }
  469. const isComposer = $("#reply-control .d-editor-input").is(":focus");
  470. let { clipboard, canPasteHtml } = clipboardData(e, isComposer);
  471. let plainText = clipboard.getData("text/plain");
  472. let html = clipboard.getData("text/html");
  473. let handled = false;
  474. if (plainText) {
  475. plainText = plainText.trim().replace(/\r/g,"");
  476. const table = this._extractTable(plainText);
  477. if (table) {
  478. this.appEvents.trigger('composer:insert-text', table);
  479. handled = true;
  480. }
  481. }
  482. const { pre, lineVal } = this._getSelected(null, {lineVal: true});
  483. const isInlinePasting = pre.match(/[^\n]$/);
  484. if (canPasteHtml && plainText) {
  485. if (isInlinePasting) {
  486. canPasteHtml = !(lineVal.match(/^```/) || isInside(pre, /`/g) || lineVal.match(/^ /));
  487. } else {
  488. canPasteHtml = !isInside(pre, /(^|\n)```/g);
  489. }
  490. }
  491. if (canPasteHtml && !handled) {
  492. let markdown = toMarkdown(html);
  493. if (!plainText || plainText.length < markdown.length) {
  494. if(isInlinePasting) {
  495. markdown = markdown.replace(/^#+/, "").trim();
  496. markdown = pre.match(/\S$/) ? ` ${markdown}` : markdown;
  497. }
  498. this.appEvents.trigger('composer:insert-text', markdown);
  499. handled = true;
  500. }
  501. }
  502. if (handled) {
  503. e.preventDefault();
  504. }
  505. },
  506. keyPress(e) {
  507. if (e.keyCode === 13) {
  508. const selected = this._getSelected();
  509. this._addText(selected, '\n');
  510. return false;
  511. }
  512. },
  513. actions: {
  514. toolbarButton(button) {
  515. const selected = this._getSelected(button.trimLeading);
  516. const toolbarEvent = {
  517. selected,
  518. selectText: (from, length) => this._selectText(from, length),
  519. applySurround: (head, tail, exampleKey, opts) => this._applySurround(selected, head, tail, exampleKey, opts),
  520. applyList: (head, exampleKey, opts) => this._applyList(selected, head, exampleKey, opts),
  521. addText: text => this._addText(selected, text),
  522. replaceText: text => this._addText({pre: '', post: ''}, text),
  523. getText: () => this.get('value'),
  524. toggleDirection: () => this._toggleDirection(),
  525. };
  526. if (button.sendAction) {
  527. return this.sendAction(button.sendAction, toolbarEvent);
  528. } else {
  529. button.perform(toolbarEvent);
  530. }
  531. },
  532. showLinkModal() {
  533. this._lastSel = this._getSelected();
  534. if (this._lastSel) {
  535. this.set("linkText", this._lastSel.value.trim());
  536. }
  537. this.set('insertLinkHidden', false);
  538. },
  539. formatCode() {
  540. const sel = this._getSelected('', { lineVal: true });
  541. const selValue = sel.value;
  542. const hasNewLine = selValue.indexOf("\n") !== -1;
  543. const isBlankLine = sel.lineVal.trim().length === 0;
  544. const isFourSpacesIndent = this.siteSettings.code_formatting_style === FOUR_SPACES_INDENT;
  545. if (!hasNewLine) {
  546. if (selValue.length === 0 && isBlankLine) {
  547. if (isFourSpacesIndent) {
  548. const example = I18n.t(`wizard_composer.code_text`);
  549. this.set('value', `${sel.pre} ${example}${sel.post}`);
  550. return this._selectText(sel.pre.length + 4, example.length);
  551. } else {
  552. return this._applySurround(sel, "```\n", "\n```", 'paste_code_text');
  553. }
  554. } else {
  555. return this._applySurround(sel, '`', '`', 'code_title');
  556. }
  557. } else {
  558. if (isFourSpacesIndent) {
  559. return this._applySurround(sel, ' ', '', 'code_text');
  560. } else {
  561. const preNewline = (sel.pre[-1] !== "\n" && sel.pre !== "") ? "\n" : "";
  562. const postNewline = sel.post[0] !== "\n" ? "\n" : "";
  563. return this._addText(sel, `${preNewline}\`\`\`\n${sel.value}\n\`\`\`${postNewline}`);
  564. }
  565. }
  566. },
  567. insertLink() {
  568. const origLink = this.get('linkUrl');
  569. const linkUrl = (origLink.indexOf('://') === -1) ? `http://${origLink}` : origLink;
  570. const sel = this._lastSel;
  571. if (Ember.isEmpty(linkUrl)) { return; }
  572. const linkText = this.get('linkText') || '';
  573. if (linkText.length) {
  574. this._addText(sel, `[${linkText}](${linkUrl})`);
  575. } else {
  576. if (sel.value) {
  577. this._addText(sel, `[${sel.value}](${linkUrl})`);
  578. } else {
  579. this._addText(sel, `[${origLink}](${linkUrl})`);
  580. this._selectText(sel.start + 1, origLink.length);
  581. }
  582. }
  583. this.set('linkUrl', '');
  584. this.set('linkText', '');
  585. }
  586. }
  587. });