|
@@ -72,7 +72,7 @@
| {{if .Version}}{{.Version}}{{else}}{{ctx.Locale.Tr "unknown"}}{{end}} |
{{.BelongsToOwnerType.LocaleString ctx.Locale}} |
- {{range .AgentLabels}}{{.}}{{end}}
+ {{range .AgentLabels}}{{.}}{{end}}
|
{{if .LastOnline}}{{DateUtils.TimeSince .LastOnline}}{{else}}{{ctx.Locale.Tr "never"}}{{end}} |
diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl
index d525ce78ff..c3c042dc38 100644
--- a/templates/user/dashboard/feeds.tmpl
+++ b/templates/user/dashboard/feeds.tmpl
@@ -56,11 +56,11 @@
{{$index := index .GetIssueInfos 0}}
{{ctx.Locale.Tr "action.delete_branch" (.GetRepoLink ctx) .GetBranch (.ShortRepoPath ctx)}}
{{else if .GetOpType.InActions "mirror_sync_push"}}
- {{ctx.Locale.Tr "action.mirror_sync_push" (.GetRepoLink ctx) (.GetRefLink ctx) .GetBranch (.ShortRepoPath ctx)}}
+ {{ctx.Locale.Tr "action.mirror_sync_push" (.GetRepoLink ctx) (.GetRefLink ctx) .RefName (.ShortRepoPath ctx)}}
{{else if .GetOpType.InActions "mirror_sync_create"}}
- {{ctx.Locale.Tr "action.mirror_sync_create" (.GetRepoLink ctx) (.GetRefLink ctx) .GetBranch (.ShortRepoPath ctx)}}
+ {{ctx.Locale.Tr "action.mirror_sync_create" (.GetRepoLink ctx) (.GetRefLink ctx) .RefName (.ShortRepoPath ctx)}}
{{else if .GetOpType.InActions "mirror_sync_delete"}}
- {{ctx.Locale.Tr "action.mirror_sync_delete" (.GetRepoLink ctx) .GetBranch (.ShortRepoPath ctx)}}
+ {{ctx.Locale.Tr "action.mirror_sync_delete" (.GetRepoLink ctx) .RefName (.ShortRepoPath ctx)}}
{{else if .GetOpType.InActions "approve_pull_request"}}
{{$index := index .GetIssueInfos 0}}
{{ctx.Locale.Tr "action.approve_pull_request" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}}
diff --git a/tests/integration/editor_test.go b/tests/integration/editor_test.go
index d4681f1699..c70bb061c9 100644
--- a/tests/integration/editor_test.go
+++ b/tests/integration/editor_test.go
@@ -148,10 +148,10 @@ func testEditFileToNewBranch(t *testing.T, session *TestSession, user, repo, bra
func testEditorDiffPreview(t *testing.T) {
session := loginUser(t, "user2")
req := NewRequestWithValues(t, "POST", "/user2/repo1/_preview/master/README.md", map[string]string{
- "content": "Hello, World (Edited)\n",
+ "content": "# repo1 (Edited)",
})
resp := session.MakeRequest(t, req, http.StatusOK)
- assert.Contains(t, resp.Body.String(), `Hello, World (Edited)`)
+ assert.Contains(t, resp.Body.String(), ` (Edited)`)
}
func testEditorPatchFile(t *testing.T) {
diff --git a/tests/integration/pull_commit_test.go b/tests/integration/pull_commit_test.go
index 9f3b1a9ef5..01b8ec1ff4 100644
--- a/tests/integration/pull_commit_test.go
+++ b/tests/integration/pull_commit_test.go
@@ -36,6 +36,6 @@ func TestListPullCommits(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req = NewRequest(t, "GET", "/user2/repo1/blob_excerpt/985f0301dba5e7b34be866819cd15ad3d8f508ee?last_left=0&last_right=0&left=2&right=2&left_hunk_size=2&right_hunk_size=2&path=README.md&style=split&direction=up")
resp = session.MakeRequest(t, req, http.StatusOK)
- assert.Contains(t, resp.Body.String(), ` | # repo1`)
+ assert.Contains(t, resp.Body.String(), ` | # repo1`+"\n"+``)
})
}
diff --git a/updates.config.ts b/updates.config.ts
index 7bf680bbde..bc9e368fb4 100644
--- a/updates.config.ts
+++ b/updates.config.ts
@@ -4,6 +4,7 @@ export default {
exclude: [
'@mcaptcha/vanilla-glue', // breaking changes in rc versions need to be handled
'cropperjs', // need to migrate to v2 but v2 is not compatible with v1
+ 'eslint', // need to migrate to v10
'tailwindcss', // need to migrate
'@eslint/json', // needs eslint 10
],
diff --git a/uv.lock b/uv.lock
index 33e9b64b2c..10fdbf2bf3 100644
--- a/uv.lock
+++ b/uv.lock
@@ -127,11 +127,11 @@ wheels = [
[[package]]
name = "pathspec"
-version = "1.0.3"
+version = "1.0.4"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" },
]
[[package]]
@@ -384,14 +384,14 @@ wheels = [
[[package]]
name = "tqdm"
-version = "4.67.1"
+version = "4.67.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
+ { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
]
[[package]]
diff --git a/web_src/css/base.css b/web_src/css/base.css
index 4dc19d9a5b..c0caee5191 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -30,6 +30,7 @@
--page-spacing: 16px; /* space between page elements */
--page-margin-x: 32px; /* minimum space on left and right side of page */
--page-space-bottom: 64px; /* space between last page element and footer */
+ --transition-hover-fade: opacity 0.2s ease; /* fade transition for elements that show on hover */
/* z-index */
--z-index-modal: 1001; /* modal dialog, hard-coded from Fomantic modal.css */
diff --git a/web_src/css/markup/codecopy.css b/web_src/css/markup/codecopy.css
index 5a7b9955e7..c48f641f68 100644
--- a/web_src/css/markup/codecopy.css
+++ b/web_src/css/markup/codecopy.css
@@ -3,8 +3,9 @@
top: 8px;
right: 6px;
padding: 9px;
- visibility: hidden;
- animation: fadeout 0.2s both;
+ visibility: hidden; /* prevent from click events even opacity=0 */
+ opacity: 0;
+ transition: var(--transition-hover-fade);
}
/* adjustments for comment content having only 14px font size */
@@ -23,8 +24,17 @@
background: var(--color-secondary-dark-1) !important;
}
+/* all rendered code-block elements are in their container,
+the manually written code-block elements on "packages" pages don't have the container */
.markup .code-block-container:hover .code-copy,
.markup .code-block:hover .code-copy {
visibility: visible;
- animation: fadein 0.2s both;
+ opacity: 1;
+}
+
+@media (hover: none) {
+ .markup .code-copy {
+ visibility: visible;
+ opacity: 1;
+ }
}
diff --git a/web_src/css/modules/animations.css b/web_src/css/modules/animations.css
index aedf53569a..65d7a90e97 100644
--- a/web_src/css/modules/animations.css
+++ b/web_src/css/modules/animations.css
@@ -82,15 +82,6 @@ code.language-math.is-loading::after {
}
}
-@keyframes fadeout {
- 0% {
- opacity: 1;
- }
- 100% {
- opacity: 0;
- }
-}
-
/* 1p5 means 1-point-5. it can't use "pulse" here, otherwise the animation is not right (maybe due to some conflicts */
@keyframes pulse-1p5 {
0% {
diff --git a/web_src/css/modules/table.css b/web_src/css/modules/table.css
index 6298471d47..c7d6cb7a48 100644
--- a/web_src/css/modules/table.css
+++ b/web_src/css/modules/table.css
@@ -196,11 +196,6 @@
margin-bottom: 0;
}
-.ui.striped.table > tr:nth-child(2n),
-.ui.striped.table > tbody > tr:nth-child(2n) {
- background: var(--color-light);
-}
-
.ui.table[class*="single line"],
.ui.table [class*="single line"] {
white-space: nowrap;
@@ -291,37 +286,10 @@
.ui.basic.table > tr > td {
background: transparent;
}
-.ui.basic.striped.table > tbody > tr:nth-child(2n) {
- background: var(--color-light);
-}
-.ui.basic.striped.selectable.table > tbody > tr:nth-child(2n):hover {
- background: var(--color-hover);
-}
.ui[class*="very basic"].table {
border: none;
}
-.ui[class*="very basic"].table:not(.striped) > tr > th:first-child,
-.ui[class*="very basic"].table:not(.striped) > thead > tr > th:first-child,
-.ui[class*="very basic"].table:not(.striped) > tbody > tr > th:first-child,
-.ui[class*="very basic"].table:not(.striped) > tfoot > tr > th:first-child,
-.ui[class*="very basic"].table:not(.striped) > tr > td:first-child,
-.ui[class*="very basic"].table:not(.striped) > tbody > tr > td:first-child,
-.ui[class*="very basic"].table:not(.striped) > tfoot > tr > td:first-child {
- padding-left: 0;
-}
-.ui[class*="very basic"].table:not(.striped) > tr > th:last-child,
-.ui[class*="very basic"].table:not(.striped) > thead > tr > th:last-child,
-.ui[class*="very basic"].table:not(.striped) > tbody > tr > th:last-child,
-.ui[class*="very basic"].table:not(.striped) > tfoot > tr > th:last-child,
-.ui[class*="very basic"].table:not(.striped) > tr > td:last-child,
-.ui[class*="very basic"].table:not(.striped) > tbody > tr > td:last-child,
-.ui[class*="very basic"].table:not(.striped) > tfoot > tr > td:last-child {
- padding-right: 0;
-}
-.ui[class*="very basic"].table:not(.striped) > thead > tr:first-child > th {
- padding-top: 0;
-}
.ui.celled.table > tr > th,
.ui.celled.table > thead > tr > th,
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 16fbfaec4a..83df3e5c29 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -774,10 +774,6 @@ td .commit-summary {
width: 200px;
}
-.repository #commits-table.ui.basic.striped.table tbody tr:nth-child(2n) {
- background-color: var(--color-light) !important;
-}
-
.repository .data-table {
width: 100%;
}
diff --git a/web_src/css/repo/commit-sign.css b/web_src/css/repo/commit-sign.css
index 56eee62ffc..101eb6d528 100644
--- a/web_src/css/repo/commit-sign.css
+++ b/web_src/css/repo/commit-sign.css
@@ -7,6 +7,10 @@
flex-shrink: 0;
}
+.ui.label.commit-sign-badge > * {
+ display: flex;
+}
+
.ui.label.commit-id-short {
font-family: var(--fonts-monospace);
height: 24px;
diff --git a/web_src/js/features/repo-diff.ts b/web_src/js/features/repo-diff.ts
index e63de7b3cf..0c8b1357b0 100644
--- a/web_src/js/features/repo-diff.ts
+++ b/web_src/js/features/repo-diff.ts
@@ -170,7 +170,9 @@ async function loadMoreFiles(btn: Element): Promise {
const respFileBoxes = respDoc.querySelector('#diff-file-boxes')!;
// the response is a full HTML page, we need to extract the relevant contents:
// * append the newly loaded file list items to the existing list
- document.querySelector('#diff-incomplete')!.replaceWith(...Array.from(respFileBoxes.children));
+ const respFileBoxesChildren = Array.from(respFileBoxes.children); // "children:HTMLCollection" will be empty after replaceWith
+ document.querySelector('#diff-incomplete')!.replaceWith(...respFileBoxesChildren);
+ for (const el of respFileBoxesChildren) window.htmx.process(el);
onShowMoreFiles();
return true;
} catch (error) {
@@ -200,7 +202,7 @@ function initRepoDiffShowMore() {
const resp = await response.text();
const respDoc = parseDom(resp, 'text/html');
const respFileBody = respDoc.querySelector('#diff-file-boxes .diff-file-body .file-body')!;
- const respFileBodyChildren = Array.from(respFileBody.children); // respFileBody.children will be empty after replaceWith
+ const respFileBodyChildren = Array.from(respFileBody.children); // "children:HTMLCollection" will be empty after replaceWith
el.parentElement!.replaceWith(...respFileBodyChildren);
for (const el of respFileBodyChildren) window.htmx.process(el);
// FIXME: calling onShowMoreFiles is not quite right here.
diff --git a/web_src/js/features/tribute.ts b/web_src/js/features/tribute.ts
index 0239204f9e..8434a9b2ac 100644
--- a/web_src/js/features/tribute.ts
+++ b/web_src/js/features/tribute.ts
@@ -1,6 +1,7 @@
import {emojiKeys, emojiHTML, emojiString} from './emoji.ts';
import {html, htmlRaw} from '../utils/html.ts';
import type {TributeCollection} from 'tributejs';
+import type {MentionValue} from '../types.ts';
export async function attachTribute(element: HTMLElement) {
const {default: Tribute} = await import(/* webpackChunkName: "tribute" */'tributejs');
@@ -28,7 +29,7 @@ export async function attachTribute(element: HTMLElement) {
},
};
- const mentionCollection: TributeCollection> = {
+ const mentionCollection: TributeCollection = {
values: window.config.mentionValues,
requireLeadingSpace: true,
menuItemTemplate: (item) => {
@@ -44,7 +45,10 @@ export async function attachTribute(element: HTMLElement) {
};
const tribute = new Tribute({
- collection: [emojiCollection as TributeCollection, mentionCollection],
+ collection: [
+ emojiCollection as TributeCollection,
+ mentionCollection as TributeCollection,
+ ],
noMatchTemplate: () => '',
});
tribute.attach(element);
diff --git a/web_src/js/globals.d.ts b/web_src/js/globals.d.ts
index ee90bc3c9c..8de6581dbc 100644
--- a/web_src/js/globals.d.ts
+++ b/web_src/js/globals.d.ts
@@ -29,13 +29,7 @@ interface Window {
pageData: Record,
notificationSettings: Record,
enableTimeTracking: boolean,
- mentionValues: Array<{
- key: string,
- value: string,
- name: string,
- fullname: string,
- avatar: string,
- }>,
+ mentionValues: Array,
mermaidMaxSourceCharacters: number,
i18n: Record,
},
diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts
index 5d37c81b8f..cd62990ffd 100644
--- a/web_src/js/markup/mermaid.ts
+++ b/web_src/js/markup/mermaid.ts
@@ -1,16 +1,56 @@
import {isDarkTheme, parseDom} from '../utils.ts';
import {makeCodeCopyButton} from './codecopy.ts';
import {displayError} from './common.ts';
-import {createElementFromAttrs, queryElems} from '../utils/dom.ts';
-import {html, htmlRaw} from '../utils/html.ts';
+import {createElementFromAttrs, createElementFromHTML, getCssRootVariablesText, queryElems} from '../utils/dom.ts';
+import {html} from '../utils/html.ts';
import {load as loadYaml} from 'js-yaml';
import type {MermaidConfig} from 'mermaid';
const {mermaidMaxSourceCharacters} = window.config;
-const iframeCss = `:root {color-scheme: normal}
-body {margin: 0; padding: 0; overflow: hidden}
-#mermaid {display: block; margin: 0 auto}`;
+function getIframeCss(): string {
+ // Inherit some styles (e.g.: root variables) from parent document.
+ // The buttons should use the same styles as `button.code-copy`, and align with it.
+ return `
+${getCssRootVariablesText()}
+
+html, body { height: 100%; }
+body { margin: 0; padding: 0; overflow: hidden; }
+#mermaid { display: block; margin: 0 auto; }
+
+.view-controller {
+ position: absolute;
+ z-index: 1;
+ right: 5px;
+ bottom: 5px;
+ display: flex;
+ gap: 4px;
+ visibility: hidden;
+ opacity: 0;
+ transition: var(--transition-hover-fade);
+ margin-right: 0.25em;
+}
+body:hover .view-controller { visibility: visible; opacity: 1; }
+@media (hover: none) {
+ .view-controller { visibility: visible; opacity: 1; }
+}
+.view-controller button {
+ cursor: pointer;
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ line-height: 1;
+ padding: 7.5px 10px;
+ border: 1px solid var(--color-light-border);
+ border-radius: var(--border-radius);
+ background: var(--color-button);
+ color: var(--color-text);
+ user-select: none;
+}
+.view-controller button:hover { background: var(--color-secondary); }
+.view-controller button:active { background: var(--color-secondary-dark-1); }
+`;
+}
function isSourceTooLarge(source: string) {
return mermaidMaxSourceCharacters >= 0 && source.length > mermaidMaxSourceCharacters;
@@ -77,6 +117,76 @@ async function loadMermaid(needElkRender: boolean) {
};
}
+function initMermaidViewController(dragElement: SVGSVGElement) {
+ let inited = false, isDragging = false;
+ let currentScale = 1, initLeft = 0, lastLeft = 0, lastTop = 0, lastPageX = 0, lastPageY = 0;
+ const container = dragElement.parentElement!;
+
+ const resetView = () => {
+ currentScale = 1;
+ lastLeft = initLeft;
+ lastTop = 0;
+ dragElement.style.left = `${lastLeft}px`;
+ dragElement.style.top = `${lastTop}px`;
+ dragElement.style.position = 'absolute';
+ dragElement.style.margin = '0';
+ };
+
+ const initAbsolutePosition = () => {
+ if (inited) return;
+ // if we need to drag or zoom, use absolute position and get the current "left" from the "margin: auto" layout.
+ inited = true;
+ initLeft = container.getBoundingClientRect().width / 2 - dragElement.getBoundingClientRect().width / 2;
+ resetView();
+ };
+
+ for (const el of queryElems(container, '[data-control-action]')) {
+ el.addEventListener('click', () => {
+ initAbsolutePosition();
+ switch (el.getAttribute('data-control-action')) {
+ case 'zoom-in':
+ currentScale *= 1.2;
+ break;
+ case 'zoom-out':
+ currentScale /= 1.2;
+ break;
+ case 'reset':
+ resetView();
+ break;
+ }
+ dragElement.style.transform = `scale(${currentScale})`;
+ });
+ }
+
+ dragElement.addEventListener('mousedown', (e) => {
+ if (e.button !== 0 || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return; // only left mouse button can drag
+ const target = e.target as Element;
+ if (target.closest('div, p, a, span, button, input')) return; // don't start the drag if the click is on an interactive element (e.g.: link, button) or text element
+
+ initAbsolutePosition();
+ isDragging = true;
+ lastPageX = e.pageX;
+ lastPageY = e.pageY;
+ dragElement.style.cursor = 'grabbing';
+ });
+
+ dragElement.ownerDocument.addEventListener('mousemove', (e) => {
+ if (!isDragging) return;
+ lastLeft = e.pageX - lastPageX + lastLeft;
+ lastTop = e.pageY - lastPageY + lastTop;
+ dragElement.style.left = `${lastLeft}px`;
+ dragElement.style.top = `${lastTop}px`;
+ lastPageX = e.pageX;
+ lastPageY = e.pageY;
+ });
+
+ dragElement.ownerDocument.addEventListener('mouseup', () => {
+ if (!isDragging) return;
+ isDragging = false;
+ dragElement.style.removeProperty('cursor');
+ });
+}
+
let elkLayoutsRegistered = false;
export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise {
@@ -107,6 +217,13 @@ export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise {
+ if (!height) return;
+ // use a min-height to make sure the buttons won't overlap.
+ iframe.style.height = `${Math.max(height, 85)}px`;
+ };
+
// mermaid is a globally shared instance, its document also says "Multiple calls to this function will be enqueued to run serially."
// so here we just simply render the mermaid blocks one by one, no need to do "Promise.all" concurrently
for (const block of mermaidBlocks) {
@@ -122,27 +239,37 @@ export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise`;
+
// create an iframe to sandbox the svg with styles, and set correct height by reading svg's viewBox height
const iframe = document.createElement('iframe');
iframe.classList.add('markup-content-iframe', 'is-loading');
- iframe.srcdoc = html``;
+ // the styles are not ready, so don't really render anything before the "load" event, to avoid flicker of unstyled content
+ iframe.srcdoc = html``;
// although the "viewBox" is optional, mermaid's output should always have a correct viewBox with width and height
const iframeHeightFromViewBox = Math.ceil(svgNode.viewBox?.baseVal?.height ?? 0);
- if (iframeHeightFromViewBox) iframe.style.height = `${iframeHeightFromViewBox}px`;
+ applyMermaidIframeHeight(iframe, iframeHeightFromViewBox);
// the iframe will be fully reloaded if its DOM context is changed (e.g.: moved in the DOM tree).
// to avoid unnecessary reloading, we should insert the iframe to its final position only once.
iframe.addEventListener('load', () => {
- // same origin, so we can operate "iframe body" and all elements directly
+ // same origin, so we can operate "iframe head/body" and all elements directly
+ const style = document.createElement('style');
+ style.textContent = iframeStyleText;
+ iframe.contentDocument!.head.append(style);
+
const iframeBody = iframe.contentDocument!.body;
iframeBody.append(svgNode);
bindFunctions?.(iframeBody); // follow "mermaid.render" doc, attach event handlers to the svg's container
+ iframeBody.append(createElementFromHTML(viewControllerHtml));
// according to mermaid, the viewBox height should always exist, here just a fallback for unknown cases.
// and keep in mind: clientHeight can be 0 if the element is hidden (display: none).
- if (!iframeHeightFromViewBox && iframeBody.clientHeight) iframe.style.height = `${iframeBody.clientHeight}px`;
+ if (!iframeHeightFromViewBox) applyMermaidIframeHeight(iframe, iframeBody.clientHeight);
iframe.classList.remove('is-loading');
+
+ initMermaidViewController(svgNode);
});
const container = createElementFromAttrs('div', {class: 'mermaid-block'}, iframe, makeCodeCopyButton({'data-clipboard-text': source}));
diff --git a/web_src/js/render/ansi.ts b/web_src/js/render/ansi.ts
index 685e916c9a..f5429ef6ad 100644
--- a/web_src/js/render/ansi.ts
+++ b/web_src/js/render/ansi.ts
@@ -31,7 +31,7 @@ export function renderAnsi(line: string): string {
// handle "\rReading...1%\rReading...5%\rReading...100%",
// convert it into a multiple-line string: "Reading...1%\nReading...5%\nReading...100%"
- const lines = [];
+ const lines: Array = [];
for (const part of line.split('\r')) {
if (part === '') continue;
const partHtml = ansi_up.ansi_to_html(part);
diff --git a/web_src/js/types.ts b/web_src/js/types.ts
index 56527729a1..815dfd2f82 100644
--- a/web_src/js/types.ts
+++ b/web_src/js/types.ts
@@ -2,6 +2,14 @@ export type IntervalId = ReturnType;
export type Intent = 'error' | 'warning' | 'info';
+export type MentionValue = {
+ key: string,
+ value: string,
+ name: string,
+ fullname: string,
+ avatar: string,
+};
+
export type RequestData = string | FormData | URLSearchParams | Record;
export type RequestOpts = {
diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts
index dc504b7056..12b984a9d5 100644
--- a/web_src/js/utils/dom.ts
+++ b/web_src/js/utils/dom.ts
@@ -352,6 +352,22 @@ export function isPlainClick(e: MouseEvent) {
return e.button === 0 && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey;
}
+let cssRootVariablesTextCache: string = '';
+export function getCssRootVariablesText(): string {
+ if (cssRootVariablesTextCache) return cssRootVariablesTextCache;
+ const style = getComputedStyle(document.documentElement);
+ let text = ':root {\n';
+ for (let i = 0; i < style.length; i++) {
+ const name = style.item(i);
+ if (name.startsWith('--')) {
+ text += ` ${name}: ${style.getPropertyValue(name)};\n`;
+ }
+ }
+ text += '}\n';
+ cssRootVariablesTextCache = text;
+ return text;
+}
+
let elemIdCounter = 0;
export function generateElemId(prefix: string = ''): string {
return `${prefix}${elemIdCounter++}`;
diff --git a/web_src/js/utils/glob.test.ts b/web_src/js/utils/glob.test.ts
index 0c5d9783c0..fbf83a6cfe 100644
--- a/web_src/js/utils/glob.test.ts
+++ b/web_src/js/utils/glob.test.ts
@@ -117,7 +117,6 @@ test('GlobCompiler', async () => {
for (const c of golangCases) {
const compiled = globCompile(c.pattern, c.separators);
const msg = `pattern: ${c.pattern}, input: ${c.input}, separators: ${c.separators || '(none)'}, compiled: ${compiled.regexpPattern}`;
- // eslint-disable-next-line vitest/valid-expect -- Unlike Jest, Vitest supports a message as the second argument
expect(compiled.regexp.test(c.input), msg).toBe(c.matched);
}
|