mirror of
https://github.com/go-gitea/gitea.git
synced 2025-04-08 17:05:45 +02:00
Merge branch 'upstream_main' into add-file-tree-to-file-view-page
This commit is contained in:
commit
775d66bd33
@ -9,7 +9,9 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
webhook_model "code.gitea.io/gitea/models/webhook"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@ -308,12 +310,16 @@ func getPackagePayloadInfo(p *api.PackagePayload, linkFormatter linkFormatter, w
|
||||
}
|
||||
|
||||
func getStatusPayloadInfo(p *api.CommitStatusPayload, linkFormatter linkFormatter, withSender bool) (text string, color int) {
|
||||
refLink := linkFormatter(p.TargetURL, p.Context+"["+p.SHA+"]:"+p.Description)
|
||||
refLink := linkFormatter(p.TargetURL, fmt.Sprintf("%s [%s]", p.Context, base.ShortSha(p.SHA)))
|
||||
|
||||
text = fmt.Sprintf("Commit Status changed: %s", refLink)
|
||||
text = fmt.Sprintf("Commit Status changed: %s - %s", refLink, p.Description)
|
||||
color = greenColor
|
||||
if withSender {
|
||||
text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName))
|
||||
if user_model.IsGiteaActionsUserName(p.Sender.UserName) {
|
||||
text += fmt.Sprintf(" by %s", p.Sender.FullName)
|
||||
} else {
|
||||
text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName))
|
||||
}
|
||||
}
|
||||
|
||||
return text, color
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"strings"
|
||||
|
||||
webhook_model "code.gitea.io/gitea/models/webhook"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@ -245,8 +246,8 @@ func (m matrixConvertor) Package(p *api.PackagePayload) (MatrixPayload, error) {
|
||||
}
|
||||
|
||||
func (m matrixConvertor) Status(p *api.CommitStatusPayload) (MatrixPayload, error) {
|
||||
refLink := htmlLinkFormatter(p.TargetURL, p.Context+"["+p.SHA+"]:"+p.Description)
|
||||
text := fmt.Sprintf("Commit Status changed: %s", refLink)
|
||||
refLink := htmlLinkFormatter(p.TargetURL, fmt.Sprintf("%s [%s]", p.Context, base.ShortSha(p.SHA)))
|
||||
text := fmt.Sprintf("Commit Status changed: %s - %s", refLink, p.Description)
|
||||
|
||||
return m.newPayload(text)
|
||||
}
|
||||
|
@ -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}}
|
||||
|
@ -14,7 +14,8 @@
|
||||
|
||||
Search "repo/branch_dropdown" in the template directory to find all occurrences.
|
||||
*/}}
|
||||
<div class="js-branch-tag-selector {{if .ContainerClasses}}{{.ContainerClasses}}{{end}}"
|
||||
<div class="{{if .ContainerClasses}}{{.ContainerClasses}}{{end}}"
|
||||
data-global-init="initRepoBranchTagSelector"
|
||||
data-text-release-compare="{{ctx.Locale.Tr "repo.release.compare"}}"
|
||||
data-text-branches="{{ctx.Locale.Tr "repo.branches"}}"
|
||||
data-text-tags="{{ctx.Locale.Tr "repo.tags"}}"
|
||||
|
@ -48,7 +48,7 @@
|
||||
{{end}}
|
||||
</span>
|
||||
{{if IsMultilineCommitMessage .Message}}
|
||||
<button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button>
|
||||
<button class="ui button ellipsis-button" aria-expanded="false" data-global-click="onRepoEllipsisButtonClick">...</button>
|
||||
{{end}}
|
||||
{{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}}
|
||||
{{if IsMultilineCommitMessage .Message}}
|
||||
|
@ -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}}
|
||||
|
@ -23,7 +23,7 @@
|
||||
{{$commitLink:= printf "%s/commit/%s" .RepoLink (PathEscape .LatestCommit.ID.String)}}
|
||||
<span class="grey commit-summary" title="{{.LatestCommit.Summary}}"><span class="message-wrapper">{{ctx.RenderUtils.RenderCommitMessageLinkSubject .LatestCommit.Message $commitLink ($.Repository.ComposeMetas ctx)}}</span>
|
||||
{{if IsMultilineCommitMessage .LatestCommit.Message}}
|
||||
<button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button>
|
||||
<button class="ui button ellipsis-button" aria-expanded="false" data-global-click="onRepoEllipsisButtonClick">...</button>
|
||||
<pre class="commit-body tw-hidden">{{ctx.RenderUtils.RenderCommitBody .LatestCommit.Message ($.Repository.ComposeMetas ctx)}}</pre>
|
||||
{{end}}
|
||||
</span>
|
||||
|
@ -21,6 +21,7 @@
|
||||
{{$compareTarget = $release.Sha1}}
|
||||
{{end}}
|
||||
{{template "repo/branch_dropdown" dict
|
||||
"ContainerClasses" "release-branch-tag-selector"
|
||||
"Repository" $.Repository
|
||||
"ShowTabTags" true
|
||||
"DropdownFixedText" (ctx.Locale.Tr "repo.release.compare")
|
||||
@ -64,7 +65,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}}
|
||||
|
@ -55,7 +55,7 @@
|
||||
{{end}}
|
||||
</div>
|
||||
<a download class="btn-octicon" data-tooltip-content="{{ctx.Locale.Tr "repo.download_file"}}" href="{{$.RawFileLink}}">{{svg "octicon-download"}}</a>
|
||||
<a id="copy-content" class="btn-octicon {{if not .CanCopyContent}} disabled{{end}}"{{if or .IsImageFile (and .HasSourceRenderedToggle (not .IsDisplayingSource))}} data-link="{{$.RawFileLink}}"{{end}} data-tooltip-content="{{if .CanCopyContent}}{{ctx.Locale.Tr "copy_content"}}{{else}}{{ctx.Locale.Tr "copy_type_unsupported"}}{{end}}">{{svg "octicon-copy"}}</a>
|
||||
<a class="btn-octicon {{if not .CanCopyContent}} disabled{{end}}" data-global-click="onCopyContentButtonClick" {{if or .IsImageFile (and .HasSourceRenderedToggle (not .IsDisplayingSource))}} data-link="{{$.RawFileLink}}"{{end}} data-tooltip-content="{{if .CanCopyContent}}{{ctx.Locale.Tr "copy_content"}}{{else}}{{ctx.Locale.Tr "copy_type_unsupported"}}{{end}}">{{svg "octicon-copy"}}</a>
|
||||
{{if .EnableFeed}}
|
||||
<a class="btn-octicon" href="{{$.RepoLink}}/rss/{{$.RefTypeNameSubURL}}/{{PathEscapeSegments .TreePath}}" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}">
|
||||
{{svg "octicon-rss"}}
|
||||
@ -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%;
|
||||
|
@ -45,7 +45,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
#release-list .js-branch-tag-selector {
|
||||
#release-list .release-branch-tag-selector {
|
||||
margin-left: auto;
|
||||
}
|
||||
#release-list .branch-selector-dropdown .menu { /* open menu to left */
|
||||
|
@ -12,7 +12,7 @@
|
||||
border-top: 1px solid var(--color-secondary);
|
||||
}
|
||||
|
||||
.milestone-card .content {
|
||||
.milestone-card .render-content {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {POST} from '../modules/fetch.ts';
|
||||
import {addDelegatedEventListener, hideElem, queryElems, showElem, toggleElem} from '../utils/dom.ts';
|
||||
import {addDelegatedEventListener, hideElem, showElem, toggleElem} from '../utils/dom.ts';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
import {camelize} from 'vue';
|
||||
|
||||
@ -74,10 +74,9 @@ export function initGlobalDeleteButton(): void {
|
||||
}
|
||||
}
|
||||
|
||||
function onShowPanelClick(e: MouseEvent) {
|
||||
function onShowPanelClick(el: HTMLElement, e: MouseEvent) {
|
||||
// a '.show-panel' element can show a panel, by `data-panel="selector"`
|
||||
// if it has "toggle" class, it toggles the panel
|
||||
const el = e.currentTarget as HTMLElement;
|
||||
e.preventDefault();
|
||||
const sel = el.getAttribute('data-panel');
|
||||
if (el.classList.contains('toggle')) {
|
||||
@ -87,9 +86,8 @@ function onShowPanelClick(e: MouseEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
function onHidePanelClick(e: MouseEvent) {
|
||||
function onHidePanelClick(el: HTMLElement, e: MouseEvent) {
|
||||
// a `.hide-panel` element can hide a panel, by `data-panel="selector"` or `data-panel-closest="selector"`
|
||||
const el = e.currentTarget as HTMLElement;
|
||||
e.preventDefault();
|
||||
let sel = el.getAttribute('data-panel');
|
||||
if (sel) {
|
||||
@ -104,7 +102,7 @@ function onHidePanelClick(e: MouseEvent) {
|
||||
throw new Error('no panel to hide'); // should never happen, otherwise there is a bug in code
|
||||
}
|
||||
|
||||
function onShowModalClick(e: MouseEvent) {
|
||||
function onShowModalClick(el: HTMLElement, e: MouseEvent) {
|
||||
// A ".show-modal" button will show a modal dialog defined by its "data-modal" attribute.
|
||||
// Each "data-modal-{target}" attribute will be filled to target element's value or text-content.
|
||||
// * First, try to query '#target'
|
||||
@ -112,7 +110,6 @@ function onShowModalClick(e: MouseEvent) {
|
||||
// * Then, try to query '.target'
|
||||
// * Then, try to query 'target' as HTML tag
|
||||
// If there is a ".{attr}" part like "data-modal-form.action", then the form's "action" attribute will be set.
|
||||
const el = e.currentTarget as HTMLElement;
|
||||
e.preventDefault();
|
||||
const modalSelector = el.getAttribute('data-modal');
|
||||
const elModal = document.querySelector(modalSelector);
|
||||
@ -160,7 +157,15 @@ export function initGlobalButtons(): void {
|
||||
// There are a few cancel buttons in non-modal forms, and there are some dynamically created forms (eg: the "Edit Issue Content")
|
||||
addDelegatedEventListener(document, 'click', 'form button.ui.cancel.button', (_ /* el */, e) => e.preventDefault());
|
||||
|
||||
queryElems(document, '.show-panel', (el) => el.addEventListener('click', onShowPanelClick));
|
||||
queryElems(document, '.hide-panel', (el) => el.addEventListener('click', onHidePanelClick));
|
||||
queryElems(document, '.show-modal', (el) => el.addEventListener('click', onShowModalClick));
|
||||
// Ideally these "button" events should be handled by registerGlobalEventFunc
|
||||
// Refactoring would involve too many changes, so at the moment, just use the global event listener.
|
||||
addDelegatedEventListener(document, 'click', '.show-panel, .hide-panel, .show-modal', (el, e: MouseEvent) => {
|
||||
if (el.classList.contains('show-panel')) {
|
||||
onShowPanelClick(el, e);
|
||||
} else if (el.classList.contains('hide-panel')) {
|
||||
onHidePanelClick(el, e);
|
||||
} else if (el.classList.contains('show-modal')) {
|
||||
onShowModalClick(el, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -2,21 +2,19 @@ import {clippie} from 'clippie';
|
||||
import {showTemporaryTooltip} from '../modules/tippy.ts';
|
||||
import {convertImage} from '../utils.ts';
|
||||
import {GET} from '../modules/fetch.ts';
|
||||
import {registerGlobalEventFunc} from '../modules/observer.ts';
|
||||
|
||||
const {i18n} = window.config;
|
||||
|
||||
export function initCopyContent() {
|
||||
const btn = document.querySelector('#copy-content');
|
||||
if (!btn || btn.classList.contains('disabled')) return;
|
||||
|
||||
btn.addEventListener('click', async () => {
|
||||
if (btn.classList.contains('is-loading')) return;
|
||||
registerGlobalEventFunc('click', 'onCopyContentButtonClick', async (btn: HTMLInputElement) => {
|
||||
if (btn.classList.contains('disabled') || btn.classList.contains('is-loading')) return;
|
||||
let content;
|
||||
let isRasterImage = false;
|
||||
const link = btn.getAttribute('data-link');
|
||||
|
||||
// when data-link is present, we perform a fetch. this is either because
|
||||
// the text to copy is not in the DOM or it is an image which should be
|
||||
// the text to copy is not in the DOM, or it is an image which should be
|
||||
// fetched to copy in full resolution
|
||||
if (link) {
|
||||
btn.classList.add('is-loading', 'loading-icon-2px');
|
||||
@ -40,7 +38,7 @@ export function initCopyContent() {
|
||||
content = Array.from(lineEls, (el) => el.textContent).join('');
|
||||
}
|
||||
|
||||
// try copy original first, if that fails and it's an image, convert it to png
|
||||
// try copy original first, if that fails, and it's an image, convert it to png
|
||||
const success = await clippie(content);
|
||||
if (success) {
|
||||
showTemporaryTooltip(btn, i18n.copy_success);
|
||||
|
@ -1,15 +1,14 @@
|
||||
import {createTippy} from '../modules/tippy.ts';
|
||||
import {toggleElem} from '../utils/dom.ts';
|
||||
import {registerGlobalEventFunc} from '../modules/observer.ts';
|
||||
|
||||
export function initRepoEllipsisButton() {
|
||||
for (const button of document.querySelectorAll<HTMLButtonElement>('.js-toggle-commit-body')) {
|
||||
button.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const expanded = this.getAttribute('aria-expanded') === 'true';
|
||||
toggleElem(this.parentElement.querySelector('.commit-body'));
|
||||
this.setAttribute('aria-expanded', String(!expanded));
|
||||
});
|
||||
}
|
||||
registerGlobalEventFunc('click', 'onRepoEllipsisButtonClick', async (el: HTMLInputElement, e: Event) => {
|
||||
e.preventDefault();
|
||||
const expanded = el.getAttribute('aria-expanded') === 'true';
|
||||
toggleElem(el.parentElement.querySelector('.commit-body'));
|
||||
el.setAttribute('aria-expanded', String(!expanded));
|
||||
});
|
||||
}
|
||||
|
||||
export function initCommitStatuses() {
|
||||
|
@ -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,3 +1,4 @@
|
||||
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
||||
import {
|
||||
initRepoCommentFormAndSidebar,
|
||||
initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel, initRepoIssueCommentDelete,
|
||||
@ -20,10 +21,10 @@ import {initRepoNew} from './repo-new.ts';
|
||||
import {createApp} from 'vue';
|
||||
import RepoBranchTagSelector from '../components/RepoBranchTagSelector.vue';
|
||||
|
||||
function initRepoBranchTagSelector(selector: string) {
|
||||
for (const elRoot of document.querySelectorAll(selector)) {
|
||||
function initRepoBranchTagSelector() {
|
||||
registerGlobalInitFunc('initRepoBranchTagSelector', async (elRoot: HTMLInputElement) => {
|
||||
createApp(RepoBranchTagSelector, {elRoot}).mount(elRoot);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function initBranchSelectorTabs() {
|
||||
@ -42,7 +43,7 @@ export function initRepository() {
|
||||
const pageContent = document.querySelector('.page-content.repository');
|
||||
if (!pageContent) return;
|
||||
|
||||
initRepoBranchTagSelector('.js-branch-tag-selector');
|
||||
initRepoBranchTagSelector();
|
||||
initRepoCommentFormAndSidebar();
|
||||
|
||||
// Labels
|
||||
|
@ -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';
|
||||
@ -103,7 +103,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…
x
Reference in New Issue
Block a user