Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions web_src/js/features/comp/ComboMarkdownEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<HTMLInputElement>('.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);
Comment thread
silverwind marked this conversation as resolved.
triggerEditorContentChanged(this.container);
});
}
});
}

Expand Down
9 changes: 9 additions & 0 deletions web_src/js/markup/tasklist.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
30 changes: 20 additions & 10 deletions web_src/js/markup/tasklist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment thread
silverwind marked this conversation as resolved.
// 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);
}
Comment thread
silverwind marked this conversation as resolved.

/**
* Attaches `input` handlers to markdown rendered tasklist checkboxes in comments.
*
Expand All @@ -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;
Expand Down