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

repo-legacy.js 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  1. import $ from 'jquery';
  2. import {
  3. initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel, initRepoIssueCommentDelete,
  4. initRepoIssueComments, initRepoIssueDependencyDelete, initRepoIssueReferenceIssue,
  5. initRepoIssueTitleEdit, initRepoIssueWipToggle,
  6. initRepoPullRequestUpdate, updateIssuesMeta, handleReply, initIssueTemplateCommentEditors, initSingleCommentEditor,
  7. } from './repo-issue.js';
  8. import {initUnicodeEscapeButton} from './repo-unicode-escape.js';
  9. import {svg} from '../svg.js';
  10. import {htmlEscape} from 'escape-goat';
  11. import {initRepoBranchTagSelector} from '../components/RepoBranchTagSelector.vue';
  12. import {
  13. initRepoCloneLink, initRepoCommonBranchOrTagDropdown, initRepoCommonFilterSearchDropdown,
  14. } from './repo-common.js';
  15. import {initCitationFileCopyContent} from './citation.js';
  16. import {initCompLabelEdit} from './comp/LabelEdit.js';
  17. import {initRepoDiffConversationNav} from './repo-diff.js';
  18. import {createDropzone} from './dropzone.js';
  19. import {initCommentContent, initMarkupContent} from '../markup/content.js';
  20. import {initCompReactionSelector} from './comp/ReactionSelector.js';
  21. import {initRepoSettingBranches} from './repo-settings.js';
  22. import {initRepoPullRequestMergeForm} from './repo-issue-pr-form.js';
  23. import {initRepoPullRequestCommitStatus} from './repo-issue-pr-status.js';
  24. import {hideElem, showElem} from '../utils/dom.js';
  25. import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
  26. import {attachRefIssueContextPopup} from './contextpopup.js';
  27. import {POST, GET} from '../modules/fetch.js';
  28. const {csrfToken} = window.config;
  29. // if there are draft comments, confirm before reloading, to avoid losing comments
  30. function reloadConfirmDraftComment() {
  31. const commentTextareas = [
  32. document.querySelector('.edit-content-zone:not(.gt-hidden) textarea'),
  33. document.querySelector('#comment-form textarea'),
  34. ];
  35. for (const textarea of commentTextareas) {
  36. // Most users won't feel too sad if they lose a comment with 10 chars, they can re-type these in seconds.
  37. // But if they have typed more (like 50) chars and the comment is lost, they will be very unhappy.
  38. if (textarea && textarea.value.trim().length > 10) {
  39. textarea.parentElement.scrollIntoView();
  40. if (!window.confirm('Page will be reloaded, but there are draft comments. Continuing to reload will discard the comments. Continue?')) {
  41. return;
  42. }
  43. break;
  44. }
  45. }
  46. window.location.reload();
  47. }
  48. export function initRepoCommentForm() {
  49. const $commentForm = $('.comment.form');
  50. if ($commentForm.length === 0) {
  51. return;
  52. }
  53. if ($commentForm.find('.field.combo-editor-dropzone').length) {
  54. // at the moment, if a form has multiple combo-markdown-editors, it must be an issue template form
  55. initIssueTemplateCommentEditors($commentForm);
  56. } else if ($commentForm.find('.combo-markdown-editor').length) {
  57. // it's quite unclear about the "comment form" elements, sometimes it's for issue comment, sometimes it's for file editor/uploader message
  58. initSingleCommentEditor($commentForm);
  59. }
  60. function initBranchSelector() {
  61. const $selectBranch = $('.ui.select-branch');
  62. const $branchMenu = $selectBranch.find('.reference-list-menu');
  63. const $isNewIssue = $branchMenu.hasClass('new-issue');
  64. $branchMenu.find('.item:not(.no-select)').on('click', async function () {
  65. const selectedValue = $(this).data('id');
  66. const editMode = $('#editing_mode').val();
  67. $($(this).data('id-selector')).val(selectedValue);
  68. if ($isNewIssue) {
  69. $selectBranch.find('.ui .branch-name').text($(this).data('name'));
  70. return;
  71. }
  72. if (editMode === 'true') {
  73. const $form = $('#update_issueref_form');
  74. const params = new URLSearchParams();
  75. params.append('ref', selectedValue);
  76. try {
  77. await POST($form.attr('action'), {data: params});
  78. window.location.reload();
  79. } catch (error) {
  80. console.error(error);
  81. }
  82. } else if (editMode === '') {
  83. $selectBranch.find('.ui .branch-name').text(selectedValue);
  84. }
  85. });
  86. $selectBranch.find('.reference.column').on('click', function () {
  87. hideElem($selectBranch.find('.scrolling.reference-list-menu'));
  88. $selectBranch.find('.reference .text').removeClass('black');
  89. showElem($($(this).data('target')));
  90. $(this).find('.text').addClass('black');
  91. return false;
  92. });
  93. }
  94. initBranchSelector();
  95. // List submits
  96. function initListSubmits(selector, outerSelector) {
  97. const $list = $(`.ui.${outerSelector}.list`);
  98. const $noSelect = $list.find('.no-select');
  99. const $listMenu = $(`.${selector} .menu`);
  100. let hasUpdateAction = $listMenu.data('action') === 'update';
  101. const items = {};
  102. $(`.${selector}`).dropdown({
  103. 'action': 'nothing', // do not hide the menu if user presses Enter
  104. fullTextSearch: 'exact',
  105. async onHide() {
  106. hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var
  107. if (hasUpdateAction) {
  108. // TODO: Add batch functionality and make this 1 network request.
  109. const itemEntries = Object.entries(items);
  110. for (const [elementId, item] of itemEntries) {
  111. await updateIssuesMeta(
  112. item['update-url'],
  113. item.action,
  114. item['issue-id'],
  115. elementId,
  116. );
  117. }
  118. if (itemEntries.length) {
  119. reloadConfirmDraftComment();
  120. }
  121. }
  122. },
  123. });
  124. $listMenu.find('.item:not(.no-select)').on('click', function (e) {
  125. e.preventDefault();
  126. if ($(this).hasClass('ban-change')) {
  127. return false;
  128. }
  129. hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var
  130. const $clickedItem = $(this);
  131. const scope = $(this).attr('data-scope');
  132. $(this).parent().find('.item').each(function () {
  133. if (scope) {
  134. // Enable only clicked item for scoped labels
  135. if ($(this).attr('data-scope') !== scope) {
  136. return true;
  137. }
  138. if (!$(this).is($clickedItem) && !$(this).hasClass('checked')) {
  139. return true;
  140. }
  141. } else if (!$(this).is($clickedItem)) {
  142. // Toggle for other labels
  143. return true;
  144. }
  145. if ($(this).hasClass('checked')) {
  146. $(this).removeClass('checked');
  147. $(this).find('.octicon-check').addClass('tw-invisible');
  148. if (hasUpdateAction) {
  149. if (!($(this).data('id') in items)) {
  150. items[$(this).data('id')] = {
  151. 'update-url': $listMenu.data('update-url'),
  152. action: 'detach',
  153. 'issue-id': $listMenu.data('issue-id'),
  154. };
  155. } else {
  156. delete items[$(this).data('id')];
  157. }
  158. }
  159. } else {
  160. $(this).addClass('checked');
  161. $(this).find('.octicon-check').removeClass('tw-invisible');
  162. if (hasUpdateAction) {
  163. if (!($(this).data('id') in items)) {
  164. items[$(this).data('id')] = {
  165. 'update-url': $listMenu.data('update-url'),
  166. action: 'attach',
  167. 'issue-id': $listMenu.data('issue-id'),
  168. };
  169. } else {
  170. delete items[$(this).data('id')];
  171. }
  172. }
  173. }
  174. });
  175. // TODO: Which thing should be done for choosing review requests
  176. // to make chosen items be shown on time here?
  177. if (selector === 'select-reviewers-modify' || selector === 'select-assignees-modify') {
  178. return false;
  179. }
  180. const listIds = [];
  181. $(this).parent().find('.item').each(function () {
  182. if ($(this).hasClass('checked')) {
  183. listIds.push($(this).data('id'));
  184. $($(this).data('id-selector')).removeClass('gt-hidden');
  185. } else {
  186. $($(this).data('id-selector')).addClass('gt-hidden');
  187. }
  188. });
  189. if (listIds.length === 0) {
  190. $noSelect.removeClass('gt-hidden');
  191. } else {
  192. $noSelect.addClass('gt-hidden');
  193. }
  194. $($(this).parent().data('id')).val(listIds.join(','));
  195. return false;
  196. });
  197. $listMenu.find('.no-select.item').on('click', function (e) {
  198. e.preventDefault();
  199. if (hasUpdateAction) {
  200. (async () => {
  201. await updateIssuesMeta(
  202. $listMenu.data('update-url'),
  203. 'clear',
  204. $listMenu.data('issue-id'),
  205. '',
  206. );
  207. reloadConfirmDraftComment();
  208. })();
  209. }
  210. $(this).parent().find('.item').each(function () {
  211. $(this).removeClass('checked');
  212. $(this).find('.octicon-check').addClass('tw-invisible');
  213. });
  214. if (selector === 'select-reviewers-modify' || selector === 'select-assignees-modify') {
  215. return false;
  216. }
  217. $list.find('.item').each(function () {
  218. $(this).addClass('gt-hidden');
  219. });
  220. $noSelect.removeClass('gt-hidden');
  221. $($(this).parent().data('id')).val('');
  222. });
  223. }
  224. // Init labels and assignees
  225. initListSubmits('select-label', 'labels');
  226. initListSubmits('select-assignees', 'assignees');
  227. initListSubmits('select-assignees-modify', 'assignees');
  228. initListSubmits('select-reviewers-modify', 'assignees');
  229. function selectItem(select_id, input_id) {
  230. const $menu = $(`${select_id} .menu`);
  231. const $list = $(`.ui${select_id}.list`);
  232. const hasUpdateAction = $menu.data('action') === 'update';
  233. $menu.find('.item:not(.no-select)').on('click', function () {
  234. $(this).parent().find('.item').each(function () {
  235. $(this).removeClass('selected active');
  236. });
  237. $(this).addClass('selected active');
  238. if (hasUpdateAction) {
  239. (async () => {
  240. await updateIssuesMeta(
  241. $menu.data('update-url'),
  242. '',
  243. $menu.data('issue-id'),
  244. $(this).data('id'),
  245. );
  246. reloadConfirmDraftComment();
  247. })();
  248. }
  249. let icon = '';
  250. if (input_id === '#milestone_id') {
  251. icon = svg('octicon-milestone', 18, 'gt-mr-3');
  252. } else if (input_id === '#project_id') {
  253. icon = svg('octicon-project', 18, 'gt-mr-3');
  254. } else if (input_id === '#assignee_id') {
  255. icon = `<img class="ui avatar image gt-mr-3" alt="avatar" src=${$(this).data('avatar')}>`;
  256. }
  257. $list.find('.selected').html(`
  258. <a class="item muted sidebar-item-link" href=${$(this).data('href')}>
  259. ${icon}
  260. ${htmlEscape($(this).text())}
  261. </a>
  262. `);
  263. $(`.ui${select_id}.list .no-select`).addClass('gt-hidden');
  264. $(input_id).val($(this).data('id'));
  265. });
  266. $menu.find('.no-select.item').on('click', function () {
  267. $(this).parent().find('.item:not(.no-select)').each(function () {
  268. $(this).removeClass('selected active');
  269. });
  270. if (hasUpdateAction) {
  271. (async () => {
  272. await updateIssuesMeta(
  273. $menu.data('update-url'),
  274. '',
  275. $menu.data('issue-id'),
  276. $(this).data('id'),
  277. );
  278. reloadConfirmDraftComment();
  279. })();
  280. }
  281. $list.find('.selected').html('');
  282. $list.find('.no-select').removeClass('gt-hidden');
  283. $(input_id).val('');
  284. });
  285. }
  286. // Milestone, Assignee, Project
  287. selectItem('.select-project', '#project_id');
  288. selectItem('.select-milestone', '#milestone_id');
  289. selectItem('.select-assignee', '#assignee_id');
  290. }
  291. async function onEditContent(event) {
  292. event.preventDefault();
  293. const $segment = $(this).closest('.header').next();
  294. const $editContentZone = $segment.find('.edit-content-zone');
  295. const $renderContent = $segment.find('.render-content');
  296. const $rawContent = $segment.find('.raw-content');
  297. let comboMarkdownEditor;
  298. const setupDropzone = async ($dropzone) => {
  299. if ($dropzone.length === 0) return null;
  300. let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event
  301. let fileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone
  302. const dz = await createDropzone($dropzone[0], {
  303. url: $dropzone.attr('data-upload-url'),
  304. headers: {'X-Csrf-Token': csrfToken},
  305. maxFiles: $dropzone.attr('data-max-file'),
  306. maxFilesize: $dropzone.attr('data-max-size'),
  307. acceptedFiles: (['*/*', ''].includes($dropzone.attr('data-accepts'))) ? null : $dropzone.attr('data-accepts'),
  308. addRemoveLinks: true,
  309. dictDefaultMessage: $dropzone.attr('data-default-message'),
  310. dictInvalidFileType: $dropzone.attr('data-invalid-input-type'),
  311. dictFileTooBig: $dropzone.attr('data-file-too-big'),
  312. dictRemoveFile: $dropzone.attr('data-remove-file'),
  313. timeout: 0,
  314. thumbnailMethod: 'contain',
  315. thumbnailWidth: 480,
  316. thumbnailHeight: 480,
  317. init() {
  318. this.on('success', (file, data) => {
  319. file.uuid = data.uuid;
  320. fileUuidDict[file.uuid] = {submitted: false};
  321. const $input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
  322. $dropzone.find('.files').append($input);
  323. });
  324. this.on('removedfile', async (file) => {
  325. if (disableRemovedfileEvent) return;
  326. $(`#${file.uuid}`).remove();
  327. if ($dropzone.attr('data-remove-url') && !fileUuidDict[file.uuid].submitted) {
  328. try {
  329. await POST($dropzone.attr('data-remove-url'), {data: new URLSearchParams({file: file.uuid})});
  330. } catch (error) {
  331. console.error(error);
  332. }
  333. }
  334. });
  335. this.on('submit', () => {
  336. $.each(fileUuidDict, (fileUuid) => {
  337. fileUuidDict[fileUuid].submitted = true;
  338. });
  339. });
  340. this.on('reload', async () => {
  341. try {
  342. const response = await GET($editContentZone.attr('data-attachment-url'));
  343. const data = await response.json();
  344. // do not trigger the "removedfile" event, otherwise the attachments would be deleted from server
  345. disableRemovedfileEvent = true;
  346. dz.removeAllFiles(true);
  347. $dropzone.find('.files').empty();
  348. fileUuidDict = {};
  349. disableRemovedfileEvent = false;
  350. for (const attachment of data) {
  351. const imgSrc = `${$dropzone.attr('data-link-url')}/${attachment.uuid}`;
  352. dz.emit('addedfile', attachment);
  353. dz.emit('thumbnail', attachment, imgSrc);
  354. dz.emit('complete', attachment);
  355. dz.files.push(attachment);
  356. fileUuidDict[attachment.uuid] = {submitted: true};
  357. $dropzone.find(`img[src='${imgSrc}']`)[0].style.maxWidth = '100%';
  358. const $input = $(`<input id="${attachment.uuid}" name="files" type="hidden">`).val(attachment.uuid);
  359. $dropzone.find('.files').append($input);
  360. }
  361. } catch (error) {
  362. console.error(error);
  363. }
  364. });
  365. },
  366. });
  367. dz.emit('reload');
  368. return dz;
  369. };
  370. const cancelAndReset = (dz) => {
  371. showElem($renderContent);
  372. hideElem($editContentZone);
  373. if (dz) {
  374. dz.emit('reload');
  375. }
  376. };
  377. const saveAndRefresh = async (dz) => {
  378. showElem($renderContent);
  379. hideElem($editContentZone);
  380. try {
  381. const params = new URLSearchParams({
  382. content: comboMarkdownEditor.value(),
  383. context: $editContentZone.attr('data-context'),
  384. });
  385. for (const file of dz.files) params.append('files[]', file.uuid);
  386. const response = await POST($editContentZone.attr('data-update-url'), {data: params});
  387. const data = await response.json();
  388. if (!data.content) {
  389. $renderContent.html($('#no-content').html());
  390. $rawContent.text('');
  391. } else {
  392. $renderContent.html(data.content);
  393. $rawContent.text(comboMarkdownEditor.value());
  394. const $refIssues = $renderContent.find('p .ref-issue');
  395. attachRefIssueContextPopup($refIssues);
  396. }
  397. const $content = $segment;
  398. if (!$content.find('.dropzone-attachments').length) {
  399. if (data.attachments !== '') {
  400. $content[0].append(data.attachments);
  401. }
  402. } else if (data.attachments === '') {
  403. $content.find('.dropzone-attachments').remove();
  404. } else {
  405. $content.find('.dropzone-attachments')[0].outerHTML = data.attachments;
  406. }
  407. if (dz) {
  408. dz.emit('submit');
  409. dz.emit('reload');
  410. }
  411. initMarkupContent();
  412. initCommentContent();
  413. } catch (error) {
  414. console.error(error);
  415. }
  416. };
  417. if (!$editContentZone.html()) {
  418. $editContentZone.html($('#issue-comment-editor-template').html());
  419. comboMarkdownEditor = await initComboMarkdownEditor($editContentZone.find('.combo-markdown-editor'));
  420. const $dropzone = $editContentZone.find('.dropzone');
  421. const dz = await setupDropzone($dropzone);
  422. $editContentZone.find('.cancel.button').on('click', (e) => {
  423. e.preventDefault();
  424. cancelAndReset(dz);
  425. });
  426. $editContentZone.find('.save.button').on('click', (e) => {
  427. e.preventDefault();
  428. saveAndRefresh(dz);
  429. });
  430. } else {
  431. comboMarkdownEditor = getComboMarkdownEditor($editContentZone.find('.combo-markdown-editor'));
  432. }
  433. // Show write/preview tab and copy raw content as needed
  434. showElem($editContentZone);
  435. hideElem($renderContent);
  436. if (!comboMarkdownEditor.value()) {
  437. comboMarkdownEditor.value($rawContent.text());
  438. }
  439. comboMarkdownEditor.focus();
  440. }
  441. export function initRepository() {
  442. if ($('.page-content.repository').length === 0) {
  443. return;
  444. }
  445. initRepoBranchTagSelector('.js-branch-tag-selector');
  446. // Options
  447. if ($('.repository.settings.options').length > 0) {
  448. // Enable or select internal/external wiki system and issue tracker.
  449. $('.enable-system').on('change', function () {
  450. if (this.checked) {
  451. $($(this).data('target')).removeClass('disabled');
  452. if (!$(this).data('context')) $($(this).data('context')).addClass('disabled');
  453. } else {
  454. $($(this).data('target')).addClass('disabled');
  455. if (!$(this).data('context')) $($(this).data('context')).removeClass('disabled');
  456. }
  457. });
  458. $('.enable-system-radio').on('change', function () {
  459. if (this.value === 'false') {
  460. $($(this).data('target')).addClass('disabled');
  461. if ($(this).data('context') !== undefined) $($(this).data('context')).removeClass('disabled');
  462. } else if (this.value === 'true') {
  463. $($(this).data('target')).removeClass('disabled');
  464. if ($(this).data('context') !== undefined) $($(this).data('context')).addClass('disabled');
  465. }
  466. });
  467. const $trackerIssueStyleRadios = $('.js-tracker-issue-style');
  468. $trackerIssueStyleRadios.on('change input', () => {
  469. const checkedVal = $trackerIssueStyleRadios.filter(':checked').val();
  470. $('#tracker-issue-style-regex-box').toggleClass('disabled', checkedVal !== 'regexp');
  471. });
  472. }
  473. // Labels
  474. initCompLabelEdit('.repository.labels');
  475. // Milestones
  476. if ($('.repository.new.milestone').length > 0) {
  477. $('#clear-date').on('click', () => {
  478. $('#deadline').val('');
  479. return false;
  480. });
  481. }
  482. // Repo Creation
  483. if ($('.repository.new.repo').length > 0) {
  484. $('input[name="gitignores"], input[name="license"]').on('change', () => {
  485. const gitignores = $('input[name="gitignores"]').val();
  486. const license = $('input[name="license"]').val();
  487. if (gitignores || license) {
  488. document.querySelector('input[name="auto_init"]').checked = true;
  489. }
  490. });
  491. }
  492. // Compare or pull request
  493. const $repoDiff = $('.repository.diff');
  494. if ($repoDiff.length) {
  495. initRepoCommonBranchOrTagDropdown('.choose.branch .dropdown');
  496. initRepoCommonFilterSearchDropdown('.choose.branch .dropdown');
  497. }
  498. initRepoCloneLink();
  499. initCitationFileCopyContent();
  500. initRepoSettingBranches();
  501. // Issues
  502. if ($('.repository.view.issue').length > 0) {
  503. initRepoIssueCommentEdit();
  504. initRepoIssueBranchSelect();
  505. initRepoIssueTitleEdit();
  506. initRepoIssueWipToggle();
  507. initRepoIssueComments();
  508. initRepoDiffConversationNav();
  509. initRepoIssueReferenceIssue();
  510. initRepoIssueCommentDelete();
  511. initRepoIssueDependencyDelete();
  512. initRepoIssueCodeCommentCancel();
  513. initRepoPullRequestUpdate();
  514. initCompReactionSelector($(document));
  515. initRepoPullRequestMergeForm();
  516. initRepoPullRequestCommitStatus();
  517. }
  518. // Pull request
  519. const $repoComparePull = $('.repository.compare.pull');
  520. if ($repoComparePull.length > 0) {
  521. // show pull request form
  522. $repoComparePull.find('button.show-form').on('click', function (e) {
  523. e.preventDefault();
  524. hideElem($(this).parent());
  525. const $form = $repoComparePull.find('.pullrequest-form');
  526. showElem($form);
  527. });
  528. }
  529. initUnicodeEscapeButton();
  530. }
  531. function initRepoIssueCommentEdit() {
  532. // Edit issue or comment content
  533. $(document).on('click', '.edit-content', onEditContent);
  534. // Quote reply
  535. $(document).on('click', '.quote-reply', async function (event) {
  536. event.preventDefault();
  537. const target = $(this).data('target');
  538. const quote = $(`#${target}`).text().replace(/\n/g, '\n> ');
  539. const content = `> ${quote}\n\n`;
  540. let editor;
  541. if ($(this).hasClass('quote-reply-diff')) {
  542. const $replyBtn = $(this).closest('.comment-code-cloud').find('button.comment-form-reply');
  543. editor = await handleReply($replyBtn);
  544. } else {
  545. // for normal issue/comment page
  546. editor = getComboMarkdownEditor($('#comment-form .combo-markdown-editor'));
  547. }
  548. if (editor) {
  549. if (editor.value()) {
  550. editor.value(`${editor.value()}\n\n${content}`);
  551. } else {
  552. editor.value(content);
  553. }
  554. editor.focus();
  555. editor.moveCursorToEnd();
  556. }
  557. });
  558. }