Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent from submitting issue/comment on uploading #32263

Merged
82 changes: 59 additions & 23 deletions web_src/js/features/comp/ComboMarkdownEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@ import '@github/text-expander-element';
import $ from 'jquery';
import {attachTribute} from '../tribute.ts';
import {hideElem, showElem, autosize, isElemVisible} from '../../utils/dom.ts';
import {initEasyMDEPaste, initTextareaEvents} from './EditorUpload.ts';
import {
EventUploadStateChanged,
initEasyMDEPaste,
initTextareaEvents,
triggerUploadStateChanged,
} from './EditorUpload.ts';
import {handleGlobalEnterQuickSubmit} from './QuickSubmit.ts';
import {renderPreviewPanelContent} from '../repo-editor.ts';
import {easyMDEToolbarActions} from './EasyMDEToolbarActions.ts';
import {initTextExpander} from './TextExpander.ts';
import {showErrorToast} from '../../modules/toast.ts';
import {POST} from '../../modules/fetch.ts';
import {initTextareaMarkdown} from './EditorMarkdown.ts';
import {EventEditorContentChanged, initTextareaMarkdown, triggerEditorContentChanged} from './EditorMarkdown.ts';
import {DropzoneCustomEventReloadFiles, initDropzone} from '../dropzone.ts';

let elementIdCounter = 0;
Expand All @@ -37,7 +42,34 @@ export function validateTextareaNonEmpty(textarea) {
return true;
}

