mirror of
https://github.com/go-gitea/gitea
synced 2024-12-22 16:44:26 +00:00
Merge branch 'main' into patch-20
This commit is contained in:
commit
a80a2e01ff
@ -642,7 +642,7 @@ rules:
|
||||
no-this-before-super: [2]
|
||||
no-throw-literal: [2]
|
||||
no-undef-init: [2]
|
||||
no-undef: [2, {typeof: true}]
|
||||
no-undef: [2, {typeof: true}] # TODO: disable this rule after tsc passes
|
||||
no-undefined: [0]
|
||||
no-underscore-dangle: [0]
|
||||
no-unexpected-multiline: [2]
|
||||
|
@ -348,6 +348,7 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
|
||||
}
|
||||
|
||||
if !baseGitRepo.IsBranchExist(pull.BaseBranch) {
|
||||
ctx.Data["BaseBranchNotExist"] = true
|
||||
ctx.Data["IsPullRequestBroken"] = true
|
||||
ctx.Data["BaseTarget"] = pull.BaseBranch
|
||||
ctx.Data["HeadTarget"] = pull.HeadBranch
|
||||
|
@ -38,8 +38,8 @@
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -231,7 +231,8 @@
|
||||
{{template "shared/user/avatarlink" dict "user" .Poster}}
|
||||
<span class="text grey muted-links">
|
||||
{{template "shared/user/authorlink" .Poster}}
|
||||
{{ctx.Locale.Tr "repo.issues.delete_branch_at" .OldRef $createdStr}}
|
||||
{{$oldRef := HTMLFormat `<span class="tw-line-through">%s</span>` .OldRef}}
|
||||
{{ctx.Locale.Tr "repo.issues.delete_branch_at" $oldRef $createdStr}}
|
||||
</span>
|
||||
</div>
|
||||
{{else if eq .Type 12}}
|
||||
|
@ -61,12 +61,16 @@
|
||||
{{if .Issue.PullRequest.IsAgitFlow}}
|
||||
{{$headHref = HTMLFormat `%s <a href="%s" target="_blank"><span class="ui label basic tiny" data-tooltip-content="%s">AGit</span></a>` $headHref "https://docs.gitea.com/usage/agit" (ctx.Locale.Tr "repo.pull.agit_documentation")}}
|
||||
{{else}}
|
||||
{{$headHref = HTMLFormat `<span data-tooltip-content="%s">%s</span>` (ctx.Locale.Tr "form.target_branch_not_exist") $headHref}}
|
||||
{{$headHref = HTMLFormat `<span class="tw-line-through" data-tooltip-content="%s">%s</span>` (ctx.Locale.Tr "form.target_branch_not_exist") $headHref}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{$baseHref := .BaseTarget}}
|
||||
{{if .BaseBranchLink}}
|
||||
{{$baseHref = HTMLFormat `<a href="%s">%s</a>` .BaseBranchLink $baseHref}}
|
||||
{{if .BaseBranchNotExist}}
|
||||
{{$baseHref = HTMLFormat `<span class="tw-line-through" data-tooltip-content="%s">%s</span>` (ctx.Locale.Tr "form.target_branch_not_exist") $baseHref}}
|
||||
{{else}}
|
||||
{{$baseHref = HTMLFormat `<a href="%s">%s</a>` .BaseBranchLink $baseHref}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{if .Issue.PullRequest.HasMerged}}
|
||||
{{$mergedStr:= DateUtils.TimeSince .Issue.PullRequest.MergedUnix}}
|
||||
|
@ -589,7 +589,8 @@ func TestPullDontRetargetChildOnWrongRepo(t *testing.T) {
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
targetBranch := htmlDoc.doc.Find("#branch_target>a").Text()
|
||||
// the branch has been deleted, so there is no a html tag instead of span
|
||||
targetBranch := htmlDoc.doc.Find("#branch_target>span").Text()
|
||||
prStatus := strings.TrimSpace(htmlDoc.doc.Find(".issue-title-meta>.issue-state-label").Text())
|
||||
|
||||
assert.EqualValues(t, "base-pr", targetBranch)
|
||||
|
@ -1,5 +1,5 @@
|
||||
export function initAutoFocusEnd() {
|
||||
for (const el of document.querySelectorAll('.js-autofocus-end')) {
|
||||
for (const el of document.querySelectorAll<HTMLInputElement>('.js-autofocus-end')) {
|
||||
el.focus(); // expects only one such element on one page. If there are many, then the last one gets the focus.
|
||||
el.setSelectionRange(el.value.length, el.value.length);
|
||||
}
|
||||
|
@ -35,9 +35,11 @@ export async function initCaptcha() {
|
||||
}
|
||||
case 'm-captcha': {
|
||||
const {default: mCaptcha} = await import(/* webpackChunkName: "mcaptcha-vanilla-glue" */'@mcaptcha/vanilla-glue');
|
||||
// @ts-expect-error
|
||||
mCaptcha.INPUT_NAME = 'm-captcha-response';
|
||||
const instanceURL = captchaEl.getAttribute('data-instance-url');
|
||||
|
||||
// @ts-expect-error
|
||||
mCaptcha.default({
|
||||
siteKey: {
|
||||
instanceUrl: new URL(instanceURL),
|
||||
|
@ -3,7 +3,7 @@ import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
|
||||
const {pageData} = window.config;
|
||||
|
||||
async function initInputCitationValue(citationCopyApa, citationCopyBibtex) {
|
||||
async function initInputCitationValue(citationCopyApa: HTMLButtonElement, citationCopyBibtex: HTMLButtonElement) {
|
||||
const [{Cite, plugins}] = await Promise.all([
|
||||
import(/* webpackChunkName: "citation-js-core" */'@citation-js/core'),
|
||||
import(/* webpackChunkName: "citation-js-formats" */'@citation-js/plugin-software-formats'),
|
||||
@ -27,9 +27,9 @@ export async function initCitationFileCopyContent() {
|
||||
|
||||
if (!pageData.citationFileContent) return;
|
||||
|
||||
const citationCopyApa = document.querySelector('#citation-copy-apa');
|
||||
const citationCopyBibtex = document.querySelector('#citation-copy-bibtex');
|
||||
const inputContent = document.querySelector('#citation-copy-content');
|
||||
const citationCopyApa = document.querySelector<HTMLButtonElement>('#citation-copy-apa');
|
||||
const citationCopyBibtex = document.querySelector<HTMLButtonElement>('#citation-copy-bibtex');
|
||||
const inputContent = document.querySelector<HTMLInputElement>('#citation-copy-content');
|
||||
|
||||
if ((!citationCopyApa && !citationCopyBibtex) || !inputContent) return;
|
||||
|
||||
@ -41,7 +41,7 @@ export async function initCitationFileCopyContent() {
|
||||
citationCopyApa.classList.toggle('primary', !isBibtex);
|
||||
};
|
||||
|
||||
document.querySelector('#cite-repo-button')?.addEventListener('click', async (e) => {
|
||||
document.querySelector('#cite-repo-button')?.addEventListener('click', async (e: MouseEvent & {target: HTMLAnchorElement}) => {
|
||||
const dropdownBtn = e.target.closest('.ui.dropdown.button');
|
||||
dropdownBtn.classList.add('is-loading');
|
||||
|
||||
|
@ -9,7 +9,7 @@ const {copy_success, copy_error} = window.config.i18n;
|
||||
// - data-clipboard-target: Holds a selector for a <input> or <textarea> whose content is copied
|
||||
// - data-clipboard-text-type: When set to 'url' will convert relative to absolute urls
|
||||
export function initGlobalCopyToClipboardListener() {
|
||||
document.addEventListener('click', async (e) => {
|
||||
document.addEventListener('click', async (e: MouseEvent & {target: HTMLElement}) => {
|
||||
const target = e.target.closest('[data-clipboard-text], [data-clipboard-target]');
|
||||
if (!target) return;
|
||||
|
||||
@ -17,7 +17,7 @@ export function initGlobalCopyToClipboardListener() {
|
||||
|
||||
let text = target.getAttribute('data-clipboard-text');
|
||||
if (!text) {
|
||||
text = document.querySelector(target.getAttribute('data-clipboard-target'))?.value;
|
||||
text = document.querySelector<HTMLInputElement>(target.getAttribute('data-clipboard-target'))?.value;
|
||||
}
|
||||
|
||||
if (text && target.getAttribute('data-clipboard-text-type') === 'url') {
|
||||
|
@ -21,7 +21,7 @@ const baseOptions = {
|
||||
automaticLayout: true,
|
||||
};
|
||||
|
||||
function getEditorconfig(input) {
|
||||
function getEditorconfig(input: HTMLInputElement) {
|
||||
try {
|
||||
return JSON.parse(input.getAttribute('data-editorconfig'));
|
||||
} catch {
|
||||
@ -58,7 +58,7 @@ function exportEditor(editor) {
|
||||
if (!window.codeEditors.includes(editor)) window.codeEditors.push(editor);
|
||||
}
|
||||
|
||||
export async function createMonaco(textarea, filename, editorOpts) {
|
||||
export async function createMonaco(textarea: HTMLTextAreaElement, filename: string, editorOpts: Record<string, any>) {
|
||||
const monaco = await import(/* webpackChunkName: "monaco" */'monaco-editor');
|
||||
|
||||
initLanguages(monaco);
|
||||
@ -72,7 +72,7 @@ export async function createMonaco(textarea, filename, editorOpts) {
|
||||
// https://github.com/microsoft/monaco-editor/issues/2427
|
||||
// also, monaco can only parse 6-digit hex colors, so we convert the colors to that format
|
||||
const styles = window.getComputedStyle(document.documentElement);
|
||||
const getColor = (name) => tinycolor(styles.getPropertyValue(name).trim()).toString('hex6');
|
||||
const getColor = (name: string) => tinycolor(styles.getPropertyValue(name).trim()).toString('hex6');
|
||||
|
||||
monaco.editor.defineTheme('gitea', {
|
||||
base: isDarkTheme() ? 'vs-dark' : 'vs',
|
||||
@ -127,13 +127,13 @@ export async function createMonaco(textarea, filename, editorOpts) {
|
||||
return {monaco, editor};
|
||||
}
|
||||
|
||||
function getFileBasedOptions(filename, lineWrapExts) {
|
||||
function getFileBasedOptions(filename: string, lineWrapExts: string[]) {
|
||||
return {
|
||||
wordWrap: (lineWrapExts || []).includes(extname(filename)) ? 'on' : 'off',
|
||||
};
|
||||
}
|
||||
|
||||
function togglePreviewDisplay(previewable) {
|
||||
function togglePreviewDisplay(previewable: boolean) {
|
||||
const previewTab = document.querySelector('a[data-tab="preview"]');
|
||||
if (!previewTab) return;
|
||||
|
||||
@ -152,7 +152,7 @@ function togglePreviewDisplay(previewable) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function createCodeEditor(textarea, filenameInput) {
|
||||
export async function createCodeEditor(textarea: HTMLTextAreaElement, filenameInput: HTMLInputElement) {
|
||||
const filename = basename(filenameInput.value);
|
||||
const previewableExts = new Set((textarea.getAttribute('data-previewable-extensions') || '').split(','));
|
||||
const lineWrapExts = (textarea.getAttribute('data-line-wrap-extensions') || '').split(',');
|
||||
@ -177,10 +177,10 @@ export async function createCodeEditor(textarea, filenameInput) {
|
||||
return editor;
|
||||
}
|
||||
|
||||
function getEditorConfigOptions(ec) {
|
||||
function getEditorConfigOptions(ec: Record<string, any>): Record<string, any> {
|
||||
if (!isObject(ec)) return {};
|
||||
|
||||
const opts = {};
|
||||
const opts: Record<string, any> = {};
|
||||
opts.detectIndentation = !('indent_style' in ec) || !('indent_size' in ec);
|
||||
if ('indent_size' in ec) opts.indentSize = Number(ec.indent_size);
|
||||
if ('tab_width' in ec) opts.tabSize = Number(ec.tab_width) || opts.indentSize;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {createTippy} from '../modules/tippy.ts';
|
||||
|
||||
export async function initColorPickers() {
|
||||
const els = document.querySelectorAll('.js-color-picker-input');
|
||||
const els = document.querySelectorAll<HTMLElement>('.js-color-picker-input');
|
||||
if (!els.length) return;
|
||||
|
||||
await Promise.all([
|
||||
@ -14,15 +14,15 @@ export async function initColorPickers() {
|
||||
}
|
||||
}
|
||||
|
||||
function updateSquare(el, newValue) {
|
||||
function updateSquare(el: HTMLElement, newValue: string): void {
|
||||
el.style.color = /#[0-9a-f]{6}/i.test(newValue) ? newValue : 'transparent';
|
||||
}
|
||||
|
||||
function updatePicker(el, newValue) {
|
||||
function updatePicker(el: HTMLElement, newValue: string): void {
|
||||
el.setAttribute('color', newValue);
|
||||
}
|
||||
|
||||
function initPicker(el) {
|
||||
function initPicker(el: HTMLElement): void {
|
||||
const input = el.querySelector('input');
|
||||
|
||||
const square = document.createElement('div');
|
||||
@ -37,7 +37,7 @@ function initPicker(el) {
|
||||
updateSquare(square, e.detail.value);
|
||||
});
|
||||
|
||||
input.addEventListener('input', (e) => {
|
||||
input.addEventListener('input', (e: Event & {target: HTMLInputElement}) => {
|
||||
updateSquare(square, e.target.value);
|
||||
updatePicker(picker, e.target.value);
|
||||
});
|
||||
@ -56,7 +56,7 @@ function initPicker(el) {
|
||||
|
||||
// init precolors
|
||||
for (const colorEl of el.querySelectorAll('.precolors .color')) {
|
||||
colorEl.addEventListener('click', (e) => {
|
||||
colorEl.addEventListener('click', (e: MouseEvent & {target: HTMLAnchorElement}) => {
|
||||
const newValue = e.target.getAttribute('data-color-hex');
|
||||
input.value = newValue;
|
||||
input.dispatchEvent(new Event('input', {bubbles: true}));
|
||||
|
@ -3,7 +3,7 @@ import {POST} from '../modules/fetch.ts';
|
||||
import {hideElem, showElem, toggleElem} from '../utils/dom.ts';
|
||||
import {showErrorToast} from '../modules/toast.ts';
|
||||
|
||||
export function initGlobalButtonClickOnEnter() {
|
||||
export function initGlobalButtonClickOnEnter(): void {
|
||||
$(document).on('keypress', 'div.ui.button,span.ui.button', (e) => {
|
||||
if (e.code === ' ' || e.code === 'Enter') {
|
||||
$(e.target).trigger('click');
|
||||
@ -12,13 +12,13 @@ export function initGlobalButtonClickOnEnter() {
|
||||
});
|
||||
}
|
||||
|
||||
export function initGlobalDeleteButton() {
|
||||
export function initGlobalDeleteButton(): void {
|
||||
// ".delete-button" shows a confirmation modal defined by `data-modal-id` attribute.
|
||||
// Some model/form elements will be filled by `data-id` / `data-name` / `data-data-xxx` attributes.
|
||||
// If there is a form defined by `data-form`, then the form will be submitted as-is (without any modification).
|
||||
// If there is no form, then the data will be posted to `data-url`.
|
||||
// TODO: it's not encouraged to use this method. `show-modal` does far better than this.
|
||||
for (const btn of document.querySelectorAll('.delete-button')) {
|
||||
for (const btn of document.querySelectorAll<HTMLElement>('.delete-button')) {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
@ -46,7 +46,7 @@ export function initGlobalDeleteButton() {
|
||||
// if `data-type="form"` exists, then submit the form by the selector provided by `data-form="..."`
|
||||
if (btn.getAttribute('data-type') === 'form') {
|
||||
const formSelector = btn.getAttribute('data-form');
|
||||
const form = document.querySelector(formSelector);
|
||||
const form = document.querySelector<HTMLFormElement>(formSelector);
|
||||
if (!form) throw new Error(`no form named ${formSelector} found`);
|
||||
form.submit();
|
||||
}
|
||||
@ -73,7 +73,7 @@ export function initGlobalDeleteButton() {
|
||||
}
|
||||
}
|
||||
|
||||
export function initGlobalButtons() {
|
||||
export function initGlobalButtons(): void {
|
||||
// There are many "cancel button" elements in modal dialogs, Fomantic UI expects they are button-like elements but never submit a form.
|
||||
// However, Gitea misuses the modal dialog and put the cancel buttons inside forms, so we must prevent the form submission.
|
||||
// There are a few cancel buttons in non-modal forms, and there are some dynamically created forms (eg: the "Edit Issue Content")
|
||||
|
@ -7,30 +7,20 @@ import {validateTextareaNonEmpty} from './comp/ComboMarkdownEditor.ts';
|
||||
import {initViewedCheckboxListenerFor, countAndUpdateViewedFiles, initExpandAndCollapseFilesButton} from './pull-view-file.ts';
|
||||
import {initImageDiff} from './imagediff.ts';
|
||||
import {showErrorToast} from '../modules/toast.ts';
|
||||
import {submitEventSubmitter, queryElemSiblings, hideElem, showElem, animateOnce} from '../utils/dom.ts';
|
||||
import {
|
||||
submitEventSubmitter,
|
||||
queryElemSiblings,
|
||||
hideElem,
|
||||
showElem,
|
||||
animateOnce,
|
||||
addDelegatedEventListener,
|
||||
createElementFromHTML,
|
||||
} from '../utils/dom.ts';
|
||||
import {POST, GET} from '../modules/fetch.ts';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
|
||||
const {pageData, i18n} = window.config;
|
||||
|
||||
function initRepoDiffReviewButton() {
|
||||
const reviewBox = document.querySelector('#review-box');
|
||||
if (!reviewBox) return;
|
||||
|
||||
const counter = reviewBox.querySelector('.review-comments-counter');
|
||||
if (!counter) return;
|
||||
|
||||
$(document).on('click', 'button[name="pending_review"]', (e) => {
|
||||
const $form = $(e.target).closest('form');
|
||||
// Watch for the form's submit event.
|
||||
$form.on('submit', () => {
|
||||
const num = parseInt(counter.getAttribute('data-pending-comment-number')) + 1 || 1;
|
||||
counter.setAttribute('data-pending-comment-number', num);
|
||||
counter.textContent = num;
|
||||
animateOnce(reviewBox, 'pulse-1p5-200');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initRepoDiffFileViewToggle() {
|
||||
$('.file-view-toggle').on('click', function () {
|
||||
for (const el of queryElemSiblings(this)) {
|
||||
@ -47,19 +37,15 @@ function initRepoDiffFileViewToggle() {
|
||||
}
|
||||
|
||||
function initRepoDiffConversationForm() {
|
||||
$(document).on('submit', '.conversation-holder form', async (e) => {
|
||||
addDelegatedEventListener<HTMLFormElement>(document, 'submit', '.conversation-holder form', async (form, e) => {
|
||||
e.preventDefault();
|
||||
const textArea = form.querySelector<HTMLTextAreaElement>('textarea');
|
||||
if (!validateTextareaNonEmpty(textArea)) return;
|
||||
if (form.classList.contains('is-loading')) return;
|
||||
|
||||
const $form = $(e.target);
|
||||
const textArea = e.target.querySelector('textarea');
|
||||
if (!validateTextareaNonEmpty(textArea)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.target.classList.contains('is-loading')) return;
|
||||
try {
|
||||
e.target.classList.add('is-loading');
|
||||
const formData = new FormData($form[0]);
|
||||
form.classList.add('is-loading');
|
||||
const formData = new FormData(form);
|
||||
|
||||
// if the form is submitted by a button, append the button's name and value to the form data
|
||||
const submitter = submitEventSubmitter(e);
|
||||
@ -68,26 +54,42 @@ function initRepoDiffConversationForm() {
|
||||
formData.append(submitter.name, submitter.value);
|
||||
}
|
||||
|
||||
const response = await POST(e.target.getAttribute('action'), {data: formData});
|
||||
const $newConversationHolder = $(await response.text());
|
||||
const {path, side, idx} = $newConversationHolder.data();
|
||||
const trLineType = form.closest('tr').getAttribute('data-line-type');
|
||||
const response = await POST(form.getAttribute('action'), {data: formData});
|
||||
const newConversationHolder = createElementFromHTML(await response.text());
|
||||
const path = newConversationHolder.getAttribute('data-path');
|
||||
const side = newConversationHolder.getAttribute('data-side');
|
||||
const idx = newConversationHolder.getAttribute('data-idx');
|
||||
|
||||
form.closest('.conversation-holder').replaceWith(newConversationHolder);
|
||||
form = null; // prevent further usage of the form because it should have been replaced
|
||||
|
||||
$form.closest('.conversation-holder').replaceWith($newConversationHolder);
|
||||
let selector;
|
||||
if ($form.closest('tr').data('line-type') === 'same') {
|
||||
if (trLineType === 'same') {
|
||||
selector = `[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`;
|
||||
} else {
|
||||
selector = `[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`;
|
||||
}
|
||||
for (const el of document.querySelectorAll(selector)) {
|
||||
el.classList.add('tw-invisible');
|
||||
el.classList.add('tw-invisible'); // TODO need to figure out why
|
||||
}
|
||||
fomanticQuery(newConversationHolder.querySelectorAll('.ui.dropdown')).dropdown();
|
||||
|
||||
// the default behavior is to add a pending review, so if no submitter, it also means "pending_review"
|
||||
if (!submitter || submitter?.matches('button[name="pending_review"]')) {
|
||||
const reviewBox = document.querySelector('#review-box');
|
||||
const counter = reviewBox?.querySelector('.review-comments-counter');
|
||||
if (!counter) return;
|
||||
const num = parseInt(counter.getAttribute('data-pending-comment-number')) + 1 || 1;
|
||||
counter.setAttribute('data-pending-comment-number', String(num));
|
||||
counter.textContent = String(num);
|
||||
animateOnce(reviewBox, 'pulse-1p5-200');
|
||||
}
|
||||
$newConversationHolder.find('.dropdown').dropdown();
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showErrorToast(i18n.network_error);
|
||||
} finally {
|
||||
e.target.classList.remove('is-loading');
|
||||
form?.classList.remove('is-loading');
|
||||
}
|
||||
});
|
||||
|
||||
@ -219,7 +221,6 @@ export function initRepoDiffView() {
|
||||
initDiffFileList();
|
||||
initDiffCommitSelect();
|
||||
initRepoDiffShowMore();
|
||||
initRepoDiffReviewButton();
|
||||
initRepoDiffFileViewToggle();
|
||||
initViewedCheckboxListenerFor();
|
||||
initExpandAndCollapseFilesButton();
|
||||
|
@ -1,7 +1,7 @@
|
||||
import $ from 'jquery';
|
||||
import {htmlEscape} from 'escape-goat';
|
||||
import {createTippy, showTemporaryTooltip} from '../modules/tippy.ts';
|
||||
import {hideElem, showElem, toggleElem} from '../utils/dom.ts';
|
||||
import {addDelegatedEventListener, createElementFromHTML, hideElem, showElem, toggleElem} from '../utils/dom.ts';
|
||||
import {setFileFolding} from './file-fold.ts';
|
||||
import {ComboMarkdownEditor, getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
|
||||
import {parseIssuePageInfo, toAbsoluteUrl} from '../utils.ts';
|
||||
@ -443,21 +443,19 @@ export function initRepoPullRequestReview() {
|
||||
});
|
||||
}
|
||||
|
||||
$(document).on('click', '.add-code-comment', async function (e) {
|
||||
if (e.target.classList.contains('btn-add-single')) return; // https://github.com/go-gitea/gitea/issues/4745
|
||||
addDelegatedEventListener(document, 'click', '.add-code-comment', async (el, e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const isSplit = this.closest('.code-diff')?.classList.contains('code-diff-split');
|
||||
const side = this.getAttribute('data-side');
|
||||
const idx = this.getAttribute('data-idx');
|
||||
const path = this.closest('[data-path]')?.getAttribute('data-path');
|
||||
const tr = this.closest('tr');
|
||||
const isSplit = el.closest('.code-diff')?.classList.contains('code-diff-split');
|
||||
const side = el.getAttribute('data-side');
|
||||
const idx = el.getAttribute('data-idx');
|
||||
const path = el.closest('[data-path]')?.getAttribute('data-path');
|
||||
const tr = el.closest('tr');
|
||||
const lineType = tr.getAttribute('data-line-type');
|
||||
|
||||
const ntr = tr.nextElementSibling;
|
||||
let $ntr = $(ntr);
|
||||
let ntr = tr.nextElementSibling;
|
||||
if (!ntr?.classList.contains('add-comment')) {
|
||||
$ntr = $(`
|
||||
ntr = createElementFromHTML(`
|
||||
<tr class="add-comment" data-line-type="${lineType}">
|
||||
${isSplit ? `
|
||||
<td class="add-comment-left" colspan="4"></td>
|
||||
@ -466,24 +464,18 @@ export function initRepoPullRequestReview() {
|
||||
<td class="add-comment-left add-comment-right" colspan="5"></td>
|
||||
`}
|
||||
</tr>`);
|
||||
$(tr).after($ntr);
|
||||
tr.after(ntr);
|
||||
}
|
||||
|
||||
const $td = $ntr.find(`.add-comment-${side}`);
|
||||
const $commentCloud = $td.find('.comment-code-cloud');
|
||||
if (!$commentCloud.length && !$ntr.find('button[name="pending_review"]').length) {
|
||||
try {
|
||||
const response = await GET(this.closest('[data-new-comment-url]')?.getAttribute('data-new-comment-url'));
|
||||
const html = await response.text();
|
||||
$td.html(html);
|
||||
$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[0].querySelector('.combo-markdown-editor'));
|
||||
editor.focus();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
const td = ntr.querySelector(`.add-comment-${side}`);
|
||||
const commentCloud = td.querySelector('.comment-code-cloud');
|
||||
if (!commentCloud && !ntr.querySelector('button[name="pending_review"]')) {
|
||||
const response = await GET(el.closest('[data-new-comment-url]')?.getAttribute('data-new-comment-url'));
|
||||
td.innerHTML = await response.text();
|
||||
td.querySelector<HTMLInputElement>("input[name='line']").value = idx;
|
||||
td.querySelector<HTMLInputElement>("input[name='side']").value = (side === 'left' ? 'previous' : 'proposed');
|
||||
td.querySelector<HTMLInputElement>("input[name='path']").value = path;
|
||||
const editor = await initComboMarkdownEditor(td.querySelector<HTMLElement>('.combo-markdown-editor'));
|
||||
editor.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
4
web_src/js/globals.d.ts
vendored
4
web_src/js/globals.d.ts
vendored
@ -58,4 +58,8 @@ interface Window {
|
||||
push: (e: ErrorEvent & PromiseRejectionEvent) => void | number,
|
||||
},
|
||||
__webpack_public_path__: string;
|
||||
grecaptcha: any,
|
||||
turnstile: any,
|
||||
hcaptcha: any,
|
||||
codeEditors: any[],
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ function targetElement(el: Element) {
|
||||
return el.classList.contains('is-loading') ? el : el.closest('pre');
|
||||
}
|
||||
|
||||
export async function renderMath(): void {
|
||||
export async function renderMath(): Promise<void> {
|
||||
const els = document.querySelectorAll('.markup code.language-math');
|
||||
if (!els.length) return;
|
||||
|
||||
|
@ -13,7 +13,7 @@ function attachDirAuto(el: DirElement) {
|
||||
}
|
||||
}
|
||||
|
||||
export function initDirAuto() {
|
||||
export function initDirAuto(): void {
|
||||
const observer = new MutationObserver((mutationList) => {
|
||||
const len = mutationList.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
|
@ -9,7 +9,7 @@ const safeMethods = new Set(['GET', 'HEAD', 'OPTIONS', 'TRACE']);
|
||||
// fetch wrapper, use below method name functions and the `data` option to pass in data
|
||||
// which will automatically set an appropriate headers. For json content, only object
|
||||
// and array types are currently supported.
|
||||
export function request(url: string, {method = 'GET', data, headers = {}, ...other}: RequestOpts = {}) {
|
||||
export function request(url: string, {method = 'GET', data, headers = {}, ...other}: RequestOpts = {}): Promise<Response> {
|
||||
let body: RequestData;
|
||||
let contentType: string;
|
||||
if (data instanceof FormData || data instanceof URLSearchParams) {
|
||||
|
@ -19,7 +19,7 @@ export function initGiteaFomantic() {
|
||||
// Do not use "cursor: pointer" for dropdown labels
|
||||
$.fn.dropdown.settings.className.label += ' tw-cursor-default';
|
||||
// Always use Gitea's SVG icons
|
||||
$.fn.dropdown.settings.templates.label = function(_value, text, preserveHTML, className) {
|
||||
$.fn.dropdown.settings.templates.label = function(_value: any, text: any, preserveHTML: any, className: Record<string, string>) {
|
||||
const escape = $.fn.dropdown.settings.templates.escape;
|
||||
return escape(text, preserveHTML) + svg('octicon-x', 16, `${className.delete} icon`);
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import $ from 'jquery';
|
||||
import type {FomanticInitFunction} from '../../types.ts';
|
||||
|
||||
export function initFomanticApiPatch() {
|
||||
//
|
||||
@ -15,7 +16,7 @@ export function initFomanticApiPatch() {
|
||||
//
|
||||
const patchKey = '_giteaFomanticApiPatch';
|
||||
const oldApi = $.api;
|
||||
$.api = $.fn.api = function(...args) {
|
||||
$.api = $.fn.api = function(...args: Parameters<FomanticInitFunction>) {
|
||||
const apiCall = oldApi.bind(this);
|
||||
const ret = oldApi.apply(this, args);
|
||||
|
||||
@ -23,7 +24,7 @@ export function initFomanticApiPatch() {
|
||||
const internalGet = apiCall('internal', 'get');
|
||||
if (!internalGet.urlEncodedValue[patchKey]) {
|
||||
const oldUrlEncodedValue = internalGet.urlEncodedValue;
|
||||
internalGet.urlEncodedValue = function (value) {
|
||||
internalGet.urlEncodedValue = function (value: any) {
|
||||
try {
|
||||
return oldUrlEncodedValue(value);
|
||||
} catch {
|
||||
|
@ -5,7 +5,7 @@ export function generateAriaId() {
|
||||
return `_aria_auto_id_${ariaIdCounter++}`;
|
||||
}
|
||||
|
||||
export function linkLabelAndInput(label, input) {
|
||||
export function linkLabelAndInput(label: Element, input: Element) {
|
||||
const labelFor = label.getAttribute('for');
|
||||
const inputId = input.getAttribute('id');
|
||||
|
||||
|
@ -3,7 +3,7 @@ import {queryElemChildren} from '../../utils/dom.ts';
|
||||
|
||||
export function initFomanticDimmer() {
|
||||
// stand-in for removed dimmer module
|
||||
$.fn.dimmer = function (arg0, arg1) {
|
||||
$.fn.dimmer = function (arg0: string, arg1: any) {
|
||||
if (arg0 === 'add content') {
|
||||
const $el = arg1;
|
||||
const existingDimmer = document.querySelector('body > .ui.dimmer');
|
||||
|
@ -1,5 +1,6 @@
|
||||
import $ from 'jquery';
|
||||
import {generateAriaId} from './base.ts';
|
||||
import type {FomanticInitFunction} from '../../types.ts';
|
||||
|
||||
const ariaPatchKey = '_giteaAriaPatchDropdown';
|
||||
const fomanticDropdownFn = $.fn.dropdown;
|
||||
@ -8,13 +9,13 @@ const fomanticDropdownFn = $.fn.dropdown;
|
||||
export function initAriaDropdownPatch() {
|
||||
if ($.fn.dropdown === ariaDropdownFn) throw new Error('initAriaDropdownPatch could only be called once');
|
||||
$.fn.dropdown = ariaDropdownFn;
|
||||
ariaDropdownFn.settings = fomanticDropdownFn.settings;
|
||||
(ariaDropdownFn as FomanticInitFunction).settings = fomanticDropdownFn.settings;
|
||||
}
|
||||
|
||||
// the patched `$.fn.dropdown` function, it passes the arguments to Fomantic's `$.fn.dropdown` function, and:
|
||||
// * it does the one-time attaching on the first call
|
||||
// * it delegates the `onLabelCreate` to the patched `onLabelCreate` to add necessary aria attributes
|
||||
function ariaDropdownFn(...args) {
|
||||
function ariaDropdownFn(...args: Parameters<FomanticInitFunction>) {
|
||||
const ret = fomanticDropdownFn.apply(this, args);
|
||||
|
||||
// if the `$().dropdown()` call is without arguments, or it has non-string (object) argument,
|
||||
@ -33,7 +34,7 @@ function ariaDropdownFn(...args) {
|
||||
|
||||
// make the item has role=option/menuitem, add an id if there wasn't one yet, make items as non-focusable
|
||||
// the elements inside the dropdown menu item should not be focusable, the focus should always be on the dropdown primary element.
|
||||
function updateMenuItem(dropdown, item) {
|
||||
function updateMenuItem(dropdown: HTMLElement, item: HTMLElement) {
|
||||
if (!item.id) item.id = generateAriaId();
|
||||
item.setAttribute('role', dropdown[ariaPatchKey].listItemRole);
|
||||
item.setAttribute('tabindex', '-1');
|
||||
@ -43,7 +44,7 @@ function updateMenuItem(dropdown, item) {
|
||||
* make the label item and its "delete icon" have correct aria attributes
|
||||
* @param {HTMLElement} label
|
||||
*/
|
||||
function updateSelectionLabel(label) {
|
||||
function updateSelectionLabel(label: HTMLElement) {
|
||||
// the "label" is like this: "<a|div class="ui label" data-value="1">the-label-name <i|svg class="delete icon"/></a>"
|
||||
if (!label.id) {
|
||||
label.id = generateAriaId();
|
||||
@ -59,7 +60,7 @@ function updateSelectionLabel(label) {
|
||||
}
|
||||
|
||||
// delegate the dropdown's template functions and callback functions to add aria attributes.
|
||||
function delegateOne($dropdown) {
|
||||
function delegateOne($dropdown: any) {
|
||||
const dropdownCall = fomanticDropdownFn.bind($dropdown);
|
||||
|
||||
// If there is a "search input" in the "menu", Fomantic will only "focus the input" but not "toggle the menu" when the "dropdown icon" is clicked.
|
||||
@ -74,7 +75,7 @@ function delegateOne($dropdown) {
|
||||
// the "template" functions are used for dynamic creation (eg: AJAX)
|
||||
const dropdownTemplates = {...dropdownCall('setting', 'templates'), t: performance.now()};
|
||||
const dropdownTemplatesMenuOld = dropdownTemplates.menu;
|
||||
dropdownTemplates.menu = function(response, fields, preserveHTML, className) {
|
||||
dropdownTemplates.menu = function(response: any, fields: any, preserveHTML: any, className: Record<string, string>) {
|
||||
// when the dropdown menu items are loaded from AJAX requests, the items are created dynamically
|
||||
const menuItems = dropdownTemplatesMenuOld(response, fields, preserveHTML, className);
|
||||
const div = document.createElement('div');
|
||||
@ -89,7 +90,7 @@ function delegateOne($dropdown) {
|
||||
|
||||
// the `onLabelCreate` is used to add necessary aria attributes for dynamically created selection labels
|
||||
const dropdownOnLabelCreateOld = dropdownCall('setting', 'onLabelCreate');
|
||||
dropdownCall('setting', 'onLabelCreate', function(value, text) {
|
||||
dropdownCall('setting', 'onLabelCreate', function(value: any, text: string) {
|
||||
const $label = dropdownOnLabelCreateOld.call(this, value, text);
|
||||
updateSelectionLabel($label[0]);
|
||||
return $label;
|
||||
@ -97,7 +98,7 @@ function delegateOne($dropdown) {
|
||||
|
||||
const oldSet = dropdownCall('internal', 'set');
|
||||
const oldSetDirection = oldSet.direction;
|
||||
oldSet.direction = function($menu) {
|
||||
oldSet.direction = function($menu: any) {
|
||||
oldSetDirection.call(this, $menu);
|
||||
const classNames = dropdownCall('setting', 'className');
|
||||
$menu = $menu || $dropdown.find('> .menu');
|
||||
@ -113,7 +114,7 @@ function delegateOne($dropdown) {
|
||||
}
|
||||
|
||||
// for static dropdown elements (generated by server-side template), prepare them with necessary aria attributes
|
||||
function attachStaticElements(dropdown, focusable, menu) {
|
||||
function attachStaticElements(dropdown: HTMLElement, focusable: HTMLElement, menu: HTMLElement) {
|
||||
// prepare static dropdown menu list popup
|
||||
if (!menu.id) {
|
||||
menu.id = generateAriaId();
|
||||
@ -125,7 +126,7 @@ function attachStaticElements(dropdown, focusable, menu) {
|
||||
menu.setAttribute('role', dropdown[ariaPatchKey].listPopupRole);
|
||||
|
||||
// prepare selection label items
|
||||
for (const label of dropdown.querySelectorAll('.ui.label')) {
|
||||
for (const label of dropdown.querySelectorAll<HTMLElement>('.ui.label')) {
|
||||
updateSelectionLabel(label);
|
||||
}
|
||||
|
||||
@ -142,7 +143,7 @@ function attachStaticElements(dropdown, focusable, menu) {
|
||||
}
|
||||
}
|
||||
|
||||
function attachInit(dropdown) {
|
||||
function attachInit(dropdown: HTMLElement) {
|
||||
dropdown[ariaPatchKey] = {};
|
||||
if (dropdown.classList.contains('custom')) return;
|
||||
|
||||
@ -161,7 +162,7 @@ function attachInit(dropdown) {
|
||||
|
||||
// TODO: multiple selection is only partially supported. Check and test them one by one in the future.
|
||||
|
||||
const textSearch = dropdown.querySelector('input.search');
|
||||
const textSearch = dropdown.querySelector<HTMLElement>('input.search');
|
||||
const focusable = textSearch || dropdown; // the primary element for focus, see comment above
|
||||
if (!focusable) return;
|
||||
|
||||
@ -191,7 +192,7 @@ function attachInit(dropdown) {
|
||||
attachStaticElements(dropdown, focusable, menu);
|
||||
}
|
||||
|
||||
function attachDomEvents(dropdown, focusable, menu) {
|
||||
function attachDomEvents(dropdown: HTMLElement, focusable: HTMLElement, menu: HTMLElement) {
|
||||
// when showing, it has class: ".animating.in"
|
||||
// when hiding, it has class: ".visible.animating.out"
|
||||
const isMenuVisible = () => (menu.classList.contains('visible') && !menu.classList.contains('out')) || menu.classList.contains('in');
|
||||
@ -215,7 +216,7 @@ function attachDomEvents(dropdown, focusable, menu) {
|
||||
}
|
||||
};
|
||||
|
||||
dropdown.addEventListener('keydown', (e) => {
|
||||
dropdown.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
// here it must use keydown event before dropdown's keyup handler, otherwise there is no Enter event in our keyup handler
|
||||
if (e.key === 'Enter') {
|
||||
const dropdownCall = fomanticDropdownFn.bind($(dropdown));
|
||||
@ -260,7 +261,7 @@ function attachDomEvents(dropdown, focusable, menu) {
|
||||
deferredRefreshAriaActiveItem(100);
|
||||
}, 0);
|
||||
}, true);
|
||||
dropdown.addEventListener('click', (e) => {
|
||||
dropdown.addEventListener('click', (e: MouseEvent) => {
|
||||
if (isMenuVisible() &&
|
||||
ignoreClickPreVisible !== 2 && // dropdown is switch from invisible to visible
|
||||
ignoreClickPreEvents === 2 // the click event is related to mousedown+focus
|
||||
|
@ -1,4 +1,5 @@
|
||||
import $ from 'jquery';
|
||||
import type {FomanticInitFunction} from '../../types.ts';
|
||||
|
||||
const fomanticModalFn = $.fn.modal;
|
||||
|
||||
@ -6,12 +7,12 @@ const fomanticModalFn = $.fn.modal;
|
||||
export function initAriaModalPatch() {
|
||||
if ($.fn.modal === ariaModalFn) throw new Error('initAriaModalPatch could only be called once');
|
||||
$.fn.modal = ariaModalFn;
|
||||
ariaModalFn.settings = fomanticModalFn.settings;
|
||||
(ariaModalFn as FomanticInitFunction).settings = fomanticModalFn.settings;
|
||||
}
|
||||
|
||||
// the patched `$.fn.modal` modal function
|
||||
// * it does the one-time attaching on the first call
|
||||
function ariaModalFn(...args) {
|
||||
function ariaModalFn(...args: Parameters<FomanticInitFunction>) {
|
||||
const ret = fomanticModalFn.apply(this, args);
|
||||
if (args[0] === 'show' || args[0]?.autoShow) {
|
||||
for (const el of this) {
|
||||
|
@ -8,13 +8,13 @@ export function initFomanticTransition() {
|
||||
'set duration', 'save conditions', 'restore conditions',
|
||||
]);
|
||||
// stand-in for removed transition module
|
||||
$.fn.transition = function (arg0, arg1, arg2) {
|
||||
$.fn.transition = function (arg0: any, arg1: any, arg2: any) {
|
||||
if (arg0 === 'is supported') return true;
|
||||
if (arg0 === 'is animating') return false;
|
||||
if (arg0 === 'is inward') return false;
|
||||
if (arg0 === 'is outward') return false;
|
||||
|
||||
let argObj;
|
||||
let argObj: Record<string, any>;
|
||||
if (typeof arg0 === 'string') {
|
||||
// many behaviors are no-op now. https://fomantic-ui.com/modules/transition.html#/usage
|
||||
if (transitionNopBehaviors.has(arg0)) return this;
|
||||
|
@ -1,17 +1,18 @@
|
||||
import type {SortableOptions} from 'sortablejs';
|
||||
import type {SortableOptions, SortableEvent} from 'sortablejs';
|
||||
|
||||
export async function createSortable(el, opts: {handle?: string} & SortableOptions = {}) {
|
||||
export async function createSortable(el: HTMLElement, opts: {handle?: string} & SortableOptions = {}) {
|
||||
// @ts-expect-error: wrong type derived by typescript
|
||||
const {Sortable} = await import(/* webpackChunkName: "sortablejs" */'sortablejs');
|
||||
|
||||
return new Sortable(el, {
|
||||
animation: 150,
|
||||
ghostClass: 'card-ghost',
|
||||
onChoose: (e) => {
|
||||
onChoose: (e: SortableEvent) => {
|
||||
const handle = opts.handle ? e.item.querySelector(opts.handle) : e.item;
|
||||
handle.classList.add('tw-cursor-grabbing');
|
||||
opts.onChoose?.(e);
|
||||
},
|
||||
onUnchoose: (e) => {
|
||||
onUnchoose: (e: SortableEvent) => {
|
||||
const handle = opts.handle ? e.item.querySelector(opts.handle) : e.item;
|
||||
handle.classList.remove('tw-cursor-grabbing');
|
||||
opts.onUnchoose?.(e);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import tippy, {followCursor} from 'tippy.js';
|
||||
import {isDocumentFragmentOrElementNode} from '../utils/dom.ts';
|
||||
import {formatDatetime} from '../utils/time.ts';
|
||||
import type {Content, Instance, Props} from 'tippy.js';
|
||||
import type {Content, Instance, Placement, Props} from 'tippy.js';
|
||||
|
||||
type TippyOpts = {
|
||||
role?: string,
|
||||
@ -16,6 +16,7 @@ export function createTippy(target: Element, opts: TippyOpts = {}): Instance {
|
||||
// because we should use our own wrapper functions to handle them, do not let the user override them
|
||||
const {onHide, onShow, onDestroy, role, theme, arrow, ...other} = opts;
|
||||
|
||||
// @ts-expect-error: wrong type derived by typescript
|
||||
const instance: Instance = tippy(target, {
|
||||
appendTo: document.body,
|
||||
animation: false,
|
||||
@ -65,7 +66,7 @@ export function createTippy(target: Element, opts: TippyOpts = {}): Instance {
|
||||
*
|
||||
* Note: "tooltip" doesn't equal to "tippy". "tooltip" means a auto-popup content, it just uses tippy as the implementation.
|
||||
*/
|
||||
function attachTooltip(target: Element, content: Content = null) {
|
||||
function attachTooltip(target: Element, content: Content = null): Instance {
|
||||
switchTitleToTooltip(target);
|
||||
|
||||
content = content ?? target.getAttribute('data-tooltip-content');
|
||||
@ -77,16 +78,16 @@ function attachTooltip(target: Element, content: Content = null) {
|
||||
const hasClipboardTarget = target.hasAttribute('data-clipboard-target');
|
||||
const hideOnClick = !hasClipboardTarget;
|
||||
|
||||
const props = {
|
||||
const props: TippyOpts = {
|
||||
content,
|
||||
delay: 100,
|
||||
role: 'tooltip',
|
||||
theme: 'tooltip',
|
||||
hideOnClick,
|
||||
placement: target.getAttribute('data-tooltip-placement') || 'top-start',
|
||||
followCursor: target.getAttribute('data-tooltip-follow-cursor') || false,
|
||||
placement: target.getAttribute('data-tooltip-placement') as Placement || 'top-start',
|
||||
followCursor: target.getAttribute('data-tooltip-follow-cursor') as Props['followCursor'] || false,
|
||||
...(target.getAttribute('data-tooltip-interactive') === 'true' ? {interactive: true, aria: {content: 'describedby', expanded: false}} : {}),
|
||||
} as TippyOpts;
|
||||
};
|
||||
|
||||
if (!target._tippy) {
|
||||
createTippy(target, props);
|
||||
@ -96,7 +97,7 @@ function attachTooltip(target: Element, content: Content = null) {
|
||||
return target._tippy;
|
||||
}
|
||||
|
||||
function switchTitleToTooltip(target: Element) {
|
||||
function switchTitleToTooltip(target: Element): void {
|
||||
let title = target.getAttribute('title');
|
||||
if (title) {
|
||||
// apply custom formatting to relative-time's tooltips
|
||||
@ -121,14 +122,14 @@ function switchTitleToTooltip(target: Element) {
|
||||
* Some browsers like PaleMoon don't support "addEventListener('mouseenter', capture)"
|
||||
* The tippy by default uses "mouseenter" event to show, so we use "mouseover" event to switch to tippy
|
||||
*/
|
||||
function lazyTooltipOnMouseHover(e: MouseEvent) {
|
||||
function lazyTooltipOnMouseHover(e: MouseEvent): void {
|
||||
e.target.removeEventListener('mouseover', lazyTooltipOnMouseHover, true);
|
||||
attachTooltip(this);
|
||||
}
|
||||
|
||||
// Activate the tooltip for current element.
|
||||
// If the element has no aria-label, use the tooltip content as aria-label.
|
||||
function attachLazyTooltip(el: Element) {
|
||||
function attachLazyTooltip(el: Element): void {
|
||||
el.addEventListener('mouseover', lazyTooltipOnMouseHover, {capture: true});
|
||||
|
||||
// meanwhile, if the element has no aria-label, use the tooltip content as aria-label
|
||||
@ -141,13 +142,13 @@ function attachLazyTooltip(el: Element) {
|
||||
}
|
||||
|
||||
// Activate the tooltip for all children elements.
|
||||
function attachChildrenLazyTooltip(target: Element) {
|
||||
function attachChildrenLazyTooltip(target: Element): void {
|
||||
for (const el of target.querySelectorAll<Element>('[data-tooltip-content]')) {
|
||||
attachLazyTooltip(el);
|
||||
}
|
||||
}
|
||||
|
||||
export function initGlobalTooltips() {
|
||||
export function initGlobalTooltips(): void {
|
||||
// use MutationObserver to detect new "data-tooltip-content" elements added to the DOM, or attributes changed
|
||||
const observerConnect = (observer: MutationObserver) => observer.observe(document, {
|
||||
subtree: true,
|
||||
@ -178,7 +179,7 @@ export function initGlobalTooltips() {
|
||||
attachChildrenLazyTooltip(document.documentElement);
|
||||
}
|
||||
|
||||
export function showTemporaryTooltip(target: Element, content: Content) {
|
||||
export function showTemporaryTooltip(target: Element, content: Content): void {
|
||||
// if the target is inside a dropdown, the menu will be hidden soon
|
||||
// so display the tooltip on the dropdown instead
|
||||
target = target.closest('.ui.dropdown') || target;
|
||||
|
@ -2,7 +2,7 @@ import {sleep} from '../utils.ts';
|
||||
|
||||
const {appSubUrl} = window.config;
|
||||
|
||||
export async function logoutFromWorker() {
|
||||
export async function logoutFromWorker(): Promise<void> {
|
||||
// wait for a while because other requests (eg: logout) may be in the flight
|
||||
await sleep(5000);
|
||||
window.location.href = `${appSubUrl}/`;
|
||||
|
@ -54,3 +54,8 @@ export type Issue = {
|
||||
merged: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type FomanticInitFunction = {
|
||||
settings?: Record<string, any>,
|
||||
(...args: any[]): any,
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import {createElementFromAttrs, createElementFromHTML, querySingleVisibleElem} f
|
||||
|
||||
test('createElementFromHTML', () => {
|
||||
expect(createElementFromHTML('<a>foo<span>bar</span></a>').outerHTML).toEqual('<a>foo<span>bar</span></a>');
|
||||
expect(createElementFromHTML('<tr data-x="1"><td>foo</td></tr>').outerHTML).toEqual('<tr data-x="1"><td>foo</td></tr>');
|
||||
});
|
||||
|
||||
test('createElementFromAttrs', () => {
|
||||
|
@ -301,10 +301,17 @@ export function replaceTextareaSelection(textarea: HTMLTextAreaElement, text: st
|
||||
}
|
||||
|
||||
// Warning: Do not enter any unsanitized variables here
|
||||
export function createElementFromHTML(htmlString: string): HTMLElement {
|
||||
export function createElementFromHTML<T extends HTMLElement>(htmlString: string): T {
|
||||
htmlString = htmlString.trim();
|
||||
// some tags like "tr" are special, it must use a correct parent container to create
|
||||
if (htmlString.startsWith('<tr')) {
|
||||
const container = document.createElement('table');
|
||||
container.innerHTML = htmlString;
|
||||
return container.querySelector<T>('tr');
|
||||
}
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = htmlString.trim();
|
||||
return div.firstChild as HTMLElement;
|
||||
div.innerHTML = htmlString;
|
||||
return div.firstChild as T;
|
||||
}
|
||||
|
||||
export function createElementFromAttrs(tagName: string, attrs: Record<string, any>, ...children: (Node|string)[]): HTMLElement {
|
||||
@ -340,3 +347,11 @@ export function querySingleVisibleElem<T extends HTMLElement>(parent: Element, s
|
||||
if (candidates.length > 1) throw new Error(`Expected exactly one visible element matching selector "${selector}", but found ${candidates.length}`);
|
||||
return candidates.length ? candidates[0] as T : null;
|
||||
}
|
||||
|
||||
export function addDelegatedEventListener<T extends HTMLElement>(parent: Node, type: string, selector: string, listener: (elem: T, e: Event) => void | Promise<any>, options?: boolean | AddEventListenerOptions) {
|
||||
parent.addEventListener(type, (e: Event) => {
|
||||
const elem = (e.target as HTMLElement).closest(selector);
|
||||
if (!elem) return;
|
||||
listener(elem as T, e);
|
||||
}, options);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import emojis from '../../../assets/emoji.json';
|
||||
import {GET} from '../modules/fetch.ts';
|
||||
import type {Issue} from '../features/issue.ts';
|
||||
import type {Issue} from '../types.ts';
|
||||
|
||||
const maxMatches = 6;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user