diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl index f6648b59d8c53..daef7afd289d7 100644 --- a/templates/base/head_script.tmpl +++ b/templates/base/head_script.tmpl @@ -36,7 +36,6 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. copy_success: {{ctx.Locale.Tr "copy_success"}}, copy_error: {{ctx.Locale.Tr "copy_error"}}, error_occurred: {{ctx.Locale.Tr "error.occurred"}}, - network_error: {{ctx.Locale.Tr "error.network_error"}}, remove_label_str: {{ctx.Locale.Tr "remove_label_str"}}, modal_confirm: {{ctx.Locale.Tr "modal.confirm"}}, modal_cancel: {{ctx.Locale.Tr "modal.cancel"}}, diff --git a/web_src/js/components/ContextPopup.vue b/web_src/js/components/ContextPopup.vue index 5ec4499e480d6..aebfaa5d26fc0 100644 --- a/web_src/js/components/ContextPopup.vue +++ b/web_src/js/components/ContextPopup.vue @@ -2,62 +2,53 @@ import {SvgIcon} from '../svg.ts'; import {GET} from '../modules/fetch.ts'; import {getIssueColor, getIssueIcon} from '../features/issue.ts'; -import {computed, onMounted, shallowRef, useTemplateRef} from 'vue'; -import type {IssuePathInfo} from '../types.ts'; +import {computed, onMounted, shallowRef} from 'vue'; -const {appSubUrl, i18n} = window.config; +const props = defineProps<{ + repoLink: string, + loadIssueInfoUrl: string, +}>(); const loading = shallowRef(false); const issue = shallowRef(null); const renderedLabels = shallowRef(''); -const i18nErrorOccurred = i18n.error_occurred; -const i18nErrorMessage = shallowRef(null); +const errorMessage = shallowRef(null); -const createdAt = computed(() => new Date(issue.value.created_at).toLocaleDateString(undefined, {year: 'numeric', month: 'short', day: 'numeric'})); -const body = computed(() => { - const body = issue.value.body.replace(/\n+/g, ' '); - if (body.length > 85) { - return `${body.substring(0, 85)}…`; - } - return body; +const createdAt = computed(() => { + return new Date(issue.value.created_at).toLocaleDateString(undefined, {year: 'numeric', month: 'short', day: 'numeric'}); }); -const root = useTemplateRef('root'); - -onMounted(() => { - root.value.addEventListener('ce-load-context-popup', (e: CustomEventInit) => { - if (!loading.value && issue.value === null) { - load(e.detail); - } - }); +const body = computed(() => { + const body = issue.value.body.replace(/\n+/g, ' '); + return body.length > 85 ? `${body.substring(0, 85)}…` : body; }); -async function load(issuePathInfo: IssuePathInfo) { +onMounted(async () => { loading.value = true; - i18nErrorMessage.value = null; - + errorMessage.value = null; try { - const response = await GET(`${appSubUrl}/${issuePathInfo.ownerName}/${issuePathInfo.repoName}/issues/${issuePathInfo.indexString}/info`); // backend: GetIssueInfo - const respJson = await response.json(); - if (!response.ok) { - i18nErrorMessage.value = respJson.message ?? i18n.network_error; + const resp = await GET(props.loadIssueInfoUrl); + if (!resp.ok) { + errorMessage.value = resp.status ? resp.statusText : 'Unknown network error'; return; } + const respJson = await resp.json(); issue.value = respJson.convertedIssue; renderedLabels.value = respJson.renderedLabels; - } catch { - i18nErrorMessage.value = i18n.network_error; } finally { loading.value = false; } -} +}); diff --git a/web_src/js/features/contextpopup.ts b/web_src/js/features/contextpopup.ts deleted file mode 100644 index 7477331dbeff5..0000000000000 --- a/web_src/js/features/contextpopup.ts +++ /dev/null @@ -1,43 +0,0 @@ -import {createApp} from 'vue'; -import ContextPopup from '../components/ContextPopup.vue'; -import {parseIssueHref} from '../utils.ts'; -import {createTippy} from '../modules/tippy.ts'; - -export function initContextPopups() { - const refIssues = document.querySelectorAll('.ref-issue'); - attachRefIssueContextPopup(refIssues); -} - -export function attachRefIssueContextPopup(refIssues: NodeListOf) { - for (const refIssue of refIssues) { - if (refIssue.classList.contains('ref-external-issue')) continue; - - const issuePathInfo = parseIssueHref(refIssue.getAttribute('href')); - if (!issuePathInfo.ownerName) continue; - - const el = document.createElement('div'); - el.classList.add('tw-p-3'); - refIssue.parentNode.insertBefore(el, refIssue.nextSibling); - - const view = createApp(ContextPopup); - - try { - view.mount(el); - } catch (err) { - console.error(err); - el.textContent = 'ContextPopup failed to load'; - } - - createTippy(refIssue, { - theme: 'default', - content: el, - placement: 'top-start', - interactive: true, - role: 'dialog', - interactiveBorder: 5, - onShow: () => { - el.firstChild.dispatchEvent(new CustomEvent('ce-load-context-popup', {detail: issuePathInfo})); - }, - }); - } -} diff --git a/web_src/js/features/repo-diff.ts b/web_src/js/features/repo-diff.ts index 24d937a252818..20cec2939d513 100644 --- a/web_src/js/features/repo-diff.ts +++ b/web_src/js/features/repo-diff.ts @@ -12,8 +12,6 @@ import {invertFileFolding} from './file-fold.ts'; import {parseDom, sleep} from '../utils.ts'; import {registerGlobalSelectorFunc} from '../modules/observer.ts'; -const {i18n} = window.config; - function initRepoDiffFileBox(el: HTMLElement) { // switch between "rendered" and "source", for image and CSV files queryElems(el, '.file-view-toggle', (btn) => btn.addEventListener('click', () => { @@ -86,7 +84,7 @@ function initRepoDiffConversationForm() { } } catch (error) { console.error('Error:', error); - showErrorToast(i18n.network_error); + showErrorToast(`Submit form failed: ${error}`); } finally { form?.classList.remove('is-loading'); } diff --git a/web_src/js/features/repo-editor.ts b/web_src/js/features/repo-editor.ts index f3ca13460cd5b..0825999edcd2a 100644 --- a/web_src/js/features/repo-editor.ts +++ b/web_src/js/features/repo-editor.ts @@ -1,7 +1,6 @@ import {html, htmlRaw} from '../utils/html.ts'; import {createCodeEditor} from './codeeditor.ts'; import {hideElem, queryElems, showElem, createElementFromHTML} from '../utils/dom.ts'; -import {attachRefIssueContextPopup} from './contextpopup.ts'; import {POST} from '../modules/fetch.ts'; import {initDropzone} from './dropzone.ts'; import {confirmModal} from './comp/ConfirmModal.ts'; @@ -199,5 +198,4 @@ export function initRepoEditor() { export function renderPreviewPanelContent(previewPanel: Element, htmlContent: string) { // the content is from the server, so it is safe to use innerHTML previewPanel.innerHTML = html`
${htmlRaw(htmlContent)}
`; - attachRefIssueContextPopup(previewPanel.querySelectorAll('p .ref-issue')); } diff --git a/web_src/js/features/repo-issue-edit.ts b/web_src/js/features/repo-issue-edit.ts index f883ee460b06c..43aee314e04f4 100644 --- a/web_src/js/features/repo-issue-edit.ts +++ b/web_src/js/features/repo-issue-edit.ts @@ -3,7 +3,6 @@ import {getComboMarkdownEditor, initComboMarkdownEditor, ComboMarkdownEditor} fr import {POST} from '../modules/fetch.ts'; import {showErrorToast} from '../modules/toast.ts'; import {hideElem, querySingleVisibleElem, showElem, type DOMEvent} from '../utils/dom.ts'; -import {attachRefIssueContextPopup} from './contextpopup.ts'; import {triggerUploadStateChanged} from './comp/EditorUpload.ts'; import {convertHtmlToMarkdown} from '../markup/html2markdown.ts'; import {applyAreYouSure, reinitializeAreYouSure} from '../vendor/jquery.are-you-sure.ts'; @@ -62,8 +61,6 @@ async function tryOnEditContent(e: DOMEvent) { renderContent = newRenderContent; rawContent.textContent = comboMarkdownEditor.value(); - const refIssues = renderContent.querySelectorAll('p .ref-issue'); - attachRefIssueContextPopup(refIssues); if (!commentContent.querySelector('.dropzone-attachments')) { if (data.attachments !== '') { diff --git a/web_src/js/index-domready.ts b/web_src/js/index-domready.ts index cfb6b89ea7296..8a3a27fa19c86 100644 --- a/web_src/js/index-domready.ts +++ b/web_src/js/index-domready.ts @@ -5,7 +5,6 @@ import '../../node_modules/easymde/dist/easymde.min.css'; // TODO: lazy load in import {initHtmx} from './htmx.ts'; import {initDashboardRepoList} from './features/dashboard.ts'; import {initGlobalCopyToClipboardListener} from './features/clipboard.ts'; -import {initContextPopups} from './features/contextpopup.ts'; import {initRepoGraphGit} from './features/repo-graph.ts'; import {initHeatmap} from './features/heatmap.ts'; import {initImageDiff} from './features/imagediff.ts'; @@ -97,7 +96,6 @@ const initPerformanceTracer = callInitFunctions([ initHeadNavbarContentToggle, initFootLanguageMenu, - initContextPopups, initHeatmap, initImageDiff, initMarkupAnchors, diff --git a/web_src/js/markup/content.ts b/web_src/js/markup/content.ts index cf88ed61de15b..d964c88989e02 100644 --- a/web_src/js/markup/content.ts +++ b/web_src/js/markup/content.ts @@ -5,6 +5,7 @@ import {initMarkupRenderAsciicast} from './asciicast.ts'; import {initMarkupTasklist} from './tasklist.ts'; import {registerGlobalSelectorFunc} from '../modules/observer.ts'; import {initMarkupRenderIframe} from './render-iframe.ts'; +import {initMarkupRefIssue} from './refissue.ts'; // code that runs for all markup content export function initMarkupContent(): void { @@ -15,5 +16,6 @@ export function initMarkupContent(): void { initMarkupCodeMath(el); initMarkupRenderAsciicast(el); initMarkupRenderIframe(el); + initMarkupRefIssue(el); }); } diff --git a/web_src/js/markup/refissue.ts b/web_src/js/markup/refissue.ts new file mode 100644 index 0000000000000..5a05de84fe912 --- /dev/null +++ b/web_src/js/markup/refissue.ts @@ -0,0 +1,41 @@ +import {queryElems} from '../utils/dom.ts'; +import {parseIssueHref} from '../utils.ts'; +import {createApp} from 'vue'; +import ContextPopup from '../components/ContextPopup.vue'; +import {createTippy, getAttachedTippyInstance} from '../modules/tippy.ts'; + +export function initMarkupRefIssue(el: HTMLElement) { + queryElems(el, '.ref-issue', (el) => { + el.addEventListener('mouseenter', showMarkupRefIssuePopup); + el.addEventListener('focus', showMarkupRefIssuePopup); + }); +} + +export function showMarkupRefIssuePopup(e: MouseEvent | FocusEvent) { + const refIssue = e.currentTarget as HTMLElement; + if (getAttachedTippyInstance(refIssue)) return; + if (refIssue.classList.contains('ref-external-issue')) return; + + const issuePathInfo = parseIssueHref(refIssue.getAttribute('href')); + if (!issuePathInfo.ownerName) return; + + const el = document.createElement('div'); + const tippy = createTippy(refIssue, { + theme: 'default', + content: el, + trigger: 'mouseenter focus', + placement: 'top-start', + interactive: true, + role: 'dialog', + interactiveBorder: 5, + // onHide() { return false }, // help to keep the popup and debug the layout + onShow: () => { + const view = createApp(ContextPopup, { + // backend: GetIssueInfo + loadIssueInfoUrl: `${window.config.appSubUrl}/${issuePathInfo.ownerName}/${issuePathInfo.repoName}/issues/${issuePathInfo.indexString}/info`, + }); + view.mount(el); + }, + }); + tippy.show(); +} diff --git a/web_src/js/modules/tippy.ts b/web_src/js/modules/tippy.ts index 2a1d998d76348..6f42a4f987a3c 100644 --- a/web_src/js/modules/tippy.ts +++ b/web_src/js/modules/tippy.ts @@ -209,3 +209,7 @@ export function showTemporaryTooltip(target: Element, content: Content): void { }, }); } + +export function getAttachedTippyInstance(el: Element): Instance | null { + return el._tippy ?? null; +}