mirror of https://github.com/go-gitea/gitea.git
Refactor markup and pdf-viewer to use new init framework (#33772)
1. Add some "render-content" classes to "markup" elements when the content is rendered 2. Use correct "markup" wrapper for "preview" (but not set that class on the tab) 3. Remove incorrect "markup" class from LFS file view, because there is no markup content * "edit-diff" is also removed because it does nothing 5. Use "initPdfViewer" for PDF viewer 6. Remove incorrect "content" class from milestone markup 7. Init all ".markup" elements by new init framework --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>pull/32921/head^2
parent
43c8d85f19
commit
f0f1737d4d
|
@ -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" .}}
|
||||
|
|
|
@ -74,9 +74,7 @@
|
|||
{{end}}
|
||||
</div>
|
||||
{{if .Description}}
|
||||
<div class="content">
|
||||
{{.RenderedContent}}
|
||||
</div>
|
||||
<div class="render-content markup">{{.RenderedContent}}</div>
|
||||
{{end}}
|
||||
</li>
|
||||
{{end}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -81,9 +81,7 @@
|
|||
{{end}}
|
||||
</div>
|
||||
{{if .Content}}
|
||||
<div class="markup content">
|
||||
{{.RenderedContent}}
|
||||
</div>
|
||||
<div class="render-content markup">{{.RenderedContent}}</div>
|
||||
{{end}}
|
||||
</li>
|
||||
{{end}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -535,7 +535,7 @@
|
|||
user-select: none;
|
||||
}
|
||||
|
||||
.markup-render {
|
||||
.markup-content-iframe {
|
||||
display: block;
|
||||
border: none;
|
||||
width: 100%;
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
border-top: 1px solid var(--color-secondary);
|
||||
}
|
||||
|
||||
.milestone-card .content {
|
||||
.milestone-card .render-content {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 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}.`));
|
||||
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}.`));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await mermaid.parse(source);
|
||||
} catch (err) {
|
||||
displayError(pre, err);
|
||||
continue;
|
||||
}
|
||||
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);
|
||||
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 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 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 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`;
|
||||
}
|
||||
};
|
||||
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');
|
||||
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);
|
||||
}, {root: document.documentElement})).observe(iframe);
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
document.body.append(mermaidBlock);
|
||||
} catch (err) {
|
||||
displayError(pre, err);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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')) {
|
||||
const container = elMarkup.parentNode;
|
||||
const checkboxes = elMarkup.querySelectorAll<HTMLInputElement>(`.task-list-item input[type=checkbox]`);
|
||||
|
||||
for (const checkbox of checkboxes) {
|
||||
if (checkbox.hasAttribute('data-editable')) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkbox.setAttribute('data-editable', 'true');
|
||||
checkbox.addEventListener('input', async () => {
|
||||
const checkboxCharacter = checkbox.checked ? 'x' : ' ';
|
||||
const position = parseInt(checkbox.getAttribute('data-source-position')) + 1;
|
||||
|
||||
const rawContent = container.querySelector('.raw-content');
|
||||
const oldContent = rawContent.textContent;
|
||||
|
||||
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);
|
||||
|
||||
if (newContent === oldContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkbox.setAttribute('data-editable', 'true');
|
||||
checkbox.addEventListener('input', async () => {
|
||||
const checkboxCharacter = checkbox.checked ? 'x' : ' ';
|
||||
const position = parseInt(checkbox.getAttribute('data-source-position')) + 1;
|
||||
// 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 rawContent = container.querySelector('.raw-content');
|
||||
const oldContent = rawContent.textContent;
|
||||
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');
|
||||
|
||||
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);
|
||||
|
||||
if (newContent === oldContent) {
|
||||
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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
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 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) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue