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

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