diff options
29 files changed, 203 insertions, 235 deletions
diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl index 826642db42..cffdfabfaa 100644 --- a/templates/org/home.tmpl +++ b/templates/org/home.tmpl @@ -6,7 +6,7 @@ <div class="ui mobile reversed stackable grid"> <div class="ui {{if .ShowMemberAndTeamTab}}eleven wide{{end}} column"> {{if .ProfileReadmeContent}} - <div id="readme_profile" class="markup" data-profile-view-as-member="{{.IsViewingOrgAsMember}}">{{.ProfileReadmeContent}}</div> + <div id="readme_profile" class="render-content markup" data-profile-view-as-member="{{.IsViewingOrgAsMember}}">{{.ProfileReadmeContent}}</div> {{end}} {{template "shared/repo_search" .}} {{template "explore/repo_list" .}} diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl index 5d40653dc6..48083811e7 100644 --- a/templates/projects/list.tmpl +++ b/templates/projects/list.tmpl @@ -74,9 +74,7 @@ {{end}} </div> {{if .Description}} - <div class="content"> - {{.RenderedContent}} - </div> + <div class="render-content markup">{{.RenderedContent}}</div> {{end}} </li> {{end}} diff --git a/templates/repo/editor/edit.tmpl b/templates/repo/editor/edit.tmpl index 577a2be9ad..ae8a60c20c 100644 --- a/templates/repo/editor/edit.tmpl +++ b/templates/repo/editor/edit.tmpl @@ -45,10 +45,10 @@ data-line-wrap-extensions="{{.LineWrapExtensions}}">{{.FileContent}}</textarea> <div class="editor-loading is-loading"></div> </div> - <div class="ui tab markup tw-px-4 tw-py-3" data-tab="preview"> + <div class="ui tab tw-px-4 tw-py-3" data-tab="preview"> {{ctx.Locale.Tr "loading"}} </div> - <div class="ui tab diff edit-diff" data-tab="diff"> + <div class="ui tab" data-tab="diff"> <div class="tw-p-16"></div> </div> </div> diff --git a/templates/repo/issue/fields/markdown.tmpl b/templates/repo/issue/fields/markdown.tmpl index da8f5e6bdf..dbf4b71ba8 100644 --- a/templates/repo/issue/fields/markdown.tmpl +++ b/templates/repo/issue/fields/markdown.tmpl @@ -1,3 +1,3 @@ <div class="field {{if not .item.VisibleOnForm}}tw-hidden{{end}}"> - <div class="markup">{{ctx.RenderUtils.MarkdownToHtml .item.Attributes.value}}</div> + <div class="render-content markup">{{ctx.RenderUtils.MarkdownToHtml .item.Attributes.value}}</div> </div> diff --git a/templates/repo/issue/milestone_issues.tmpl b/templates/repo/issue/milestone_issues.tmpl index abb4e3290d..ac5d7f16dd 100644 --- a/templates/repo/issue/milestone_issues.tmpl +++ b/templates/repo/issue/milestone_issues.tmpl @@ -22,7 +22,7 @@ {{end}} </div> {{if .Milestone.RenderedContent}} - <div class="markup content tw-mb-4"> + <div class="render-content markup tw-mb-4"> {{.Milestone.RenderedContent}} </div> {{end}} diff --git a/templates/repo/issue/milestones.tmpl b/templates/repo/issue/milestones.tmpl index e7dfe08ee0..5701c1faa6 100644 --- a/templates/repo/issue/milestones.tmpl +++ b/templates/repo/issue/milestones.tmpl @@ -81,9 +81,7 @@ {{end}} </div> {{if .Content}} - <div class="markup content"> - {{.RenderedContent}} - </div> + <div class="render-content markup">{{.RenderedContent}}</div> {{end}} </li> {{end}} diff --git a/templates/repo/release/list.tmpl b/templates/repo/release/list.tmpl index 041890ca9c..88bd85ef4d 100644 --- a/templates/repo/release/list.tmpl +++ b/templates/repo/release/list.tmpl @@ -64,7 +64,7 @@ | <span class="ahead"><a href="{{$.RepoLink}}/compare/{{$release.TagName | PathEscapeSegments}}...{{$release.TargetBehind | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.release.ahead.commits" $release.NumCommitsBehind}}</a> {{ctx.Locale.Tr "repo.release.ahead.target" $release.TargetBehind}}</span> {{end}} </p> - <div class="markup desc"> + <div class="render-content markup"> {{$release.RenderedNote}} </div> <div class="divider"></div> diff --git a/templates/repo/settings/lfs_file.tmpl b/templates/repo/settings/lfs_file.tmpl index f6fac05b69..9f72d764ae 100644 --- a/templates/repo/settings/lfs_file.tmpl +++ b/templates/repo/settings/lfs_file.tmpl @@ -13,7 +13,7 @@ </h4> <div class="ui bottom attached table unstackable segment"> {{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}} - <div class="file-view{{if .IsMarkup}} markup {{.MarkupType}}{{else if .IsPlainText}} plain-text{{else if .IsTextFile}} code-view{{end}}"> + <div class="file-view {{if .IsPlainText}}plain-text{{else if .IsTextFile}}code-view{{end}}"> {{if .IsFileTooLarge}} {{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}} {{else if not .FileSize}} @@ -31,7 +31,7 @@ <strong>{{ctx.Locale.Tr "repo.audio_not_supported_in_browser"}}</strong> </audio> {{else if .IsPDFFile}} - <div class="pdf-content is-loading" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "diff.view_file"}}"></div> + <div class="pdf-content is-loading" data-global-init="initPdfViewer" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "diff.view_file"}}"></div> {{else}} <a href="{{$.RawFileLink}}" rel="nofollow" class="tw-p-4">{{ctx.Locale.Tr "repo.file_view_raw"}}</a> {{end}} diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index 4907d87301..9f1b2a5e8f 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -108,7 +108,7 @@ <strong>{{ctx.Locale.Tr "repo.audio_not_supported_in_browser"}}</strong> </audio> {{else if .IsPDFFile}} - <div class="pdf-content is-loading" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "repo.diff.view_file"}}"></div> + <div class="pdf-content is-loading" data-global-init="initPdfViewer" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "repo.diff.view_file"}}"></div> {{else}} <a href="{{$.RawFileLink}}" rel="nofollow" class="tw-p-4">{{ctx.Locale.Tr "repo.file_view_raw"}}</a> {{end}} diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl index 843a977e3e..efb614280a 100644 --- a/templates/repo/wiki/view.tmpl +++ b/templates/repo/wiki/view.tmpl @@ -63,18 +63,18 @@ <div class="wiki-content-parts"> {{if .sidebarTocContent}} - <div class="markup wiki-content-sidebar wiki-content-toc"> + <div class="render-content markup wiki-content-sidebar wiki-content-toc"> {{.sidebarTocContent | SafeHTML}} </div> {{end}} - <div class="markup wiki-content-main {{if or .sidebarTocContent .sidebarPresent}}with-sidebar{{end}}"> + <div class="render-content markup wiki-content-main {{if or .sidebarTocContent .sidebarPresent}}with-sidebar{{end}}"> {{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}} {{.content | SafeHTML}} </div> {{if .sidebarPresent}} - <div class="markup wiki-content-sidebar"> + <div class="render-content markup wiki-content-sidebar"> {{if and .CanWriteWiki (not .Repository.IsMirror)}} <a class="tw-float-right muted" href="{{.RepoLink}}/wiki/_Sidebar?action=_edit" aria-label="{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}">{{svg "octicon-pencil"}}</a> {{end}} @@ -86,7 +86,7 @@ <div class="tw-clear-both"></div> {{if .footerPresent}} - <div class="markup wiki-content-footer"> + <div class="render-content markup wiki-content-footer"> {{if and .CanWriteWiki (not .Repository.IsMirror)}} <a class="tw-float-right muted" href="{{.RepoLink}}/wiki/_Footer?action=_edit" aria-label="{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}">{{svg "octicon-pencil"}}</a> {{end}} diff --git a/templates/shared/combomarkdowneditor.tmpl b/templates/shared/combomarkdowneditor.tmpl index b1c3b29cf3..fa3e6c6ade 100644 --- a/templates/shared/combomarkdowneditor.tmpl +++ b/templates/shared/combomarkdowneditor.tmpl @@ -81,7 +81,7 @@ } </script> </div> - <div class="ui tab markup" data-tab-panel="markdown-previewer"> + <div class="ui tab" data-tab-panel="markdown-previewer"> {{ctx.Locale.Tr "loading"}} </div> <div class="markdown-add-table-panel tippy-target"> diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl index 739be586b8..47686dd442 100644 --- a/templates/user/dashboard/feeds.tmpl +++ b/templates/user/dashboard/feeds.tmpl @@ -110,7 +110,7 @@ <a href="{{.GetCommentLink ctx}}" class="text truncate issue title">{{(.GetIssueTitle ctx) | ctx.RenderUtils.RenderIssueSimpleTitle}}</a> {{$comment := index .GetIssueInfos 1}} {{if $comment}} - <div class="markup tw-text-14">{{ctx.RenderUtils.MarkdownToHtml $comment}}</div> + <div class="render-content markup tw-text-14">{{ctx.RenderUtils.MarkdownToHtml $comment}}</div> {{end}} {{else if .GetOpType.InActions "merge_pull_request"}} <div class="flex-item-body text black">{{index .GetIssueInfos 1}}</div> diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl index 7c1a69a6f5..d0fe0abbc9 100644 --- a/templates/user/dashboard/milestones.tmpl +++ b/templates/user/dashboard/milestones.tmpl @@ -33,7 +33,7 @@ {{end}} </div> </div> - <div class="flex-container-main content"> + <div class="flex-container-main"> <div class="list-header"> <div class="small-menu-items ui compact tiny menu list-header-toggle"> <a class="item{{if not .IsShowClosed}} active{{end}}" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=open&q={{$.Keyword}}"> @@ -140,9 +140,7 @@ {{end}} </div> {{if .Content}} - <div class="markup content"> - {{.RenderedContent}} - </div> + <div class="render-content markup">{{.RenderedContent}}</div> {{end}} </li> {{end}} diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl index 345872b00d..e5c3412ddd 100644 --- a/templates/user/profile.tmpl +++ b/templates/user/profile.tmpl @@ -26,7 +26,7 @@ {{else if eq .TabName "followers"}} {{template "repo/user_cards" .}} {{else if eq .TabName "overview"}} - <div id="readme_profile" class="markup">{{.ProfileReadmeContent}}</div> + <div id="readme_profile" class="render-content markup">{{.ProfileReadmeContent}}</div> {{else if eq .TabName "organizations"}} {{template "repo/user_cards" .}} {{else}} diff --git a/web_src/css/editor/fileeditor.css b/web_src/css/editor/fileeditor.css index 444ee8c7e7..698efffc99 100644 --- a/web_src/css/editor/fileeditor.css +++ b/web_src/css/editor/fileeditor.css @@ -74,12 +74,3 @@ padding: 1rem; text-align: center; } - -.edit-diff { - padding: 0 !important; -} - -.edit-diff > div > .ui.table { - border-top: none !important; - border-bottom: none !important; -} diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css index d2dcf2ec6e..fabf5b3a8f 100644 --- a/web_src/css/markup/content.css +++ b/web_src/css/markup/content.css @@ -535,7 +535,7 @@ user-select: none; } -.markup-render { +.markup-content-iframe { display: block; border: none; width: 100%; diff --git a/web_src/css/shared/milestone.css b/web_src/css/shared/milestone.css index 91e6b5e387..47e822f8d3 100644 --- a/web_src/css/shared/milestone.css +++ b/web_src/css/shared/milestone.css @@ -12,7 +12,7 @@ border-top: 1px solid var(--color-secondary); } -.milestone-card .content { +.milestone-card .render-content { padding-top: 10px; } diff --git a/web_src/js/features/repo-editor.ts b/web_src/js/features/repo-editor.ts index 0f3fb7bbcf..0f77508f70 100644 --- a/web_src/js/features/repo-editor.ts +++ b/web_src/js/features/repo-editor.ts @@ -1,7 +1,6 @@ import {htmlEscape} from 'escape-goat'; import {createCodeEditor} from './codeeditor.ts'; import {hideElem, queryElems, showElem, createElementFromHTML} from '../utils/dom.ts'; -import {initMarkupContent} from '../markup/content.ts'; import {attachRefIssueContextPopup} from './contextpopup.ts'; import {POST} from '../modules/fetch.ts'; import {initDropzone} from './dropzone.ts'; @@ -199,7 +198,6 @@ export function initRepoEditor() { } export function renderPreviewPanelContent(previewPanel: Element, content: string) { - previewPanel.innerHTML = content; - initMarkupContent(); + previewPanel.innerHTML = `<div class="render-content markup">${content}</div>`; 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 f3e8a0beba..b3de91c3bd 100644 --- a/web_src/js/features/repo-issue-edit.ts +++ b/web_src/js/features/repo-issue-edit.ts @@ -4,7 +4,6 @@ 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 {initCommentContent, initMarkupContent} from '../markup/content.ts'; import {triggerUploadStateChanged} from './comp/EditorUpload.ts'; import {convertHtmlToMarkdown} from '../markup/html2markdown.ts'; import {applyAreYouSure, reinitializeAreYouSure} from '../vendor/jquery.are-you-sure.ts'; @@ -74,8 +73,6 @@ async function tryOnEditContent(e: DOMEvent<MouseEvent>) { content.querySelector('.dropzone-attachments').outerHTML = data.attachments; } comboMarkdownEditor.dropzoneSubmitReload(); - initMarkupContent(); - initCommentContent(); } catch (error) { showErrorToast(`Failed to save the content: ${error}`); console.error(error); diff --git a/web_src/js/features/repo-wiki.ts b/web_src/js/features/repo-wiki.ts index 9ffa8a3275..f94d3ef3d1 100644 --- a/web_src/js/features/repo-wiki.ts +++ b/web_src/js/features/repo-wiki.ts @@ -1,4 +1,3 @@ -import {initMarkupContent} from '../markup/content.ts'; import {validateTextareaNonEmpty, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts'; import {fomanticMobileScreen} from '../modules/fomantic.ts'; import {POST} from '../modules/fetch.ts'; @@ -31,8 +30,7 @@ async function initRepoWikiFormEditor() { const response = await POST(editor.previewUrl, {data: formData}); const data = await response.text(); lastContent = newContent; - previewTarget.innerHTML = `<div class="markup ui segment">${data}</div>`; - initMarkupContent(); + previewTarget.innerHTML = `<div class="render-content markup ui segment">${data}</div>`; } catch (error) { console.error('Error rendering preview:', error); } finally { diff --git a/web_src/js/index.ts b/web_src/js/index.ts index f48074316e..c1ec78539d 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -18,7 +18,7 @@ import {initNotificationCount, initNotificationsTable} from './features/notifica import {initRepoIssueContentHistory} from './features/repo-issue-content.ts'; import {initStopwatch} from './features/stopwatch.ts'; import {initFindFileInRepo} from './features/repo-findfile.ts'; -import {initCommentContent, initMarkupContent} from './markup/content.ts'; +import {initMarkupContent} from './markup/content.ts'; import {initPdfViewer} from './render/pdf.ts'; import {initUserAuthOauth2, initUserCheckAppUrl} from './features/user-auth.ts'; @@ -102,7 +102,6 @@ onDomReady(() => { initHeadNavbarContentToggle, initFootLanguageMenu, - initCommentContent, initContextPopups, initHeatmap, initImageDiff, diff --git a/web_src/js/markup/asciicast.ts b/web_src/js/markup/asciicast.ts index 9baae6ba85..22dbff2d46 100644 --- a/web_src/js/markup/asciicast.ts +++ b/web_src/js/markup/asciicast.ts @@ -1,6 +1,6 @@ -export async function renderAsciicast() { - const els = document.querySelectorAll('.asciinema-player-container'); - if (!els.length) return; +export async function initMarkupRenderAsciicast(elMarkup: HTMLElement): Promise<void> { + const el = elMarkup.querySelector('.asciinema-player-container'); + if (!el) return; const [player] = await Promise.all([ // @ts-expect-error: module exports no types @@ -8,11 +8,9 @@ export async function renderAsciicast() { import(/* webpackChunkName: "asciinema-player" */'asciinema-player/dist/bundle/asciinema-player.css'), ]); - for (const el of els) { - player.create(el.getAttribute('data-asciinema-player-src'), el, { - // poster (a preview frame) to display until the playback is started. - // Set it to 1 hour (also means the end if the video is shorter) to make the preview frame show more. - poster: 'npt:1:0:0', - }); - } + player.create(el.getAttribute('data-asciinema-player-src'), el, { + // poster (a preview frame) to display until the playback is started. + // Set it to 1 hour (also means the end if the video is shorter) to make the preview frame show more. + poster: 'npt:1:0:0', + }); } diff --git a/web_src/js/markup/codecopy.ts b/web_src/js/markup/codecopy.ts index f45b7a8e04..4430256848 100644 --- a/web_src/js/markup/codecopy.ts +++ b/web_src/js/markup/codecopy.ts @@ -7,15 +7,12 @@ export function makeCodeCopyButton(): HTMLButtonElement { return button; } -export function renderCodeCopy(): void { - const els = document.querySelectorAll('.markup .code-block code'); - if (!els.length) return; +export function initMarkupCodeCopy(elMarkup: HTMLElement): void { + const el = elMarkup.querySelector('.code-block code'); // .markup .code-block code + if (!el || !el.textContent) return; - for (const el of els) { - if (!el.textContent) continue; - const btn = makeCodeCopyButton(); - // remove final trailing newline introduced during HTML rendering - btn.setAttribute('data-clipboard-text', el.textContent.replace(/\r?\n$/, '')); - el.after(btn); - } + const btn = makeCodeCopyButton(); + // remove final trailing newline introduced during HTML rendering + btn.setAttribute('data-clipboard-text', el.textContent.replace(/\r?\n$/, '')); + el.after(btn); } diff --git a/web_src/js/markup/content.ts b/web_src/js/markup/content.ts index b9190b15ce..55db4aa810 100644 --- a/web_src/js/markup/content.ts +++ b/web_src/js/markup/content.ts @@ -1,18 +1,17 @@ -import {renderMermaid} from './mermaid.ts'; -import {renderMath} from './math.ts'; -import {renderCodeCopy} from './codecopy.ts'; -import {renderAsciicast} from './asciicast.ts'; +import {initMarkupCodeMermaid} from './mermaid.ts'; +import {initMarkupCodeMath} from './math.ts'; +import {initMarkupCodeCopy} from './codecopy.ts'; +import {initMarkupRenderAsciicast} from './asciicast.ts'; import {initMarkupTasklist} from './tasklist.ts'; +import {registerGlobalSelectorFunc} from '../modules/observer.ts'; // code that runs for all markup content export function initMarkupContent(): void { - renderMermaid(); - renderMath(); - renderCodeCopy(); - renderAsciicast(); -} - -// code that only runs for comments -export function initCommentContent(): void { - initMarkupTasklist(); + registerGlobalSelectorFunc('.markup', (el: HTMLElement) => { + initMarkupCodeCopy(el); + initMarkupTasklist(el); + initMarkupCodeMermaid(el); + initMarkupCodeMath(el); + initMarkupRenderAsciicast(el); + }); } diff --git a/web_src/js/markup/math.ts b/web_src/js/markup/math.ts index 4777805e3c..2a4468bf2e 100644 --- a/web_src/js/markup/math.ts +++ b/web_src/js/markup/math.ts @@ -11,9 +11,9 @@ function targetElement(el: Element): {target: Element, displayAsBlock: boolean} }; } -export async function renderMath(): Promise<void> { - const els = document.querySelectorAll('.markup code.language-math'); - if (!els.length) return; +export async function initMarkupCodeMath(elMarkup: HTMLElement): Promise<void> { + const el = elMarkup.querySelector('code.language-math'); // .markup code.language-math' + if (!el) return; const [{default: katex}] = await Promise.all([ import(/* webpackChunkName: "katex" */'katex'), @@ -24,25 +24,23 @@ export async function renderMath(): Promise<void> { const MAX_SIZE = 25; const MAX_EXPAND = 1000; - for (const el of els) { - const {target, displayAsBlock} = targetElement(el); - if (target.hasAttribute('data-render-done')) continue; - const source = el.textContent; + const {target, displayAsBlock} = targetElement(el); + if (target.hasAttribute('data-render-done')) return; + const source = el.textContent; - if (source.length > MAX_CHARS) { - displayError(target, new Error(`Math source of ${source.length} characters exceeds the maximum allowed length of ${MAX_CHARS}.`)); - continue; - } - try { - const tempEl = document.createElement(displayAsBlock ? 'p' : 'span'); - katex.render(source, tempEl, { - maxSize: MAX_SIZE, - maxExpand: MAX_EXPAND, - displayMode: displayAsBlock, // katex: true for display (block) mode, false for inline mode - }); - target.replaceWith(tempEl); - } catch (error) { - displayError(target, error); - } + if (source.length > MAX_CHARS) { + displayError(target, new Error(`Math source of ${source.length} characters exceeds the maximum allowed length of ${MAX_CHARS}.`)); + return; + } + try { + const tempEl = document.createElement(displayAsBlock ? 'p' : 'span'); + katex.render(source, tempEl, { + maxSize: MAX_SIZE, + maxExpand: MAX_EXPAND, + displayMode: displayAsBlock, // katex: true for display (block) mode, false for inline mode + }); + target.replaceWith(tempEl); + } catch (error) { + displayError(target, error); } } diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index 2dbed280c2..b4bf3153ea 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -10,9 +10,9 @@ body {margin: 0; padding: 0; overflow: hidden} #mermaid {display: block; margin: 0 auto} blockquote, dd, dl, figure, h1, h2, h3, h4, h5, h6, hr, p, pre {margin: 0}`; -export async function renderMermaid(): Promise<void> { - const els = document.querySelectorAll('.markup code.language-mermaid'); - if (!els.length) return; +export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise<void> { + const el = elMarkup.querySelector('code.language-mermaid'); // .markup code.language-mermaid + if (!el) return; const {default: mermaid} = await import(/* webpackChunkName: "mermaid" */'mermaid'); @@ -23,67 +23,65 @@ export async function renderMermaid(): Promise<void> { suppressErrorRendering: true, }); - for (const el of els) { - const pre = el.closest('pre'); - if (pre.hasAttribute('data-render-done')) continue; - - const source = el.textContent; - if (mermaidMaxSourceCharacters >= 0 && source.length > mermaidMaxSourceCharacters) { - displayError(pre, new Error(`Mermaid source of ${source.length} characters exceeds the maximum allowed length of ${mermaidMaxSourceCharacters}.`)); - continue; - } - - try { - await mermaid.parse(source); - } catch (err) { - displayError(pre, err); - continue; - } - - try { - // can't use bindFunctions here because we can't cross the iframe boundary. This - // means js-based interactions won't work but they aren't intended to work either - const {svg} = await mermaid.render('mermaid', source); - - const iframe = document.createElement('iframe'); - iframe.classList.add('markup-render', 'tw-invisible'); - iframe.srcdoc = `<html><head><style>${iframeCss}</style></head><body>${svg}</body></html>`; - - const mermaidBlock = document.createElement('div'); - mermaidBlock.classList.add('mermaid-block', 'is-loading', 'tw-hidden'); - mermaidBlock.append(iframe); - - const btn = makeCodeCopyButton(); - btn.setAttribute('data-clipboard-text', source); - mermaidBlock.append(btn); - - const updateIframeHeight = () => { - const body = iframe.contentWindow?.document?.body; - if (body) { - iframe.style.height = `${body.clientHeight}px`; - } - }; - - iframe.addEventListener('load', () => { - pre.replaceWith(mermaidBlock); - mermaidBlock.classList.remove('tw-hidden'); + const pre = el.closest('pre'); + if (pre.hasAttribute('data-render-done')) return; + + const source = el.textContent; + if (mermaidMaxSourceCharacters >= 0 && source.length > mermaidMaxSourceCharacters) { + displayError(pre, new Error(`Mermaid source of ${source.length} characters exceeds the maximum allowed length of ${mermaidMaxSourceCharacters}.`)); + return; + } + + try { + await mermaid.parse(source); + } catch (err) { + displayError(pre, err); + return; + } + + try { + // can't use bindFunctions here because we can't cross the iframe boundary. This + // means js-based interactions won't work but they aren't intended to work either + const {svg} = await mermaid.render('mermaid', source); + + const iframe = document.createElement('iframe'); + iframe.classList.add('markup-content-iframe', 'tw-invisible'); + iframe.srcdoc = `<html><head><style>${iframeCss}</style></head><body>${svg}</body></html>`; + + const mermaidBlock = document.createElement('div'); + mermaidBlock.classList.add('mermaid-block', 'is-loading', 'tw-hidden'); + mermaidBlock.append(iframe); + + const btn = makeCodeCopyButton(); + btn.setAttribute('data-clipboard-text', source); + mermaidBlock.append(btn); + + const updateIframeHeight = () => { + const body = iframe.contentWindow?.document?.body; + if (body) { + iframe.style.height = `${body.clientHeight}px`; + } + }; + + iframe.addEventListener('load', () => { + pre.replaceWith(mermaidBlock); + mermaidBlock.classList.remove('tw-hidden'); + updateIframeHeight(); + setTimeout(() => { // avoid flash of iframe background + mermaidBlock.classList.remove('is-loading'); + iframe.classList.remove('tw-invisible'); + }, 0); + + // update height when element's visibility state changes, for example when the diagram is inside + // a <details> + <summary> block and the <details> block becomes visible upon user interaction, it + // would initially set a incorrect height and the correct height is set during this callback. + (new IntersectionObserver(() => { updateIframeHeight(); - setTimeout(() => { // avoid flash of iframe background - mermaidBlock.classList.remove('is-loading'); - iframe.classList.remove('tw-invisible'); - }, 0); - - // update height when element's visibility state changes, for example when the diagram is inside - // a <details> + <summary> block and the <details> block becomes visible upon user interaction, it - // would initially set a incorrect height and the correct height is set during this callback. - (new IntersectionObserver(() => { - updateIframeHeight(); - }, {root: document.documentElement})).observe(iframe); - }); - - document.body.append(mermaidBlock); - } catch (err) { - displayError(pre, err); - } + }, {root: document.documentElement})).observe(iframe); + }); + + document.body.append(mermaidBlock); + } catch (err) { + displayError(pre, err); } } diff --git a/web_src/js/markup/tasklist.ts b/web_src/js/markup/tasklist.ts index 95db7fc845..dc4bbd9519 100644 --- a/web_src/js/markup/tasklist.ts +++ b/web_src/js/markup/tasklist.ts @@ -7,80 +7,80 @@ const preventListener = (e: Event) => e.preventDefault(); * Attaches `input` handlers to markdown rendered tasklist checkboxes in comments. * * When a checkbox value changes, the corresponding [ ] or [x] in the markdown string - * is set accordingly and sent to the server. On success it updates the raw-content on + * is set accordingly and sent to the server. On success, it updates the raw-content on * error it resets the checkbox to its original value. */ -export function initMarkupTasklist(): void { - for (const el of document.querySelectorAll(`.markup[data-can-edit=true]`) || []) { - const container = el.parentNode; - const checkboxes = el.querySelectorAll<HTMLInputElement>(`.task-list-item input[type=checkbox]`); +export function initMarkupTasklist(elMarkup: HTMLElement): void { + if (!elMarkup.matches('[data-can-edit=true]')) return; - for (const checkbox of checkboxes) { - if (checkbox.hasAttribute('data-editable')) { - return; - } + const container = elMarkup.parentNode; + const checkboxes = elMarkup.querySelectorAll<HTMLInputElement>(`.task-list-item input[type=checkbox]`); - checkbox.setAttribute('data-editable', 'true'); - checkbox.addEventListener('input', async () => { - const checkboxCharacter = checkbox.checked ? 'x' : ' '; - const position = parseInt(checkbox.getAttribute('data-source-position')) + 1; + for (const checkbox of checkboxes) { + if (checkbox.hasAttribute('data-editable')) { + return; + } - const rawContent = container.querySelector('.raw-content'); - const oldContent = rawContent.textContent; + checkbox.setAttribute('data-editable', 'true'); + checkbox.addEventListener('input', async () => { + const checkboxCharacter = checkbox.checked ? 'x' : ' '; + const position = parseInt(checkbox.getAttribute('data-source-position')) + 1; - const encoder = new TextEncoder(); - const buffer = encoder.encode(oldContent); - // Indexes may fall off the ends and return undefined. - if (buffer[position - 1] !== '['.codePointAt(0) || - buffer[position] !== ' '.codePointAt(0) && buffer[position] !== 'x'.codePointAt(0) || - buffer[position + 1] !== ']'.codePointAt(0)) { - // Position is probably wrong. Revert and don't allow change. - checkbox.checked = !checkbox.checked; - throw new Error(`Expected position to be space or x and surrounded by brackets, but it's not: position=${position}`); - } - buffer.set(encoder.encode(checkboxCharacter), position); - const newContent = new TextDecoder().decode(buffer); + const rawContent = container.querySelector('.raw-content'); + const oldContent = rawContent.textContent; - if (newContent === oldContent) { - return; - } + const encoder = new TextEncoder(); + const buffer = encoder.encode(oldContent); + // Indexes may fall off the ends and return undefined. + if (buffer[position - 1] !== '['.codePointAt(0) || + buffer[position] !== ' '.codePointAt(0) && buffer[position] !== 'x'.codePointAt(0) || + buffer[position + 1] !== ']'.codePointAt(0)) { + // Position is probably wrong. Revert and don't allow change. + checkbox.checked = !checkbox.checked; + throw new Error(`Expected position to be space or x and surrounded by brackets, but it's not: position=${position}`); + } + buffer.set(encoder.encode(checkboxCharacter), position); + const newContent = new TextDecoder().decode(buffer); - // Prevent further inputs until the request is done. This does not use the - // `disabled` attribute because it causes the border to flash on click. - for (const checkbox of checkboxes) { - checkbox.addEventListener('click', preventListener); - } + if (newContent === oldContent) { + return; + } - try { - const editContentZone = container.querySelector<HTMLDivElement>('.edit-content-zone'); - const updateUrl = editContentZone.getAttribute('data-update-url'); - const context = editContentZone.getAttribute('data-context'); - const contentVersion = editContentZone.getAttribute('data-content-version'); + // Prevent further inputs until the request is done. This does not use the + // `disabled` attribute because it causes the border to flash on click. + for (const checkbox of checkboxes) { + checkbox.addEventListener('click', preventListener); + } - const requestBody = new FormData(); - requestBody.append('ignore_attachments', 'true'); - requestBody.append('content', newContent); - requestBody.append('context', context); - requestBody.append('content_version', contentVersion); - const response = await POST(updateUrl, {data: requestBody}); - const data = await response.json(); - if (response.status === 400) { - showErrorToast(data.errorMessage); - return; - } - editContentZone.setAttribute('data-content-version', data.contentVersion); - rawContent.textContent = newContent; - } catch (err) { - checkbox.checked = !checkbox.checked; - console.error(err); - } + try { + const editContentZone = container.querySelector<HTMLDivElement>('.edit-content-zone'); + const updateUrl = editContentZone.getAttribute('data-update-url'); + const context = editContentZone.getAttribute('data-context'); + const contentVersion = editContentZone.getAttribute('data-content-version'); - // Enable input on checkboxes again - for (const checkbox of checkboxes) { - checkbox.removeEventListener('click', preventListener); + const requestBody = new FormData(); + requestBody.append('ignore_attachments', 'true'); + requestBody.append('content', newContent); + requestBody.append('context', context); + requestBody.append('content_version', contentVersion); + const response = await POST(updateUrl, {data: requestBody}); + const data = await response.json(); + if (response.status === 400) { + showErrorToast(data.errorMessage); + return; } - }); - } + editContentZone.setAttribute('data-content-version', data.contentVersion); + rawContent.textContent = newContent; + } catch (err) { + checkbox.checked = !checkbox.checked; + console.error(err); + } + + // Enable input on checkboxes again + for (const checkbox of checkboxes) { + checkbox.removeEventListener('click', preventListener); + } + }); // Enable the checkboxes as they are initially disabled by the markdown renderer for (const checkbox of checkboxes) { diff --git a/web_src/js/modules/observer.ts b/web_src/js/modules/observer.ts index f60c033cf2..06208d0507 100644 --- a/web_src/js/modules/observer.ts +++ b/web_src/js/modules/observer.ts @@ -20,6 +20,9 @@ export function registerGlobalEventFunc<T extends HTMLElement, E extends Event>( // It handles the global init functions by a selector, for example: // > registerGlobalSelectorObserver('.ui.dropdown:not(.custom)', (el) => { initDropdown(el, ...) }); +// ATTENTION: For most cases, it's recommended to use registerGlobalInitFunc instead, +// Because this selector-based approach is less efficient and less maintainable. +// But if there are already a lot of elements on many pages, this selector-based approach is more convenient for exiting code. export function registerGlobalSelectorFunc(selector: string, handler: (el: HTMLElement) => void) { selectorHandlers.push({selector, handler}); // Then initAddedElementObserver will call this handler for all existing elements after all handlers are added. diff --git a/web_src/js/render/pdf.ts b/web_src/js/render/pdf.ts index f31f161e6e..283b4ed85c 100644 --- a/web_src/js/render/pdf.ts +++ b/web_src/js/render/pdf.ts @@ -1,12 +1,10 @@ import {htmlEscape} from 'escape-goat'; +import {registerGlobalInitFunc} from '../modules/observer.ts'; export async function initPdfViewer() { - const els = document.querySelectorAll('.pdf-content'); - if (!els.length) return; + registerGlobalInitFunc('initPdfViewer', async (el: HTMLInputElement) => { + const pdfobject = await import(/* webpackChunkName: "pdfobject" */'pdfobject'); - const pdfobject = await import(/* webpackChunkName: "pdfobject" */'pdfobject'); - - for (const el of els) { const src = el.getAttribute('data-src'); const fallbackText = el.getAttribute('data-fallback-button-text'); pdfobject.embed(src, el, { @@ -15,5 +13,5 @@ export async function initPdfViewer() { `, }); el.classList.remove('is-loading'); - } + }); } |