(this.rootElement = el as HTMLDivElement)}
- />,
- ];
+ return
(this.rootElement = el)} />;
+ }
+
+ private setupTaskListHandlers() {
+ // Make task list checkboxes interactive and sync back to markdown
+ const checkboxes = this.rootElement.querySelectorAll(
+ '.task-list-item input[type="checkbox"]'
+ );
+
+ // Parse the current markdown to find task list items
+ const lines = this.value.split('\n');
+ let taskListIndex = 0;
+
+ for (const checkbox of checkboxes) {
+ const inputElement = checkbox as HTMLInputElement;
+ const currentTaskIndex = taskListIndex++;
+
+ inputElement.addEventListener('change', () => {
+ // Find the corresponding line in the markdown
+ let taskCounter = 0;
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+ // Match both checked and unchecked task list items
+ // Using a more specific regex to avoid backtracking issues
+ const taskListRegex = /^(\s*)- \[([x ])\] (.+)$/;
+ const taskListMatch = taskListRegex.exec(line);
+
+ if (taskListMatch) {
+ if (taskCounter === currentTaskIndex) {
+ // Update this line
+ const indent = taskListMatch[1];
+ const newState = inputElement.checked ? 'x' : ' ';
+ const text = taskListMatch[3];
+ lines[i] = `${indent}- [${newState}] ${text}`;
+
+ // Emit the updated markdown
+ const updatedMarkdown = lines.join('\n');
+ this.taskListChange.emit(updatedMarkdown);
+ break;
+ }
+ taskCounter++;
+ }
+ }
+ });
+ }
}
private setupImageIntersectionObserver() {
diff --git a/src/components/markdown/partial-styles/_lists.scss b/src/components/markdown/partial-styles/_lists.scss
index 7be8767685..6e8853a88c 100644
--- a/src/components/markdown/partial-styles/_lists.scss
+++ b/src/components/markdown/partial-styles/_lists.scss
@@ -1,8 +1,12 @@
ul {
list-style: none;
+ margin-top: 0.25rem;
+ padding-left: 0;
+
li {
position: relative;
margin-left: 0.75rem;
+ margin-bottom: 0.25rem;
&:before {
content: '';
@@ -17,6 +21,15 @@ ul {
background-color: rgb(var(--contrast-700));
display: block;
}
+
+ // Task list items should not have bullet points
+ &.task-list-item {
+ margin-left: 0;
+
+ &:before {
+ display: none;
+ }
+ }
}
}
@@ -25,9 +38,76 @@ ol {
padding-left: 1rem;
}
-ul {
- margin-top: 0.25rem;
+// Task list specific styles
+.task-list,
+.contains-task-list {
+ list-style: none;
padding-left: 0;
+
+ .task-list-item {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.5rem;
+ margin-bottom: 0.25rem;
+ margin-left: 0;
+
+ &:before {
+ display: none; // Remove bullet points
+ }
+
+ input[type='checkbox'] {
+ margin: 0;
+ margin-top: 0.125rem; // Align with first line of text
+ flex-shrink: 0;
+ width: 1rem;
+ height: 1rem;
+ cursor: pointer;
+
+ // Ensure checkbox is interactive
+ pointer-events: auto;
+
+ // Style to match other form elements
+ border: 1px solid rgb(var(--contrast-600));
+ border-radius: 0.125rem;
+ background-color: rgb(var(--contrast-100));
+
+ &:checked {
+ background-color: rgb(var(--color-sky-default));
+ border-color: rgb(var(--color-sky-default));
+ }
+
+ &:hover {
+ border-color: rgb(var(--contrast-800));
+ }
+
+ &:focus {
+ outline: 2px solid rgb(var(--color-sky-light));
+ outline-offset: 1px;
+ }
+ }
+
+ // Handle both paragraph-wrapped and direct text content
+ p {
+ margin: 0;
+ flex: 1;
+ line-height: 1.5;
+ }
+
+ // Direct text content (when not wrapped in paragraphs)
+ > * {
+ &:not(input):not(ul):not(ol) {
+ flex: 1;
+ line-height: 1.5;
+ }
+ }
+
+ // Nested task lists
+ .task-list,
+ .contains-task-list {
+ margin-top: 0.25rem;
+ margin-left: 1.5rem;
+ }
+ }
}
ul ul,
diff --git a/src/components/text-editor/examples/text-editor-with-task-lists.tsx b/src/components/text-editor/examples/text-editor-with-task-lists.tsx
new file mode 100644
index 0000000000..53ff7d9c9a
--- /dev/null
+++ b/src/components/text-editor/examples/text-editor-with-task-lists.tsx
@@ -0,0 +1,52 @@
+import { Component, h, State } from '@stencil/core';
+/**
+ * Task list example
+ *
+ * This example demonstrates the task list functionality in the text editor.
+ * You can create interactive checkbox lists that can be toggled and managed.
+ */
+@Component({
+ tag: 'limel-example-text-editor-with-task-lists',
+ shadow: true,
+})
+export class TextEditorTaskListExample {
+ @State()
+ private value: string = `# Task List Example
+
+Here's an example with task lists:
+
+- [ ] First unchecked task
+- [x] This task is completed
+- [ ] Another unchecked task
+- [ ] Task with some **bold** text
+
+You can click the checkbox button in the toolbar to create more task lists!
+
+## Regular list for comparison
+
+- Regular bullet point
+- Another bullet point
+- Third bullet point
+
+## Keyboard shortcuts
+- **Enter**: Create new list item
+- **Tab**: Indent list item (nest deeper)
+- **Shift+Tab**: Outdent list item (nest shallower)
+`;
+
+ public render() {
+ return [
+ ,
+ ,
+ ];
+ }
+
+ private handleChange = (event: CustomEvent) => {
+ this.value = event.detail;
+ };
+}
diff --git a/src/components/text-editor/prosemirror-adapter/menu/menu-commands.ts b/src/components/text-editor/prosemirror-adapter/menu/menu-commands.ts
index 8c79a826c2..077e40292f 100644
--- a/src/components/text-editor/prosemirror-adapter/menu/menu-commands.ts
+++ b/src/components/text-editor/prosemirror-adapter/menu/menu-commands.ts
@@ -329,6 +329,7 @@ const commandMapping: CommandMapping = {
createListCommand(schema, EditorMenuTypes.OrderedList),
bullet_list: (schema) =>
createListCommand(schema, EditorMenuTypes.BulletList),
+ task_list: (schema) => createListCommand(schema, EditorMenuTypes.TaskList),
};
export class MenuCommandFactory {
diff --git a/src/components/text-editor/prosemirror-adapter/menu/menu-items.ts b/src/components/text-editor/prosemirror-adapter/menu/menu-items.ts
index 40377e5b3e..a6c8254a92 100644
--- a/src/components/text-editor/prosemirror-adapter/menu/menu-items.ts
+++ b/src/components/text-editor/prosemirror-adapter/menu/menu-items.ts
@@ -66,6 +66,13 @@ const textEditorMenuItems: Array<
iconOnly: true,
selected: false,
},
+ {
+ value: EditorMenuTypes.TaskList,
+ text: 'Task list',
+ icon: 'checklist',
+ iconOnly: true,
+ selected: false,
+ },
{ separator: true },
{
value: EditorMenuTypes.HeaderLevel1,
@@ -129,6 +136,7 @@ export const menuTranslationIDs = {
bullet_list: 'editor-menu.bulleted-list',
ordered_list: 'editor-menu.numbered-list',
+ task_list: 'editor-menu.task-list',
code_block: 'editor-menu.code-block',
blockquote: 'editor-menu.blockquote',
diff --git a/src/components/text-editor/prosemirror-adapter/menu/types.ts b/src/components/text-editor/prosemirror-adapter/menu/types.ts
index 8de1093fd8..e5cd0c99ef 100644
--- a/src/components/text-editor/prosemirror-adapter/menu/types.ts
+++ b/src/components/text-editor/prosemirror-adapter/menu/types.ts
@@ -15,6 +15,7 @@ export const EditorMenuTypes = {
Link: 'link',
OrderedList: 'ordered_list',
BulletList: 'bullet_list',
+ TaskList: 'task_list',
Strikethrough: 'strikethrough',
Code: 'code',
CodeBlock: 'code_block',
diff --git a/src/components/text-editor/prosemirror-adapter/plugins/list-indentation-plugin.ts b/src/components/text-editor/prosemirror-adapter/plugins/list-indentation-plugin.ts
new file mode 100644
index 0000000000..95d7e160dd
--- /dev/null
+++ b/src/components/text-editor/prosemirror-adapter/plugins/list-indentation-plugin.ts
@@ -0,0 +1,90 @@
+import { Plugin, PluginKey } from 'prosemirror-state';
+import { keymap } from 'prosemirror-keymap';
+import { Schema } from 'prosemirror-model';
+import { liftListItem, sinkListItem } from 'prosemirror-schema-list';
+import {
+ liftTaskListItem,
+ sinkTaskListItem,
+} from './task-list/task-list-commands';
+
+const listIndentationKey = new PluginKey('listIndentation');
+
+export const createListIndentationPlugin = (schema: Schema): Plugin => {
+ const listItemType = schema.nodes.list_item;
+ const taskListItemType = schema.nodes.task_list_item;
+
+ // Create the task list commands
+ const sinkTaskList = sinkTaskListItem(schema);
+ const liftTaskList = liftTaskListItem(schema);
+
+ console.log('List indentation plugin initialized');
+ console.log('listItemType:', listItemType);
+ console.log('taskListItemType:', taskListItemType);
+
+ if (!listItemType && !taskListItemType) {
+ console.log('No list types found in schema');
+ return new Plugin({
+ key: listIndentationKey,
+ });
+ }
+
+ return keymap({
+ Tab: (state, dispatch) => {
+ console.log('Tab key pressed in list indentation plugin');
+ const { selection } = state;
+ const { $from } = selection;
+
+ // Check if we're in any type of list item
+ for (let d = $from.depth; d >= 0; d--) {
+ const node = $from.node(d);
+ console.log(`Tab - Depth ${d}:`, node.type.name, node);
+
+ // Handle regular list items
+ if (listItemType && node.type === listItemType) {
+ console.log('Indenting regular list item');
+ return sinkListItem(listItemType)(state, dispatch);
+ }
+
+ // Handle task list items
+ if (taskListItemType && node.type === taskListItemType) {
+ console.log('Indenting task list item');
+ return sinkTaskList(state, dispatch);
+ }
+ }
+
+ console.log('Not in a list item, consuming Tab to keep focus');
+ // Return true to prevent default browser Tab behavior (losing focus)
+ // but don't dispatch any changes since we're not in a list
+ return true;
+ },
+ 'Shift-Tab': (state, dispatch) => {
+ console.log('Shift-Tab key pressed in list indentation plugin');
+ const { selection } = state;
+ const { $from } = selection;
+
+ // Check if we're in any type of list item
+ for (let d = $from.depth; d >= 0; d--) {
+ const node = $from.node(d);
+ console.log(`Shift-Tab - Depth ${d}:`, node.type.name, node);
+
+ // Handle regular list items
+ if (listItemType && node.type === listItemType) {
+ console.log('Outdenting regular list item');
+ return liftListItem(listItemType)(state, dispatch);
+ }
+
+ // Handle task list items
+ if (taskListItemType && node.type === taskListItemType) {
+ console.log('Outdenting task list item');
+ return liftTaskList(state, dispatch);
+ }
+ }
+
+ console.log(
+ 'Not in a list item, consuming Shift-Tab to keep focus'
+ );
+ // Return true to prevent default browser Shift-Tab behavior
+ return true;
+ },
+ });
+};
diff --git a/src/components/text-editor/prosemirror-adapter/plugins/task-list/task-list-commands.ts b/src/components/text-editor/prosemirror-adapter/plugins/task-list/task-list-commands.ts
new file mode 100644
index 0000000000..39eff2c8c9
--- /dev/null
+++ b/src/components/text-editor/prosemirror-adapter/plugins/task-list/task-list-commands.ts
@@ -0,0 +1,72 @@
+import { Command } from 'prosemirror-state';
+import {
+ wrapInList,
+ liftListItem,
+ sinkListItem,
+} from 'prosemirror-schema-list';
+import { Schema } from 'prosemirror-model';
+import { CommandWithActive } from '../../menu/menu-commands';
+
+export const createTaskList = (schema: Schema): CommandWithActive => {
+ const taskListType = schema.nodes.task_list;
+ const taskListItemType = schema.nodes.task_list_item;
+
+ if (!taskListType || !taskListItemType) {
+ return () => false;
+ }
+
+ const command: Command = wrapInList(taskListType, { checked: false });
+
+ // Add active state detection
+ const commandWithActive = command as CommandWithActive;
+ commandWithActive.active = (state) => {
+ const { $from } = state.selection;
+ for (let d = $from.depth; d >= 0; d--) {
+ const node = $from.node(d);
+ if (node && node.type === taskListType) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ return commandWithActive;
+};
+
+export const toggleTaskListItem = (schema: Schema): Command => {
+ const taskListItemType = schema.nodes.task_list_item;
+
+ return (state, dispatch) => {
+ const { from } = state.selection;
+ const $from = state.doc.resolve(from);
+
+ // Find the task list item
+ for (let d = $from.depth; d >= 0; d--) {
+ const node = $from.node(d);
+ if (node && node.type === taskListItemType) {
+ const pos = $from.before(d);
+ const checked = !node.attrs.checked;
+
+ if (dispatch) {
+ const tr = state.tr.setNodeMarkup(pos, null, {
+ ...node.attrs,
+ checked,
+ });
+ dispatch(tr);
+ }
+ return true;
+ }
+ }
+ return false;
+ };
+};
+
+export const liftTaskListItem = (schema: Schema): Command => {
+ const taskListItemType = schema.nodes.task_list_item;
+ return liftListItem(taskListItemType);
+};
+
+export const sinkTaskListItem = (schema: Schema): Command => {
+ const taskListItemType = schema.nodes.task_list_item;
+ return sinkListItem(taskListItemType);
+};
diff --git a/src/components/text-editor/prosemirror-adapter/plugins/task-list/task-list-node-view.ts b/src/components/text-editor/prosemirror-adapter/plugins/task-list/task-list-node-view.ts
new file mode 100644
index 0000000000..382a875643
--- /dev/null
+++ b/src/components/text-editor/prosemirror-adapter/plugins/task-list/task-list-node-view.ts
@@ -0,0 +1,70 @@
+import { NodeView } from 'prosemirror-view';
+import { Node as ProseMirrorNode } from 'prosemirror-model';
+
+export class TaskListItemView implements NodeView {
+ dom: HTMLElement;
+ contentDOM: HTMLElement;
+ checkbox: HTMLInputElement;
+ private getPos: () => number;
+ private view: any;
+
+ constructor(node: ProseMirrorNode, view: any, getPos: () => number) {
+ this.getPos = getPos;
+ this.view = view;
+
+ // Create the list item element
+ this.dom = document.createElement('li');
+ this.dom.className = 'task-list-item';
+
+ // Create the checkbox
+ this.checkbox = document.createElement('input');
+ this.checkbox.type = 'checkbox';
+ this.checkbox.checked = node.attrs.checked;
+
+ // Create content container
+ this.contentDOM = document.createElement('div');
+ this.contentDOM.className = 'task-list-item-content';
+
+ // Assemble the DOM
+ this.dom.append(this.checkbox);
+ this.dom.append(this.contentDOM);
+
+ // Add click handler for checkbox
+ this.checkbox.addEventListener('click', this.handleCheckboxClick);
+ }
+
+ private handleCheckboxClick = (event: Event) => {
+ // Don't prevent default - let the checkbox toggle naturally
+ const newChecked = (event.target as HTMLInputElement).checked;
+ const pos = this.getPos();
+ const tr = this.view.state.tr.setNodeMarkup(pos, null, {
+ checked: newChecked,
+ });
+ this.view.dispatch(tr);
+ };
+
+ update(node: ProseMirrorNode): boolean {
+ if (node.type.name !== 'task_list_item') {
+ return false;
+ }
+ this.checkbox.checked = node.attrs.checked;
+ return true;
+ }
+
+ stopEvent(event: Event): boolean {
+ // Allow checkbox clicks to be handled by our custom handler
+ return event.target === this.checkbox;
+ }
+
+ destroy(): void {
+ this.checkbox.removeEventListener('click', this.handleCheckboxClick);
+ }
+}
+
+export const createTaskListItemNodeView = (
+ node: ProseMirrorNode,
+ view: any,
+ getPos: () => number
+) => {
+ return new TaskListItemView(node, view, getPos);
+};
diff --git a/src/components/text-editor/prosemirror-adapter/plugins/task-list/task-list-plugin.ts b/src/components/text-editor/prosemirror-adapter/plugins/task-list/task-list-plugin.ts
new file mode 100644
index 0000000000..616741360c
--- /dev/null
+++ b/src/components/text-editor/prosemirror-adapter/plugins/task-list/task-list-plugin.ts
@@ -0,0 +1,46 @@
+import { Plugin, PluginKey } from 'prosemirror-state';
+import { keymap } from 'prosemirror-keymap';
+import { Schema } from 'prosemirror-model';
+import { splitListItem } from 'prosemirror-schema-list';
+
+const taskListKey = new PluginKey('taskList');
+
+export const createTaskListPlugin = (schema: Schema): Plugin => {
+ const taskListItemType = schema.nodes.task_list_item;
+ const listItemType = schema.nodes.list_item; // Also check regular list items
+
+ console.log('Task list plugin initialized');
+ console.log('taskListItemType:', taskListItemType);
+ console.log('listItemType:', listItemType);
+
+ if (!taskListItemType) {
+ console.log('No task_list_item type found in schema');
+ return new Plugin({
+ key: taskListKey,
+ });
+ }
+
+ return keymap({
+ Enter: (state, dispatch) => {
+ console.log('Enter key pressed in task list plugin');
+
+ // Debug: check what type of node we're in
+ const { selection } = state;
+ const { $from } = selection;
+
+ for (let d = $from.depth; d >= 0; d--) {
+ const node = $from.node(d);
+ console.log(`Depth ${d}:`, node.type.name, node.attrs);
+
+ if (node.type === taskListItemType) {
+ console.log('Found task_list_item, using splitListItem');
+ return splitListItem(taskListItemType)(state, dispatch);
+ }
+ }
+
+ console.log('Not in a task_list_item, allowing other handlers');
+ return false;
+ },
+ // Removed Mod-[ and Mod-] - now handled by list indentation plugin
+ });
+};
diff --git a/src/components/text-editor/prosemirror-adapter/plugins/task-list/task-list-schema.ts b/src/components/text-editor/prosemirror-adapter/plugins/task-list/task-list-schema.ts
new file mode 100644
index 0000000000..f8fea6a49b
--- /dev/null
+++ b/src/components/text-editor/prosemirror-adapter/plugins/task-list/task-list-schema.ts
@@ -0,0 +1,69 @@
+import { NodeSpec } from 'prosemirror-model';
+
+export const taskListNodeSpec: NodeSpec = {
+ content: 'task_list_item+',
+ group: 'block',
+ parseDOM: [
+ {
+ tag: 'ul.task-list',
+ getAttrs: (dom) => {
+ if (dom instanceof HTMLElement) {
+ return {};
+ }
+ return false;
+ },
+ },
+ {
+ tag: 'ul.contains-task-list',
+ getAttrs: (dom) => {
+ if (dom instanceof HTMLElement) {
+ return {};
+ }
+ return false;
+ },
+ },
+ ],
+ toDOM: () => ['ul', { class: 'task-list' }, 0],
+};
+
+export const taskListItemNodeSpec: NodeSpec = {
+ content: 'paragraph',
+ attrs: {
+ checked: { default: false },
+ },
+ parseDOM: [
+ {
+ tag: 'li.task-list-item',
+ getAttrs: (dom) => {
+ if (dom instanceof HTMLElement) {
+ const checkbox = dom.querySelector(
+ 'input[type="checkbox"]'
+ );
+ return {
+ checked: checkbox
+ ? (checkbox as HTMLInputElement).checked
+ : false,
+ };
+ }
+ return false;
+ },
+ },
+ ],
+ toDOM: (node) => {
+ const { checked } = node.attrs;
+ return [
+ 'li',
+ { class: 'task-list-item' },
+ [
+ 'input',
+ {
+ type: 'checkbox',
+ checked: checked ? 'checked' : null,
+ disabled: 'disabled', // Make checkbox non-interactive in HTML output
+ },
+ ],
+ ['div', { class: 'task-list-item-content' }, 0],
+ ];
+ },
+ defining: true,
+};
diff --git a/src/components/text-editor/prosemirror-adapter/plugins/task-list/task-list.scss b/src/components/text-editor/prosemirror-adapter/plugins/task-list/task-list.scss
new file mode 100644
index 0000000000..623a6ee120
--- /dev/null
+++ b/src/components/text-editor/prosemirror-adapter/plugins/task-list/task-list.scss
@@ -0,0 +1,49 @@
+/* Task list styles with native checkbox */
+.task-list {
+ list-style: none;
+ padding-left: 0;
+ margin-left: 0;
+}
+
+.task-list-item {
+ display: flex;
+ align-items: flex-start;
+ margin: 0.25rem 0;
+ position: relative;
+ list-style: none;
+ gap: 0.5rem; // Gap between checkbox and content
+}
+
+/* Make sure no bullets appear */
+.task-list-item::before {
+ display: none !important;
+}
+
+.task-list-item::marker {
+ display: none;
+}
+
+/* Native checkbox styling */
+.task-list-item input[type='checkbox'] {
+ flex-shrink: 0;
+ margin-top: 0.125rem; // Slight offset to align with text baseline
+ cursor: pointer;
+}
+
+.task-list-item-content {
+ flex: 1;
+ min-width: 0;
+}
+
+.task-list-item-content p {
+ margin: 0;
+}
+
+/* Completed task styling */
+.task-list-item:has(input[type='checkbox']:checked) .task-list-item-content {
+ opacity: 0.6;
+ text-decoration: line-through;
+}
+.task-list-item input[type='checkbox']:checked + .task-list-item-content p {
+ text-decoration: line-through;
+}
diff --git a/src/components/text-editor/prosemirror-adapter/prosemirror-adapter.scss b/src/components/text-editor/prosemirror-adapter/prosemirror-adapter.scss
index 8694219e73..4c6572ade7 100644
--- a/src/components/text-editor/prosemirror-adapter/prosemirror-adapter.scss
+++ b/src/components/text-editor/prosemirror-adapter/prosemirror-adapter.scss
@@ -9,6 +9,7 @@
@forward '../../markdown/partial-styles/kbd';
@forward '../../markdown/partial-styles/img';
@forward 'plugins/image/view.scss';
+@forward 'plugins/task-list/task-list.scss';
:host(limel-prosemirror-adapter) {
display: flex;
diff --git a/src/components/text-editor/prosemirror-adapter/prosemirror-adapter.tsx b/src/components/text-editor/prosemirror-adapter/prosemirror-adapter.tsx
index fdaa341604..487c085a03 100644
--- a/src/components/text-editor/prosemirror-adapter/prosemirror-adapter.tsx
+++ b/src/components/text-editor/prosemirror-adapter/prosemirror-adapter.tsx
@@ -43,6 +43,13 @@ import { createActionBarInteractionPlugin } from './plugins/menu-action-interact
import { CustomElementDefinition } from '../../../global/shared-types/custom-element.types';
import { createNodeSpec } from '../utils/plugin-factory';
import { createTriggerPlugin } from './plugins/trigger/factory';
+import {
+ taskListNodeSpec,
+ taskListItemNodeSpec,
+} from './plugins/task-list/task-list-schema';
+import { createTaskListPlugin } from './plugins/task-list/task-list-plugin';
+import { createTaskListItemNodeView } from './plugins/task-list/task-list-node-view';
+import { createListIndentationPlugin } from './plugins/list-indentation-plugin';
import {
TriggerCharacter,
ImageInserter,
@@ -359,6 +366,9 @@ export class ProsemirrorAdapter {
{
state: this.createEditorState(initialDoc),
dispatchTransaction: this.handleTransaction,
+ nodeViews: {
+ task_list_item: createTaskListItemNodeView,
+ },
}
);
@@ -381,6 +391,12 @@ export class ProsemirrorAdapter {
}
nodes = addListNodes(nodes, 'paragraph block*', 'block');
+ // Add task list nodes
+ nodes = nodes.append({
+ task_list: taskListNodeSpec,
+ task_list_item: taskListItemNodeSpec,
+ });
+
if (this.contentType === 'html') {
nodes = nodes.append(getTableNodes());
}
@@ -416,6 +432,8 @@ export class ProsemirrorAdapter {
return EditorState.create({
doc: initialDoc,
plugins: [
+ createTaskListPlugin(this.schema), // Move task list plugin FIRST
+ createListIndentationPlugin(this.schema), // Add list indentation plugin
...exampleSetup({ schema: this.schema, menuBar: false }),
keymap(this.menuCommandFactory.buildKeymap()),
createTriggerPlugin(
diff --git a/src/components/text-editor/text-editor.tsx b/src/components/text-editor/text-editor.tsx
index 7cf8deb602..0f2cde8b1b 100644
--- a/src/components/text-editor/text-editor.tsx
+++ b/src/components/text-editor/text-editor.tsx
@@ -26,6 +26,7 @@ import { EditorUiType } from './types';
* @exampleComponent limel-example-text-editor-with-markdown
* @exampleComponent limel-example-text-editor-with-html
* @exampleComponent limel-example-text-editor-with-tables
+ * @exampleComponent limel-example-text-editor-with-task-lists
* @exampleComponent limel-example-text-editor-with-inline-images-file-storage
* @exampleComponent limel-example-text-editor-with-inline-images-base64
* @exampleComponent limel-example-text-editor-allow-resize
diff --git a/src/components/text-editor/utils/markdown-converter.ts b/src/components/text-editor/utils/markdown-converter.ts
index f27ddaa06a..ceee9b91a3 100644
--- a/src/components/text-editor/utils/markdown-converter.ts
+++ b/src/components/text-editor/utils/markdown-converter.ts
@@ -51,6 +51,19 @@ const buildMarkdownSerializer = (
...defaultMarkdownSerializer.nodes,
...getImageNodeMarkdownSerializer(language),
...customNodes,
+ // Task list serialization
+ task_list: (state: MarkdownSerializerState, node: ProseMirrorNode) => {
+ state.renderList(node, '', () => '');
+ },
+ task_list_item: (
+ state: MarkdownSerializerState,
+ node: ProseMirrorNode
+ ) => {
+ const checked = node.attrs.checked;
+ const checkbox = checked ? '[x]' : '[ ]';
+ state.write(`- ${checkbox} `);
+ state.renderContent(node);
+ },
};
const marks = {
@@ -78,7 +91,9 @@ export class MarkdownConverter implements ContentTypeConverter {
this.customNodes = plugins;
}
public parseAsHTML = (text: string): Promise => {
- return markdownToHTML(text, { whitelist: this.customNodes });
+ return markdownToHTML(text, {
+ whitelist: this.customNodes,
+ });
};
public serialize = (view: EditorView): string => {
diff --git a/src/translations/en.ts b/src/translations/en.ts
index 482361b034..f5601e904b 100644
--- a/src/translations/en.ts
+++ b/src/translations/en.ts
@@ -39,6 +39,7 @@ export default {
'editor-menu.h3': 'Heading 3',
'editor-menu.bulleted-list': 'Bulleted list',
'editor-menu.numbered-list': 'Numbered list',
+ 'editor-menu.task-list': 'Task list',
'editor-menu.blockquote': 'Blockquote',
'editor-menu.link': 'Link',
'editor-link-menu.text': 'Text',
diff --git a/src/translations/no.ts b/src/translations/no.ts
index 41db144b7a..e19fbf5236 100644
--- a/src/translations/no.ts
+++ b/src/translations/no.ts
@@ -38,6 +38,7 @@ export default {
'editor-menu.h3': 'Overskrifts 3',
'editor-menu.bulleted-list': 'Punktliste',
'editor-menu.numbered-list': 'Nummerert liste',
+ 'editor-menu.task-list': 'Oppgaveliste',
'editor-menu.blockquote': 'Blokksitat',
'editor-menu.link': 'Legg til lenke',
'editor-link-menu.text': 'Tekst',
diff --git a/src/translations/sv.ts b/src/translations/sv.ts
index bc3dc15fd8..618b0bd951 100644
--- a/src/translations/sv.ts
+++ b/src/translations/sv.ts
@@ -39,6 +39,7 @@ export default {
'editor-menu.h3': 'Rubrik 3',
'editor-menu.bulleted-list': 'Punktlista',
'editor-menu.numbered-list': 'Numrerad lista',
+ 'editor-menu.task-list': 'Uppgiftslista',
'editor-menu.blockquote': 'Blockcitat',
'editor-menu.link': 'Lägg till länk',
'editor-link-menu.text': 'Text',