refactor: improve types in the frontend, misc fixes (#39142)

This commit is contained in:
silverwind
2026-08-31 04:12:25 -07:00
committed by GitHub
parent 535fc29ae8
commit 7c93ead628
46 changed files with 392 additions and 217 deletions
+31 -35
View File
@@ -11,7 +11,7 @@ import {
import {handleGlobalEnterQuickSubmit} from './QuickSubmit.ts';
import {renderPreviewPanelContent} from '../repo-editor.ts';
import {toggleTasklistCheckbox} from '../../markup/tasklist.ts';
import {easyMDEToolbarActions} from './EasyMDEToolbarActions.ts';
import {easyMDEToolbarActions, type EasyMdeToolbarAction} from './EasyMDEToolbarActions.ts';
import {initTextExpander} from './TextExpander.ts';
import {showErrorToast} from '../../modules/toast.ts';
import {POST} from '../../modules/fetch.ts';
@@ -25,6 +25,7 @@ import {DropzoneCustomEventReloadFiles, initDropzone} from '../dropzone.ts';
import {createTippy} from '../../modules/tippy.ts';
import {initTabSwitcher} from '../../modules/fomantic/tab.ts';
import type EasyMDE from 'easymde';
import type Dropzone from '@deltablot/dropzone';
import {localUserSettings} from '../../modules/user-settings.ts';
/**
@@ -57,11 +58,11 @@ type Heights = {
type ComboMarkdownEditorOptions = {
editorHeights?: Heights,
easyMDEOptions?: EasyMDE.Options,
easyMDEOptions?: Omit<EasyMDE.Options, 'toolbar'> & {toolbar?: ReadonlyArray<string>},
};
type ComboMarkdownEditorTextarea = HTMLTextAreaElement & {_giteaComboMarkdownEditor: any};
type ComboMarkdownEditorContainer = HTMLElement & {_giteaComboMarkdownEditor?: any};
type ComboMarkdownEditorTextarea = HTMLTextAreaElement & {_giteaComboMarkdownEditor: ComboMarkdownEditor};
type ComboMarkdownEditorContainer = HTMLElement & {_giteaComboMarkdownEditor?: ComboMarkdownEditor};
export class ComboMarkdownEditor {
static EventEditorContentChanged = EventEditorContentChanged;
@@ -75,18 +76,18 @@ export class ComboMarkdownEditor {
tabPreviewer?: HTMLElement;
supportEasyMDE!: boolean;
easyMDE: any;
easyMDEToolbarActions: any;
easyMDEToolbarDefault: any;
easyMDE: EasyMDE | null = null;
easyMDEToolbarActions?: Record<string, EasyMdeToolbarAction>;
easyMDEToolbarDefault!: string[];
textarea!: ComboMarkdownEditorTextarea;
textareaMarkdownToolbar!: HTMLElement;
textareaAutosize: any;
textareaAutosize?: ReturnType<typeof autosize>;
buttonMonospace!: HTMLButtonElement;
dropzone: HTMLElement | null = null;
attachedDropzoneInst: any;
attachedDropzoneInst?: Dropzone;
previewMode!: string;
previewUrl!: string;
@@ -188,19 +189,19 @@ export class ComboMarkdownEditor {
}
dropzoneReloadFiles() {
if (!this.dropzone) return;
if (!this.attachedDropzoneInst) return;
this.attachedDropzoneInst.emit(DropzoneCustomEventReloadFiles);
}
dropzoneSubmitReload() {
if (!this.dropzone) return;
if (!this.attachedDropzoneInst) return;
this.attachedDropzoneInst.emit('submit');
this.attachedDropzoneInst.emit(DropzoneCustomEventReloadFiles);
}
isUploading() {
if (!this.dropzone) return false;
return this.attachedDropzoneInst.getQueuedFiles().length || this.attachedDropzoneInst.getUploadingFiles().length;
if (!this.attachedDropzoneInst) return false;
return Boolean(this.attachedDropzoneInst.getQueuedFiles().length || this.attachedDropzoneInst.getUploadingFiles().length);
}
setupTab() {
@@ -300,9 +301,9 @@ export class ComboMarkdownEditor {
];
}
parseEasyMDEToolbar(easyMde: typeof EasyMDE, actions: any) {
parseEasyMDEToolbar(easyMde: typeof EasyMDE, actions: ReadonlyArray<string>) {
this.easyMDEToolbarActions = this.easyMDEToolbarActions || easyMDEToolbarActions(easyMde, this);
const processed = [];
const processed: EasyMdeToolbarAction[] = [];
for (const action of actions) {
const actionButton = this.easyMDEToolbarActions[action];
if (!actionButton) throw new Error(`Unknown EasyMDE toolbar action ${action}`);
@@ -345,27 +346,27 @@ export class ComboMarkdownEditor {
inputStyle: 'contenteditable', // nativeSpellcheck requires contenteditable
nativeSpellcheck: true,
...this.options.easyMDEOptions,
toolbar: this.parseEasyMDEToolbar(EasyMDE, this.options.easyMDEOptions?.toolbar ?? this.easyMDEToolbarDefault) as EasyMDE.Options['toolbar'],
};
easyMDEOpt.toolbar = this.parseEasyMDEToolbar(EasyMDE, easyMDEOpt.toolbar ?? this.easyMDEToolbarDefault);
this.easyMDE = new EasyMDE(easyMDEOpt);
this.easyMDE.codemirror.on('change', () => triggerEditorContentChanged(this.container));
this.easyMDE.codemirror.setOption('extraKeys', {
'Cmd-Enter': (cm: any) => handleGlobalEnterQuickSubmit(cm.getTextArea()),
'Ctrl-Enter': (cm: any) => handleGlobalEnterQuickSubmit(cm.getTextArea()),
Enter: (cm: any) => {
'Cmd-Enter': () => { handleGlobalEnterQuickSubmit(this.textarea) },
'Ctrl-Enter': () => { handleGlobalEnterQuickSubmit(this.textarea) },
Enter: (cm) => {
const tributeContainer = document.querySelector<HTMLElement>('.tribute-container');
if (!tributeContainer || tributeContainer.style.display === 'none') {
cm.execCommand('newlineAndIndent');
}
},
Up: (cm: any) => {
Up: (cm) => {
const tributeContainer = document.querySelector<HTMLElement>('.tribute-container');
if (!tributeContainer || tributeContainer.style.display === 'none') {
return cm.execCommand('goLineUp');
}
},
Down: (cm: any) => {
Down: (cm) => {
const tributeContainer = document.querySelector<HTMLElement>('.tribute-container');
if (!tributeContainer || tributeContainer.style.display === 'none') {
return cm.execCommand('goLineDown');
@@ -380,20 +381,16 @@ export class ComboMarkdownEditor {
hideElem(this.textareaMarkdownToolbar);
}
value(v?: any) {
if (v === undefined) {
value(v?: string): string {
if (v !== undefined) {
if (this.easyMDE) {
return this.easyMDE.value();
this.easyMDE.value(v);
} else {
this.textarea.value = v;
}
return this.textarea.value;
this.textareaAutosize?.resizeToFit();
}
if (this.easyMDE) {
this.easyMDE.value(v);
} else {
this.textarea.value = v;
}
this.textareaAutosize?.resizeToFit();
return this.easyMDE ? this.easyMDE.value() : this.textarea.value;
}
focus() {
@@ -438,10 +435,9 @@ function applyMonospaceToAllEditors() {
}
}
export function getComboMarkdownEditor(el: any): ComboMarkdownEditor | null {
export function getComboMarkdownEditor(el: Element | null): ComboMarkdownEditor | null {
if (!el) return null;
if (el.length) el = el[0];
return el._giteaComboMarkdownEditor;
return (el as ComboMarkdownEditorContainer)._giteaComboMarkdownEditor ?? null;
}
export async function initComboMarkdownEditor(container: HTMLElement, options:ComboMarkdownEditorOptions = {}) {
@@ -2,8 +2,15 @@ import {svg} from '../../svg.ts';
import type EasyMDE from 'easymde';
import type {ComboMarkdownEditor} from './ComboMarkdownEditor.ts';
export function easyMDEToolbarActions(easyMde: typeof EasyMDE, editor: ComboMarkdownEditor): Record<string, Partial<EasyMDE.ToolbarIcon | string>> {
const actions: Record<string, Partial<EasyMDE.ToolbarIcon> | string> = {
export type EasyMdeToolbarAction = {
name?: string,
action: EasyMDE.ToolbarIcon['action'],
icon: string,
title: string,
} | '|';
export function easyMDEToolbarActions(easyMde: typeof EasyMDE, editor: ComboMarkdownEditor): Record<string, EasyMdeToolbarAction> {
const actions: Record<string, EasyMdeToolbarAction> = {
'|': '|',
'heading-1': {
action: easyMde.toggleHeading1,
+7 -5
View File
@@ -12,18 +12,20 @@ import type Dropzone from '@deltablot/dropzone';
let uploadIdCounter = 0;
type UploadFile = File & {_giteaUploadId?: number, uuid?: string};
export const EventUploadStateChanged = 'ce-upload-state-changed';
export function triggerUploadStateChanged(target: HTMLElement) {
target.dispatchEvent(new CustomEvent(EventUploadStateChanged, {bubbles: true}));
}
function uploadFile(dropzoneEl: HTMLElement, file: File) {
return new Promise((resolve) => {
function uploadFile(dropzoneEl: HTMLElement, file: UploadFile) {
return new Promise<UploadFile>((resolve) => {
const curUploadId = uploadIdCounter++;
(file as any)._giteaUploadId = curUploadId;
file._giteaUploadId = curUploadId;
const dropzoneInst = dropzoneEl.dropzone;
const onUploadDone = ({file}: {file: any}) => {
const onUploadDone = ({file}: {file: UploadFile}) => {
if (file._giteaUploadId === curUploadId) {
dropzoneInst.off(DropzoneCustomEventUploadDone, onUploadDone);
resolve(file);
@@ -131,7 +133,7 @@ function getPastedImages(e: ClipboardEvent) {
}
export function initEasyMDEPaste(easyMDE: EasyMDE, dropzoneEl: HTMLElement) {
const editor = new CodeMirrorEditor(easyMDE.codemirror as any);
const editor = new CodeMirrorEditor(easyMDE.codemirror as CodeMirror.EditorFromTextArea);
easyMDE.codemirror.on('paste', (_, e) => {
const images = getPastedImages(e);
if (!images.length) return;
+7 -10
View File
@@ -10,16 +10,12 @@ import type TextExpanderElement from '@github/text-expander-element';
import type {TextExpanderChangeEvent, TextExpanderResult} from '@github/text-expander-element';
async function fetchIssueSuggestions(key: string, text: string, signal: AbortSignal): Promise<TextExpanderResult> {
const issuePathInfo = parseIssueHref(window.location.href);
if (!issuePathInfo.ownerName) {
const repoOwnerPathInfo = parseRepoOwnerPathInfo(window.location.pathname);
issuePathInfo.ownerName = repoOwnerPathInfo.ownerName;
issuePathInfo.repoName = repoOwnerPathInfo.repoName;
// then no issuePathInfo.indexString here, it is only used to exclude the current issue when "matchIssue"
}
if (!issuePathInfo.ownerName) return {matched: false};
const hrefPathInfo = parseIssueHref(window.location.href);
// the fallback has no indexString, it is only used to exclude the current issue when "matchIssue"
const pathInfo = hrefPathInfo ?? parseRepoOwnerPathInfo(window.location.pathname);
if (!pathInfo) return {matched: false};
const matches = await matchIssue(issuePathInfo.ownerName, issuePathInfo.repoName, issuePathInfo.indexString, text, signal);
const matches = await matchIssue(pathInfo.ownerName, pathInfo.repoName, hrefPathInfo?.indexString, text, signal);
if (!matches.length) return {matched: false};
const ul = createElementFromAttrs('ul', {class: 'suggestions'});
@@ -131,7 +127,8 @@ export function initTextExpander(expander: TextExpanderElement) {
}
});
expander.addEventListener('text-expander-value', ({detail}: Record<string, any>) => {
expander.addEventListener('text-expander-value', (event) => {
const {detail} = event as CustomEvent<{item: HTMLElement, key: string, value: string}>;
if (detail?.item) {
// add a space after @mentions and #issue as it's likely the user wants one
const suffix = ['@', '#'].includes(detail.key) ? ' ' : '';