From eefc914741c331c8eb46aeee0bf53294a441f47b Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Sun, 9 May 2021 06:59:48 +0000 Subject: [PATCH 1/8] Update content on checkbox change. --- modules/markup/markdown/goldmark.go | 19 ++++---- modules/markup/sanitizer.go | 2 +- templates/repo/diff/comments.tmpl | 2 +- templates/repo/issue/view_content.tmpl | 2 +- .../repo/issue/view_content/comments.tmpl | 4 +- web_src/js/index.js | 2 + web_src/js/markup/tasklist.js | 47 +++++++++++++++++++ 7 files changed, 64 insertions(+), 14 deletions(-) create mode 100644 web_src/js/markup/tasklist.js diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index ad77177db439a..f1c259f82429d 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/sanitizer.go b/modules/markup/sanitizer.go index 9f336d8330d09..0e05ddb085e29 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 c55da8576e185..9c088d5587bec 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 35c7cdd7a2c30..00ce61921d0e6 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 77757207cf2e9..53005cc82032b 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 6cb47cf104998..90bc4f3725874 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -19,6 +19,7 @@ import initServiceWorker from './features/serviceworker.js'; import initTableSort from './features/tablesort.js'; import {createCodeEditor, createMonaco} from './features/codeeditor.js'; import {initMarkupAnchors} from './markup/anchors.js'; +import initMarkupTasklist from './markup/tasklist.js'; import {initNotificationsTable, initNotificationCount} from './features/notification.js'; import {initStopwatch} from './features/stopwatch.js'; import {renderMarkupContent} from './markup/content.js'; @@ -2733,6 +2734,7 @@ $(document).ready(async () => { searchRepositories(); initMarkupAnchors(); + initMarkupTasklist(); initCommentForm(); initInstall(); initArchiveLinks(); diff --git a/web_src/js/markup/tasklist.js b/web_src/js/markup/tasklist.js new file mode 100644 index 0000000000000..f6e26b6910f05 --- /dev/null +++ b/web_src/js/markup/tasklist.js @@ -0,0 +1,47 @@ +/** + * Attaches `change` 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. + */ +export default function initMarkupTasklist() { + $(`.render-content.markup[data-can-edit='true']`).parent().each((_, container) => { + const $container = $(container); + const $checkboxes = $container.find(`.task-list-item input:checkbox`); + + $checkboxes.on('change', async (ev) => { + const $checkbox = $(ev.target); + const checkboxCharacter = $checkbox.is(':checked') ? 'x' : ' '; + const position = parseInt($checkbox.data('source-position')) + 1; + + const $rawContent = $container.find('.raw-content'); + const oldContent = $rawContent.text(); + const newContent = oldContent.substring(0, position) + checkboxCharacter + oldContent.substring(position + 1); + + if (newContent !== oldContent) { + $checkboxes.prop('disabled', true); + + try { + const $contentZone = $container.find('.edit-content-zone'); + const url = $contentZone.data('update-url'); + const context = $contentZone.data('context'); + + await $.post(url, { + _csrf: window.config.csrf, + content: newContent, + context, + }); + + $rawContent.text(newContent); + } catch (e) { + $checkbox.prop('checked', !$checkbox.is(':checked')); + + console.error(e); + } finally { + $checkboxes.prop('disabled', false); + } + } + }); + + $checkboxes.prop('disabled', false); + }); +} From 035c60ee2a8a87e26a8f3e7a0dedf3b87eaf5b44 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Sun, 9 May 2021 09:25:56 +0000 Subject: [PATCH 2/8] Fixed test. --- modules/markup/markdown/markdown_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index 46a380aa6c2ee..76c6d28d07b7d 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

      -
    • unchecked
    • -
    • checked
    • -
    • still unchecked
    • +
    • unchecked
    • +
    • checked
    • +
    • still unchecked

    Definition list

    From a9a85a33d8a4e70409959d05a6da782b2efbbeb3 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Mon, 10 May 2021 14:44:10 +0000 Subject: [PATCH 3/8] Replaced jQuery with vanilla javascript. --- web_src/js/markup/tasklist.js | 36 +++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/web_src/js/markup/tasklist.js b/web_src/js/markup/tasklist.js index f6e26b6910f05..58f337a233694 100644 --- a/web_src/js/markup/tasklist.js +++ b/web_src/js/markup/tasklist.js @@ -4,26 +4,26 @@ * On success it updates the raw-content on error it resets the checkbox to its original value. */ export default function initMarkupTasklist() { - $(`.render-content.markup[data-can-edit='true']`).parent().each((_, container) => { - const $container = $(container); - const $checkboxes = $container.find(`.task-list-item input:checkbox`); + document.querySelectorAll(`.render-content.markup[data-can-edit='true']`).forEach((el) => { + const container = el.parentNode; + const checkboxes = container.querySelectorAll(`.task-list-item input[type=checkbox]`); - $checkboxes.on('change', async (ev) => { - const $checkbox = $(ev.target); - const checkboxCharacter = $checkbox.is(':checked') ? 'x' : ' '; - const position = parseInt($checkbox.data('source-position')) + 1; + checkboxes.forEach((cb) => cb.addEventListener('change', async (ev) => { + const checkbox = ev.target; + const checkboxCharacter = checkbox.checked ? 'x' : ' '; + const position = parseInt(checkbox.dataset.sourcePosition) + 1; - const $rawContent = $container.find('.raw-content'); - const oldContent = $rawContent.text(); + const rawContent = container.querySelector('.raw-content'); + const oldContent = rawContent.textContent; const newContent = oldContent.substring(0, position) + checkboxCharacter + oldContent.substring(position + 1); if (newContent !== oldContent) { - $checkboxes.prop('disabled', true); + checkboxes.forEach((cb) => cb.disabled = true); try { - const $contentZone = $container.find('.edit-content-zone'); - const url = $contentZone.data('update-url'); - const context = $contentZone.data('context'); + const contentZone = container.querySelector('.edit-content-zone'); + const url = contentZone.dataset.updateUrl; + const context = contentZone.dataset.context; await $.post(url, { _csrf: window.config.csrf, @@ -31,17 +31,17 @@ export default function initMarkupTasklist() { context, }); - $rawContent.text(newContent); + rawContent.textContent = newContent; } catch (e) { - $checkbox.prop('checked', !$checkbox.is(':checked')); + checkbox.checked = !checkbox.checked; console.error(e); } finally { - $checkboxes.prop('disabled', false); + checkboxes.forEach((cb) => cb.disabled = false); } } - }); + })); - $checkboxes.prop('disabled', false); + checkboxes.forEach((cb) => cb.disabled = false); }); } From 2e16e955ee364ffc6180caa0d8809e0bad3022b9 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 19 May 2021 22:32:17 +0200 Subject: [PATCH 4/8] js improvements --- web_src/js/markup/tasklist.js | 72 ++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/web_src/js/markup/tasklist.js b/web_src/js/markup/tasklist.js index 58f337a233694..9f2003e61fd4d 100644 --- a/web_src/js/markup/tasklist.js +++ b/web_src/js/markup/tasklist.js @@ -1,47 +1,59 @@ /** - * Attaches `change` 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. + * 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. */ -export default function initMarkupTasklist() { - document.querySelectorAll(`.render-content.markup[data-can-edit='true']`).forEach((el) => { - const container = el.parentNode; - const checkboxes = container.querySelectorAll(`.task-list-item input[type=checkbox]`); - checkboxes.forEach((cb) => cb.addEventListener('change', async (ev) => { - const checkbox = ev.target; - const checkboxCharacter = checkbox.checked ? 'x' : ' '; - const position = parseInt(checkbox.dataset.sourcePosition) + 1; +const preventListener = (e) => e.preventDefault(); - const rawContent = container.querySelector('.raw-content'); - const oldContent = rawContent.textContent; - const newContent = oldContent.substring(0, position) + checkboxCharacter + oldContent.substring(position + 1); - - if (newContent !== oldContent) { - checkboxes.forEach((cb) => cb.disabled = true); +export default 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) { + 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 contentZone = container.querySelector('.edit-content-zone'); - const url = contentZone.dataset.updateUrl; - const context = contentZone.dataset.context; + const editContentZone = container.querySelector('.edit-content-zone'); + const {updateUrl, context} = editContentZone.dataset; - await $.post(url, { + await $.post(updateUrl, { _csrf: window.config.csrf, content: newContent, context, }); rawContent.textContent = newContent; - } catch (e) { + } catch (err) { checkbox.checked = !checkbox.checked; - - console.error(e); - } finally { - checkboxes.forEach((cb) => cb.disabled = false); + console.error(err); } - } - })); - checkboxes.forEach((cb) => cb.disabled = false); - }); + // 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; + } + } } From a93f192c3a32125113b617c7792e62f9e328f5ae Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 22 May 2021 00:59:22 +0200 Subject: [PATCH 5/8] fix checkboxes after comment edit --- web_src/js/index.js | 16 ++++++++-------- web_src/js/markup/content.js | 9 ++++++++- web_src/js/markup/tasklist.js | 8 +++++--- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/web_src/js/index.js b/web_src/js/index.js index 90bc4f3725874..e6c88df0bc904 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -19,11 +19,10 @@ import initServiceWorker from './features/serviceworker.js'; import initTableSort from './features/tablesort.js'; import {createCodeEditor, createMonaco} from './features/codeeditor.js'; import {initMarkupAnchors} from './markup/anchors.js'; -import initMarkupTasklist from './markup/tasklist.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'; @@ -53,7 +52,7 @@ function initCommentPreviewTab($form) { }, (data) => { const $previewPanel = $form.find(`.tab[data-tab="${$tabMenu.data('preview')}"]`); $previewPanel.html(data); - renderMarkupContent(); + initMarkupContent(); }); }); @@ -83,7 +82,7 @@ function initEditPreviewTab($form) { }, (data) => { const $previewPanel = $form.find(`.tab[data-tab="${$tabMenu.data('preview')}"]`); $previewPanel.html(data); - renderMarkupContent(); + initMarkupContent(); }); }); } @@ -1109,7 +1108,8 @@ async function initRepository() { dz.emit('submit'); dz.emit('reload'); } - renderMarkupContent(); + initMarkupContent(); + initCommentContent(); }); }); } else { @@ -1482,7 +1482,7 @@ function initWikiForm() { wiki: true }, (data) => { preview.innerHTML = `
    ${data}
    `; - renderMarkupContent(); + initMarkupContent(); }); }; @@ -2734,7 +2734,7 @@ $(document).ready(async () => { searchRepositories(); initMarkupAnchors(); - initMarkupTasklist(); + initCommentContent(); initCommentForm(); initInstall(); initArchiveLinks(); @@ -2792,7 +2792,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 f06c9908f2856..adb3dbee4f4a9 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 async function initCommentContent() { + initMarkupTasklist(); +} diff --git a/web_src/js/markup/tasklist.js b/web_src/js/markup/tasklist.js index 9f2003e61fd4d..2722de900e452 100644 --- a/web_src/js/markup/tasklist.js +++ b/web_src/js/markup/tasklist.js @@ -1,5 +1,7 @@ /** * Attaches `input` handlers to markdown rendered tasklist checkboxes in comments. + * The code assumes the checkbox will be initially disabled by the markdown renderer. + * * 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. @@ -7,10 +9,10 @@ const preventListener = (e) => e.preventDefault(); -export default function initMarkupTasklist() { - for (const el of document.querySelectorAll(`.markup[data-can-edit='true']`) || []) { +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]`); + const checkboxes = el.querySelectorAll(`.task-list-item input[type=checkbox][disabled]`); for (const checkbox of checkboxes) { checkbox.addEventListener('input', async () => { From ed87af7e782a0d4aac2ab77d0720a1ed5406a7a2 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 22 May 2021 01:18:22 +0200 Subject: [PATCH 6/8] add tracking dataset for bound checkboxes --- web_src/js/markup/tasklist.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web_src/js/markup/tasklist.js b/web_src/js/markup/tasklist.js index 2722de900e452..5a7291b4d0083 100644 --- a/web_src/js/markup/tasklist.js +++ b/web_src/js/markup/tasklist.js @@ -1,6 +1,5 @@ /** * Attaches `input` handlers to markdown rendered tasklist checkboxes in comments. - * The code assumes the checkbox will be initially disabled by the markdown renderer. * * 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 @@ -12,9 +11,11 @@ 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][disabled]`); + 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; From 9c584e3eb5da55ed9d3f5816bf85d02bd5c4d799 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 22 May 2021 02:40:06 +0200 Subject: [PATCH 7/8] remove useless async --- web_src/js/markup/content.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/markup/content.js b/web_src/js/markup/content.js index adb3dbee4f4a9..19b749aaabfc3 100644 --- a/web_src/js/markup/content.js +++ b/web_src/js/markup/content.js @@ -7,6 +7,6 @@ export async function initMarkupContent() { } // code that only runs for comments -export async function initCommentContent() { +export function initCommentContent() { initMarkupTasklist(); } From 2b718c51377324046e5cb7fd32a8cfa1556757ec Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Sat, 22 May 2021 12:15:11 +0000 Subject: [PATCH 8/8] Fixed comparison. --- templates/repo/diff/comments.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl index 9c088d5587bec..86e314dc50756 100644 --- a/templates/repo/diff/comments.tmpl +++ b/templates/repo/diff/comments.tmpl @@ -51,7 +51,7 @@
    -
    +
    {{if .RenderedContent}} {{.RenderedContent|Str2html}} {{else}}