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 mobile reversed stackable grid">
|
||||||
<div class="ui {{if .ShowMemberAndTeamTab}}eleven wide{{end}} column">
|
<div class="ui {{if .ShowMemberAndTeamTab}}eleven wide{{end}} column">
|
||||||
{{if .ProfileReadmeContent}}
|
{{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}}
|
{{end}}
|
||||||
{{template "shared/repo_search" .}}
|
{{template "shared/repo_search" .}}
|
||||||
{{template "explore/repo_list" .}}
|
{{template "explore/repo_list" .}}
|
||||||
|
|
|
@ -74,9 +74,7 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{if .Description}}
|
{{if .Description}}
|
||||||
<div class="content">
|
<div class="render-content markup">{{.RenderedContent}}</div>
|
||||||
{{.RenderedContent}}
|
|
||||||
</div>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -45,10 +45,10 @@
|
||||||
data-line-wrap-extensions="{{.LineWrapExtensions}}">{{.FileContent}}</textarea>
|
data-line-wrap-extensions="{{.LineWrapExtensions}}">{{.FileContent}}</textarea>
|
||||||
<div class="editor-loading is-loading"></div>
|
<div class="editor-loading is-loading"></div>
|
||||||
</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"}}
|
{{ctx.Locale.Tr "loading"}}
|
||||||
</div>
|
</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 class="tw-p-16"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
<div class="field {{if not .item.VisibleOnForm}}tw-hidden{{end}}">
|
<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>
|
</div>
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{if .Milestone.RenderedContent}}
|
{{if .Milestone.RenderedContent}}
|
||||||
<div class="markup content tw-mb-4">
|
<div class="render-content markup tw-mb-4">
|
||||||
{{.Milestone.RenderedContent}}
|
{{.Milestone.RenderedContent}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -81,9 +81,7 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{if .Content}}
|
{{if .Content}}
|
||||||
<div class="markup content">
|
<div class="render-content markup">{{.RenderedContent}}</div>
|
||||||
{{.RenderedContent}}
|
|
||||||
</div>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{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>
|
| <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}}
|
{{end}}
|
||||||
</p>
|
</p>
|
||||||
<div class="markup desc">
|
<div class="render-content markup">
|
||||||
{{$release.RenderedNote}}
|
{{$release.RenderedNote}}
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
</h4>
|
</h4>
|
||||||
<div class="ui bottom attached table unstackable segment">
|
<div class="ui bottom attached table unstackable segment">
|
||||||
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}}
|
{{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}}
|
{{if .IsFileTooLarge}}
|
||||||
{{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}}
|
{{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}}
|
||||||
{{else if not .FileSize}}
|
{{else if not .FileSize}}
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
<strong>{{ctx.Locale.Tr "repo.audio_not_supported_in_browser"}}</strong>
|
<strong>{{ctx.Locale.Tr "repo.audio_not_supported_in_browser"}}</strong>
|
||||||
</audio>
|
</audio>
|
||||||
{{else if .IsPDFFile}}
|
{{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}}
|
{{else}}
|
||||||
<a href="{{$.RawFileLink}}" rel="nofollow" class="tw-p-4">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
|
<a href="{{$.RawFileLink}}" rel="nofollow" class="tw-p-4">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -108,7 +108,7 @@
|
||||||
<strong>{{ctx.Locale.Tr "repo.audio_not_supported_in_browser"}}</strong>
|
<strong>{{ctx.Locale.Tr "repo.audio_not_supported_in_browser"}}</strong>
|
||||||
</audio>
|
</audio>
|
||||||
{{else if .IsPDFFile}}
|
{{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}}
|
{{else}}
|
||||||
<a href="{{$.RawFileLink}}" rel="nofollow" class="tw-p-4">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
|
<a href="{{$.RawFileLink}}" rel="nofollow" class="tw-p-4">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -63,18 +63,18 @@
|
||||||
|
|
||||||
<div class="wiki-content-parts">
|
<div class="wiki-content-parts">
|
||||||
{{if .sidebarTocContent}}
|
{{if .sidebarTocContent}}
|
||||||
<div class="markup wiki-content-sidebar wiki-content-toc">
|
<div class="render-content markup wiki-content-sidebar wiki-content-toc">
|
||||||
{{.sidebarTocContent | SafeHTML}}
|
{{.sidebarTocContent | SafeHTML}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{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" $}}
|
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}}
|
||||||
{{.content | SafeHTML}}
|
{{.content | SafeHTML}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if .sidebarPresent}}
|
{{if .sidebarPresent}}
|
||||||
<div class="markup wiki-content-sidebar">
|
<div class="render-content markup wiki-content-sidebar">
|
||||||
{{if and .CanWriteWiki (not .Repository.IsMirror)}}
|
{{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>
|
<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}}
|
{{end}}
|
||||||
|
@ -86,7 +86,7 @@
|
||||||
<div class="tw-clear-both"></div>
|
<div class="tw-clear-both"></div>
|
||||||
|
|
||||||
{{if .footerPresent}}
|
{{if .footerPresent}}
|
||||||
<div class="markup wiki-content-footer">
|
<div class="render-content markup wiki-content-footer">
|
||||||
{{if and .CanWriteWiki (not .Repository.IsMirror)}}
|
{{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>
|
<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}}
|
{{end}}
|
||||||
|
|
|
@ -81,7 +81,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui tab markup" data-tab-panel="markdown-previewer">
|
<div class="ui tab" data-tab-panel="markdown-previewer">
|
||||||
{{ctx.Locale.Tr "loading"}}
|
{{ctx.Locale.Tr "loading"}}
|
||||||
</div>
|
</div>
|
||||||
<div class="markdown-add-table-panel tippy-target">
|
<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>
|
<a href="{{.GetCommentLink ctx}}" class="text truncate issue title">{{(.GetIssueTitle ctx) | ctx.RenderUtils.RenderIssueSimpleTitle}}</a>
|
||||||
{{$comment := index .GetIssueInfos 1}}
|
{{$comment := index .GetIssueInfos 1}}
|
||||||
{{if $comment}}
|
{{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}}
|
{{end}}
|
||||||
{{else if .GetOpType.InActions "merge_pull_request"}}
|
{{else if .GetOpType.InActions "merge_pull_request"}}
|
||||||
<div class="flex-item-body text black">{{index .GetIssueInfos 1}}</div>
|
<div class="flex-item-body text black">{{index .GetIssueInfos 1}}</div>
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-container-main content">
|
<div class="flex-container-main">
|
||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
<div class="small-menu-items ui compact tiny menu list-header-toggle">
|
<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}}">
|
<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}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{if .Content}}
|
{{if .Content}}
|
||||||
<div class="markup content">
|
<div class="render-content markup">{{.RenderedContent}}</div>
|
||||||
{{.RenderedContent}}
|
|
||||||
</div>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
{{else if eq .TabName "followers"}}
|
{{else if eq .TabName "followers"}}
|
||||||
{{template "repo/user_cards" .}}
|
{{template "repo/user_cards" .}}
|
||||||
{{else if eq .TabName "overview"}}
|
{{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"}}
|
{{else if eq .TabName "organizations"}}
|
||||||
{{template "repo/user_cards" .}}
|
{{template "repo/user_cards" .}}
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|
|
@ -74,12 +74,3 @@
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
text-align: center;
|
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;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markup-render {
|
.markup-content-iframe {
|
||||||
display: block;
|
display: block;
|
||||||
border: none;
|
border: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
border-top: 1px solid var(--color-secondary);
|
border-top: 1px solid var(--color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.milestone-card .content {
|
.milestone-card .render-content {
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import {htmlEscape} from 'escape-goat';
|
import {htmlEscape} from 'escape-goat';
|
||||||
import {createCodeEditor} from './codeeditor.ts';
|
import {createCodeEditor} from './codeeditor.ts';
|
||||||
import {hideElem, queryElems, showElem, createElementFromHTML} from '../utils/dom.ts';
|
import {hideElem, queryElems, showElem, createElementFromHTML} from '../utils/dom.ts';
|
||||||
import {initMarkupContent} from '../markup/content.ts';
|
|
||||||
import {attachRefIssueContextPopup} from './contextpopup.ts';
|
import {attachRefIssueContextPopup} from './contextpopup.ts';
|
||||||
import {POST} from '../modules/fetch.ts';
|
import {POST} from '../modules/fetch.ts';
|
||||||
import {initDropzone} from './dropzone.ts';
|
import {initDropzone} from './dropzone.ts';
|
||||||
|
@ -199,7 +198,6 @@ export function initRepoEditor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderPreviewPanelContent(previewPanel: Element, content: string) {
|
export function renderPreviewPanelContent(previewPanel: Element, content: string) {
|
||||||
previewPanel.innerHTML = content;
|
previewPanel.innerHTML = `<div class="render-content markup">${content}</div>`;
|
||||||
initMarkupContent();
|
|
||||||
attachRefIssueContextPopup(previewPanel.querySelectorAll('p .ref-issue'));
|
attachRefIssueContextPopup(previewPanel.querySelectorAll('p .ref-issue'));
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import {POST} from '../modules/fetch.ts';
|
||||||
import {showErrorToast} from '../modules/toast.ts';
|
import {showErrorToast} from '../modules/toast.ts';
|
||||||
import {hideElem, querySingleVisibleElem, showElem, type DOMEvent} from '../utils/dom.ts';
|
import {hideElem, querySingleVisibleElem, showElem, type DOMEvent} from '../utils/dom.ts';
|
||||||
import {attachRefIssueContextPopup} from './contextpopup.ts';
|
import {attachRefIssueContextPopup} from './contextpopup.ts';
|
||||||
import {initCommentContent, initMarkupContent} from '../markup/content.ts';
|
|
||||||
import {triggerUploadStateChanged} from './comp/EditorUpload.ts';
|
import {triggerUploadStateChanged} from './comp/EditorUpload.ts';
|
||||||
import {convertHtmlToMarkdown} from '../markup/html2markdown.ts';
|
import {convertHtmlToMarkdown} from '../markup/html2markdown.ts';
|
||||||
import {applyAreYouSure, reinitializeAreYouSure} from '../vendor/jquery.are-you-sure.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;
|
content.querySelector('.dropzone-attachments').outerHTML = data.attachments;
|
||||||
}
|
}
|
||||||
comboMarkdownEditor.dropzoneSubmitReload();
|
comboMarkdownEditor.dropzoneSubmitReload();
|
||||||
initMarkupContent();
|
|
||||||
initCommentContent();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showErrorToast(`Failed to save the content: ${error}`);
|
showErrorToast(`Failed to save the content: ${error}`);
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import {initMarkupContent} from '../markup/content.ts';
|
|
||||||
import {validateTextareaNonEmpty, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
|
import {validateTextareaNonEmpty, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
|
||||||
import {fomanticMobileScreen} from '../modules/fomantic.ts';
|
import {fomanticMobileScreen} from '../modules/fomantic.ts';
|
||||||
import {POST} from '../modules/fetch.ts';
|
import {POST} from '../modules/fetch.ts';
|
||||||
|
@ -31,8 +30,7 @@ async function initRepoWikiFormEditor() {
|
||||||
const response = await POST(editor.previewUrl, {data: formData});
|
const response = await POST(editor.previewUrl, {data: formData});
|
||||||
const data = await response.text();
|
const data = await response.text();
|
||||||
lastContent = newContent;
|
lastContent = newContent;
|
||||||
previewTarget.innerHTML = `<div class="markup ui segment">${data}</div>`;
|
previewTarget.innerHTML = `<div class="render-content markup ui segment">${data}</div>`;
|
||||||
initMarkupContent();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error rendering preview:', error);
|
console.error('Error rendering preview:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -18,7 +18,7 @@ import {initNotificationCount, initNotificationsTable} from './features/notifica
|
||||||
import {initRepoIssueContentHistory} from './features/repo-issue-content.ts';
|
import {initRepoIssueContentHistory} from './features/repo-issue-content.ts';
|
||||||
import {initStopwatch} from './features/stopwatch.ts';
|
import {initStopwatch} from './features/stopwatch.ts';
|
||||||
import {initFindFileInRepo} from './features/repo-findfile.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 {initPdfViewer} from './render/pdf.ts';
|
||||||
|
|
||||||
import {initUserAuthOauth2, initUserCheckAppUrl} from './features/user-auth.ts';
|
import {initUserAuthOauth2, initUserCheckAppUrl} from './features/user-auth.ts';
|
||||||
|
@ -102,7 +102,6 @@ onDomReady(() => {
|
||||||
initHeadNavbarContentToggle,
|
initHeadNavbarContentToggle,
|
||||||
initFootLanguageMenu,
|
initFootLanguageMenu,
|
||||||
|
|
||||||
initCommentContent,
|
|
||||||
initContextPopups,
|
initContextPopups,
|
||||||
initHeatmap,
|
initHeatmap,
|
||||||
initImageDiff,
|
initImageDiff,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
export async function renderAsciicast() {
|
export async function initMarkupRenderAsciicast(elMarkup: HTMLElement): Promise<void> {
|
||||||
const els = document.querySelectorAll('.asciinema-player-container');
|
const el = elMarkup.querySelector('.asciinema-player-container');
|
||||||
if (!els.length) return;
|
if (!el) return;
|
||||||
|
|
||||||
const [player] = await Promise.all([
|
const [player] = await Promise.all([
|
||||||
// @ts-expect-error: module exports no types
|
// @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'),
|
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, {
|
||||||
player.create(el.getAttribute('data-asciinema-player-src'), el, {
|
// poster (a preview frame) to display until the playback is started.
|
||||||
// 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.
|
||||||
// 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',
|
||||||
poster: 'npt:1:0:0',
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,15 +7,12 @@ export function makeCodeCopyButton(): HTMLButtonElement {
|
||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderCodeCopy(): void {
|
export function initMarkupCodeCopy(elMarkup: HTMLElement): void {
|
||||||
const els = document.querySelectorAll('.markup .code-block code');
|
const el = elMarkup.querySelector('.code-block code'); // .markup .code-block code
|
||||||
if (!els.length) return;
|
if (!el || !el.textContent) return;
|
||||||
|
|
||||||
for (const el of els) {
|
const btn = makeCodeCopyButton();
|
||||||
if (!el.textContent) continue;
|
// remove final trailing newline introduced during HTML rendering
|
||||||
const btn = makeCodeCopyButton();
|
btn.setAttribute('data-clipboard-text', el.textContent.replace(/\r?\n$/, ''));
|
||||||
// remove final trailing newline introduced during HTML rendering
|
el.after(btn);
|
||||||
btn.setAttribute('data-clipboard-text', el.textContent.replace(/\r?\n$/, ''));
|
|
||||||
el.after(btn);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
import {renderMermaid} from './mermaid.ts';
|
import {initMarkupCodeMermaid} from './mermaid.ts';
|
||||||
import {renderMath} from './math.ts';
|
import {initMarkupCodeMath} from './math.ts';
|
||||||
import {renderCodeCopy} from './codecopy.ts';
|
import {initMarkupCodeCopy} from './codecopy.ts';
|
||||||
import {renderAsciicast} from './asciicast.ts';
|
import {initMarkupRenderAsciicast} from './asciicast.ts';
|
||||||
import {initMarkupTasklist} from './tasklist.ts';
|
import {initMarkupTasklist} from './tasklist.ts';
|
||||||
|
import {registerGlobalSelectorFunc} from '../modules/observer.ts';
|
||||||
|
|
||||||
// code that runs for all markup content
|
// code that runs for all markup content
|
||||||
export function initMarkupContent(): void {
|
export function initMarkupContent(): void {
|
||||||
renderMermaid();
|
registerGlobalSelectorFunc('.markup', (el: HTMLElement) => {
|
||||||
renderMath();
|
initMarkupCodeCopy(el);
|
||||||
renderCodeCopy();
|
initMarkupTasklist(el);
|
||||||
renderAsciicast();
|
initMarkupCodeMermaid(el);
|
||||||
}
|
initMarkupCodeMath(el);
|
||||||
|
initMarkupRenderAsciicast(el);
|
||||||
// code that only runs for comments
|
});
|
||||||
export function initCommentContent(): void {
|
|
||||||
initMarkupTasklist();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,9 @@ function targetElement(el: Element): {target: Element, displayAsBlock: boolean}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renderMath(): Promise<void> {
|
export async function initMarkupCodeMath(elMarkup: HTMLElement): Promise<void> {
|
||||||
const els = document.querySelectorAll('.markup code.language-math');
|
const el = elMarkup.querySelector('code.language-math'); // .markup code.language-math'
|
||||||
if (!els.length) return;
|
if (!el) return;
|
||||||
|
|
||||||
const [{default: katex}] = await Promise.all([
|
const [{default: katex}] = await Promise.all([
|
||||||
import(/* webpackChunkName: "katex" */'katex'),
|
import(/* webpackChunkName: "katex" */'katex'),
|
||||||
|
@ -24,25 +24,23 @@ export async function renderMath(): Promise<void> {
|
||||||
const MAX_SIZE = 25;
|
const MAX_SIZE = 25;
|
||||||
const MAX_EXPAND = 1000;
|
const MAX_EXPAND = 1000;
|
||||||
|
|
||||||
for (const el of els) {
|
const {target, displayAsBlock} = targetElement(el);
|
||||||
const {target, displayAsBlock} = targetElement(el);
|
if (target.hasAttribute('data-render-done')) return;
|
||||||
if (target.hasAttribute('data-render-done')) continue;
|
const source = el.textContent;
|
||||||
const source = el.textContent;
|
|
||||||
|
|
||||||
if (source.length > MAX_CHARS) {
|
if (source.length > MAX_CHARS) {
|
||||||
displayError(target, new Error(`Math source of ${source.length} characters exceeds the maximum allowed length of ${MAX_CHARS}.`));
|
displayError(target, new Error(`Math source of ${source.length} characters exceeds the maximum allowed length of ${MAX_CHARS}.`));
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const tempEl = document.createElement(displayAsBlock ? 'p' : 'span');
|
const tempEl = document.createElement(displayAsBlock ? 'p' : 'span');
|
||||||
katex.render(source, tempEl, {
|
katex.render(source, tempEl, {
|
||||||
maxSize: MAX_SIZE,
|
maxSize: MAX_SIZE,
|
||||||
maxExpand: MAX_EXPAND,
|
maxExpand: MAX_EXPAND,
|
||||||
displayMode: displayAsBlock, // katex: true for display (block) mode, false for inline mode
|
displayMode: displayAsBlock, // katex: true for display (block) mode, false for inline mode
|
||||||
});
|
});
|
||||||
target.replaceWith(tempEl);
|
target.replaceWith(tempEl);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
displayError(target, error);
|
displayError(target, error);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,9 +10,9 @@ body {margin: 0; padding: 0; overflow: hidden}
|
||||||
#mermaid {display: block; margin: 0 auto}
|
#mermaid {display: block; margin: 0 auto}
|
||||||
blockquote, dd, dl, figure, h1, h2, h3, h4, h5, h6, hr, p, pre {margin: 0}`;
|
blockquote, dd, dl, figure, h1, h2, h3, h4, h5, h6, hr, p, pre {margin: 0}`;
|
||||||
|
|
||||||
export async function renderMermaid(): Promise<void> {
|
export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise<void> {
|
||||||
const els = document.querySelectorAll('.markup code.language-mermaid');
|
const el = elMarkup.querySelector('code.language-mermaid'); // .markup code.language-mermaid
|
||||||
if (!els.length) return;
|
if (!el) return;
|
||||||
|
|
||||||
const {default: mermaid} = await import(/* webpackChunkName: "mermaid" */'mermaid');
|
const {default: mermaid} = await import(/* webpackChunkName: "mermaid" */'mermaid');
|
||||||
|
|
||||||
|
@ -23,67 +23,65 @@ export async function renderMermaid(): Promise<void> {
|
||||||
suppressErrorRendering: true,
|
suppressErrorRendering: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const el of els) {
|
const pre = el.closest('pre');
|
||||||
const pre = el.closest('pre');
|
if (pre.hasAttribute('data-render-done')) return;
|
||||||
if (pre.hasAttribute('data-render-done')) continue;
|
|
||||||
|
|
||||||
const source = el.textContent;
|
const source = el.textContent;
|
||||||
if (mermaidMaxSourceCharacters >= 0 && source.length > mermaidMaxSourceCharacters) {
|
if (mermaidMaxSourceCharacters >= 0 && source.length > mermaidMaxSourceCharacters) {
|
||||||
displayError(pre, new Error(`Mermaid source of ${source.length} characters exceeds the maximum allowed length of ${mermaidMaxSourceCharacters}.`));
|
displayError(pre, new Error(`Mermaid source of ${source.length} characters exceeds the maximum allowed length of ${mermaidMaxSourceCharacters}.`));
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await mermaid.parse(source);
|
await mermaid.parse(source);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
displayError(pre, err);
|
displayError(pre, err);
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// can't use bindFunctions here because we can't cross the iframe boundary. This
|
// 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
|
// means js-based interactions won't work but they aren't intended to work either
|
||||||
const {svg} = await mermaid.render('mermaid', source);
|
const {svg} = await mermaid.render('mermaid', source);
|
||||||
|
|
||||||
const iframe = document.createElement('iframe');
|
const iframe = document.createElement('iframe');
|
||||||
iframe.classList.add('markup-render', 'tw-invisible');
|
iframe.classList.add('markup-content-iframe', 'tw-invisible');
|
||||||
iframe.srcdoc = `<html><head><style>${iframeCss}</style></head><body>${svg}</body></html>`;
|
iframe.srcdoc = `<html><head><style>${iframeCss}</style></head><body>${svg}</body></html>`;
|
||||||
|
|
||||||
const mermaidBlock = document.createElement('div');
|
const mermaidBlock = document.createElement('div');
|
||||||
mermaidBlock.classList.add('mermaid-block', 'is-loading', 'tw-hidden');
|
mermaidBlock.classList.add('mermaid-block', 'is-loading', 'tw-hidden');
|
||||||
mermaidBlock.append(iframe);
|
mermaidBlock.append(iframe);
|
||||||
|
|
||||||
const btn = makeCodeCopyButton();
|
const btn = makeCodeCopyButton();
|
||||||
btn.setAttribute('data-clipboard-text', source);
|
btn.setAttribute('data-clipboard-text', source);
|
||||||
mermaidBlock.append(btn);
|
mermaidBlock.append(btn);
|
||||||
|
|
||||||
const updateIframeHeight = () => {
|
const updateIframeHeight = () => {
|
||||||
const body = iframe.contentWindow?.document?.body;
|
const body = iframe.contentWindow?.document?.body;
|
||||||
if (body) {
|
if (body) {
|
||||||
iframe.style.height = `${body.clientHeight}px`;
|
iframe.style.height = `${body.clientHeight}px`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
iframe.addEventListener('load', () => {
|
iframe.addEventListener('load', () => {
|
||||||
pre.replaceWith(mermaidBlock);
|
pre.replaceWith(mermaidBlock);
|
||||||
mermaidBlock.classList.remove('tw-hidden');
|
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();
|
updateIframeHeight();
|
||||||
setTimeout(() => { // avoid flash of iframe background
|
}, {root: document.documentElement})).observe(iframe);
|
||||||
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
|
document.body.append(mermaidBlock);
|
||||||
// a <details> + <summary> block and the <details> block becomes visible upon user interaction, it
|
} catch (err) {
|
||||||
// would initially set a incorrect height and the correct height is set during this callback.
|
displayError(pre, err);
|
||||||
(new IntersectionObserver(() => {
|
|
||||||
updateIframeHeight();
|
|
||||||
}, {root: document.documentElement})).observe(iframe);
|
|
||||||
});
|
|
||||||
|
|
||||||
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.
|
* Attaches `input` handlers to markdown rendered tasklist checkboxes in comments.
|
||||||
*
|
*
|
||||||
* When a checkbox value changes, the corresponding [ ] or [x] in the markdown string
|
* 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.
|
* error it resets the checkbox to its original value.
|
||||||
*/
|
*/
|
||||||
export function initMarkupTasklist(): void {
|
export function initMarkupTasklist(elMarkup: HTMLElement): void {
|
||||||
for (const el of document.querySelectorAll(`.markup[data-can-edit=true]`) || []) {
|
if (!elMarkup.matches('[data-can-edit=true]')) return;
|
||||||
const container = el.parentNode;
|
|
||||||
const checkboxes = el.querySelectorAll<HTMLInputElement>(`.task-list-item input[type=checkbox]`);
|
|
||||||
|
|
||||||
for (const checkbox of checkboxes) {
|
const container = elMarkup.parentNode;
|
||||||
if (checkbox.hasAttribute('data-editable')) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkbox.setAttribute('data-editable', 'true');
|
// Prevent further inputs until the request is done. This does not use the
|
||||||
checkbox.addEventListener('input', async () => {
|
// `disabled` attribute because it causes the border to flash on click.
|
||||||
const checkboxCharacter = checkbox.checked ? 'x' : ' ';
|
for (const checkbox of checkboxes) {
|
||||||
const position = parseInt(checkbox.getAttribute('data-source-position')) + 1;
|
checkbox.addEventListener('click', preventListener);
|
||||||
|
}
|
||||||
|
|
||||||
const rawContent = container.querySelector('.raw-content');
|
try {
|
||||||
const oldContent = rawContent.textContent;
|
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 requestBody = new FormData();
|
||||||
const buffer = encoder.encode(oldContent);
|
requestBody.append('ignore_attachments', 'true');
|
||||||
// Indexes may fall off the ends and return undefined.
|
requestBody.append('content', newContent);
|
||||||
if (buffer[position - 1] !== '['.codePointAt(0) ||
|
requestBody.append('context', context);
|
||||||
buffer[position] !== ' '.codePointAt(0) && buffer[position] !== 'x'.codePointAt(0) ||
|
requestBody.append('content_version', contentVersion);
|
||||||
buffer[position + 1] !== ']'.codePointAt(0)) {
|
const response = await POST(updateUrl, {data: requestBody});
|
||||||
// Position is probably wrong. Revert and don't allow change.
|
const data = await response.json();
|
||||||
checkbox.checked = !checkbox.checked;
|
if (response.status === 400) {
|
||||||
throw new Error(`Expected position to be space or x and surrounded by brackets, but it's not: position=${position}`);
|
showErrorToast(data.errorMessage);
|
||||||
}
|
|
||||||
buffer.set(encoder.encode(checkboxCharacter), position);
|
|
||||||
const newContent = new TextDecoder().decode(buffer);
|
|
||||||
|
|
||||||
if (newContent === oldContent) {
|
|
||||||
return;
|
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
|
// Enable input on checkboxes again
|
||||||
// `disabled` attribute because it causes the border to flash on click.
|
for (const checkbox of checkboxes) {
|
||||||
for (const checkbox of checkboxes) {
|
checkbox.removeEventListener('click', preventListener);
|
||||||
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 the checkboxes as they are initially disabled by the markdown renderer
|
// Enable the checkboxes as they are initially disabled by the markdown renderer
|
||||||
for (const checkbox of checkboxes) {
|
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:
|
// It handles the global init functions by a selector, for example:
|
||||||
// > registerGlobalSelectorObserver('.ui.dropdown:not(.custom)', (el) => { initDropdown(el, ...) });
|
// > 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) {
|
export function registerGlobalSelectorFunc(selector: string, handler: (el: HTMLElement) => void) {
|
||||||
selectorHandlers.push({selector, handler});
|
selectorHandlers.push({selector, handler});
|
||||||
// Then initAddedElementObserver will call this handler for all existing elements after all handlers are added.
|
// 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 {htmlEscape} from 'escape-goat';
|
||||||
|
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
||||||
|
|
||||||
export async function initPdfViewer() {
|
export async function initPdfViewer() {
|
||||||
const els = document.querySelectorAll('.pdf-content');
|
registerGlobalInitFunc('initPdfViewer', async (el: HTMLInputElement) => {
|
||||||
if (!els.length) return;
|
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 src = el.getAttribute('data-src');
|
||||||
const fallbackText = el.getAttribute('data-fallback-button-text');
|
const fallbackText = el.getAttribute('data-fallback-button-text');
|
||||||
pdfobject.embed(src, el, {
|
pdfobject.embed(src, el, {
|
||||||
|
@ -15,5 +13,5 @@ export async function initPdfViewer() {
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
el.classList.remove('is-loading');
|
el.classList.remove('is-loading');
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue