diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index ad77177db4..f1c259f824 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -384,18 +384,19 @@ func (r *HTMLRenderer) renderTaskCheckBoxListItem(w util.BufWriter, source []byt } else { _, _ = w.WriteString("
  • ") } - end := ">" - if r.XHTML { - end = " />" + _, _ = w.WriteString(` 0 { + segment := segments.At(0) + _, _ = w.WriteString(fmt.Sprintf(` data-source-position="%d"`, segment.Start)) } - var err error if n.IsChecked { - _, err = w.WriteString(``) + } else { + _ = w.WriteByte('>') } fc := n.FirstChild() if fc != nil { diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index 46a380aa6c..76c6d28d07 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -166,9 +166,9 @@ func testAnswers(baseURLContent, baseURLImages string) []string {

    (from https://www.markdownguide.org/extended-syntax/)

    Checkboxes

    Definition list

    diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go index 9f336d8330..0e05ddb085 100644 --- a/modules/markup/sanitizer.go +++ b/modules/markup/sanitizer.go @@ -43,7 +43,7 @@ func ReplaceSanitizer() { // Checkboxes sanitizer.policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input") - sanitizer.policy.AllowAttrs("checked", "disabled").OnElements("input") + sanitizer.policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input") // Custom URL-Schemes if len(setting.Markdown.CustomURLSchemes) > 0 { diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl index c55da8576e..86e314dc50 100644 --- a/templates/repo/diff/comments.tmpl +++ b/templates/repo/diff/comments.tmpl @@ -51,7 +51,7 @@
    -
    +
    {{if .RenderedContent}} {{.RenderedContent|Str2html}} {{else}} diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl index 35c7cdd7a2..00ce61921d 100644 --- a/templates/repo/issue/view_content.tmpl +++ b/templates/repo/issue/view_content.tmpl @@ -57,7 +57,7 @@
    -
    +
    {{if .Issue.RenderedContent}} {{.Issue.RenderedContent|Str2html}} {{else}} diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 77757207cf..53005cc820 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -64,7 +64,7 @@
    -
    +
    {{if .RenderedContent}} {{.RenderedContent|Str2html}} {{else}} @@ -552,7 +552,7 @@
    -
    +
    {{if .RenderedContent}} {{.RenderedContent|Str2html}} {{else}} diff --git a/web_src/js/index.js b/web_src/js/index.js index a01bd6ca08..c3d74b1395 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -21,8 +21,8 @@ import {createCodeEditor, createMonaco} from './features/codeeditor.js'; import {initMarkupAnchors} from './markup/anchors.js'; import {initNotificationsTable, initNotificationCount} from './features/notification.js'; import {initStopwatch} from './features/stopwatch.js'; -import {renderMarkupContent} from './markup/content.js'; import {showLineButton} from './code/linebutton.js'; +import {initMarkupContent, initCommentContent} from './markup/content.js'; import {stripTags, mqBinarySearch} from './utils.js'; import {svg, svgs} from './svg.js'; @@ -52,7 +52,7 @@ function initCommentPreviewTab($form) { }, (data) => { const $previewPanel = $form.find(`.tab[data-tab="${$tabMenu.data('preview')}"]`); $previewPanel.html(data); - renderMarkupContent(); + initMarkupContent(); }); }); @@ -82,7 +82,7 @@ function initEditPreviewTab($form) { }, (data) => { const $previewPanel = $form.find(`.tab[data-tab="${$tabMenu.data('preview')}"]`); $previewPanel.html(data); - renderMarkupContent(); + initMarkupContent(); }); }); } @@ -1108,7 +1108,8 @@ async function initRepository() { dz.emit('submit'); dz.emit('reload'); } - renderMarkupContent(); + initMarkupContent(); + initCommentContent(); }); }); } else { @@ -1481,7 +1482,7 @@ function initWikiForm() { wiki: true }, (data) => { preview.innerHTML = `
    ${data}
    `; - renderMarkupContent(); + initMarkupContent(); }); }; @@ -2732,6 +2733,7 @@ $(document).ready(async () => { searchRepositories(); initMarkupAnchors(); + initCommentContent(); initCommentForm(); initInstall(); initArchiveLinks(); @@ -2789,7 +2791,7 @@ $(document).ready(async () => { initServiceWorker(), initNotificationCount(), initStopwatch(), - renderMarkupContent(), + initMarkupContent(), initGithook(), initImageDiff(), ]); diff --git a/web_src/js/markup/content.js b/web_src/js/markup/content.js index f06c9908f2..19b749aaab 100644 --- a/web_src/js/markup/content.js +++ b/web_src/js/markup/content.js @@ -1,5 +1,12 @@ import {renderMermaid} from './mermaid.js'; +import {initMarkupTasklist} from './tasklist.js'; -export async function renderMarkupContent() { +// code that runs for all markup content +export async function initMarkupContent() { await renderMermaid(document.querySelectorAll('code.language-mermaid')); } + +// code that only runs for comments +export function initCommentContent() { + initMarkupTasklist(); +} diff --git a/web_src/js/markup/tasklist.js b/web_src/js/markup/tasklist.js new file mode 100644 index 0000000000..5a7291b4d0 --- /dev/null +++ b/web_src/js/markup/tasklist.js @@ -0,0 +1,62 @@ +/** + * 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 + * error it resets the checkbox to its original value. + */ + +const preventListener = (e) => e.preventDefault(); + +export function initMarkupTasklist() { + for (const el of document.querySelectorAll(`.markup[data-can-edit=true]`) || []) { + const container = el.parentNode; + const checkboxes = el.querySelectorAll(`.task-list-item input[type=checkbox]`); + + for (const checkbox of checkboxes) { + if (checkbox.dataset.editable) return; + checkbox.dataset.editable = 'true'; + checkbox.addEventListener('input', async () => { + const checkboxCharacter = checkbox.checked ? 'x' : ' '; + const position = parseInt(checkbox.dataset.sourcePosition) + 1; + + const rawContent = container.querySelector('.raw-content'); + const oldContent = rawContent.textContent; + const newContent = oldContent.substring(0, position) + checkboxCharacter + oldContent.substring(position + 1); + if (newContent === oldContent) return; + + // 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('.edit-content-zone'); + const {updateUrl, context} = editContentZone.dataset; + + await $.post(updateUrl, { + _csrf: window.config.csrf, + content: newContent, + context, + }); + + rawContent.textContent = newContent; + } catch (err) { + checkbox.checked = !checkbox.checked; + console.error(err); + } + + // Enable input on checkboxes again + for (const checkbox of checkboxes) { + checkbox.removeEventListener('click', preventListener); + } + }); + } + + // Enable the checkboxes as they are initially disabled by the markdown renderer + for (const checkbox of checkboxes) { + checkbox.disabled = false; + } + } +}