diff options
Diffstat (limited to 'web_src/js/features/repo-editor.ts')
-rw-r--r-- | web_src/js/features/repo-editor.ts | 167 |
1 files changed, 167 insertions, 0 deletions
diff --git a/web_src/js/features/repo-editor.ts b/web_src/js/features/repo-editor.ts new file mode 100644 index 0000000000..8bf23fc60e --- /dev/null +++ b/web_src/js/features/repo-editor.ts @@ -0,0 +1,167 @@ +import $ from 'jquery'; +import {htmlEscape} from 'escape-goat'; +import {createCodeEditor} from './codeeditor.ts'; +import {hideElem, queryElems, showElem} from '../utils/dom.ts'; +import {initMarkupContent} from '../markup/content.ts'; +import {attachRefIssueContextPopup} from './contextpopup.ts'; +import {POST} from '../modules/fetch.ts'; +import {initDropzone} from './dropzone.ts'; + +function initEditPreviewTab($form) { + const $tabMenu = $form.find('.repo-editor-menu'); + $tabMenu.find('.item').tab(); + const $previewTab = $tabMenu.find('a[data-tab="preview"]'); + if ($previewTab.length) { + $previewTab.on('click', async function () { + const $this = $(this); + let context = `${$this.data('context')}/`; + const mode = $this.data('markup-mode') || 'comment'; + const $treePathEl = $form.find('input#tree_path'); + if ($treePathEl.length > 0) { + context += $treePathEl.val(); + } + context = context.substring(0, context.lastIndexOf('/')); + + const formData = new FormData(); + formData.append('mode', mode); + formData.append('context', context); + formData.append('text', $form.find('.tab[data-tab="write"] textarea').val()); + formData.append('file_path', $treePathEl.val()); + try { + const response = await POST($this.data('url'), {data: formData}); + const data = await response.text(); + const $previewPanel = $form.find('.tab[data-tab="preview"]'); + if ($previewPanel.length) { + renderPreviewPanelContent($previewPanel, data); + } + } catch (error) { + console.error('Error:', error); + } + }); + } +} + +export function initRepoEditor() { + const dropzoneUpload = document.querySelector('.page-content.repository.editor.upload .dropzone'); + if (dropzoneUpload) initDropzone(dropzoneUpload); + + const editArea = document.querySelector('.page-content.repository.editor textarea#edit_area'); + if (!editArea) return; + + for (const el of queryElems('.js-quick-pull-choice-option')) { + el.addEventListener('input', () => { + if (el.value === 'commit-to-new-branch') { + showElem('.quick-pull-branch-name'); + document.querySelector('.quick-pull-branch-name input').required = true; + } else { + hideElem('.quick-pull-branch-name'); + document.querySelector('.quick-pull-branch-name input').required = false; + } + document.querySelector('#commit-button').textContent = el.getAttribute('data-button-text'); + }); + } + + const filenameInput = document.querySelector('#file-name'); + 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('#tree_path').value = parts.join('/'); + } + filenameInput.addEventListener('input', function () { + const parts = filenameInput.value.split('/'); + if (parts.length > 1) { + for (let i = 0; i < parts.length; ++i) { + const value = parts[i]; + if (i < parts.length - 1) { + if (value.length) { + $(`<span class="section"><a href="#">${htmlEscape(value)}</a></span>`).insertBefore($(filenameInput)); + $('<div class="breadcrumb-divider">/</div>').insertBefore($(filenameInput)); + } + } else { + filenameInput.value = value; + } + this.setSelectionRange(0, 0); + } + } + joinTreePath(); + }); + filenameInput.addEventListener('keydown', function (e) { + const sections = queryElems('.breadcrumb span.section'); + const dividers = queryElems('.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 $form = $('.repository.editor .edit.form'); + initEditPreviewTab($form); + + (async () => { + const editor = await createCodeEditor(editArea, filenameInput); + + // Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage + // to enable or disable the commit button + const commitButton = document.querySelector('#commit-button'); + const $editForm = $('.ui.edit.form'); + const dirtyFileClass = 'dirty-file'; + + // Disabling the button at the start + if ($('input[name="page_has_posted"]').val() !== 'true') { + commitButton.disabled = true; + } + + // Registering a custom listener for the file path and the file content + $editForm.areYouSure({ + silent: true, + dirtyClass: dirtyFileClass, + fieldSelector: ':input:not(.commit-form-wrapper :input)', + change($form) { + const dirty = $form[0]?.classList.contains(dirtyFileClass); + commitButton.disabled = !dirty; + }, + }); + + // 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', (e) => { + // A modal which asks if an empty file should be committed + if (!editArea.value) { + $('#edit-empty-content-modal').modal({ + onApprove() { + $('.edit.form').trigger('submit'); + }, + }).modal('show'); + e.preventDefault(); + } + }); + })(); +} + +export function renderPreviewPanelContent($previewPanel, data) { + $previewPanel.html(data); + initMarkupContent(); + + const $refIssues = $previewPanel.find('p .ref-issue'); + attachRefIssueContextPopup($refIssues); +} |