gitea源码

repo-editor.ts 8.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. import {html, htmlRaw} from '../utils/html.ts';
  2. import {createCodeEditor} from './codeeditor.ts';
  3. import {hideElem, queryElems, showElem, createElementFromHTML} from '../utils/dom.ts';
  4. import {attachRefIssueContextPopup} from './contextpopup.ts';
  5. import {POST} from '../modules/fetch.ts';
  6. import {initDropzone} from './dropzone.ts';
  7. import {confirmModal} from './comp/ConfirmModal.ts';
  8. import {applyAreYouSure, ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts';
  9. import {fomanticQuery} from '../modules/fomantic/base.ts';
  10. import {submitFormFetchAction} from './common-fetch-action.ts';
  11. function initEditPreviewTab(elForm: HTMLFormElement) {
  12. const elTabMenu = elForm.querySelector('.repo-editor-menu');
  13. fomanticQuery(elTabMenu.querySelectorAll('.item')).tab();
  14. const elPreviewTab = elTabMenu.querySelector('a[data-tab="preview"]');
  15. const elPreviewPanel = elForm.querySelector('.tab[data-tab="preview"]');
  16. if (!elPreviewTab || !elPreviewPanel) return;
  17. elPreviewTab.addEventListener('click', async () => {
  18. const elTreePath = elForm.querySelector<HTMLInputElement>('input#tree_path');
  19. const previewUrl = elPreviewTab.getAttribute('data-preview-url');
  20. const previewContextRef = elPreviewTab.getAttribute('data-preview-context-ref');
  21. let previewContext = `${previewContextRef}/${elTreePath.value}`;
  22. previewContext = previewContext.substring(0, previewContext.lastIndexOf('/'));
  23. const formData = new FormData();
  24. formData.append('mode', 'file');
  25. formData.append('context', previewContext);
  26. formData.append('text', elForm.querySelector<HTMLTextAreaElement>('.tab[data-tab="write"] textarea').value);
  27. formData.append('file_path', elTreePath.value);
  28. const response = await POST(previewUrl, {data: formData});
  29. const data = await response.text();
  30. renderPreviewPanelContent(elPreviewPanel, data);
  31. });
  32. }
  33. export function initRepoEditor() {
  34. const dropzoneUpload = document.querySelector<HTMLElement>('.page-content.repository.editor.upload .dropzone');
  35. if (dropzoneUpload) initDropzone(dropzoneUpload);
  36. for (const el of queryElems<HTMLInputElement>(document, '.js-quick-pull-choice-option')) {
  37. el.addEventListener('input', () => {
  38. if (el.value === 'commit-to-new-branch') {
  39. showElem('.quick-pull-branch-name');
  40. document.querySelector<HTMLInputElement>('.quick-pull-branch-name input').required = true;
  41. } else {
  42. hideElem('.quick-pull-branch-name');
  43. document.querySelector<HTMLInputElement>('.quick-pull-branch-name input').required = false;
  44. }
  45. document.querySelector('#commit-button').textContent = el.getAttribute('data-button-text');
  46. });
  47. }
  48. const filenameInput = document.querySelector<HTMLInputElement>('#file-name');
  49. if (!filenameInput) return;
  50. function joinTreePath() {
  51. const parts = [];
  52. for (const el of document.querySelectorAll('.breadcrumb span.section')) {
  53. const link = el.querySelector('a');
  54. parts.push(link ? link.textContent : el.textContent);
  55. }
  56. if (filenameInput.value) {
  57. parts.push(filenameInput.value);
  58. }
  59. document.querySelector<HTMLInputElement>('#tree_path').value = parts.join('/');
  60. }
  61. filenameInput.addEventListener('input', function () {
  62. const parts = filenameInput.value.split('/');
  63. const links = Array.from(document.querySelectorAll('.breadcrumb span.section'));
  64. const dividers = Array.from(document.querySelectorAll('.breadcrumb .breadcrumb-divider'));
  65. let warningDiv = document.querySelector<HTMLDivElement>('.ui.warning.message.flash-message.flash-warning.space-related');
  66. let containSpace = false;
  67. if (parts.length > 1) {
  68. for (let i = 0; i < parts.length; ++i) {
  69. const value = parts[i];
  70. const trimValue = value.trim();
  71. if (trimValue === '..') {
  72. // remove previous tree path
  73. if (links.length > 0) {
  74. const link = links.pop();
  75. const divider = dividers.pop();
  76. link.remove();
  77. divider.remove();
  78. }
  79. continue;
  80. }
  81. if (i < parts.length - 1) {
  82. if (trimValue.length) {
  83. const linkElement = createElementFromHTML(
  84. html`<span class="section"><a href="#">${value}</a></span>`,
  85. );
  86. const dividerElement = createElementFromHTML(
  87. html`<div class="breadcrumb-divider">/</div>`,
  88. );
  89. links.push(linkElement);
  90. dividers.push(dividerElement);
  91. filenameInput.before(linkElement);
  92. filenameInput.before(dividerElement);
  93. }
  94. } else {
  95. filenameInput.value = value;
  96. }
  97. this.setSelectionRange(0, 0);
  98. containSpace = containSpace || (trimValue !== value && trimValue !== '');
  99. }
  100. }
  101. containSpace = containSpace || Array.from(links).some((link) => {
  102. const value = link.querySelector('a').textContent;
  103. return value.trim() !== value;
  104. });
  105. containSpace = containSpace || parts[parts.length - 1].trim() !== parts[parts.length - 1];
  106. if (containSpace) {
  107. if (!warningDiv) {
  108. warningDiv = document.createElement('div');
  109. warningDiv.classList.add('ui', 'warning', 'message', 'flash-message', 'flash-warning', 'space-related');
  110. warningDiv.innerHTML = html`<p>File path contains leading or trailing whitespace.</p>`;
  111. // Add display 'block' because display is set to 'none' in formantic\build\semantic.css
  112. warningDiv.style.display = 'block';
  113. const inputContainer = document.querySelector('.repo-editor-header');
  114. inputContainer.insertAdjacentElement('beforebegin', warningDiv);
  115. }
  116. showElem(warningDiv);
  117. } else if (warningDiv) {
  118. hideElem(warningDiv);
  119. }
  120. joinTreePath();
  121. });
  122. filenameInput.addEventListener('keydown', function (e) {
  123. const sections = queryElems(document, '.breadcrumb span.section');
  124. const dividers = queryElems(document, '.breadcrumb .breadcrumb-divider');
  125. // Jump back to last directory once the filename is empty
  126. if (e.code === 'Backspace' && filenameInput.selectionStart === 0 && sections.length > 0) {
  127. e.preventDefault();
  128. const lastSection = sections[sections.length - 1];
  129. const lastDivider = dividers.length ? dividers[dividers.length - 1] : null;
  130. const value = lastSection.querySelector('a').textContent;
  131. filenameInput.value = value + filenameInput.value;
  132. this.setSelectionRange(value.length, value.length);
  133. lastDivider?.remove();
  134. lastSection.remove();
  135. joinTreePath();
  136. }
  137. });
  138. const elForm = document.querySelector<HTMLFormElement>('.repository.editor .edit.form');
  139. // on the upload page, there is no editor(textarea)
  140. const editArea = document.querySelector<HTMLTextAreaElement>('.page-content.repository.editor textarea#edit_area');
  141. if (!editArea) return;
  142. // Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage
  143. // to enable or disable the commit button
  144. const commitButton = document.querySelector<HTMLButtonElement>('#commit-button');
  145. const dirtyFileClass = 'dirty-file';
  146. const syncCommitButtonState = () => {
  147. const dirty = elForm.classList.contains(dirtyFileClass);
  148. commitButton.disabled = !dirty;
  149. };
  150. // Registering a custom listener for the file path and the file content
  151. // FIXME: it is not quite right here (old bug), it causes double-init, the global areYouSure "dirty" class will also be added
  152. applyAreYouSure(elForm, {
  153. silent: true,
  154. dirtyClass: dirtyFileClass,
  155. fieldSelector: ':input:not(.commit-form-wrapper :input)',
  156. change: syncCommitButtonState,
  157. });
  158. syncCommitButtonState(); // disable the "commit" button when no content changes
  159. initEditPreviewTab(elForm);
  160. (async () => {
  161. const editor = await createCodeEditor(editArea, filenameInput);
  162. // Update the editor from query params, if available,
  163. // only after the dirtyFileClass initialization
  164. const params = new URLSearchParams(window.location.search);
  165. const value = params.get('value');
  166. if (value) {
  167. editor.setValue(value);
  168. }
  169. commitButton.addEventListener('click', async (e) => {
  170. // A modal which asks if an empty file should be committed
  171. if (!editArea.value) {
  172. e.preventDefault();
  173. if (await confirmModal({
  174. header: elForm.getAttribute('data-text-empty-confirm-header'),
  175. content: elForm.getAttribute('data-text-empty-confirm-content'),
  176. })) {
  177. ignoreAreYouSure(elForm);
  178. submitFormFetchAction(elForm);
  179. }
  180. }
  181. });
  182. })();
  183. }
  184. export function renderPreviewPanelContent(previewPanel: Element, htmlContent: string) {
  185. // the content is from the server, so it is safe to use innerHTML
  186. previewPanel.innerHTML = html`<div class="render-content markup">${htmlRaw(htmlContent)}</div>`;
  187. attachRefIssueContextPopup(previewPanel.querySelectorAll('p .ref-issue'));
  188. }