mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-30 19:15:23 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			204 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			204 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import {html, htmlRaw} from '../utils/html.ts';
 | |
| import {createCodeEditor} from './codeeditor.ts';
 | |
| import {hideElem, queryElems, showElem, createElementFromHTML} from '../utils/dom.ts';
 | |
| import {attachRefIssueContextPopup} from './contextpopup.ts';
 | |
| import {POST} from '../modules/fetch.ts';
 | |
| import {initDropzone} from './dropzone.ts';
 | |
| import {confirmModal} from './comp/ConfirmModal.ts';
 | |
| import {applyAreYouSure, ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts';
 | |
| import {fomanticQuery} from '../modules/fomantic/base.ts';
 | |
| import {submitFormFetchAction} from './common-fetch-action.ts';
 | |
| 
 | |
| function initEditPreviewTab(elForm: HTMLFormElement) {
 | |
|   const elTabMenu = elForm.querySelector('.repo-editor-menu');
 | |
|   fomanticQuery(elTabMenu.querySelectorAll('.item')).tab();
 | |
| 
 | |
|   const elPreviewTab = elTabMenu.querySelector('a[data-tab="preview"]');
 | |
|   const elPreviewPanel = elForm.querySelector('.tab[data-tab="preview"]');
 | |
|   if (!elPreviewTab || !elPreviewPanel) return;
 | |
| 
 | |
|   elPreviewTab.addEventListener('click', async () => {
 | |
|     const elTreePath = elForm.querySelector<HTMLInputElement>('input#tree_path');
 | |
|     const previewUrl = elPreviewTab.getAttribute('data-preview-url');
 | |
|     const previewContextRef = elPreviewTab.getAttribute('data-preview-context-ref');
 | |
|     let previewContext = `${previewContextRef}/${elTreePath.value}`;
 | |
|     previewContext = previewContext.substring(0, previewContext.lastIndexOf('/'));
 | |
|     const formData = new FormData();
 | |
|     formData.append('mode', 'file');
 | |
|     formData.append('context', previewContext);
 | |
|     formData.append('text', elForm.querySelector<HTMLTextAreaElement>('.tab[data-tab="write"] textarea').value);
 | |
|     formData.append('file_path', elTreePath.value);
 | |
|     const response = await POST(previewUrl, {data: formData});
 | |
|     const data = await response.text();
 | |
|     renderPreviewPanelContent(elPreviewPanel, data);
 | |
|   });
 | |
| }
 | |
| 
 | |
| export function initRepoEditor() {
 | |
|   const dropzoneUpload = document.querySelector<HTMLElement>('.page-content.repository.editor.upload .dropzone');
 | |
|   if (dropzoneUpload) initDropzone(dropzoneUpload);
 | |
| 
 | |
|   for (const el of queryElems<HTMLInputElement>(document, '.js-quick-pull-choice-option')) {
 | |
|     el.addEventListener('input', () => {
 | |
|       if (el.value === 'commit-to-new-branch') {
 | |
|         showElem('.quick-pull-branch-name');
 | |
|         document.querySelector<HTMLInputElement>('.quick-pull-branch-name input').required = true;
 | |
|       } else {
 | |
|         hideElem('.quick-pull-branch-name');
 | |
|         document.querySelector<HTMLInputElement>('.quick-pull-branch-name input').required = false;
 | |
|       }
 | |
|       document.querySelector('#commit-button').textContent = el.getAttribute('data-button-text');
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   const filenameInput = document.querySelector<HTMLInputElement>('#file-name');
 | |
|   if (!filenameInput) return;
 | |
|   function joinTreePath() {
 | |
|     const parts = [];
 | |
|     for (const el of document.querySelectorAll('.breadcrumb span.section')) {
 | |
|       const link = el.querySelector('a');
 | |
|       parts.push(link ? link.textContent : el.textContent);
 | |
|     }
 | |
|     if (filenameInput.value) {
 | |
|       parts.push(filenameInput.value);
 | |
|     }
 | |
|     document.querySelector<HTMLInputElement>('#tree_path').value = parts.join('/');
 | |
|   }
 | |
|   filenameInput.addEventListener('input', function () {
 | |
|     const parts = filenameInput.value.split('/');
 | |
|     const links = Array.from(document.querySelectorAll('.breadcrumb span.section'));
 | |
|     const dividers = Array.from(document.querySelectorAll('.breadcrumb .breadcrumb-divider'));
 | |
|     let warningDiv = document.querySelector<HTMLDivElement>('.ui.warning.message.flash-message.flash-warning.space-related');
 | |
|     let containSpace = false;
 | |
|     if (parts.length > 1) {
 | |
|       for (let i = 0; i < parts.length; ++i) {
 | |
|         const value = parts[i];
 | |
|         const trimValue = value.trim();
 | |
|         if (trimValue === '..') {
 | |
|           // remove previous tree path
 | |
|           if (links.length > 0) {
 | |
|             const link = links.pop();
 | |
|             const divider = dividers.pop();
 | |
|             link.remove();
 | |
|             divider.remove();
 | |
|           }
 | |
|           continue;
 | |
|         }
 | |
|         if (i < parts.length - 1) {
 | |
|           if (trimValue.length) {
 | |
|             const linkElement = createElementFromHTML(
 | |
|               html`<span class="section"><a href="#">${value}</a></span>`,
 | |
|             );
 | |
|             const dividerElement = createElementFromHTML(
 | |
|               html`<div class="breadcrumb-divider">/</div>`,
 | |
|             );
 | |
|             links.push(linkElement);
 | |
|             dividers.push(dividerElement);
 | |
|             filenameInput.before(linkElement);
 | |
|             filenameInput.before(dividerElement);
 | |
|           }
 | |
|         } else {
 | |
|           filenameInput.value = value;
 | |
|         }
 | |
|         this.setSelectionRange(0, 0);
 | |
|         containSpace = containSpace || (trimValue !== value && trimValue !== '');
 | |
|       }
 | |
|     }
 | |
|     containSpace = containSpace || Array.from(links).some((link) => {
 | |
|       const value = link.querySelector('a').textContent;
 | |
|       return value.trim() !== value;
 | |
|     });
 | |
|     containSpace = containSpace || parts[parts.length - 1].trim() !== parts[parts.length - 1];
 | |
|     if (containSpace) {
 | |
|       if (!warningDiv) {
 | |
|         warningDiv = document.createElement('div');
 | |
|         warningDiv.classList.add('ui', 'warning', 'message', 'flash-message', 'flash-warning', 'space-related');
 | |
|         warningDiv.innerHTML = html`<p>File path contains leading or trailing whitespace.</p>`;
 | |
|         // Add display 'block' because display is set to 'none' in formantic\build\semantic.css
 | |
|         warningDiv.style.display = 'block';
 | |
|         const inputContainer = document.querySelector('.repo-editor-header');
 | |
|         inputContainer.insertAdjacentElement('beforebegin', warningDiv);
 | |
|       }
 | |
|       showElem(warningDiv);
 | |
|     } else if (warningDiv) {
 | |
|       hideElem(warningDiv);
 | |
|     }
 | |
|     joinTreePath();
 | |
|   });
 | |
|   filenameInput.addEventListener('keydown', function (e) {
 | |
|     const sections = queryElems(document, '.breadcrumb span.section');
 | |
|     const dividers = queryElems(document, '.breadcrumb .breadcrumb-divider');
 | |
|     // Jump back to last directory once the filename is empty
 | |
|     if (e.code === 'Backspace' && filenameInput.selectionStart === 0 && sections.length > 0) {
 | |
|       e.preventDefault();
 | |
|       const lastSection = sections[sections.length - 1];
 | |
|       const lastDivider = dividers.length ? dividers[dividers.length - 1] : null;
 | |
|       const value = lastSection.querySelector('a').textContent;
 | |
|       filenameInput.value = value + filenameInput.value;
 | |
|       this.setSelectionRange(value.length, value.length);
 | |
|       lastDivider?.remove();
 | |
|       lastSection.remove();
 | |
|       joinTreePath();
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   const elForm = document.querySelector<HTMLFormElement>('.repository.editor .edit.form');
 | |
| 
 | |
|   // on the upload page, there is no editor(textarea)
 | |
|   const editArea = document.querySelector<HTMLTextAreaElement>('.page-content.repository.editor textarea#edit_area');
 | |
|   if (!editArea) return;
 | |
| 
 | |
|   // Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage
 | |
|   // to enable or disable the commit button
 | |
|   const commitButton = document.querySelector<HTMLButtonElement>('#commit-button');
 | |
|   const dirtyFileClass = 'dirty-file';
 | |
| 
 | |
|   const syncCommitButtonState = () => {
 | |
|     const dirty = elForm.classList.contains(dirtyFileClass);
 | |
|     commitButton.disabled = !dirty;
 | |
|   };
 | |
|   // Registering a custom listener for the file path and the file content
 | |
|   // FIXME: it is not quite right here (old bug), it causes double-init, the global areYouSure "dirty" class will also be added
 | |
|   applyAreYouSure(elForm, {
 | |
|     silent: true,
 | |
|     dirtyClass: dirtyFileClass,
 | |
|     fieldSelector: ':input:not(.commit-form-wrapper :input)',
 | |
|     change: syncCommitButtonState,
 | |
|   });
 | |
|   syncCommitButtonState(); // disable the "commit" button when no content changes
 | |
| 
 | |
|   initEditPreviewTab(elForm);
 | |
| 
 | |
|   (async () => {
 | |
|     const editor = await createCodeEditor(editArea, filenameInput);
 | |
| 
 | |
|     // Update the editor from query params, if available,
 | |
|     // only after the dirtyFileClass initialization
 | |
|     const params = new URLSearchParams(window.location.search);
 | |
|     const value = params.get('value');
 | |
|     if (value) {
 | |
|       editor.setValue(value);
 | |
|     }
 | |
| 
 | |
|     commitButton.addEventListener('click', async (e) => {
 | |
|       // A modal which asks if an empty file should be committed
 | |
|       if (!editArea.value) {
 | |
|         e.preventDefault();
 | |
|         if (await confirmModal({
 | |
|           header: elForm.getAttribute('data-text-empty-confirm-header'),
 | |
|           content: elForm.getAttribute('data-text-empty-confirm-content'),
 | |
|         })) {
 | |
|           ignoreAreYouSure(elForm);
 | |
|           submitFormFetchAction(elForm);
 | |
|         }
 | |
|       }
 | |
|     });
 | |
|   })();
 | |
| }
 | |
| 
 | |
| export function renderPreviewPanelContent(previewPanel: Element, htmlContent: string) {
 | |
|   // the content is from the server, so it is safe to use innerHTML
 | |
|   previewPanel.innerHTML = html`<div class="render-content markup">${htmlRaw(htmlContent)}</div>`;
 | |
|   attachRefIssueContextPopup(previewPanel.querySelectorAll('p .ref-issue'));
 | |
| }
 |