mirror of
https://github.com/go-gitea/gitea.git
synced 2026-03-04 23:35:33 +01:00
The banner allows site operators to communicate important announcements (e.g., maintenance windows, policy updates, service notices) directly within the UI. The maintenance mode only allows admin to access the web UI. * Fix #2345 * Fix #9618 --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
202 lines
8.6 KiB
TypeScript
202 lines
8.6 KiB
TypeScript
import {html, htmlRaw} from '../utils/html.ts';
|
|
import {createCodeEditor} from './codeeditor.ts';
|
|
import {hideElem, queryElems, showElem, createElementFromHTML} from '../utils/dom.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>`;
|
|
// Change to `block` display because it is set to 'none' in fomantic/build/semantic.css
|
|
warningDiv.classList.add('tw-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 render-preview markup">${htmlRaw(htmlContent)}</div>`;
|
|
}
|