diff --git a/docs/basic-formatting.md b/docs/basic-formatting.md index 3bf9110c..56cac949 100644 --- a/docs/basic-formatting.md +++ b/docs/basic-formatting.md @@ -38,7 +38,7 @@ const markdown = "> This is a quote" ## Lists -The Lists plugin enables the usage of ordered and unordered lists, including multiple levels of nesting. +The Lists plugin enables the usage of ordered, unordered and check lists, including multiple levels of nesting. ```tsx diff --git a/docs/live-demo-contents.md b/docs/live-demo-contents.md index e248a8b7..875a4866 100644 --- a/docs/live-demo-contents.md +++ b/docs/live-demo-contents.md @@ -14,6 +14,7 @@ In here, you can find the following markdown elements: * Lists * Unordered * Ordered + * Check lists * And nested ;) * Links * Bold/Italic/Underline formatting diff --git a/package-lock.json b/package-lock.json index 7fa1fe69..25a0eee6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "mdast-util-from-markdown": "^1.3.0", "mdast-util-frontmatter": "1.0.1", "mdast-util-gfm-table": "^1.0.7", + "mdast-util-gfm-task-list-item": "1.0.2", "mdast-util-mdx": "2.0.1", "mdast-util-mdx-jsx": "^2.1.4", "mdast-util-to-hast": "^12.3.0", @@ -51,6 +52,7 @@ "micromark-extension-directive": "2.2.0", "micromark-extension-frontmatter": "1.1.0", "micromark-extension-gfm-table": "^1.0.6", + "micromark-extension-gfm-task-list-item": "1.0.5", "micromark-extension-mdxjs": "1.0.1", "react-hook-form": "^7.44.2", "unidiff": "^1.0.2" @@ -11793,7 +11795,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz", "integrity": "sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==", - "dev": true, "dependencies": { "@types/mdast": "^3.0.0", "mdast-util-to-markdown": "^1.3.0" @@ -12355,7 +12356,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.5.tgz", "integrity": "sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==", - "dev": true, "dependencies": { "micromark-factory-space": "^1.0.0", "micromark-util-character": "^1.0.0", diff --git a/package.json b/package.json index 6f70de32..995a3404 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "mdast-util-from-markdown": "^1.3.0", "mdast-util-frontmatter": "1.0.1", "mdast-util-gfm-table": "^1.0.7", + "mdast-util-gfm-task-list-item": "1.0.2", "mdast-util-mdx": "2.0.1", "mdast-util-mdx-jsx": "^2.1.4", "mdast-util-to-hast": "^12.3.0", @@ -77,6 +78,7 @@ "micromark-extension-directive": "2.2.0", "micromark-extension-frontmatter": "1.1.0", "micromark-extension-gfm-table": "^1.0.6", + "micromark-extension-gfm-task-list-item": "1.0.5", "micromark-extension-mdxjs": "1.0.1", "react-hook-form": "^7.44.2", "unidiff": "^1.0.2" diff --git a/src/examples/basics.tsx b/src/examples/basics.tsx index abbf9c84..af1e3319 100644 --- a/src/examples/basics.tsx +++ b/src/examples/basics.tsx @@ -124,6 +124,10 @@ const listsMarkdown = ` 1. more 2. more + +* [x] Walk the dog +* [ ] Watch movie +* [ ] Have dinner with family ` export function Lists() { diff --git a/src/icons/format_list_checked.svg b/src/icons/format_list_checked.svg new file mode 100644 index 00000000..fdfab2c4 --- /dev/null +++ b/src/icons/format_list_checked.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/plugins/lists/LexicalListItemVisitor.ts b/src/plugins/lists/LexicalListItemVisitor.ts index 6f7a6b71..da2da14b 100644 --- a/src/plugins/lists/LexicalListItemVisitor.ts +++ b/src/plugins/lists/LexicalListItemVisitor.ts @@ -16,6 +16,7 @@ export const LexicalListItemVisitor: LexicalExportVisitor = { testNode: 'listItem', - visitNode({ actions }) { - actions.addAndStepInto($createListItemNode()) + visitNode({ mdastNode, actions }) { + actions.addAndStepInto($createListItemNode(mdastNode.checked ?? undefined)) } } diff --git a/src/plugins/lists/MdastListVisitor.ts b/src/plugins/lists/MdastListVisitor.ts index 068169d7..a4d07ce6 100644 --- a/src/plugins/lists/MdastListVisitor.ts +++ b/src/plugins/lists/MdastListVisitor.ts @@ -6,7 +6,8 @@ import { MdastImportVisitor } from '../../importMarkdownToLexical' export const MdastListVisitor: MdastImportVisitor = { testNode: 'list', visitNode: function ({ mdastNode, lexicalParent, actions }): void { - const lexicalNode = $createListNode(mdastNode.ordered ? 'number' : 'bullet') + const listType = mdastNode.children?.some((e) => typeof e.checked === 'boolean') ? 'check' : mdastNode.ordered ? 'number' : 'bullet' + const lexicalNode = $createListNode(listType) if ($isListItemNode(lexicalParent)) { const dedicatedParent = $createListItemNode() diff --git a/src/plugins/lists/index.ts b/src/plugins/lists/index.ts index c4925893..818b94cf 100644 --- a/src/plugins/lists/index.ts +++ b/src/plugins/lists/index.ts @@ -5,6 +5,7 @@ import { MdastListItemVisitor } from './MdastListItemVisitor' import { LexicalListVisitor } from './LexicalListVisitor' import { LexicalListItemVisitor } from './LexicalListItemVisitor' import { + INSERT_CHECK_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListItemNode, @@ -16,12 +17,18 @@ import { $isRootOrShadowRoot, LexicalCommand, RangeSelection } from 'lexical' import { $getListDepth, $isListItemNode, $isListNode } from '@lexical/list' import { $getSelection, $isElementNode, $isRangeSelection, COMMAND_PRIORITY_CRITICAL, ElementNode, INDENT_CONTENT_COMMAND } from 'lexical' import { TabIndentationPlugin } from '@lexical/react/LexicalTabIndentationPlugin.js' +import { CheckListPlugin } from '@lexical/react/LexicalCheckListPlugin.js' import { ListPlugin } from '@lexical/react/LexicalListPlugin.js' + import { $findMatchingParent, $getNearestNodeOfType } from '@lexical/utils' +import { gfmTaskListItem } from 'micromark-extension-gfm-task-list-item' +import { gfmTaskListItemFromMarkdown, gfmTaskListItemToMarkdown } from 'mdast-util-gfm-task-list-item' + const ListTypeCommandMap = new Map>([ ['number', INSERT_ORDERED_LIST_COMMAND], ['bullet', INSERT_UNORDERED_LIST_COMMAND], + ['check', INSERT_CHECK_LIST_COMMAND], ['', REMOVE_LIST_COMMAND] ]) @@ -82,16 +89,20 @@ export const [ systemSpec: listsSystem, init: (realm) => { + realm.pubKey('addMdastExtension', gfmTaskListItemFromMarkdown) + realm.pubKey('addSyntaxExtension', gfmTaskListItem) realm.pubKey('addImportVisitor', MdastListVisitor) realm.pubKey('addImportVisitor', MdastListItemVisitor) realm.pubKey('addLexicalNode', ListItemNode) realm.pubKey('addLexicalNode', ListNode) realm.pubKey('addExportVisitor', LexicalListVisitor) realm.pubKey('addExportVisitor', LexicalListItemVisitor) + realm.pubKey('addToMarkdownExtension', gfmTaskListItemToMarkdown) realm.getKeyValue('rootEditor')?.registerCommand(INDENT_CONTENT_COMMAND, () => !isIndentPermitted(7), COMMAND_PRIORITY_CRITICAL) realm.pubKey('addComposerChild', TabIndentationPlugin) realm.pubKey('addComposerChild', ListPlugin) + realm.pubKey('addComposerChild', CheckListPlugin) } }) diff --git a/src/plugins/markdown-shortcut/index.tsx b/src/plugins/markdown-shortcut/index.tsx index 9bdf9ad5..bac8ca72 100644 --- a/src/plugins/markdown-shortcut/index.tsx +++ b/src/plugins/markdown-shortcut/index.tsx @@ -12,7 +12,8 @@ import { ORDERED_LIST, QUOTE, TextFormatTransformer, - UNORDERED_LIST + UNORDERED_LIST, + CHECK_LIST } from '@lexical/markdown' import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin.js' import React from 'react' @@ -97,7 +98,7 @@ function pickTransformersForActivePlugins(pluginIds: string[], allowedHeadingLev transformers.push(LINK) } if (pluginIds.includes('lists')) { - transformers.push(ORDERED_LIST, UNORDERED_LIST) + transformers.push(ORDERED_LIST, UNORDERED_LIST, CHECK_LIST) } if (pluginIds.includes('codeblock')) { diff --git a/src/plugins/table/index.ts b/src/plugins/table/index.ts index e5174e1d..6990fdff 100644 --- a/src/plugins/table/index.ts +++ b/src/plugins/table/index.ts @@ -18,30 +18,30 @@ type InsertTablePayload = { columns?: number } -function seedTable(rows: number = 1, columns : number = 1): Mdast.Table { +function seedTable(rows: number = 1, columns: number = 1): Mdast.Table { const table: Mdast.Table = { type: 'table', children: [] - }; + } for (let i = 0; i < rows; i++) { const tableRow: Mdast.TableRow = { type: 'tableRow', children: [] - }; + } for (let j = 0; j < columns; j++) { const cell: Mdast.TableCell = { type: 'tableCell', children: [] - }; - tableRow.children.push(cell); + } + tableRow.children.push(cell) } - table.children.push(tableRow); + table.children.push(tableRow) } - return table; + return table } /** @internal */ @@ -52,7 +52,7 @@ export const tableSystem = system( r.link( r.pipe( insertTable, - r.o.map(({rows, columns}) => { + r.o.map(({ rows, columns }) => { return () => $createTableNode(seedTable(rows, columns)) }) ), diff --git a/src/plugins/toolbar/components/InsertTable.tsx b/src/plugins/toolbar/components/InsertTable.tsx index 402a0582..7aa29638 100644 --- a/src/plugins/toolbar/components/InsertTable.tsx +++ b/src/plugins/toolbar/components/InsertTable.tsx @@ -14,7 +14,7 @@ export const InsertTable: React.FC = () => { { - insertTable({rows: 3, columns: 3}) + insertTable({ rows: 3, columns: 3 }) }} > diff --git a/src/plugins/toolbar/components/ListsToggle.tsx b/src/plugins/toolbar/components/ListsToggle.tsx index 1792aa9a..815cf569 100644 --- a/src/plugins/toolbar/components/ListsToggle.tsx +++ b/src/plugins/toolbar/components/ListsToggle.tsx @@ -1,6 +1,7 @@ import React from 'react' import BulletedListIcon from '../../../icons/format_list_bulleted.svg' import NumberedListIcon from '../../../icons/format_list_numbered.svg' +import CheckedListIcon from '../../../icons/format_list_checked.svg' import { listsPluginHooks } from '../../lists' import { SingleChoiceToggleGroup } from '.././primitives/toolbar' @@ -17,7 +18,8 @@ export const ListsToggle: React.FC = () => { value={currentListType || ''} items={[ { title: 'Bulleted list', contents: , value: 'bullet' }, - { title: 'Numbered list', contents: , value: 'number' } + { title: 'Numbered list', contents: , value: 'number' }, + { title: 'Check list', contents: , value: 'check' } ]} onChange={applyListType} /> diff --git a/src/styles/lexical-theme.module.css b/src/styles/lexical-theme.module.css index e82a9491..a97ff826 100644 --- a/src/styles/lexical-theme.module.css +++ b/src/styles/lexical-theme.module.css @@ -60,6 +60,79 @@ list-style:none; } +.listitem { + margin: var(--spacing-2) 0; +} + +.listItemChecked, +.listItemUnchecked { + position: relative; + margin-left: 0; + margin-right: 0; + margin-inline-start: -1rem; + padding-left: var(--spacing-6); + padding-right: var(--spacing-6); + list-style-type: none; + outline: none; +} + +.listItemChecked { + text-decoration: line-through; +} + +.listItemUnchecked:before, +.listItemChecked:before { + content: ''; + width: var(--spacing-4); + height: var(--spacing-4); + top: 0; + left: 0; + cursor: pointer; + display: block; + background-size: cover; + position: absolute; +} + +.listItemUnchecked[dir='rtl']:before, +.listItemChecked[dir='rtl']:before { + left: auto; + right: 0; +} + +.listItemUnchecked:focus:before, +.listItemChecked:focus:before { + box-shadow: 0 0 0 2px var(--accentBgActive); + border-radius: var(--radius-small); +} + +.listItemUnchecked:before { + border: 1px solid var(--baseBorder); + border-radius: var(--radius-small); +} + +.listItemChecked:before { + border: 1px solid var(--accentBorder); + border-radius: var(--radius-small); + background-color: var(--accentSolid); + background-repeat: no-repeat; +} + +.listItemChecked:after { + content: ''; + cursor: pointer; + border-color: var(--baseBase); + border-style: solid; + position: absolute; + display: block; + top: var(--spacing-0_5); + width: var(--spacing-1); + left: var(--spacing-1_5); + right: var(--spacing-1_5); + height: var(--spacing-2); + transform: rotate(45deg); + border-width: 0 var(--spacing-0_5) var(--spacing-0_5) 0; +} + .admonitionDanger, .admonitionInfo, .admonitionNote, .admonitionTip, .admonitionCaution { padding: var(--spacing-2); margin-top: var(--spacing-2); diff --git a/src/styles/lexicalTheme.ts b/src/styles/lexicalTheme.ts index fa9532a2..d939a779 100644 --- a/src/styles/lexicalTheme.ts +++ b/src/styles/lexicalTheme.ts @@ -14,6 +14,9 @@ export const lexicalTheme: EditorThemeClasses = { }, list: { + listitem: styles.listitem, + listitemChecked: styles.listItemChecked, + listitemUnchecked: styles.listItemUnchecked, nested: { listitem: styles.nestedListItem }