Skip to content

Commit

Permalink
feat: checklist item type support (#159)
Browse files Browse the repository at this point in the history
* feat: Add check list type in the 'list' plugin

* Update check list icon

* Update check list svg icon

* Downgrade list task item dependencies and use variable for check list style

* Touch-ups on icon and styling

---------

Co-authored-by: Haiping Fu <[email protected]>
Co-authored-by: Petyo Ivanov <[email protected]>
  • Loading branch information
3 people authored Nov 7, 2023
1 parent 1c99eb9 commit ec46d93
Show file tree
Hide file tree
Showing 16 changed files with 127 additions and 18 deletions.
2 changes: 1 addition & 1 deletion docs/basic-formatting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions docs/live-demo-contents.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,15 @@
"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",
"mdast-util-to-markdown": "1.5.0",
"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"
Expand Down
4 changes: 4 additions & 0 deletions src/examples/basics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ const listsMarkdown = `
1. more
2. more
* [x] Walk the dog
* [ ] Watch movie
* [ ] Have dinner with family
`

export function Lists() {
Expand Down
10 changes: 10 additions & 0 deletions src/icons/format_list_checked.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/plugins/lists/LexicalListItemVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const LexicalListItemVisitor: LexicalExportVisitor<ListItemNode, Mdast.Li
// nest the children in a paragraph for MDAST compatibility
const listItem = actions.appendToParent(mdastParent, {
type: 'listItem' as const,
checked: lexicalNode.getChecked(),
spread: false,
children: [{ type: 'paragraph' as const, children: [] }]
}) as Mdast.ListItem
Expand Down
4 changes: 2 additions & 2 deletions src/plugins/lists/MdastListItemVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { MdastImportVisitor } from '../../importMarkdownToLexical'

export const MdastListItemVisitor: MdastImportVisitor<Mdast.ListItem> = {
testNode: 'listItem',
visitNode({ actions }) {
actions.addAndStepInto($createListItemNode())
visitNode({ mdastNode, actions }) {
actions.addAndStepInto($createListItemNode(mdastNode.checked ?? undefined))
}
}
3 changes: 2 additions & 1 deletion src/plugins/lists/MdastListVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { MdastImportVisitor } from '../../importMarkdownToLexical'
export const MdastListVisitor: MdastImportVisitor<Mdast.List> = {
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()
Expand Down
11 changes: 11 additions & 0 deletions src/plugins/lists/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<ListType | '', LexicalCommand<void>>([
['number', INSERT_ORDERED_LIST_COMMAND],
['bullet', INSERT_UNORDERED_LIST_COMMAND],
['check', INSERT_CHECK_LIST_COMMAND],
['', REMOVE_LIST_COMMAND]
])

Expand Down Expand Up @@ -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)
}
})

Expand Down
5 changes: 3 additions & 2 deletions src/plugins/markdown-shortcut/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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')) {
Expand Down
16 changes: 8 additions & 8 deletions src/plugins/table/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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))
})
),
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/toolbar/components/InsertTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const InsertTable: React.FC = () => {
<ButtonWithTooltip
title="Insert table"
onClick={() => {
insertTable({rows: 3, columns: 3})
insertTable({ rows: 3, columns: 3 })
}}
>
<TableIcon />
Expand Down
4 changes: 3 additions & 1 deletion src/plugins/toolbar/components/ListsToggle.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -17,7 +18,8 @@ export const ListsToggle: React.FC = () => {
value={currentListType || ''}
items={[
{ title: 'Bulleted list', contents: <BulletedListIcon />, value: 'bullet' },
{ title: 'Numbered list', contents: <NumberedListIcon />, value: 'number' }
{ title: 'Numbered list', contents: <NumberedListIcon />, value: 'number' },
{ title: 'Check list', contents: <CheckedListIcon />, value: 'check' }
]}
onChange={applyListType}
/>
Expand Down
73 changes: 73 additions & 0 deletions src/styles/lexical-theme.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions src/styles/lexicalTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export const lexicalTheme: EditorThemeClasses = {
},

list: {
listitem: styles.listitem,
listitemChecked: styles.listItemChecked,
listitemUnchecked: styles.listItemUnchecked,
nested: {
listitem: styles.nestedListItem
}
Expand Down

0 comments on commit ec46d93

Please sign in to comment.