Merge branch 'main' into update-eslint-plugins

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
wxiaoguang authored and GitHub committed 2026-06-12 00:56:22 +08:00
commit 5194f6f631
43 files changed
+722 -246

No files matched your search

+1
View File
@@ -848,6 +848,7 @@ table th[data-sortt-desc] .svg {
}
/* this is useful to make a left-right (e.g.: title .... operations) layout with default gap, and it wrap for small widths */
.ui.modal .header.flex-left-right,
.flex-left-right {
display: flex;
flex-wrap: wrap;
+3 -1
View File
@@ -1855,8 +1855,10 @@ tbody.commit-list {
width: 100%;
}
.commit-status-item {
.commit-status-item { /* the item can be used at 2 places: PR's merge box (commit-status-list), commit's status popup (no commit-status-list) */
height: 40px;
padding-top: 0 !important; /* use "height" + "align items center", don't use padding-y (from the list container) to layout */
padding-bottom: 0 !important;
display: flex;
gap: var(--gap-block);
align-items: center;
+6 -5
View File
@@ -1,4 +1,4 @@
import {svg} from '../svg.ts';
import {svgRaw} from '../svg.ts';
import {html} from '../utils/html.ts';
import {copyToClipboardWithFeedback} from '../modules/clipboard.ts';
import {GET, POST} from '../modules/fetch.ts';
@@ -45,10 +45,11 @@ export function generateMarkdownLinkForAttachment(file: Partial<CustomDropzoneFi
function addCopyLink(file: Partial<CustomDropzoneFile>) {
// Create a "Copy Link" element, to conveniently copy the image or file link as Markdown to the clipboard
// The "<a>" element has a hardcoded cursor: pointer because the default is overridden by .dropzone
const copyLinkEl = createElementFromHTML<HTMLDivElement>(`
<div class="tw-text-center">
<a href="#" class="tw-cursor-pointer">${svg('octicon-copy', 14)} Copy link</a>
</div>`);
const copyLinkEl = createElementFromHTML<HTMLDivElement>(html`
<div class="tw-text-center">
<a href="#" class="tw-cursor-pointer">${svgRaw('octicon-copy', 14)} Copy link</a>
</div>
`);
copyLinkEl.addEventListener('click', async (e) => {
e.preventDefault();
await copyToClipboardWithFeedback(copyLinkEl, generateMarkdownLinkForAttachment(file));
+22 -19
View File
@@ -1,10 +1,11 @@
import {svg} from '../svg.ts';
import {svgRaw} from '../svg.ts';
import {showErrorToast} from '../modules/toast.ts';
import {GET, POST} from '../modules/fetch.ts';
import {createElementFromHTML, showElem} from '../utils/dom.ts';
import {parseIssuePageInfo} from '../utils.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
import {hideFomanticModal, showFomanticModal} from '../modules/fomantic/modal.ts';
import {html, htmlRaw} from '../utils/html.ts';
let i18nTextEdited: string;
let i18nTextOptions: string;
@@ -12,21 +13,22 @@ let i18nTextDeleteFromHistory: string;
let i18nTextDeleteFromHistoryConfirm: string;
function showContentHistoryDetail(issueBaseUrl: string, commentId: string, historyId: string, itemTitleHtml: string) {
const elDetailDialog = createElementFromHTML(`
<div class="ui modal content-history-detail-dialog">
${svg('octicon-x', 16, 'close icon inside')}
<div class="header flex-left-right">
<div>${itemTitleHtml}</div>
<div class="ui dropdown dialog-header-options tw-mr-8 tw-hidden">
${i18nTextOptions}
${svg('octicon-triangle-down', 14, 'dropdown icon')}
<div class="menu">
<div class="item tw-text-red" data-option-item="delete">${i18nTextDeleteFromHistory}</div>
const elDetailDialog = createElementFromHTML(html`
<div class="ui modal content-history-detail-dialog">
${svgRaw('octicon-x', 16, 'close icon inside')}
<div class="header flex-left-right">
<div>${htmlRaw(itemTitleHtml)}</div>
<div class="ui dropdown dialog-header-options tw-mr-8 tw-hidden">
${i18nTextOptions}
${svgRaw('octicon-triangle-down', 14, 'dropdown icon')}
<div class="menu">
<div class="item tw-text-red" data-option-item="delete">${i18nTextDeleteFromHistory}</div>
</div>
</div>
</div>
<div class="comment-diff-data is-loading"></div>
</div>
</div>
<div class="comment-diff-data is-loading"></div>
</div>`);
`);
document.body.append(elDetailDialog);
const elOptionsDropdown = elDetailDialog.querySelector('.ui.dropdown.dialog-header-options')!;
const $fomanticDropdownOptions = fomanticQuery(elOptionsDropdown);
@@ -93,12 +95,13 @@ function showContentHistoryDetail(issueBaseUrl: string, commentId: string, histo
function showContentHistoryMenu(issueBaseUrl: string, elCommentItem: Element, commentId: string) {
const elHeaderLeft = elCommentItem.querySelector('.comment-header-left')!;
const menuHtml = `
<div class="ui dropdown interact-fg content-history-menu tw-flex-shrink-0" data-comment-id="${commentId}">
&bull; ${i18nTextEdited}${svg('octicon-triangle-down', 14, 'dropdown icon')}
<div class="menu">
const menuHtml = html`
<div class="ui dropdown interact-fg content-history-menu tw-flex-shrink-0" data-comment-id="${commentId}">
&bull; ${i18nTextEdited}${svgRaw('octicon-triangle-down', 14, 'dropdown icon')}
<div class="menu">
</div>
</div>
</div>`;
`;
elHeaderLeft.querySelector(`.ui.dropdown.content-history-menu`)?.remove(); // remove the old one if exists
elHeaderLeft.append(createElementFromHTML(menuHtml));
+8 -10
View File
@@ -1,5 +1,5 @@
import {errorMessage} from '../modules/errors.ts';
import {htmlEscape} from '../utils/html.ts';
import {html, htmlEscape, htmlRaw} from '../utils/html.ts';
import {createTippy} from '../modules/tippy.ts';
import {
addDelegatedEventListener,
@@ -274,15 +274,13 @@ export function initRepoPullRequestReview() {
let ntr = tr.nextElementSibling;
if (!ntr?.classList.contains('add-comment')) {
ntr = createElementFromHTML(`
<tr class="add-comment" data-line-type="${htmlEscape(lineType)}">
${isSplit ? `
<td class="add-comment-left" colspan="4"></td>
<td class="add-comment-right" colspan="4"></td>
` : `
<td class="add-comment-left add-comment-right" colspan="5"></td>
`}
</tr>`);
const tdSplit = html`<td class="add-comment-left" colspan="4"></td><td class="add-comment-right" colspan="4"></td>`;
const tdUnified = html`<td class="add-comment-left add-comment-right" colspan="5"></td>`;
ntr = createElementFromHTML(html`
<tr class="add-comment" data-line-type="${lineType}">
${isSplit ? htmlRaw(tdSplit) : htmlRaw(tdUnified)}
</tr>
`);
tr.after(ntr);
}
const td = ntr.querySelector(`.add-comment-${side}`)!;
+8 -8
View File
@@ -1,5 +1,5 @@
import {htmlEscape} from '../utils/html.ts';
import {svg} from '../svg.ts';
import {html, htmlEscape, htmlRaw} from '../utils/html.ts';
import {svgRaw} from '../svg.ts';
import {animateOnce, queryElems, showElem} from '../utils/dom.ts';
import Toastify from 'toastify-js'; // don't use "async import", because when network error occurs, the "async import" also fails and nothing is shown
import type {Intent} from '../types.ts';
@@ -44,9 +44,8 @@ type ToastifyElement = HTMLElement & {_giteaToastifyInstance?: Toast};
/** See https://github.com/apvarun/toastify-js#api for options */
function showToast(message: string, level: Intent, {gravity, position, duration, useHtmlBody, preventDuplicates = true, ...other}: ToastOpts = {}): Toast | null {
const body = useHtmlBody ? message : htmlEscape(message);
const parent = document.querySelector('.ui.dimmer.active') ?? document.body;
const duplicateKey = preventDuplicates ? (preventDuplicates === true ? `${level}-${body}` : preventDuplicates) : '';
const duplicateKey = preventDuplicates ? (preventDuplicates === true ? `${level}-${message}` : preventDuplicates) : '';
// prevent showing duplicate toasts with the same level and message, and give visual feedback for end users
if (preventDuplicates) {
@@ -61,12 +60,13 @@ function showToast(message: string, level: Intent, {gravity, position, duration,
}
const {icon, background, duration: levelDuration} = levels[level ?? 'info'];
const bodyHtml = useHtmlBody ? message : htmlEscape(message);
const toast = Toastify({
selector: parent,
text: `
<div class='toast-icon'>${svg(icon)}</div>
<div class='toast-body'><span class="toast-duplicate-number tw-hidden">1</span>${body}</div>
<button class='btn toast-close'>${svg('octicon-x')}</button>
text: html`
<div class='toast-icon'>${svgRaw(icon)}</div>
<div class='toast-body'><span class="toast-duplicate-number tw-hidden">1</span>${htmlRaw(bodyHtml)}</div>
<button class='btn toast-close'>${svgRaw('octicon-x')}</button>
`,
escapeMarkup: false,
gravity: gravity ?? 'top',
+4
View File
@@ -199,6 +199,10 @@ export function svg(name: SvgName, size = 16, classNames?: string | string[]): s
return serializeXml(svgNode);
}
export function svgRaw(name: SvgName, size = 16, classNames?: string | string[]) {
return htmlRaw(svg(name, size, classNames));
}
export function svgParseOuterInner(name: SvgName) {
const svgStr = svgs[name];
if (!svgStr) throw new Error(`Unknown SVG icon: ${name}`);
+2
View File
@@ -9,6 +9,8 @@ import {
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>');
expect(createElementFromHTML('<TR data-x="1"><td>foo</td></TR>').outerHTML).toEqual('<tr data-x="1"><td>foo</td></tr>');
expect(createElementFromHTML('<trx></trx>').outerHTML).toEqual('<trx></trx>');
});
test('createElementFromAttrs', () => {
+7 -1
View File
@@ -267,8 +267,14 @@ export function isElemVisible(el: HTMLElement): boolean {
export function createElementFromHTML<T extends Element>(htmlString: string): T {
htmlString = htmlString.trim();
const isLetter = (code: number) => (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
const startsWithTag = (s: string, tag: string) => {
return s.startsWith('<') &&
s.substring(1, 1 + tag.length).toLowerCase() === tag.toLowerCase() &&
!isLetter(s[1 + tag.length].charCodeAt(0));
};
// There is no way to create some elements without a proper parent, jQuery's approach: https://github.com/jquery/jquery/blob/main/src/manipulation/wrapMap.js
if (htmlString.startsWith('<tr')) {
if (startsWithTag(htmlString, 'tr')) {
const container = document.createElement('table');
container.innerHTML = htmlString;
return container.querySelector<T>('tr')!;
+5 -3
View File
@@ -1,3 +1,5 @@
import {html, htmlRaw} from './html.ts';
export function urlQueryEscape(s: string) {
// See "TestQueryEscape" in backend
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#encoding_for_rfc3986
@@ -35,9 +37,9 @@ const urlLinkifyPattern = /(<([-\w]+)[^>]*>)|(<\/([-\w]+)[^>]*>)|(https?:\/\/[^\
const trailingPunctPattern = /[.,;:!?]+$/;
// Convert URLs to clickable links in HTML, preserving existing HTML tags
export function linkifyURLs(html: string): string {
export function linkifyURLs(htmlString: string): string {
let inAnchor = false;
return html.replace(urlLinkifyPattern, (match, _openTagFull, openTag, _closeTagFull, closeTag, url) => {
return htmlString.replace(urlLinkifyPattern, (match, _openTagFull, openTag, _closeTagFull, closeTag, url) => {
// skip URLs inside existing <a> tags
if (openTag === 'a') {
inAnchor = true;
@@ -54,6 +56,6 @@ export function linkifyURLs(html: string): string {
const cleanUrl = trailingPunct ? url.slice(0, -trailingPunct[0].length) : url;
const trailing = trailingPunct ? trailingPunct[0] : '';
// safe because regexp only matches valid URLs (no quotes or angle brackets)
return `<a href="${cleanUrl}" target="_blank">${cleanUrl}</a>${trailing}`;
return html`<a href="${htmlRaw(cleanUrl)}" target="_blank">${htmlRaw(cleanUrl)}</a>${htmlRaw(trailing)}`;
});
}