mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-03 02:21:17 +02:00
Merge branch 'main' into update-eslint-plugins
Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
43 files changed
+722
-246
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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}">
|
||||
• ${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}">
|
||||
• ${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));
|
||||
|
||||
@@ -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}`)!;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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')!;
|
||||
|
||||
@@ -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)}`;
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user