code.gitea.io/gitea@v1.22.3/web_src/js/features/repo-editor.js (about) 1 import $ from 'jquery'; 2 import {htmlEscape} from 'escape-goat'; 3 import {createCodeEditor} from './codeeditor.js'; 4 import {hideElem, showElem} from '../utils/dom.js'; 5 import {initMarkupContent} from '../markup/content.js'; 6 import {attachRefIssueContextPopup} from './contextpopup.js'; 7 import {POST} from '../modules/fetch.js'; 8 9 function initEditPreviewTab($form) { 10 const $tabMenu = $form.find('.repo-editor-menu'); 11 $tabMenu.find('.item').tab(); 12 const $previewTab = $tabMenu.find('a[data-tab="preview"]'); 13 if ($previewTab.length) { 14 $previewTab.on('click', async function () { 15 const $this = $(this); 16 let context = `${$this.data('context')}/`; 17 const mode = $this.data('markup-mode') || 'comment'; 18 const $treePathEl = $form.find('input#tree_path'); 19 if ($treePathEl.length > 0) { 20 context += $treePathEl.val(); 21 } 22 context = context.substring(0, context.lastIndexOf('/')); 23 24 const formData = new FormData(); 25 formData.append('mode', mode); 26 formData.append('context', context); 27 formData.append('text', $form.find('.tab[data-tab="write"] textarea').val()); 28 formData.append('file_path', $treePathEl.val()); 29 try { 30 const response = await POST($this.data('url'), {data: formData}); 31 const data = await response.text(); 32 const $previewPanel = $form.find('.tab[data-tab="preview"]'); 33 if ($previewPanel.length) { 34 renderPreviewPanelContent($previewPanel, data); 35 } 36 } catch (error) { 37 console.error('Error:', error); 38 } 39 }); 40 } 41 } 42 43 function initEditorForm() { 44 const $form = $('.repository .edit.form'); 45 if (!$form) return; 46 initEditPreviewTab($form); 47 } 48 49 function getCursorPosition($e) { 50 const el = $e.get(0); 51 let pos = 0; 52 if ('selectionStart' in el) { 53 pos = el.selectionStart; 54 } else if ('selection' in document) { 55 el.focus(); 56 const Sel = document.selection.createRange(); 57 const SelLength = document.selection.createRange().text.length; 58 Sel.moveStart('character', -el.value.length); 59 pos = Sel.text.length - SelLength; 60 } 61 return pos; 62 } 63 64 export function initRepoEditor() { 65 initEditorForm(); 66 67 $('.js-quick-pull-choice-option').on('change', function () { 68 if ($(this).val() === 'commit-to-new-branch') { 69 showElem('.quick-pull-branch-name'); 70 document.querySelector('.quick-pull-branch-name input').required = true; 71 } else { 72 hideElem('.quick-pull-branch-name'); 73 document.querySelector('.quick-pull-branch-name input').required = false; 74 } 75 $('#commit-button').text(this.getAttribute('button_text')); 76 }); 77 78 const joinTreePath = ($fileNameEl) => { 79 const parts = []; 80 $('.breadcrumb span.section').each(function () { 81 const $element = $(this); 82 if ($element.find('a').length) { 83 parts.push($element.find('a').text()); 84 } else { 85 parts.push($element.text()); 86 } 87 }); 88 if ($fileNameEl.val()) parts.push($fileNameEl.val()); 89 $('#tree_path').val(parts.join('/')); 90 }; 91 92 const $editFilename = $('#file-name'); 93 $editFilename.on('input', function () { 94 const parts = $(this).val().split('/'); 95 96 if (parts.length > 1) { 97 for (let i = 0; i < parts.length; ++i) { 98 const value = parts[i]; 99 if (i < parts.length - 1) { 100 if (value.length) { 101 $(`<span class="section"><a href="#">${htmlEscape(value)}</a></span>`).insertBefore($(this)); 102 $('<div class="breadcrumb-divider">/</div>').insertBefore($(this)); 103 } 104 } else { 105 $(this).val(value); 106 } 107 this.setSelectionRange(0, 0); 108 } 109 } 110 111 joinTreePath($(this)); 112 }); 113 114 $editFilename.on('keydown', function (e) { 115 const $section = $('.breadcrumb span.section'); 116 117 // Jump back to last directory once the filename is empty 118 if (e.code === 'Backspace' && getCursorPosition($(this)) === 0 && $section.length > 0) { 119 e.preventDefault(); 120 const $divider = $('.breadcrumb .breadcrumb-divider'); 121 const value = $section.last().find('a').text(); 122 $(this).val(value + $(this).val()); 123 this.setSelectionRange(value.length, value.length); 124 $section.last().remove(); 125 $divider.last().remove(); 126 joinTreePath($(this)); 127 } 128 }); 129 130 const $editArea = $('.repository.editor textarea#edit_area'); 131 if (!$editArea.length) return; 132 133 (async () => { 134 const editor = await createCodeEditor($editArea[0], $editFilename[0]); 135 136 // Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage 137 // to enable or disable the commit button 138 const commitButton = document.getElementById('commit-button'); 139 const $editForm = $('.ui.edit.form'); 140 const dirtyFileClass = 'dirty-file'; 141 142 // Disabling the button at the start 143 if ($('input[name="page_has_posted"]').val() !== 'true') { 144 commitButton.disabled = true; 145 } 146 147 // Registering a custom listener for the file path and the file content 148 $editForm.areYouSure({ 149 silent: true, 150 dirtyClass: dirtyFileClass, 151 fieldSelector: ':input:not(.commit-form-wrapper :input)', 152 change($form) { 153 const dirty = $form[0]?.classList.contains(dirtyFileClass); 154 commitButton.disabled = !dirty; 155 }, 156 }); 157 158 // Update the editor from query params, if available, 159 // only after the dirtyFileClass initialization 160 const params = new URLSearchParams(window.location.search); 161 const value = params.get('value'); 162 if (value) { 163 editor.setValue(value); 164 } 165 166 commitButton?.addEventListener('click', (e) => { 167 // A modal which asks if an empty file should be committed 168 if (!$editArea.val()) { 169 $('#edit-empty-content-modal').modal({ 170 onApprove() { 171 $('.edit.form').trigger('submit'); 172 }, 173 }).modal('show'); 174 e.preventDefault(); 175 } 176 }); 177 })(); 178 } 179 180 export function renderPreviewPanelContent($previewPanel, data) { 181 $previewPanel.html(data); 182 initMarkupContent(); 183 184 const $refIssues = $previewPanel.find('p .ref-issue'); 185 attachRefIssueContextPopup($refIssues); 186 }