class ComboMarkdownEditor {
export class ComboMarkdownEditor {
static EventEditorContentChanged = EventEditorContentChanged;
static EventUploadStateChanged = EventUploadStateChanged;

public container : HTMLElement;

// TODO: use correct types to replace these "any" types
options: any;

tabEditor: HTMLElement;
tabPreviewer: HTMLElement;

easyMDE: any;
easyMDEToolbarActions: any;
easyMDEToolbarDefault: any;

textarea: HTMLTextAreaElement & {_giteaComboMarkdownEditor: any};
textareaMarkdownToolbar: HTMLElement;
textareaAutosize: any;

dropzone: HTMLElement;
attachedDropzoneInst: any;

previewUrl: string;
previewContext: string;
previewMode: string;
previewWiki: boolean;

constructor(container, options = {}) {
container._giteaComboMarkdownEditor = this;
this.options = options;
Expand All @@ -63,14 +95,13 @@ class ComboMarkdownEditor {

setupContainer() {
initTextExpander(this.container.querySelector('text-expander'));
this.container.addEventListener('ce-editor-content-changed', (e) => this.options?.onContentChanged?.(this, e));
}

setupTextarea() {
this.textarea = this.container.querySelector('.markdown-text-editor');
this.textarea._giteaComboMarkdownEditor = this;
this.textarea.id = `_combo_markdown_editor_${String(elementIdCounter++)}`;
this.textarea.addEventListener('input', (e) => this.options?.onContentChanged?.(this, e));
this.textarea.addEventListener('input', () => triggerEditorContentChanged(this.container));
this.applyEditorHeights(this.textarea, this.options.editorHeights);

if (this.textarea.getAttribute('data-disable-autosize') !== 'true') {
Expand Down Expand Up @@ -115,15 +146,21 @@ class ComboMarkdownEditor {

async setupDropzone() {
const dropzoneParentContainer = this.container.getAttribute('data-dropzone-parent-container');
if (dropzoneParentContainer) {
this.dropzone = this.container.closest(this.container.getAttribute('data-dropzone-parent-container'))?.querySelector('.dropzone');
if (this.dropzone) this.attachedDropzoneInst = await initDropzone(this.dropzone);
}
if (!dropzoneParentContainer) return;
this.dropzone = this.container.closest(this.container.getAttribute('data-dropzone-parent-container'))?.querySelector('.dropzone');
if (!this.dropzone) return;

this.attachedDropzoneInst = await initDropzone(this.dropzone);
// dropzone events
// * "processing" means a file is being uploaded
// * "queuecomplete" means all files have been uploaded
this.attachedDropzoneInst.on('processing', () => triggerUploadStateChanged(this.container));
this.attachedDropzoneInst.on('queuecomplete', () => triggerUploadStateChanged(this.container));
}

dropzoneGetFiles() {
if (!this.dropzone) return null;
return Array.from(this.dropzone.querySelectorAll('.files [name=files]'), (el) => el.value);
return Array.from(this.dropzone.querySelectorAll<HTMLInputElement>('.files [name=files]'), (el) => el.value);
}

dropzoneReloadFiles() {
Expand All @@ -137,8 +174,13 @@ class ComboMarkdownEditor {
this.attachedDropzoneInst.emit(DropzoneCustomEventReloadFiles);
}

isUploading() {
if (!this.dropzone) return false;
return this.attachedDropzoneInst.getQueuedFiles().length || this.attachedDropzoneInst.getUploadingFiles().length;
}

setupTab() {
const tabs = this.container.querySelectorAll('.tabular.menu > .item');
const tabs = this.container.querySelectorAll<HTMLElement>('.tabular.menu > .item');

// Fomantic Tab requires the "data-tab" to be globally unique.
// So here it uses our defined "data-tab-for" and "data-tab-panel" to generate the "data-tab" attribute for Fomantic.
Expand Down Expand Up @@ -170,7 +212,7 @@ class ComboMarkdownEditor {
formData.append('mode', this.previewMode);
formData.append('context', this.previewContext);
formData.append('text', this.value());
formData.append('wiki', this.previewWiki);
formData.append('wiki', String(this.previewWiki));
const response = await POST(this.previewUrl, {data: formData});
const data = await response.text();
renderPreviewPanelContent($(panelPreviewer), data);
Expand Down Expand Up @@ -237,24 +279,24 @@ class ComboMarkdownEditor {
easyMDEOpt.toolbar = this.parseEasyMDEToolbar(EasyMDE, easyMDEOpt.toolbar ?? this.easyMDEToolbarDefault);

this.easyMDE = new EasyMDE(easyMDEOpt);
this.easyMDE.codemirror.on('change', (...args) => {this.options?.onContentChanged?.(this, ...args)});
this.easyMDE.codemirror.on('change', () => triggerEditorContentChanged(this.container));
this.easyMDE.codemirror.setOption('extraKeys', {
'Cmd-Enter': (cm) => handleGlobalEnterQuickSubmit(cm.getTextArea()),
'Ctrl-Enter': (cm) => handleGlobalEnterQuickSubmit(cm.getTextArea()),
Enter: (cm) => {
const tributeContainer = document.querySelector('.tribute-container');
const tributeContainer = document.querySelector<HTMLElement>('.tribute-container');
if (!tributeContainer || tributeContainer.style.display === 'none') {
cm.execCommand('newlineAndIndent');
}
},
Up: (cm) => {
const tributeContainer = document.querySelector('.tribute-container');
const tributeContainer = document.querySelector<HTMLElement>('.tribute-container');
if (!tributeContainer || tributeContainer.style.display === 'none') {
return cm.execCommand('goLineUp');
}
},
Down: (cm) => {
const tributeContainer = document.querySelector('.tribute-container');
const tributeContainer = document.querySelector<HTMLElement>('.tribute-container');
if (!tributeContainer || tributeContainer.style.display === 'none') {
return cm.execCommand('goLineDown');
}
Expand Down Expand Up @@ -314,13 +356,7 @@ export function getComboMarkdownEditor(el) {
return el?._giteaComboMarkdownEditor;
}

export async function initComboMarkdownEditor(container, options = {}) {
if (container instanceof $) {
if (container.length !== 1) {
throw new Error('initComboMarkdownEditor: container must be a single element');
}
container = container[0];
}
export async function initComboMarkdownEditor(container: HTMLElement, options = {}) {
if (!container) {
throw new Error('initComboMarkdownEditor: container is null');
}
Expand Down
4 changes: 3 additions & 1 deletion web_src/js/features/comp/EditorMarkdown.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export const EventEditorContentChanged = 'ce-editor-content-changed';

export function triggerEditorContentChanged(target) {
target.dispatchEvent(new CustomEvent('ce-editor-content-changed', {bubbles: true}));
target.dispatchEvent(new CustomEvent(EventEditorContentChanged, {bubbles: true}));
}

function handleIndentSelection(textarea, e) {
Expand Down
13 changes: 12 additions & 1 deletion web_src/js/features/comp/EditorUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@ import {
DropzoneCustomEventUploadDone,
generateMarkdownLinkForAttachment,
} from '../dropzone.ts';
import type CodeMirror from 'codemirror';

let uploadIdCounter = 0;

export const EventUploadStateChanged = 'ce-upload-state-changed';

export function triggerUploadStateChanged(target) {
target.dispatchEvent(new CustomEvent(EventUploadStateChanged, {bubbles: true}));
}

function uploadFile(dropzoneEl, file) {
return new Promise((resolve) => {
const curUploadId = uploadIdCounter++;
Expand All @@ -18,7 +25,7 @@ function uploadFile(dropzoneEl, file) {
const onUploadDone = ({file}) => {
if (file._giteaUploadId === curUploadId) {
dropzoneInst.off(DropzoneCustomEventUploadDone, onUploadDone);
resolve();
resolve(file);
}
};
dropzoneInst.on(DropzoneCustomEventUploadDone, onUploadDone);
Expand All @@ -27,6 +34,8 @@ function uploadFile(dropzoneEl, file) {
}

class TextareaEditor {
editor : HTMLTextAreaElement;

constructor(editor) {
this.editor = editor;
}
Expand Down Expand Up @@ -61,6 +70,8 @@ class TextareaEditor {
}

class CodeMirrorEditor {
editor: CodeMirror.EditorFromTextArea;

constructor(editor) {
this.editor = editor;
}
Expand Down
11 changes: 8 additions & 3 deletions web_src/js/features/repo-issue-edit.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import $ from 'jquery';
import {handleReply} from './repo-issue.ts';
import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
import {getComboMarkdownEditor, initComboMarkdownEditor, ComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
import {POST} from '../modules/fetch.ts';
import {showErrorToast} from '../modules/toast.ts';
import {hideElem, showElem} from '../utils/dom.ts';
import {attachRefIssueContextPopup} from './contextpopup.ts';
import {initCommentContent, initMarkupContent} from '../markup/content.ts';
import {triggerUploadStateChanged} from './comp/EditorUpload.ts';

async function onEditContent(event) {
event.preventDefault();
Expand All @@ -15,7 +16,7 @@ async function onEditContent(event) {
const renderContent = segment.querySelector('.render-content');
const rawContent = segment.querySelector('.raw-content');

let comboMarkdownEditor;
let comboMarkdownEditor : ComboMarkdownEditor;

const cancelAndReset = (e) => {
e.preventDefault();
Expand Down Expand Up @@ -79,9 +80,12 @@ async function onEditContent(event) {
comboMarkdownEditor = getComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
if (!comboMarkdownEditor) {
editContentZone.innerHTML = document.querySelector('#issue-comment-editor-template').innerHTML;
const saveButton = editContentZone.querySelector('.ui.primary.button');
comboMarkdownEditor = await initComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
const syncUiState = () => saveButton.disabled = comboMarkdownEditor.isUploading();
comboMarkdownEditor.container.addEventListener(ComboMarkdownEditor.EventUploadStateChanged, syncUiState);
editContentZone.querySelector('.ui.cancel.button').addEventListener('click', cancelAndReset);
editContentZone.querySelector('.ui.primary.button').addEventListener('click', saveAndRefresh);
saveButton.addEventListener('click', saveAndRefresh);
}

// Show write/preview tab and copy raw content as needed
Expand All @@ -93,6 +97,7 @@ async function onEditContent(event) {
}
comboMarkdownEditor.switchTabToEditor();
comboMarkdownEditor.focus();
triggerUploadStateChanged(comboMarkdownEditor.container);
}

export function initRepoIssueCommentEdit() {
Expand Down
49 changes: 24 additions & 25 deletions web_src/js/features/repo-issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {htmlEscape} from 'escape-goat';
import {createTippy, showTemporaryTooltip} from '../modules/tippy.ts';
import {hideElem, showElem, toggleElem} from '../utils/dom.ts';
import {setFileFolding} from './file-fold.ts';
import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
import {ComboMarkdownEditor, getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
import {toAbsoluteUrl} from '../utils.ts';
import {GET, POST} from '../modules/fetch.ts';
import {showErrorToast} from '../modules/toast.ts';
Expand Down Expand Up @@ -483,9 +483,9 @@ export function initRepoPullRequestReview() {
await handleReply(this);
});

const $reviewBox = $('.review-box-panel');
if ($reviewBox.length === 1) {
const _promise = initComboMarkdownEditor($reviewBox.find('.combo-markdown-editor'));
const elReviewBox = document.querySelector('.review-box-panel');
if (elReviewBox) {
initComboMarkdownEditor(elReviewBox.querySelector('.combo-markdown-editor'));
}

// The following part is only for diff views
Expand Down Expand Up @@ -548,7 +548,7 @@ export function initRepoPullRequestReview() {
$td.find("input[name='line']").val(idx);
$td.find("input[name='side']").val(side === 'left' ? 'previous' : 'proposed');
$td.find("input[name='path']").val(path);
const editor = await initComboMarkdownEditor($td.find('.combo-markdown-editor'));
const editor = await initComboMarkdownEditor($td[0].querySelector('.combo-markdown-editor'));
editor.focus();
} catch (error) {
console.error(error);
Expand Down Expand Up @@ -669,37 +669,36 @@ export async function initSingleCommentEditor($commentForm) {
// pages:
// * normal new issue/pr page: no status-button, no comment-button (there is only a normal submit button which can submit empty content)
// * issue/pr view page: with comment form, has status-button and comment-button
const opts = {};
const statusButton = document.querySelector('#status-button');
const commentButton = document.querySelector('#comment-button');
opts.onContentChanged = (editor) => {
const editorText = editor.value().trim();
const editor = await initComboMarkdownEditor($commentForm[0].querySelector('.combo-markdown-editor'));
const statusButton = document.querySelector<HTMLButtonElement>('#status-button');
const commentButton = document.querySelector<HTMLButtonElement>('#comment-button');
const syncUiState = () => {
const editorText = editor.value().trim(), isUploading = editor.isUploading();
if (statusButton) {
statusButton.textContent = statusButton.getAttribute(editorText ? 'data-status-and-comment' : 'data-status');
statusButton.disabled = isUploading;
}
if (commentButton) {
commentButton.disabled = !editorText;
commentButton.disabled = !editorText || isUploading;
}
};
const editor = await initComboMarkdownEditor($commentForm.find('.combo-markdown-editor'), opts);
opts.onContentChanged(editor); // sync state of buttons with the initial content
editor.container.addEventListener(ComboMarkdownEditor.EventUploadStateChanged, syncUiState);
editor.container.addEventListener(ComboMarkdownEditor.EventEditorContentChanged, syncUiState);
syncUiState();
}

export function initIssueTemplateCommentEditors($commentForm) {
// pages:
// * new issue with issue template
const $comboFields = $commentForm.find('.combo-editor-dropzone');

const initCombo = async ($combo) => {
const $dropzoneContainer = $combo.find('.form-field-dropzone');
const $formField = $combo.find('.form-field-real');
const $markdownEditor = $combo.find('.combo-markdown-editor');
const initCombo = async (elCombo) => {
const $formField = $(elCombo.querySelector('.form-field-real'));
const dropzoneContainer = elCombo.querySelector('.form-field-dropzone');
const markdownEditor = elCombo.querySelector('.combo-markdown-editor');

const editor = await initComboMarkdownEditor($markdownEditor, {
onContentChanged: (editor) => {
$formField.val(editor.value());
},
});
const editor = await initComboMarkdownEditor(markdownEditor);
editor.container.addEventListener(ComboMarkdownEditor.EventEditorContentChanged, () => $formField.val(editor.value()));

$formField.on('focus', async () => {
// deactivate all markdown editors
Expand All @@ -709,16 +708,16 @@ export function initIssueTemplateCommentEditors($commentForm) {

// activate this markdown editor
hideElem($formField);
showElem($markdownEditor);
showElem($dropzoneContainer);
showElem(markdownEditor);
showElem(dropzoneContainer);

await editor.switchToUserPreference();
editor.focus();
});
};

for (const el of $comboFields) {
initCombo($(el));
initCombo(el);
}
}

Expand Down
2 changes: 1 addition & 1 deletion web_src/js/features/repo-release.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function initTagNameEditor() {
}

function initRepoReleaseEditor() {
const editor = document.querySelector('.repository.new.release .combo-markdown-editor');
const editor = document.querySelector<HTMLElement>('.repository.new.release .combo-markdown-editor');
if (!editor) {
return;
}
Expand Down
4 changes: 2 additions & 2 deletions web_src/js/features/repo-wiki.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import {fomanticMobileScreen} from '../modules/fomantic.ts';
import {POST} from '../modules/fetch.ts';

async function initRepoWikiFormEditor() {
const editArea = document.querySelector('.repository.wiki .combo-markdown-editor textarea');
const editArea = document.querySelector<HTMLTextAreaElement>('.repository.wiki .combo-markdown-editor textarea');
if (!editArea) return;

const form = document.querySelector('.repository.wiki.new .ui.form');
const editorContainer = form.querySelector('.combo-markdown-editor');
const editorContainer = form.querySelector<HTMLElement>('.combo-markdown-editor');
let editor;

let renderRequesting = false;
Expand Down