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
Kerwin Bryant 2025-03-04 03:49:15 +08:00 committed by GitHub
parent 43c8d85f19
commit f0f1737d4d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 195 additions and 227 deletions

View File

@ -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" .}}

View File

@ -74,9 +74,7 @@
{{end}}
</div>
{{if .Description}}
<div class="content">
{{.RenderedContent}}
</div>
<div class="render-content markup">{{.RenderedContent}}</div>
{{end}}
</li>
{{end}}

View File

@ -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>

View File

@ -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>

View File

@ -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}}

View File

@ -81,9 +81,7 @@
{{end}}
</div>
{{if .Content}}
<div class="markup content">
{{.RenderedContent}}
</div>
<div class="render-content markup">{{.RenderedContent}}</div>
{{end}}
</li>
{{end}}

View File

@ -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>

View File

@ -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}}

View File

@ -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}}

View File

@ -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}}

View File

@ -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">

View File

@ -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>

View File

@ -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}}

View File

@ -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}}

View File

@ -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;
}

View File

@ -535,7 +535,7 @@
user-select: none;
}
.markup-render {
.markup-content-iframe {
display: block;
border: none;
width: 100%;

View File

@ -12,7 +12,7 @@
border-top: 1px solid var(--color-secondary);
}
.milestone-card .content {
.milestone-card .render-content {
padding-top: 10px;
}

View File

@ -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'));
}

View File

@ -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);

View File

@ -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 {

View File

@ -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,

View File

@ -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',
});
}

View File

@ -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);
}

View File

@ -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);
});
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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) {

View File

@ -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.

View File

@ -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');
}
});
}