diff --git a/web_src/js/features/comp/ComboMarkdownEditor.ts b/web_src/js/features/comp/ComboMarkdownEditor.ts index 468f3fc5ca..f16a71a6c5 100644 --- a/web_src/js/features/comp/ComboMarkdownEditor.ts +++ b/web_src/js/features/comp/ComboMarkdownEditor.ts @@ -10,6 +10,7 @@ import { } from './EditorUpload.ts'; import {handleGlobalEnterQuickSubmit} from './QuickSubmit.ts'; import {renderPreviewPanelContent} from '../repo-editor.ts'; +import {toggleTasklistCheckbox} from '../../markup/tasklist.ts'; import {easyMDEToolbarActions} from './EasyMDEToolbarActions.ts'; import {initTextExpander} from './TextExpander.ts'; import {showErrorToast} from '../../modules/toast.ts'; @@ -236,6 +237,20 @@ export class ComboMarkdownEditor { const response = await POST(this.previewUrl, {data: formData}); const data = await response.text(); renderPreviewPanelContent(panelPreviewer, data); + // enable task list checkboxes in preview and sync state back to the editor + for (const checkbox of panelPreviewer.querySelectorAll('.task-list-item input[type=checkbox]')) { + checkbox.disabled = false; + checkbox.addEventListener('input', () => { + const position = parseInt(checkbox.getAttribute('data-source-position')!) + 1; + const newContent = toggleTasklistCheckbox(this.value(), position, checkbox.checked); + if (newContent === null) { + checkbox.checked = !checkbox.checked; + return; + } + this.value(newContent); + triggerEditorContentChanged(this.container); + }); + } }); } diff --git a/web_src/js/markup/tasklist.test.ts b/web_src/js/markup/tasklist.test.ts new file mode 100644 index 0000000000..ec5eceebd0 --- /dev/null +++ b/web_src/js/markup/tasklist.test.ts @@ -0,0 +1,9 @@ +import {toggleTasklistCheckbox} from './tasklist.ts'; + +test('toggleTasklistCheckbox', () => { + expect(toggleTasklistCheckbox('- [ ] task', 3, true)).toEqual('- [x] task'); + expect(toggleTasklistCheckbox('- [x] task', 3, false)).toEqual('- [ ] task'); + expect(toggleTasklistCheckbox('- [ ] task', 0, true)).toBeNull(); + expect(toggleTasklistCheckbox('- [ ] task', 99, true)).toBeNull(); + expect(toggleTasklistCheckbox('😀 - [ ] task', 8, true)).toEqual('😀 - [x] task'); +}); diff --git a/web_src/js/markup/tasklist.ts b/web_src/js/markup/tasklist.ts index 7f3417c2bb..557afeaea5 100644 --- a/web_src/js/markup/tasklist.ts +++ b/web_src/js/markup/tasklist.ts @@ -3,6 +3,23 @@ import {showErrorToast} from '../modules/toast.ts'; const preventListener = (e: Event) => e.preventDefault(); +/** + * Toggle a task list checkbox in markdown content. + * `position` is the byte offset of the space or `x` character inside `[ ]`. + * Returns the updated content, or null if the position is invalid. + */ +export function toggleTasklistCheckbox(content: string, position: number, checked: boolean): string | null { + const buffer = new TextEncoder().encode(content); + // Indexes may fall off the ends and return undefined. + if (buffer[position - 1] !== '['.charCodeAt(0) || + buffer[position] !== ' '.charCodeAt(0) && buffer[position] !== 'x'.charCodeAt(0) || + buffer[position + 1] !== ']'.charCodeAt(0)) { + return null; + } + buffer[position] = checked ? 'x'.charCodeAt(0) : ' '.charCodeAt(0); + return new TextDecoder().decode(buffer); +} + /** * Attaches `input` handlers to markdown rendered tasklist checkboxes in comments. * @@ -23,24 +40,17 @@ export function initMarkupTasklist(elMarkup: HTMLElement): void { 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. + const newContent = toggleTasklistCheckbox(oldContent, position, checkbox.checked); + if (newContent === null) { + // 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;