Migrate from webpack to vite (#37002)
Replace webpack with Vite 8 as the frontend bundler. Frontend build is around 3-4 times faster than before. Will work on all platforms including riscv64 (via wasm). `iife.js` is a classic render-blocking script in `<head>` (handles web components/early DOM setup). `index.js` is loaded as a `type="module"` script in the footer. All other JS chunks are also module scripts (supported in all browsers since 2018). Entry filenames are content-hashed (e.g. `index.C6Z2MRVQ.js`) and resolved at runtime via the Vite manifest, eliminating the `?v=` cache busting (which was unreliable in some scenarios like vscode dev build). Replaces: https://github.com/go-gitea/gitea/pull/36896 Fixes: https://github.com/go-gitea/gitea/issues/17793 Signed-off-by: silverwind <me@silverwind.io> Signed-off-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Claude (Opus 4.6) <noreply@anthropic.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
+5
-77
@@ -1,82 +1,12 @@
|
||||
// DO NOT IMPORT window.config HERE!
|
||||
// to make sure the error handler always works, we should never import `window.config`, because
|
||||
// some user's custom template breaks it.
|
||||
import type {Intent} from './types.ts';
|
||||
import {html} from './utils/html.ts';
|
||||
import {showGlobalErrorMessage, processWindowErrorEvent} from './modules/errors.ts';
|
||||
|
||||
// This sets up the URL prefix used in webpack's chunk loading.
|
||||
// This file must be imported before any lazy-loading is being attempted.
|
||||
window.__webpack_public_path__ = `${window.config?.assetUrlPrefix ?? '/assets'}/`;
|
||||
|
||||
export function shouldIgnoreError(err: Error) {
|
||||
const ignorePatterns: Array<RegExp> = [
|
||||
// https://github.com/go-gitea/gitea/issues/30861
|
||||
// https://github.com/microsoft/monaco-editor/issues/4496
|
||||
// https://github.com/microsoft/monaco-editor/issues/4679
|
||||
/\/assets\/js\/.*monaco/,
|
||||
];
|
||||
for (const pattern of ignorePatterns) {
|
||||
if (pattern.test(err.stack ?? '')) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function showGlobalErrorMessage(msg: string, msgType: Intent = 'error') {
|
||||
const msgContainer = document.querySelector('.page-content') ?? document.body;
|
||||
if (!msgContainer) {
|
||||
alert(`${msgType}: ${msg}`);
|
||||
return;
|
||||
}
|
||||
const msgCompact = msg.replace(/\W/g, '').trim(); // compact the message to a data attribute to avoid too many duplicated messages
|
||||
let msgDiv = msgContainer.querySelector<HTMLDivElement>(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`);
|
||||
if (!msgDiv) {
|
||||
const el = document.createElement('div');
|
||||
el.innerHTML = html`<div class="ui container js-global-error tw-my-[--page-spacing]"><div class="ui ${msgType} message tw-text-center tw-whitespace-pre-line"></div></div>`;
|
||||
msgDiv = el.childNodes[0] as HTMLDivElement;
|
||||
}
|
||||
// merge duplicated messages into "the message (count)" format
|
||||
const msgCount = Number(msgDiv.getAttribute(`data-global-error-msg-count`)) + 1;
|
||||
msgDiv.setAttribute(`data-global-error-msg-compact`, msgCompact);
|
||||
msgDiv.setAttribute(`data-global-error-msg-count`, msgCount.toString());
|
||||
msgDiv.querySelector('.ui.message')!.textContent = msg + (msgCount > 1 ? ` (${msgCount})` : '');
|
||||
msgContainer.prepend(msgDiv);
|
||||
}
|
||||
|
||||
function processWindowErrorEvent({error, reason, message, type, filename, lineno, colno}: ErrorEvent & PromiseRejectionEvent) {
|
||||
const err = error ?? reason;
|
||||
const assetBaseUrl = String(new URL(window.__webpack_public_path__, window.location.origin));
|
||||
const {runModeIsProd} = window.config ?? {};
|
||||
|
||||
// `error` and `reason` are not guaranteed to be errors. If the value is falsy, it is likely a
|
||||
// non-critical event from the browser. We log them but don't show them to users. Examples:
|
||||
// - https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#observation_errors
|
||||
// - https://github.com/mozilla-mobile/firefox-ios/issues/10817
|
||||
// - https://github.com/go-gitea/gitea/issues/20240
|
||||
if (!err) {
|
||||
if (message) console.error(new Error(message));
|
||||
if (runModeIsProd) return;
|
||||
}
|
||||
|
||||
if (err instanceof Error) {
|
||||
// If the error stack trace does not include the base URL of our script assets, it likely came
|
||||
// from a browser extension or inline script. Do not show such errors in production.
|
||||
if (!err.stack?.includes(assetBaseUrl) && runModeIsProd) return;
|
||||
// Ignore some known errors that are unable to fix
|
||||
if (shouldIgnoreError(err)) return;
|
||||
}
|
||||
|
||||
let msg = err?.message ?? message;
|
||||
if (lineno) msg += ` (${filename} @ ${lineno}:${colno})`;
|
||||
const dot = msg.endsWith('.') ? '' : '.';
|
||||
const renderedType = type === 'unhandledrejection' ? 'promise rejection' : type;
|
||||
showGlobalErrorMessage(`JavaScript ${renderedType}: ${msg}${dot} Open browser console to see more details.`);
|
||||
}
|
||||
|
||||
function initGlobalErrorHandler() {
|
||||
if (window._globalHandlerErrors?._inited) {
|
||||
showGlobalErrorMessage(`The global error handler has been initialized, do not initialize it again`);
|
||||
return;
|
||||
}
|
||||
// A module should not be imported twice, otherwise there will be bugs when a module has its internal states.
|
||||
// A real example is "generateElemId" in "utils/dom.ts", if it is imported twice in different module scopes,
|
||||
// It will generate duplicate IDs (ps: don't try to use "random" to fix, it is just a real example to show the importance of "do not import a module twice")
|
||||
if (!window._globalHandlerErrors?._inited) {
|
||||
if (!window.config) {
|
||||
showGlobalErrorMessage(`Gitea JavaScript code couldn't run correctly, please check your custom templates`);
|
||||
}
|
||||
@@ -90,5 +20,3 @@ function initGlobalErrorHandler() {
|
||||
// events directly
|
||||
window._globalHandlerErrors = {_inited: true, push: (e: ErrorEvent & PromiseRejectionEvent) => processWindowErrorEvent(e)} as any;
|
||||
}
|
||||
|
||||
initGlobalErrorHandler();
|
||||
|
||||
@@ -34,7 +34,7 @@ export async function initCaptcha() {
|
||||
break;
|
||||
}
|
||||
case 'm-captcha': {
|
||||
const mCaptcha = await import(/* webpackChunkName: "mcaptcha-vanilla-glue" */'@mcaptcha/vanilla-glue');
|
||||
const mCaptcha = await import('@mcaptcha/vanilla-glue');
|
||||
|
||||
// FIXME: the mCaptcha code is not right, it's a miracle that the wrong code could run
|
||||
// * the "vanilla-glue" has some problems with es6 module.
|
||||
|
||||
@@ -6,10 +6,10 @@ const {pageData} = window.config;
|
||||
|
||||
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'),
|
||||
import(/* webpackChunkName: "citation-js-bibtex" */'@citation-js/plugin-bibtex'),
|
||||
import(/* webpackChunkName: "citation-js-csl" */'@citation-js/plugin-csl'),
|
||||
import('@citation-js/core'),
|
||||
import('@citation-js/plugin-software-formats'),
|
||||
import('@citation-js/plugin-bibtex'),
|
||||
import('@citation-js/plugin-csl'),
|
||||
]);
|
||||
const citationFileContent = pageData.citationFileContent!;
|
||||
const config = plugins.config.get('@bibtex');
|
||||
|
||||
@@ -4,7 +4,7 @@ export async function initRepoCodeFrequency() {
|
||||
const el = document.querySelector('#repo-code-frequency-chart');
|
||||
if (!el) return;
|
||||
|
||||
const {default: RepoCodeFrequency} = await import(/* webpackChunkName: "code-frequency-graph" */'../components/RepoCodeFrequency.vue');
|
||||
const {default: RepoCodeFrequency} = await import('../components/RepoCodeFrequency.vue');
|
||||
try {
|
||||
const View = createApp(RepoCodeFrequency, {
|
||||
locale: {
|
||||
|
||||
@@ -129,7 +129,7 @@ function updateTheme(monaco: Monaco): void {
|
||||
type CreateMonacoOpts = MonacoOpts & {language?: string};
|
||||
|
||||
export async function createMonaco(textarea: HTMLTextAreaElement, filename: string, opts: CreateMonacoOpts): Promise<{monaco: Monaco, editor: IStandaloneCodeEditor}> {
|
||||
const monaco = await import(/* webpackChunkName: "monaco" */'monaco-editor');
|
||||
const monaco = await import('../modules/monaco.ts');
|
||||
|
||||
initLanguages(monaco);
|
||||
let {language, ...other} = opts;
|
||||
|
||||
@@ -6,8 +6,8 @@ export async function initColorPickers() {
|
||||
registerGlobalInitFunc('initColorPicker', async (el) => {
|
||||
if (!imported) {
|
||||
await Promise.all([
|
||||
import(/* webpackChunkName: "colorpicker" */'vanilla-colorful/hex-color-picker.js'),
|
||||
import(/* webpackChunkName: "colorpicker" */'../../css/features/colorpicker.css'),
|
||||
import('vanilla-colorful/hex-color-picker.js'),
|
||||
import('../../css/features/colorpicker.css'),
|
||||
]);
|
||||
imported = true;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {GET, POST} from '../modules/fetch.ts';
|
||||
import {showGlobalErrorMessage} from '../bootstrap.ts';
|
||||
import {showGlobalErrorMessage} from '../modules/errors.ts';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
import {addDelegatedEventListener, queryElems} from '../utils/dom.ts';
|
||||
import {registerGlobalInitFunc, registerGlobalSelectorFunc} from '../modules/observer.ts';
|
||||
|
||||
@@ -319,8 +319,8 @@ export class ComboMarkdownEditor {
|
||||
async switchToEasyMDE() {
|
||||
if (this.easyMDE) return;
|
||||
const [{default: EasyMDE}] = await Promise.all([
|
||||
import(/* webpackChunkName: "easymde" */'easymde'),
|
||||
import(/* webpackChunkName: "easymde" */'../../../css/easymde.css'),
|
||||
import('easymde'),
|
||||
import('../../../css/easymde.css'),
|
||||
]);
|
||||
const easyMDEOpt: EasyMDE.Options = {
|
||||
autoDownloadFontAwesome: false,
|
||||
|
||||
@@ -7,7 +7,7 @@ type CropperOpts = {
|
||||
};
|
||||
|
||||
async function initCompCropper({container, fileInput, imageSource}: CropperOpts) {
|
||||
const {default: Cropper} = await import(/* webpackChunkName: "cropperjs" */'cropperjs');
|
||||
const {default: Cropper} = await import('cropperjs');
|
||||
let currentFileName = '';
|
||||
let currentFileLastModified = 0;
|
||||
const cropper = new Cropper(imageSource, {
|
||||
|
||||
@@ -4,7 +4,7 @@ export async function initRepoContributors() {
|
||||
const el = document.querySelector('#repo-contributors-chart');
|
||||
if (!el) return;
|
||||
|
||||
const {default: RepoContributors} = await import(/* webpackChunkName: "contributors-graph" */'../components/RepoContributors.vue');
|
||||
const {default: RepoContributors} = await import('../components/RepoContributors.vue');
|
||||
try {
|
||||
const View = createApp(RepoContributors, {
|
||||
repoLink: el.getAttribute('data-repo-link'),
|
||||
|
||||
@@ -19,8 +19,8 @@ export const DropzoneCustomEventUploadDone = 'dropzone-custom-upload-done';
|
||||
|
||||
async function createDropzone(el: HTMLElement, opts: DropzoneOptions) {
|
||||
const [{default: Dropzone}] = await Promise.all([
|
||||
import(/* webpackChunkName: "dropzone" */'dropzone'),
|
||||
import(/* webpackChunkName: "dropzone" */'dropzone/dist/dropzone.css'),
|
||||
import('dropzone'),
|
||||
import('dropzone/dist/dropzone.css'),
|
||||
]);
|
||||
return new Dropzone(el, opts);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export async function initHeatmap() {
|
||||
noDataText: el.getAttribute('data-locale-no-contributions'),
|
||||
};
|
||||
|
||||
const {default: ActivityHeatmap} = await import(/* webpackChunkName: "ActivityHeatmap" */ '../components/ActivityHeatmap.vue');
|
||||
const {default: ActivityHeatmap} = await import('../components/ActivityHeatmap.vue');
|
||||
const View = createApp(ActivityHeatmap, {values, locale});
|
||||
View.mount(el);
|
||||
el.classList.remove('is-loading');
|
||||
|
||||
@@ -4,7 +4,7 @@ export async function initRepoRecentCommits() {
|
||||
const el = document.querySelector('#repo-recent-commits-chart');
|
||||
if (!el) return;
|
||||
|
||||
const {default: RepoRecentCommits} = await import(/* webpackChunkName: "recent-commits-graph" */'../components/RepoRecentCommits.vue');
|
||||
const {default: RepoRecentCommits} = await import('../components/RepoRecentCommits.vue');
|
||||
try {
|
||||
const View = createApp(RepoRecentCommits, {
|
||||
locale: {
|
||||
|
||||
@@ -69,7 +69,7 @@ export function filterRepoFilesWeighted(files: Array<string>, filter: string) {
|
||||
|
||||
export function initRepoFileSearch() {
|
||||
registerGlobalInitFunc('initRepoFileSearch', async (el) => {
|
||||
const {default: RepoFileSearch} = await import(/* webpackChunkName: "RepoFileSearch" */ '../components/RepoFileSearch.vue');
|
||||
const {default: RepoFileSearch} = await import('../components/RepoFileSearch.vue');
|
||||
createApp(RepoFileSearch, {
|
||||
repoLink: el.getAttribute('data-repo-link'),
|
||||
currentRefNameSubURL: el.getAttribute('data-current-ref-name-sub-url'),
|
||||
|
||||
@@ -66,7 +66,7 @@ async function initRepoPullRequestMergeForm(box: HTMLElement) {
|
||||
const el = box.querySelector('#pull-request-merge-form');
|
||||
if (!el) return;
|
||||
|
||||
const {default: PullRequestMergeForm} = await import(/* webpackChunkName: "PullRequestMergeForm" */ '../components/PullRequestMergeForm.vue');
|
||||
const {default: PullRequestMergeForm} = await import('../components/PullRequestMergeForm.vue');
|
||||
const view = createApp(PullRequestMergeForm);
|
||||
view.mount(el);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {TributeCollection} from 'tributejs';
|
||||
import type {Mention} from '../types.ts';
|
||||
|
||||
export async function attachTribute(element: HTMLElement) {
|
||||
const {default: Tribute} = await import(/* webpackChunkName: "tribute" */'tributejs');
|
||||
const {default: Tribute} = await import('tributejs');
|
||||
const mentionsUrl = element.closest('[data-mentions-url]')?.getAttribute('data-mentions-url');
|
||||
|
||||
const emojiCollection: TributeCollection<string> = { // emojis
|
||||
|
||||
Vendored
+10
-1
@@ -22,8 +22,8 @@ interface Window {
|
||||
config: {
|
||||
appUrl: string,
|
||||
appSubUrl: string,
|
||||
assetVersionEncoded: string,
|
||||
assetUrlPrefix: string,
|
||||
sharedWorkerUri: string,
|
||||
runModeIsProd: boolean,
|
||||
customEmojis: Record<string, string>,
|
||||
pageData: Record<string, any> & {
|
||||
@@ -64,6 +64,10 @@ interface Window {
|
||||
codeEditors: any[], // export editor for customization
|
||||
localUserSettings: typeof import('./modules/user-settings.ts').localUserSettings,
|
||||
|
||||
MonacoEnvironment?: {
|
||||
getWorker: (workerId: string, label: string) => Worker,
|
||||
},
|
||||
|
||||
// various captcha plugins
|
||||
grecaptcha: any,
|
||||
turnstile: any,
|
||||
@@ -71,3 +75,8 @@ interface Window {
|
||||
|
||||
// do not add more properties here unless it is a must
|
||||
}
|
||||
|
||||
declare module '*?worker' {
|
||||
const workerConstructor: new () => Worker;
|
||||
export default workerConstructor;
|
||||
}
|
||||
|
||||
+16
-2
@@ -1,2 +1,16 @@
|
||||
import jquery from 'jquery';
|
||||
window.$ = window.jQuery = jquery; // only for Fomantic UI
|
||||
import jquery from 'jquery'; // eslint-disable-line no-restricted-imports
|
||||
import htmx from 'htmx.org'; // eslint-disable-line no-restricted-imports
|
||||
import 'idiomorph/htmx'; // eslint-disable-line no-restricted-imports
|
||||
|
||||
// Some users still use inline scripts and expect jQuery to be available globally.
|
||||
// To avoid breaking existing users and custom plugins, import jQuery globally without ES module.
|
||||
window.$ = window.jQuery = jquery;
|
||||
|
||||
// There is a bug in htmx, it incorrectly checks "readyState === 'complete'" when the DOM tree is ready and won't trigger DOMContentLoaded
|
||||
// The bug makes htmx impossible to be loaded from an ES module: importing the htmx in onDomReady will make htmx skip its initialization.
|
||||
// ref: https://github.com/bigskysoftware/htmx/pull/3365
|
||||
window.htmx = htmx;
|
||||
|
||||
// https://htmx.org/reference/#config
|
||||
htmx.config.requestClass = 'is-loading';
|
||||
htmx.config.scrollIntoViewOnBoost = false;
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import htmx from 'htmx.org';
|
||||
import 'idiomorph/htmx';
|
||||
import type {HtmxResponseInfo} from 'htmx.org';
|
||||
import {showErrorToast} from './modules/toast.ts';
|
||||
|
||||
type HtmxEvent = Event & {detail: HtmxResponseInfo};
|
||||
|
||||
export function initHtmx() {
|
||||
window.htmx = htmx;
|
||||
|
||||
// https://htmx.org/reference/#config
|
||||
htmx.config.requestClass = 'is-loading';
|
||||
htmx.config.scrollIntoViewOnBoost = false;
|
||||
|
||||
// https://htmx.org/events/#htmx:sendError
|
||||
document.body.addEventListener('htmx:sendError', (event: Partial<HtmxEvent>) => {
|
||||
// TODO: add translations
|
||||
showErrorToast(`Network error when calling ${event.detail!.requestConfig.path}`);
|
||||
});
|
||||
|
||||
// https://htmx.org/events/#htmx:responseError
|
||||
document.body.addEventListener('htmx:responseError', (event: Partial<HtmxEvent>) => {
|
||||
// TODO: add translations
|
||||
showErrorToast(`Error ${event.detail!.xhr.status} when calling ${event.detail!.requestConfig.path}`);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// This file is the entry point for the code which should block the page rendering, it is compiled by our "iife" vite plugin
|
||||
|
||||
// bootstrap module must be the first one to be imported, it handles global errors
|
||||
import './bootstrap.ts';
|
||||
|
||||
// many users expect to use jQuery in their custom scripts (https://docs.gitea.com/administration/customizing-gitea#example-plantuml)
|
||||
// so load globals (including jQuery) as early as possible
|
||||
import './globals.ts';
|
||||
|
||||
import './webcomponents/index.ts';
|
||||
import './modules/user-settings.ts'; // templates also need to use localUserSettings in inline scripts
|
||||
@@ -1,175 +0,0 @@
|
||||
import '../fomantic/build/fomantic.js';
|
||||
|
||||
import {initHtmx} from './htmx.ts';
|
||||
import {initDashboardRepoList} from './features/dashboard.ts';
|
||||
import {initGlobalCopyToClipboardListener} from './features/clipboard.ts';
|
||||
import {initRepoGraphGit} from './features/repo-graph.ts';
|
||||
import {initHeatmap} from './features/heatmap.ts';
|
||||
import {initImageDiff} from './features/imagediff.ts';
|
||||
import {initRepoMigration} from './features/repo-migration.ts';
|
||||
import {initRepoProject} from './features/repo-projects.ts';
|
||||
import {initTableSort} from './features/tablesort.ts';
|
||||
import {initAdminUserListSearchForm} from './features/admin/users.ts';
|
||||
import {initAdminConfigs} from './features/admin/config.ts';
|
||||
import {initMarkupAnchors} from './markup/anchors.ts';
|
||||
import {initNotificationCount} from './features/notification.ts';
|
||||
import {initRepoIssueContentHistory} from './features/repo-issue-content.ts';
|
||||
import {initStopwatch} from './features/stopwatch.ts';
|
||||
import {initRepoFileSearch} from './features/repo-findfile.ts';
|
||||
import {initMarkupContent} from './markup/content.ts';
|
||||
import {initRepoFileView} from './features/file-view.ts';
|
||||
import {initUserAuthOauth2, initUserCheckAppUrl} from './features/user-auth.ts';
|
||||
import {initRepoPullRequestAllowMaintainerEdit, initRepoPullRequestReview, initRepoIssueSidebarDependency, initRepoIssueFilterItemLabel} from './features/repo-issue.ts';
|
||||
import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts';
|
||||
import {initRepoTopicBar} from './features/repo-home.ts';
|
||||
import {initAdminCommon} from './features/admin/common.ts';
|
||||
import {initRepoCodeView} from './features/repo-code.ts';
|
||||
import {initSshKeyFormParser} from './features/sshkey-helper.ts';
|
||||
import {initUserSettings} from './features/user-settings.ts';
|
||||
import {initRepoActivityTopAuthorsChart, initRepoArchiveLinks} from './features/repo-common.ts';
|
||||
import {initRepoMigrationStatusChecker} from './features/repo-migrate.ts';
|
||||
import {initRepoDiffView} from './features/repo-diff.ts';
|
||||
import {initOrgTeam} from './features/org-team.ts';
|
||||
import {initUserAuthWebAuthn, initUserAuthWebAuthnRegister} from './features/user-auth-webauthn.ts';
|
||||
import {initRepoReleaseNew} from './features/repo-release.ts';
|
||||
import {initRepoEditor} from './features/repo-editor.ts';
|
||||
import {initCompSearchUserBox} from './features/comp/SearchUserBox.ts';
|
||||
import {initInstall} from './features/install.ts';
|
||||
import {initCompWebHookEditor} from './features/comp/WebHookEditor.ts';
|
||||
import {initRepoBranchButton} from './features/repo-branch.ts';
|
||||
import {initCommonOrganization} from './features/common-organization.ts';
|
||||
import {initRepoWikiForm} from './features/repo-wiki.ts';
|
||||
import {initRepository, initBranchSelectorTabs} from './features/repo-legacy.ts';
|
||||
import {initCopyContent} from './features/copycontent.ts';
|
||||
import {initCaptcha} from './features/captcha.ts';
|
||||
import {initRepositoryActionView} from './features/repo-actions.ts';
|
||||
import {initGlobalTooltips} from './modules/tippy.ts';
|
||||
import {initGiteaFomantic} from './modules/fomantic.ts';
|
||||
import {initSubmitEventPolyfill} from './utils/dom.ts';
|
||||
import {initRepoIssueList} from './features/repo-issue-list.ts';
|
||||
import {initCommonIssueListQuickGoto} from './features/common-issue-list.ts';
|
||||
import {initRepoContributors} from './features/contributors.ts';
|
||||
import {initRepoCodeFrequency} from './features/code-frequency.ts';
|
||||
import {initRepoRecentCommits} from './features/recent-commits.ts';
|
||||
import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.ts';
|
||||
import {initGlobalSelectorObserver} from './modules/observer.ts';
|
||||
import {initRepositorySearch} from './features/repo-search.ts';
|
||||
import {initColorPickers} from './features/colorpicker.ts';
|
||||
import {initAdminSelfCheck} from './features/admin/selfcheck.ts';
|
||||
import {initOAuth2SettingsDisableCheckbox} from './features/oauth2-settings.ts';
|
||||
import {initGlobalFetchAction} from './features/common-fetch-action.ts';
|
||||
import {initCommmPageComponents, initGlobalComponent, initGlobalDropdown, initGlobalInput} from './features/common-page.ts';
|
||||
import {initGlobalButtonClickOnEnter, initGlobalButtons, initGlobalDeleteButton} from './features/common-button.ts';
|
||||
import {initGlobalComboMarkdownEditor, initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts';
|
||||
import {callInitFunctions} from './modules/init.ts';
|
||||
import {initRepoViewFileTree} from './features/repo-view-file-tree.ts';
|
||||
import {initActionsPermissionsForm} from './features/common-actions-permissions.ts';
|
||||
import {initGlobalShortcut} from './modules/shortcut.ts';
|
||||
|
||||
const initStartTime = performance.now();
|
||||
const initPerformanceTracer = callInitFunctions([
|
||||
initHtmx,
|
||||
initSubmitEventPolyfill,
|
||||
initGiteaFomantic,
|
||||
|
||||
initGlobalComponent,
|
||||
initGlobalDropdown,
|
||||
initGlobalFetchAction,
|
||||
initGlobalTooltips,
|
||||
initGlobalButtonClickOnEnter,
|
||||
initGlobalButtons,
|
||||
initGlobalCopyToClipboardListener,
|
||||
initGlobalEnterQuickSubmit,
|
||||
initGlobalFormDirtyLeaveConfirm,
|
||||
initGlobalComboMarkdownEditor,
|
||||
initGlobalDeleteButton,
|
||||
initGlobalInput,
|
||||
initGlobalShortcut,
|
||||
|
||||
initCommonOrganization,
|
||||
initCommonIssueListQuickGoto,
|
||||
|
||||
initCompSearchUserBox,
|
||||
initCompWebHookEditor,
|
||||
|
||||
initInstall,
|
||||
|
||||
initCommmPageComponents,
|
||||
|
||||
initHeatmap,
|
||||
initImageDiff,
|
||||
initMarkupAnchors,
|
||||
initMarkupContent,
|
||||
initSshKeyFormParser,
|
||||
initStopwatch,
|
||||
initTableSort,
|
||||
initRepoFileSearch,
|
||||
initCopyContent,
|
||||
|
||||
initAdminCommon,
|
||||
initAdminUserListSearchForm,
|
||||
initAdminConfigs,
|
||||
initAdminSelfCheck,
|
||||
|
||||
initDashboardRepoList,
|
||||
|
||||
initNotificationCount,
|
||||
|
||||
initOrgTeam,
|
||||
|
||||
initRepoActivityTopAuthorsChart,
|
||||
initRepoArchiveLinks,
|
||||
initRepoBranchButton,
|
||||
initRepoCodeView,
|
||||
initBranchSelectorTabs,
|
||||
initRepoEllipsisButton,
|
||||
initRepoDiffCommitBranchesAndTags,
|
||||
initRepoEditor,
|
||||
initRepoGraphGit,
|
||||
initRepoIssueContentHistory,
|
||||
initRepoIssueList,
|
||||
initRepoIssueFilterItemLabel,
|
||||
initRepoIssueSidebarDependency,
|
||||
initRepoMigration,
|
||||
initRepoMigrationStatusChecker,
|
||||
initRepoProject,
|
||||
initRepoPullRequestAllowMaintainerEdit,
|
||||
initRepoPullRequestReview,
|
||||
initRepoReleaseNew,
|
||||
initRepoTopicBar,
|
||||
initRepoViewFileTree,
|
||||
initRepoWikiForm,
|
||||
initRepository,
|
||||
initRepositoryActionView,
|
||||
initRepositorySearch,
|
||||
initRepoContributors,
|
||||
initRepoCodeFrequency,
|
||||
initRepoRecentCommits,
|
||||
|
||||
initCommitStatuses,
|
||||
initCaptcha,
|
||||
|
||||
initUserCheckAppUrl,
|
||||
initUserAuthOauth2,
|
||||
initUserAuthWebAuthn,
|
||||
initUserAuthWebAuthnRegister,
|
||||
initUserSettings,
|
||||
initRepoDiffView,
|
||||
initColorPickers,
|
||||
|
||||
initOAuth2SettingsDisableCheckbox,
|
||||
|
||||
initRepoFileView,
|
||||
initActionsPermissionsForm,
|
||||
]);
|
||||
|
||||
// it must be the last one, then the "querySelectorAll" only needs to be executed once for global init functions.
|
||||
initGlobalSelectorObserver(initPerformanceTracer);
|
||||
if (initPerformanceTracer) initPerformanceTracer.printResults();
|
||||
|
||||
const initDur = performance.now() - initStartTime;
|
||||
if (initDur > 500) {
|
||||
console.error(`slow init functions took ${initDur.toFixed(3)}ms`);
|
||||
}
|
||||
|
||||
document.dispatchEvent(new CustomEvent('gitea:index-ready'));
|
||||
+183
-24
@@ -1,29 +1,188 @@
|
||||
// bootstrap module must be the first one to be imported, it handles webpack lazy-loading and global errors
|
||||
import './bootstrap.ts';
|
||||
import '../fomantic/build/fomantic.js';
|
||||
import '../css/index.css';
|
||||
import type {HtmxResponseInfo} from 'htmx.org';
|
||||
import {showErrorToast} from './modules/toast.ts';
|
||||
|
||||
// many users expect to use jQuery in their custom scripts (https://docs.gitea.com/administration/customizing-gitea#example-plantuml)
|
||||
// so load globals (including jQuery) as early as possible
|
||||
import './globals.ts';
|
||||
import {initDashboardRepoList} from './features/dashboard.ts';
|
||||
import {initGlobalCopyToClipboardListener} from './features/clipboard.ts';
|
||||
import {initRepoGraphGit} from './features/repo-graph.ts';
|
||||
import {initHeatmap} from './features/heatmap.ts';
|
||||
import {initImageDiff} from './features/imagediff.ts';
|
||||
import {initRepoMigration} from './features/repo-migration.ts';
|
||||
import {initRepoProject} from './features/repo-projects.ts';
|
||||
import {initTableSort} from './features/tablesort.ts';
|
||||
import {initAdminUserListSearchForm} from './features/admin/users.ts';
|
||||
import {initAdminConfigs} from './features/admin/config.ts';
|
||||
import {initMarkupAnchors} from './markup/anchors.ts';
|
||||
import {initNotificationCount} from './features/notification.ts';
|
||||
import {initRepoIssueContentHistory} from './features/repo-issue-content.ts';
|
||||
import {initStopwatch} from './features/stopwatch.ts';
|
||||
import {initRepoFileSearch} from './features/repo-findfile.ts';
|
||||
import {initMarkupContent} from './markup/content.ts';
|
||||
import {initRepoFileView} from './features/file-view.ts';
|
||||
import {initUserAuthOauth2, initUserCheckAppUrl} from './features/user-auth.ts';
|
||||
import {initRepoPullRequestAllowMaintainerEdit, initRepoPullRequestReview, initRepoIssueSidebarDependency, initRepoIssueFilterItemLabel} from './features/repo-issue.ts';
|
||||
import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts';
|
||||
import {initRepoTopicBar} from './features/repo-home.ts';
|
||||
import {initAdminCommon} from './features/admin/common.ts';
|
||||
import {initRepoCodeView} from './features/repo-code.ts';
|
||||
import {initSshKeyFormParser} from './features/sshkey-helper.ts';
|
||||
import {initUserSettings} from './features/user-settings.ts';
|
||||
import {initRepoActivityTopAuthorsChart, initRepoArchiveLinks} from './features/repo-common.ts';
|
||||
import {initRepoMigrationStatusChecker} from './features/repo-migrate.ts';
|
||||
import {initRepoDiffView} from './features/repo-diff.ts';
|
||||
import {initOrgTeam} from './features/org-team.ts';
|
||||
import {initUserAuthWebAuthn, initUserAuthWebAuthnRegister} from './features/user-auth-webauthn.ts';
|
||||
import {initRepoReleaseNew} from './features/repo-release.ts';
|
||||
import {initRepoEditor} from './features/repo-editor.ts';
|
||||
import {initCompSearchUserBox} from './features/comp/SearchUserBox.ts';
|
||||
import {initInstall} from './features/install.ts';
|
||||
import {initCompWebHookEditor} from './features/comp/WebHookEditor.ts';
|
||||
import {initRepoBranchButton} from './features/repo-branch.ts';
|
||||
import {initCommonOrganization} from './features/common-organization.ts';
|
||||
import {initRepoWikiForm} from './features/repo-wiki.ts';
|
||||
import {initRepository, initBranchSelectorTabs} from './features/repo-legacy.ts';
|
||||
import {initCopyContent} from './features/copycontent.ts';
|
||||
import {initCaptcha} from './features/captcha.ts';
|
||||
import {initRepositoryActionView} from './features/repo-actions.ts';
|
||||
import {initGlobalTooltips} from './modules/tippy.ts';
|
||||
import {initGiteaFomantic} from './modules/fomantic.ts';
|
||||
import {initSubmitEventPolyfill} from './utils/dom.ts';
|
||||
import {initRepoIssueList} from './features/repo-issue-list.ts';
|
||||
import {initCommonIssueListQuickGoto} from './features/common-issue-list.ts';
|
||||
import {initRepoContributors} from './features/contributors.ts';
|
||||
import {initRepoCodeFrequency} from './features/code-frequency.ts';
|
||||
import {initRepoRecentCommits} from './features/recent-commits.ts';
|
||||
import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.ts';
|
||||
import {initGlobalSelectorObserver} from './modules/observer.ts';
|
||||
import {initRepositorySearch} from './features/repo-search.ts';
|
||||
import {initColorPickers} from './features/colorpicker.ts';
|
||||
import {initAdminSelfCheck} from './features/admin/selfcheck.ts';
|
||||
import {initOAuth2SettingsDisableCheckbox} from './features/oauth2-settings.ts';
|
||||
import {initGlobalFetchAction} from './features/common-fetch-action.ts';
|
||||
import {initCommmPageComponents, initGlobalComponent, initGlobalDropdown, initGlobalInput} from './features/common-page.ts';
|
||||
import {initGlobalButtonClickOnEnter, initGlobalButtons, initGlobalDeleteButton} from './features/common-button.ts';
|
||||
import {initGlobalComboMarkdownEditor, initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts';
|
||||
import {callInitFunctions} from './modules/init.ts';
|
||||
import {initRepoViewFileTree} from './features/repo-view-file-tree.ts';
|
||||
import {initActionsPermissionsForm} from './features/common-actions-permissions.ts';
|
||||
import {initGlobalShortcut} from './modules/shortcut.ts';
|
||||
|
||||
import './webcomponents/index.ts';
|
||||
import './modules/user-settings.ts'; // templates also need to use localUserSettings in inline scripts
|
||||
import {onDomReady} from './utils/dom.ts';
|
||||
const initStartTime = performance.now();
|
||||
const initPerformanceTracer = callInitFunctions([
|
||||
initSubmitEventPolyfill,
|
||||
initGiteaFomantic,
|
||||
|
||||
// TODO: There is a bug in htmx, it incorrectly checks "readyState === 'complete'" when the DOM tree is ready and won't trigger DOMContentLoaded
|
||||
// Then importing the htmx in our onDomReady will make htmx skip its initialization.
|
||||
// If the bug would be fixed (https://github.com/bigskysoftware/htmx/pull/3365), then we can only import htmx in "onDomReady"
|
||||
import 'htmx.org';
|
||||
initGlobalComponent,
|
||||
initGlobalDropdown,
|
||||
initGlobalFetchAction,
|
||||
initGlobalTooltips,
|
||||
initGlobalButtonClickOnEnter,
|
||||
initGlobalButtons,
|
||||
initGlobalCopyToClipboardListener,
|
||||
initGlobalEnterQuickSubmit,
|
||||
initGlobalFormDirtyLeaveConfirm,
|
||||
initGlobalComboMarkdownEditor,
|
||||
initGlobalDeleteButton,
|
||||
initGlobalInput,
|
||||
initGlobalShortcut,
|
||||
|
||||
onDomReady(async () => {
|
||||
// when navigate before the import complete, there will be an error from webpack chunk loader:
|
||||
// JavaScript promise rejection: Loading chunk index-domready failed.
|
||||
try {
|
||||
await import(/* webpackChunkName: "index-domready" */'./index-domready.ts');
|
||||
} catch (e) {
|
||||
if (e.name === 'ChunkLoadError') {
|
||||
console.error('Error loading index-domready:', e);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
initCommonOrganization,
|
||||
initCommonIssueListQuickGoto,
|
||||
|
||||
initCompSearchUserBox,
|
||||
initCompWebHookEditor,
|
||||
|
||||
initInstall,
|
||||
|
||||
initCommmPageComponents,
|
||||
|
||||
initHeatmap,
|
||||
initImageDiff,
|
||||
initMarkupAnchors,
|
||||
initMarkupContent,
|
||||
initSshKeyFormParser,
|
||||
initStopwatch,
|
||||
initTableSort,
|
||||
initRepoFileSearch,
|
||||
initCopyContent,
|
||||
|
||||
initAdminCommon,
|
||||
initAdminUserListSearchForm,
|
||||
initAdminConfigs,
|
||||
initAdminSelfCheck,
|
||||
|
||||
initDashboardRepoList,
|
||||
|
||||
initNotificationCount,
|
||||
|
||||
initOrgTeam,
|
||||
|
||||
initRepoActivityTopAuthorsChart,
|
||||
initRepoArchiveLinks,
|
||||
initRepoBranchButton,
|
||||
initRepoCodeView,
|
||||
initBranchSelectorTabs,
|
||||
initRepoEllipsisButton,
|
||||
initRepoDiffCommitBranchesAndTags,
|
||||
initRepoEditor,
|
||||
initRepoGraphGit,
|
||||
initRepoIssueContentHistory,
|
||||
initRepoIssueList,
|
||||
initRepoIssueFilterItemLabel,
|
||||
initRepoIssueSidebarDependency,
|
||||
initRepoMigration,
|
||||
initRepoMigrationStatusChecker,
|
||||
initRepoProject,
|
||||
initRepoPullRequestAllowMaintainerEdit,
|
||||
initRepoPullRequestReview,
|
||||
initRepoReleaseNew,
|
||||
initRepoTopicBar,
|
||||
initRepoViewFileTree,
|
||||
initRepoWikiForm,
|
||||
initRepository,
|
||||
initRepositoryActionView,
|
||||
initRepositorySearch,
|
||||
initRepoContributors,
|
||||
initRepoCodeFrequency,
|
||||
initRepoRecentCommits,
|
||||
|
||||
initCommitStatuses,
|
||||
initCaptcha,
|
||||
|
||||
initUserCheckAppUrl,
|
||||
initUserAuthOauth2,
|
||||
initUserAuthWebAuthn,
|
||||
initUserAuthWebAuthnRegister,
|
||||
initUserSettings,
|
||||
initRepoDiffView,
|
||||
initColorPickers,
|
||||
|
||||
initOAuth2SettingsDisableCheckbox,
|
||||
|
||||
initRepoFileView,
|
||||
initActionsPermissionsForm,
|
||||
]);
|
||||
|
||||
// it must be the last one, then the "querySelectorAll" only needs to be executed once for global init functions.
|
||||
initGlobalSelectorObserver(initPerformanceTracer);
|
||||
if (initPerformanceTracer) initPerformanceTracer.printResults();
|
||||
|
||||
const initDur = performance.now() - initStartTime;
|
||||
if (initDur > 500) {
|
||||
console.error(`slow init functions took ${initDur.toFixed(3)}ms`);
|
||||
}
|
||||
|
||||
// https://htmx.org/events/#htmx:sendError
|
||||
type HtmxEvent = Event & {detail: HtmxResponseInfo};
|
||||
document.body.addEventListener('htmx:sendError', (event) => {
|
||||
// TODO: add translations
|
||||
showErrorToast(`Network error when calling ${(event as HtmxEvent).detail.requestConfig.path}`);
|
||||
});
|
||||
// https://htmx.org/events/#htmx:responseError
|
||||
document.body.addEventListener('htmx:responseError', (event) => {
|
||||
// TODO: add translations
|
||||
showErrorToast(`Error ${(event as HtmxEvent).detail.xhr.status} when calling ${(event as HtmxEvent).detail.requestConfig.path}`);
|
||||
});
|
||||
|
||||
document.dispatchEvent(new CustomEvent('gitea:index-ready'));
|
||||
|
||||
@@ -3,8 +3,8 @@ import {queryElems} from '../utils/dom.ts';
|
||||
export async function initMarkupRenderAsciicast(elMarkup: HTMLElement): Promise<void> {
|
||||
queryElems(elMarkup, '.asciinema-player-container', async (el) => {
|
||||
const [player] = await Promise.all([
|
||||
import(/* webpackChunkName: "asciinema-player" */'asciinema-player'),
|
||||
import(/* webpackChunkName: "asciinema-player" */'asciinema-player/dist/bundle/asciinema-player.css'),
|
||||
import('asciinema-player'),
|
||||
import('asciinema-player/dist/bundle/asciinema-player.css'),
|
||||
]);
|
||||
|
||||
player.create(el.getAttribute('data-asciinema-player-src')!, el, {
|
||||
|
||||
@@ -16,8 +16,8 @@ export async function initMarkupCodeMath(elMarkup: HTMLElement): Promise<void> {
|
||||
// .markup code.language-math'
|
||||
queryElems(elMarkup, 'code.language-math', async (el) => {
|
||||
const [{default: katex}] = await Promise.all([
|
||||
import(/* webpackChunkName: "katex" */'katex'),
|
||||
import(/* webpackChunkName: "katex" */'katex/dist/katex.css'),
|
||||
import('katex'),
|
||||
import('katex/dist/katex.css'),
|
||||
]);
|
||||
|
||||
const MAX_CHARS = 1000;
|
||||
|
||||
@@ -72,8 +72,8 @@ export function sourceNeedsElk(source: string) {
|
||||
}
|
||||
|
||||
async function loadMermaid(needElkRender: boolean) {
|
||||
const mermaidPromise = import(/* webpackChunkName: "mermaid" */'mermaid');
|
||||
const elkPromise = needElkRender ? import(/* webpackChunkName: "mermaid-layout-elk" */'@mermaid-js/layout-elk') : null;
|
||||
const mermaidPromise = import('mermaid');
|
||||
const elkPromise = needElkRender ? import('@mermaid-js/layout-elk') : null;
|
||||
const results = await Promise.all([mermaidPromise, elkPromise]);
|
||||
return {
|
||||
mermaid: results[0].default,
|
||||
|
||||
@@ -20,7 +20,7 @@ function showMarkupRefIssuePopup(e: MouseEvent | FocusEvent) {
|
||||
|
||||
const el = document.createElement('div');
|
||||
const onShowAsync = async () => {
|
||||
const {default: ContextPopup} = await import(/* webpackChunkName: "ContextPopup" */ '../components/ContextPopup.vue');
|
||||
const {default: ContextPopup} = await import('../components/ContextPopup.vue');
|
||||
const view = createApp(ContextPopup, {
|
||||
// backend: GetIssueInfo
|
||||
loadIssueInfoUrl: `${window.config.appSubUrl}/${issuePathInfo.ownerName}/${issuePathInfo.repoName}/issues/${issuePathInfo.indexString}/info`,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {showGlobalErrorMessage, shouldIgnoreError} from './bootstrap.ts';
|
||||
import {showGlobalErrorMessage, shouldIgnoreError} from './errors.ts';
|
||||
|
||||
test('showGlobalErrorMessage', () => {
|
||||
document.body.innerHTML = '<div class="page-content"></div>';
|
||||
@@ -13,9 +13,9 @@ test('showGlobalErrorMessage', () => {
|
||||
|
||||
test('shouldIgnoreError', () => {
|
||||
for (const url of [
|
||||
'https://gitea.test/assets/js/monaco.b359ef7e.js',
|
||||
'https://gitea.test/assets/js/monaco-editor.4a969118.worker.js',
|
||||
'https://gitea.test/assets/js/vendors-node_modules_pnpm_monaco-editor_0_55_1_node_modules_monaco-editor_esm_vs_base_common_-e11c7c.966a028d.js',
|
||||
'https://gitea.test/assets/js/monaco.D14TzjS9.js',
|
||||
'https://gitea.test/assets/js/editor.api2.BdhK7zNg.js',
|
||||
'https://gitea.test/assets/js/editor.worker.BYgvyFya.js',
|
||||
]) {
|
||||
const err = new Error('test');
|
||||
err.stack = `Error: test\n at ${url}:1:1`;
|
||||
@@ -0,0 +1,67 @@
|
||||
// keep this file lightweight, it's imported into IIFE chunk in bootstrap
|
||||
import {html} from '../utils/html.ts';
|
||||
import type {Intent} from '../types.ts';
|
||||
|
||||
export function showGlobalErrorMessage(msg: string, msgType: Intent = 'error') {
|
||||
const msgContainer = document.querySelector('.page-content') ?? document.body;
|
||||
if (!msgContainer) {
|
||||
alert(`${msgType}: ${msg}`);
|
||||
return;
|
||||
}
|
||||
const msgCompact = msg.replace(/\W/g, '').trim(); // compact the message to a data attribute to avoid too many duplicated messages
|
||||
let msgDiv = msgContainer.querySelector<HTMLDivElement>(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`);
|
||||
if (!msgDiv) {
|
||||
const el = document.createElement('div');
|
||||
el.innerHTML = html`<div class="ui container js-global-error tw-my-[--page-spacing]"><div class="ui ${msgType} message tw-text-center tw-whitespace-pre-line"></div></div>`;
|
||||
msgDiv = el.childNodes[0] as HTMLDivElement;
|
||||
}
|
||||
// merge duplicated messages into "the message (count)" format
|
||||
const msgCount = Number(msgDiv.getAttribute(`data-global-error-msg-count`)) + 1;
|
||||
msgDiv.setAttribute(`data-global-error-msg-compact`, msgCompact);
|
||||
msgDiv.setAttribute(`data-global-error-msg-count`, msgCount.toString());
|
||||
msgDiv.querySelector('.ui.message')!.textContent = msg + (msgCount > 1 ? ` (${msgCount})` : '');
|
||||
msgContainer.prepend(msgDiv);
|
||||
}
|
||||
|
||||
export function shouldIgnoreError(err: Error) {
|
||||
const ignorePatterns: Array<RegExp> = [
|
||||
// https://github.com/go-gitea/gitea/issues/30861
|
||||
// https://github.com/microsoft/monaco-editor/issues/4496
|
||||
// https://github.com/microsoft/monaco-editor/issues/4679
|
||||
/\/assets\/js\/.*(monaco|editor\.(api|worker))/,
|
||||
];
|
||||
for (const pattern of ignorePatterns) {
|
||||
if (pattern.test(err.stack ?? '')) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function processWindowErrorEvent({error, reason, message, type, filename, lineno, colno}: ErrorEvent & PromiseRejectionEvent) {
|
||||
const err = error ?? reason;
|
||||
const assetBaseUrl = String(new URL(`${window.config?.assetUrlPrefix ?? '/assets'}/`, window.location.origin));
|
||||
const {runModeIsProd} = window.config ?? {};
|
||||
|
||||
// `error` and `reason` are not guaranteed to be errors. If the value is falsy, it is likely a
|
||||
// non-critical event from the browser. We log them but don't show them to users. Examples:
|
||||
// - https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#observation_errors
|
||||
// - https://github.com/mozilla-mobile/firefox-ios/issues/10817
|
||||
// - https://github.com/go-gitea/gitea/issues/20240
|
||||
if (!err) {
|
||||
if (message) console.error(new Error(message));
|
||||
if (runModeIsProd) return;
|
||||
}
|
||||
|
||||
if (err instanceof Error) {
|
||||
// If the error stack trace does not include the base URL of our script assets, it likely came
|
||||
// from a browser extension or inline script. Do not show such errors in production.
|
||||
if (!err.stack?.includes(assetBaseUrl) && runModeIsProd) return;
|
||||
// Ignore some known errors that are unable to fix
|
||||
if (shouldIgnoreError(err)) return;
|
||||
}
|
||||
|
||||
let msg = err?.message ?? message;
|
||||
if (lineno) msg += ` (${filename} @ ${lineno}:${colno})`;
|
||||
const dot = msg.endsWith('.') ? '' : '.';
|
||||
const renderedType = type === 'unhandledrejection' ? 'promise rejection' : type;
|
||||
showGlobalErrorMessage(`JavaScript ${renderedType}: ${msg}${dot} Open browser console to see more details.`);
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import $ from 'jquery';
|
||||
import {initAriaCheckboxPatch} from './fomantic/checkbox.ts';
|
||||
import {initAriaFormFieldPatch} from './fomantic/form.ts';
|
||||
import {initAriaDropdownPatch} from './fomantic/dropdown.ts';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import $ from 'jquery';
|
||||
import {generateElemId} from '../../utils/dom.ts';
|
||||
|
||||
export function linkLabelAndInput(label: Element, input: Element) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import $ from 'jquery';
|
||||
import {queryElemChildren} from '../../utils/dom.ts';
|
||||
|
||||
export function initFomanticDimmer() {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import $ from 'jquery';
|
||||
import type {FomanticInitFunction} from '../../types.ts';
|
||||
import {generateElemId, queryElems} from '../../utils/dom.ts';
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import $ from 'jquery';
|
||||
import type {FomanticInitFunction} from '../../types.ts';
|
||||
import {queryElems} from '../../utils/dom.ts';
|
||||
import {hideToastsFrom} from '../toast.ts';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import $ from 'jquery';
|
||||
import {queryElemSiblings} from '../../utils/dom.ts';
|
||||
|
||||
export function initFomanticTab() {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import $ from 'jquery';
|
||||
|
||||
export function initFomanticTransition() {
|
||||
const transitionNopBehaviors = new Set([
|
||||
'clear queue', 'stop', 'stop all', 'destroy',
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
|
||||
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
|
||||
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker';
|
||||
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker';
|
||||
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
|
||||
|
||||
window.MonacoEnvironment = {
|
||||
getWorker(_: string, label: string) {
|
||||
if (label === 'json') return new jsonWorker();
|
||||
if (label === 'css' || label === 'scss' || label === 'less') return new cssWorker();
|
||||
if (label === 'html' || label === 'handlebars' || label === 'razor') return new htmlWorker();
|
||||
if (label === 'typescript' || label === 'javascript') return new tsWorker();
|
||||
return new editorWorker();
|
||||
},
|
||||
};
|
||||
|
||||
export * from 'monaco-editor';
|
||||
@@ -3,7 +3,7 @@ import type SortableType from 'sortablejs';
|
||||
|
||||
export async function createSortable(el: HTMLElement, opts: {handle?: string} & SortableOptions = {}): Promise<SortableType> {
|
||||
// type reassigned because typescript derives the wrong type from this import
|
||||
const {Sortable} = (await import(/* webpackChunkName: "sortablejs" */'sortablejs') as unknown as {Sortable: typeof SortableType});
|
||||
const {Sortable} = (await import('sortablejs') as unknown as {Sortable: typeof SortableType});
|
||||
|
||||
return new Sortable(el, {
|
||||
animation: 150,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
const {appSubUrl, assetVersionEncoded} = window.config;
|
||||
const {appSubUrl, sharedWorkerUri} = window.config;
|
||||
|
||||
export class UserEventsSharedWorker {
|
||||
sharedWorker: SharedWorker;
|
||||
|
||||
// options can be either a string (the debug name of the worker) or an object of type WorkerOptions
|
||||
constructor(options?: string | WorkerOptions) {
|
||||
const worker = new SharedWorker(`${window.__webpack_public_path__}js/eventsource.sharedworker.js?v=${assetVersionEncoded}`, options);
|
||||
const worker = new SharedWorker(sharedWorkerUri, options);
|
||||
this.sharedWorker = worker;
|
||||
worker.addEventListener('error', (event) => {
|
||||
console.error('worker error', event);
|
||||
|
||||
@@ -47,7 +47,7 @@ export function newRenderPlugin3DViewer(): FileRenderPlugin {
|
||||
|
||||
async render(container: HTMLElement, fileUrl: string): Promise<void> {
|
||||
// TODO: height and/or max-height?
|
||||
const OV = await import(/* webpackChunkName: "online-3d-viewer" */'online-3d-viewer');
|
||||
const OV = await import('online-3d-viewer');
|
||||
const viewer = new OV.EmbeddedViewer(container, {
|
||||
backgroundColor: new OV.RGBAColor(59, 68, 76, 0),
|
||||
defaultColor: new OV.RGBColor(65, 131, 196),
|
||||
|
||||
@@ -9,7 +9,7 @@ export function newRenderPluginPdfViewer(): FileRenderPlugin {
|
||||
},
|
||||
|
||||
async render(container: HTMLElement, fileUrl: string): Promise<void> {
|
||||
const PDFObject = await import(/* webpackChunkName: "pdfobject" */'pdfobject');
|
||||
const PDFObject = await import('pdfobject');
|
||||
// TODO: the PDFObject library does not support dynamic height adjustment,
|
||||
container.style.height = `${window.innerHeight - 100}px`;
|
||||
if (!PDFObject.default.embed(fileUrl, container)) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import '../../css/standalone/devtest.css';
|
||||
import {showInfoToast, showWarningToast, showErrorToast, type Toast} from '../modules/toast.ts';
|
||||
|
||||
type LevelMap = Record<string, (message: string) => Toast | null>;
|
||||
|
||||
@@ -11,6 +11,8 @@ RENDER_COMMAND = `echo '<div style="width: 100%; height: 2000px; border: 10px so
|
||||
|
||||
*/
|
||||
|
||||
import '../../css/standalone/external-render-iframe.css';
|
||||
|
||||
function mainExternalRenderIframe() {
|
||||
const u = new URL(window.location.href);
|
||||
const iframeId = u.searchParams.get('gitea-iframe-id');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import '../../css/standalone/swagger.css';
|
||||
import SwaggerUI from 'swagger-ui-dist/swagger-ui-es-bundle.js';
|
||||
import 'swagger-ui-dist/swagger-ui.css';
|
||||
import {load as loadYaml} from 'js-yaml';
|
||||
import {GET} from '../modules/fetch.ts';
|
||||
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const apply = () => document.documentElement.classList.toggle('dark-mode', prefersDark.matches);
|
||||
@@ -13,7 +13,7 @@ window.addEventListener('load', async () => {
|
||||
const url = elSwaggerUi.getAttribute('data-source')!;
|
||||
let spec: any;
|
||||
if (url) {
|
||||
const res = await GET(url);
|
||||
const res = await fetch(url); // eslint-disable-line no-restricted-globals
|
||||
spec = await res.json();
|
||||
} else {
|
||||
const elSpecContent = elSwaggerUi.querySelector<HTMLTextAreaElement>('.swagger-spec-content')!;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// even if backend is in testing mode, frontend could be complied in production mode
|
||||
// so this function only checks if the frontend is in unit testing mode (usually from *.test.ts files)
|
||||
export function isInFrontendUnitTest() {
|
||||
return import.meta.env.TEST === 'true';
|
||||
return import.meta.env.MODE === 'test';
|
||||
}
|
||||
|
||||
/** strip common indentation from a string and trim it */
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
window.__webpack_public_path__ = '';
|
||||
// Stub APIs not implemented by happy-dom but needed by dependencies
|
||||
// XPathEvaluator is used by htmx at module evaluation time
|
||||
// TODO: Remove after https://github.com/capricorn86/happy-dom/pull/2103 is released
|
||||
if (!globalThis.XPathEvaluator) {
|
||||
globalThis.XPathEvaluator = class {
|
||||
createExpression() { return {evaluate: () => ({iterateNext: () => null})} }
|
||||
} as any;
|
||||
}
|
||||
|
||||
// Dynamic import so polyfills above are applied before htmx evaluates
|
||||
await import('./globals.ts');
|
||||
|
||||
window.config = {
|
||||
appUrl: 'http://localhost:3000/',
|
||||
appSubUrl: '',
|
||||
assetVersionEncoded: '',
|
||||
assetUrlPrefix: '',
|
||||
sharedWorkerUri: '',
|
||||
runModeIsProd: true,
|
||||
customEmojis: {},
|
||||
pageData: {},
|
||||
@@ -13,3 +23,5 @@ window.config = {
|
||||
mermaidMaxSourceCharacters: 5000,
|
||||
i18n: {},
|
||||
};
|
||||
|
||||
export {}; // mark as module for top-level await
|
||||
|
||||
@@ -8,4 +8,4 @@ https://developer.mozilla.org/en-US/docs/Web/Web_Components
|
||||
|
||||
* These components are loaded in `<head>` (before DOM body) in a separate entry point, they need to be lightweight to not affect the page loading time too much.
|
||||
* Do not import `svg.js` into a web component because that file is currently not tree-shakeable, import svg files individually insteat.
|
||||
* All our components must be added to `webpack.config.js` so they work correctly in Vue.
|
||||
* All our components must be added to `vite.config.ts` so they work correctly in Vue.
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import {throttle} from 'throttle-debounce';
|
||||
import {createTippy} from '../modules/tippy.ts';
|
||||
import {addDelegatedEventListener, isDocumentFragmentOrElementNode} from '../utils/dom.ts';
|
||||
import {addDelegatedEventListener, generateElemId, isDocumentFragmentOrElementNode} from '../utils/dom.ts';
|
||||
import octiconKebabHorizontal from '../../../public/assets/img/svg/octicon-kebab-horizontal.svg';
|
||||
|
||||
window.customElements.define('overflow-menu', class extends HTMLElement {
|
||||
tippyContent: HTMLDivElement;
|
||||
tippyItems: Array<HTMLElement>;
|
||||
popup: HTMLDivElement;
|
||||
overflowItems: Array<HTMLElement>;
|
||||
button: HTMLButtonElement | null;
|
||||
menuItemsEl: HTMLElement;
|
||||
resizeObserver: ResizeObserver;
|
||||
@@ -13,18 +12,42 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
|
||||
lastWidth: number;
|
||||
|
||||
updateButtonActivationState() {
|
||||
if (!this.button || !this.tippyContent) return;
|
||||
this.button.classList.toggle('active', Boolean(this.tippyContent.querySelector('.item.active')));
|
||||
if (!this.button || !this.popup) return;
|
||||
this.button.classList.toggle('active', Boolean(this.popup.querySelector('.item.active')));
|
||||
}
|
||||
|
||||
showPopup() {
|
||||
if (!this.popup || this.popup.style.display !== 'none') return;
|
||||
this.popup.style.display = '';
|
||||
this.button!.setAttribute('aria-expanded', 'true');
|
||||
setTimeout(() => this.popup.focus(), 0);
|
||||
document.addEventListener('click', this.onClickOutside, true);
|
||||
}
|
||||
|
||||
hidePopup() {
|
||||
if (!this.popup || this.popup.style.display === 'none') return;
|
||||
this.popup.style.display = 'none';
|
||||
this.button?.setAttribute('aria-expanded', 'false');
|
||||
document.removeEventListener('click', this.onClickOutside, true);
|
||||
}
|
||||
|
||||
onClickOutside = (e: Event) => {
|
||||
if (!this.popup?.contains(e.target as Node) && !this.button?.contains(e.target as Node)) {
|
||||
this.hidePopup();
|
||||
}
|
||||
};
|
||||
|
||||
updateItems = throttle(100, () => {
|
||||
if (!this.tippyContent) {
|
||||
if (!this.popup) {
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('overflow-menu-popup');
|
||||
div.setAttribute('role', 'menu');
|
||||
div.tabIndex = -1; // for initial focus, programmatic focus only
|
||||
div.style.display = 'none';
|
||||
div.addEventListener('keydown', (e) => {
|
||||
if (e.isComposing) return;
|
||||
if (e.key === 'Tab') {
|
||||
const items = this.tippyContent.querySelectorAll<HTMLElement>('[role="menuitem"]');
|
||||
const items = this.popup.querySelectorAll<HTMLElement>('[role="menuitem"]');
|
||||
if (e.shiftKey) {
|
||||
if (document.activeElement === items[0]) {
|
||||
e.preventDefault();
|
||||
@@ -39,7 +62,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.button?._tippy.hide();
|
||||
this.hidePopup();
|
||||
this.button?.focus();
|
||||
} else if (e.key === ' ' || e.code === 'Enter') {
|
||||
if (document.activeElement?.matches('[role="menuitem"]')) {
|
||||
@@ -48,20 +71,20 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
|
||||
(document.activeElement as HTMLElement).click();
|
||||
}
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
if (document.activeElement?.matches('.tippy-target')) {
|
||||
if (document.activeElement === this.popup) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
document.activeElement.querySelector<HTMLElement>('[role="menuitem"]:first-of-type')?.focus();
|
||||
this.popup.querySelector<HTMLElement>('[role="menuitem"]:first-of-type')?.focus();
|
||||
} else if (document.activeElement?.matches('[role="menuitem"]')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
(document.activeElement.nextElementSibling as HTMLElement)?.focus();
|
||||
}
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
if (document.activeElement?.matches('.tippy-target')) {
|
||||
if (document.activeElement === this.popup) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
document.activeElement.querySelector<HTMLElement>('[role="menuitem"]:last-of-type')?.focus();
|
||||
this.popup.querySelector<HTMLElement>('[role="menuitem"]:last-of-type')?.focus();
|
||||
} else if (document.activeElement?.matches('[role="menuitem"]')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -69,16 +92,15 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
|
||||
}
|
||||
}
|
||||
});
|
||||
div.classList.add('tippy-target');
|
||||
this.handleItemClick(div, '.tippy-target > .item');
|
||||
this.tippyContent = div;
|
||||
} // end if: no tippyContent and create a new one
|
||||
this.handleItemClick(div, '.overflow-menu-popup > .item');
|
||||
this.popup = div;
|
||||
} // end if: no popup and create a new one
|
||||
|
||||
const itemFlexSpace = this.menuItemsEl.querySelector<HTMLSpanElement>('.item-flex-space');
|
||||
const itemOverFlowMenuButton = this.querySelector<HTMLButtonElement>('.overflow-menu-button');
|
||||
|
||||
// move items in tippy back into the menu items for subsequent measurement
|
||||
for (const item of this.tippyItems || []) {
|
||||
// move items in popup back into the menu items for subsequent measurement
|
||||
for (const item of this.overflowItems || []) {
|
||||
if (!itemFlexSpace || item.getAttribute('data-after-flex-space')) {
|
||||
this.menuItemsEl.append(item);
|
||||
} else {
|
||||
@@ -90,7 +112,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
|
||||
// flex space and overflow menu are excluded from measurement
|
||||
itemFlexSpace?.style.setProperty('display', 'none', 'important');
|
||||
itemOverFlowMenuButton?.style.setProperty('display', 'none', 'important');
|
||||
this.tippyItems = [];
|
||||
this.overflowItems = [];
|
||||
const menuRight = this.offsetLeft + this.offsetWidth;
|
||||
const menuItems = this.menuItemsEl.querySelectorAll<HTMLElement>('.item, .item-flex-space');
|
||||
let afterFlexSpace = false;
|
||||
@@ -102,64 +124,64 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
|
||||
if (afterFlexSpace) item.setAttribute('data-after-flex-space', 'true');
|
||||
const itemRight = item.offsetLeft + item.offsetWidth;
|
||||
if (menuRight - itemRight < 38) { // roughly the width of .overflow-menu-button with some extra space
|
||||
const onlyLastItem = idx === menuItems.length - 1 && this.tippyItems.length === 0;
|
||||
const onlyLastItem = idx === menuItems.length - 1 && this.overflowItems.length === 0;
|
||||
const lastItemFit = onlyLastItem && menuRight - itemRight > 0;
|
||||
const moveToPopup = !onlyLastItem || !lastItemFit;
|
||||
if (moveToPopup) this.tippyItems.push(item);
|
||||
if (moveToPopup) this.overflowItems.push(item);
|
||||
}
|
||||
}
|
||||
itemFlexSpace?.style.removeProperty('display');
|
||||
itemOverFlowMenuButton?.style.removeProperty('display');
|
||||
|
||||
// if there are no overflown items, remove any previously created button
|
||||
if (!this.tippyItems?.length) {
|
||||
const btn = this.querySelector('.overflow-menu-button');
|
||||
btn?._tippy?.destroy();
|
||||
btn?.remove();
|
||||
if (!this.overflowItems?.length) {
|
||||
this.hidePopup();
|
||||
this.button?.remove();
|
||||
this.popup?.remove();
|
||||
this.button = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// remove aria role from items that moved from tippy to menu
|
||||
// remove aria role from items that moved from popup to menu
|
||||
for (const item of menuItems) {
|
||||
if (!this.tippyItems.includes(item)) {
|
||||
if (!this.overflowItems.includes(item)) {
|
||||
item.removeAttribute('role');
|
||||
}
|
||||
}
|
||||
|
||||
// move all items that overflow into tippy
|
||||
for (const item of this.tippyItems) {
|
||||
// move all items that overflow into popup
|
||||
for (const item of this.overflowItems) {
|
||||
item.setAttribute('role', 'menuitem');
|
||||
this.tippyContent.append(item);
|
||||
this.popup.append(item);
|
||||
}
|
||||
|
||||
// update existing tippy
|
||||
if (this.button?._tippy) {
|
||||
this.button._tippy.setContent(this.tippyContent);
|
||||
// update existing popup
|
||||
if (this.button) {
|
||||
this.updateButtonActivationState();
|
||||
return;
|
||||
}
|
||||
|
||||
// create button initially
|
||||
// create button and attach popup
|
||||
const popupId = generateElemId('overflow-popup-');
|
||||
this.popup.id = popupId;
|
||||
|
||||
this.button = document.createElement('button');
|
||||
this.button.classList.add('overflow-menu-button');
|
||||
this.button.setAttribute('aria-label', window.config.i18n.more_items);
|
||||
this.button.setAttribute('aria-haspopup', 'true');
|
||||
this.button.setAttribute('aria-expanded', 'false');
|
||||
this.button.setAttribute('aria-controls', popupId);
|
||||
this.button.innerHTML = octiconKebabHorizontal;
|
||||
this.append(this.button);
|
||||
createTippy(this.button, {
|
||||
trigger: 'click',
|
||||
hideOnClick: true,
|
||||
interactive: true,
|
||||
placement: 'bottom-end',
|
||||
role: 'menu',
|
||||
theme: 'menu',
|
||||
content: this.tippyContent,
|
||||
onShow: () => { // FIXME: onShown doesn't work (never be called)
|
||||
setTimeout(() => {
|
||||
this.tippyContent.focus();
|
||||
}, 0);
|
||||
},
|
||||
this.button.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (this.popup.style.display === 'none') {
|
||||
this.showPopup();
|
||||
} else {
|
||||
this.hidePopup();
|
||||
}
|
||||
});
|
||||
this.append(this.button);
|
||||
this.append(this.popup);
|
||||
this.updateButtonActivationState();
|
||||
});
|
||||
|
||||
@@ -202,7 +224,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
|
||||
|
||||
handleItemClick(el: Element, selector: string) {
|
||||
addDelegatedEventListener(el, 'click', selector, () => {
|
||||
this.button?._tippy?.hide();
|
||||
this.hidePopup();
|
||||
this.updateButtonActivationState();
|
||||
});
|
||||
}
|
||||
@@ -239,5 +261,6 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
|
||||
disconnectedCallback() {
|
||||
this.mutationObserver?.disconnect();
|
||||
this.resizeObserver?.disconnect();
|
||||
document.removeEventListener('click', this.onClickOutside, true);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user