diff options
author | Max <max@nextcloud.com> | 2022-02-15 10:02:07 +0300 |
---|---|---|
committer | Max <max@nextcloud.com> | 2022-03-02 15:26:11 +0300 |
commit | 0afc3236c2d773791de05d457367c73faf113ca0 (patch) | |
tree | e6c78d3228e7b915645e605f5cf414f6002a160b /src/nodes | |
parent | 69a9acce3a66b13dd7a22d2eef3b2e285ec91a71 (diff) |
fix: indicator of the task list.
See #2018.
Use tiptap TaskList and TaskItem.
Markdown-it happily mixes tasks and bullet points in the same list.
Tiptap lists are strictly separated.
Split bullet and tasks into BulletList and TaskList in markdown-io.
Just like this will turn into three different lists:
* one
- two
+ three
This will now also turn into three different lists
with the middle one being a task list:
* first list
* [ ] todo
* [x] done
* third list
Signed-off-by: Max <max@nextcloud.com>
Diffstat (limited to 'src/nodes')
-rw-r--r-- | src/nodes/BulletList.js | 16 | ||||
-rw-r--r-- | src/nodes/ListItem.js | 203 | ||||
-rw-r--r-- | src/nodes/TaskItem.js | 134 | ||||
-rw-r--r-- | src/nodes/TaskList.js | 40 | ||||
-rw-r--r-- | src/nodes/index.js | 6 |
5 files changed, 189 insertions, 210 deletions
diff --git a/src/nodes/BulletList.js b/src/nodes/BulletList.js index 0e22a0ec7..4d4d0f537 100644 --- a/src/nodes/BulletList.js +++ b/src/nodes/BulletList.js @@ -21,16 +21,22 @@ */ import TiptapBulletList from '@tiptap/extension-bullet-list' +import { listInputRule } from '../commands' -/* We want to allow for mixed lists with todo items and bullet points. - * Therefore the list input rules are handled in the ListItem node. - * This way we can make sure that "- [ ]" can still trigger todo list items - * even inside a list with bullet points. +/* We want to allow for `* [ ]` as an input rule for bullet lists. + * Therefore the list input rules need to check the input + * until the first char after the space. + * Only there we know the user is not trying to create a task list. */ const BulletList = TiptapBulletList.extend({ addInputRules() { - return [] + return [ + listInputRule( + /^\s*([-+*])\s([^\s[])$/, + this.type + ), + ] }, }) diff --git a/src/nodes/ListItem.js b/src/nodes/ListItem.js deleted file mode 100644 index 6594929e8..000000000 --- a/src/nodes/ListItem.js +++ /dev/null @@ -1,203 +0,0 @@ -/* - * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net> - * - * @author Julius Härtl <jus@bitgrid.net> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ - -import TipTapListItem from '@tiptap/extension-list-item' -import { wrappingInputRule, mergeAttributes } from '@tiptap/core' -import { Plugin } from 'prosemirror-state' -import { findParentNode, findParentNodeClosestToPos } from 'prosemirror-utils' -import { listInputRule } from '../commands' - -const TYPES = { - BULLET: 0, - CHECKBOX: 1, -} - -const getParentList = (schema, selection) => { - return findParentNode(function(node) { - return node.type === schema.nodes.listItem - })(selection) -} - -const ListItem = TipTapListItem.extend({ - - addOptions() { - return { - nested: true, - } - }, - - addAttributes() { - return { - done: { - default: false, - }, - type: { - default: TYPES.BULLET, - }, - } - }, - - draggable: false, - - content: 'paragraph block*', - - renderHTML({ node, HTMLAttributes }) { - if (node.attrs.type === TYPES.BULLET) { - return ['li', HTMLAttributes, 0] - } - const listAttributes = { class: 'checkbox-item' } - const checkboxAttributes = { type: 'checkbox', class: '', contenteditable: false } - if (node.attrs.done) { - checkboxAttributes.checked = true - listAttributes.class += ' checked' - } - return [ - 'li', - mergeAttributes(HTMLAttributes, listAttributes), - [ - 'input', - checkboxAttributes, - ], - [ - 'label', - 0, - ], - ] - }, - - parseHTML: [ - { - priority: 100, - tag: 'li', - getAttrs: el => { - const checkbox = el.querySelector('input[type=checkbox]') - return { done: checkbox && checkbox.checked, type: checkbox ? TYPES.CHECKBOX : TYPES.BULLET } - }, - }, - ], - - toMarkdown: (state, node) => { - if (node.attrs.type === TYPES.CHECKBOX) { - state.write(`[${node.attrs.done ? 'x' : ' '}] `) - } - state.renderContent(node) - }, - - addCommands() { - return { - bulletListItem: () => ({ commands }) => { - return commands.toggleList('bulletList', 'listItem') - }, - // TODO: understand this, maybe fix it and / or tweak it. - todo_item: () => ({ chain, commands, state }) => { - const schema = state.schema - const selection = state.selection - const $from = selection.$from - const $to = selection.$to - const range = $from.blockRange($to) - - if (!range) { - return false - } - - let parentList = getParentList(schema, selection) - - const start = (typeof parentList === 'undefined') - ? chain().bulletListItem() - : chain() - - start - .command(({ tr }) => { - if (typeof parentList === 'undefined') { - parentList = getParentList(schema, tr.selection) - } - - if (typeof parentList === 'undefined') { - return false - } - if (parentList.node.attrs.type === TYPES.CHECKBOX) { - return commands.toggleList('bulletList', 'listItem') - } - - tr.doc.nodesBetween(tr.selection.from, tr.selection.to, (node, pos) => { - if (node.type === schema.nodes.listItem) { - tr.setNodeMarkup(pos, node.type, { - type: parentList.node.attrs.type === TYPES.CHECKBOX ? TYPES.BULLET : TYPES.CHECKBOX, - }) - } - }) - tr.scrollIntoView() - }) - .run() - return true - }, - } - }, - - addInputRules() { - return [ - wrappingInputRule({ - find: /^\s*([-+*])\s(\[(x|X| ?)\])\s$/, - type: this.type, - getAttributes: match => ({ - type: TYPES.CHECKBOX, - done: 'xX'.includes(match[match.length - 1]), - }), - }), - listInputRule( - /^\s*([-+*])\s([^\s[])$/, - this.type, - _match => ({ type: TYPES.BULLET }), - ), - ] - }, - - addProseMirrorPlugins() { - return [ - new Plugin({ - props: { - handleClick: (view, pos, event) => { - const state = view.state - const schema = state.schema - - const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY }) - const position = state.doc.resolve(coordinates.pos) - const parentList = findParentNodeClosestToPos(position, function(node) { - return node.type === schema.nodes.listItem - }) - const isListClicked = event.target.tagName.toLowerCase() === 'li' - if (typeof parentList === 'undefined' || parentList.node.attrs.type !== TYPES.CHECKBOX || !isListClicked) { - return - } - - const tr = state.tr - tr.setNodeMarkup(parentList.pos, schema.nodes.listItem, { done: !parentList.node.attrs.done, type: TYPES.CHECKBOX }) - view.dispatch(tr) - }, - }, - }), - ] - }, - -}) - -export default ListItem diff --git a/src/nodes/TaskItem.js b/src/nodes/TaskItem.js new file mode 100644 index 000000000..74db3e82a --- /dev/null +++ b/src/nodes/TaskItem.js @@ -0,0 +1,134 @@ +/* + * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +import TipTapTaskItem from '@tiptap/extension-task-item' +import { wrappingInputRule, mergeAttributes } from '@tiptap/core' +import { Plugin } from 'prosemirror-state' +import { findParentNodeClosestToPos } from 'prosemirror-utils' + +const TaskItem = TipTapTaskItem.extend({ + + addOptions() { + return { + nested: true, + HTMLAttributes: {}, + } + }, + + draggable: false, + + content: 'paragraph block*', + + addAttributes() { + const adjust = { ...this.parent() } + adjust.checked.parseHTML = el => { + return el.querySelector('input[type=checkbox]')?.checked + } + return adjust + }, + + parseHTML: [ + { + priority: 101, + tag: 'li', + getAttrs: el => { + const checkbox = el.querySelector('input[type=checkbox]') + return checkbox + }, + context: 'taskList/', + }, + ], + + renderHTML({ node, HTMLAttributes }) { + const listAttributes = { class: 'checkbox-item' } + const checkboxAttributes = { type: 'checkbox', class: '', contenteditable: false } + if (node.attrs.checked) { + checkboxAttributes.checked = true + listAttributes.class += ' checked' + } + return [ + 'li', + mergeAttributes(HTMLAttributes, listAttributes), + [ + 'input', + checkboxAttributes, + ], + [ + 'label', + 0, + ], + ] + }, + + // overwrite the parent node view so renderHTML gets used + addNodeView: false, + + toMarkdown: (state, node) => { + state.write(`[${node.attrs.checked ? 'x' : ' '}] `) + state.renderContent(node) + }, + + addInputRules() { + return [ + ...this.parent(), + wrappingInputRule({ + find: /^\s*([-+*])\s(\[(x|X| ?)\])\s$/, + type: this.type, + getAttributes: match => ({ + checked: 'xX'.includes(match[match.length - 1]), + }), + }), + ] + }, + + addProseMirrorPlugins() { + return [ + new Plugin({ + props: { + handleClick: (view, pos, event) => { + const state = view.state + const schema = state.schema + + const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY }) + const position = state.doc.resolve(coordinates.pos) + const parentList = findParentNodeClosestToPos(position, function(node) { + return node.type === schema.nodes.taskItem + || node.type === schema.nodes.listItem + }) + const isListClicked = event.target.tagName.toLowerCase() === 'li' + if (!isListClicked + || !parentList + || parentList.node.type !== schema.nodes.taskItem) { + return + } + const tr = state.tr + tr.setNodeMarkup(parentList.pos, schema.nodes.taskItem, { checked: !parentList.node.attrs.checked }) + view.dispatch(tr) + }, + }, + }), + ] + }, + +}) + +export default TaskItem diff --git a/src/nodes/TaskList.js b/src/nodes/TaskList.js new file mode 100644 index 000000000..5bb4be796 --- /dev/null +++ b/src/nodes/TaskList.js @@ -0,0 +1,40 @@ +/* + * @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +import TiptapTaskList from '@tiptap/extension-task-list' + +const TaskList = TiptapTaskList.extend({ + + parseHTML: [ + { + priority: 100, + tag: 'ul.contains-task-list', + }, + ], + + toMarkdown: (state, node) => { + state.renderList(node, ' ', () => (node.attrs.bullet || '*') + ' ') + }, + +}) + +export default TaskList diff --git a/src/nodes/index.js b/src/nodes/index.js index c0ebf5aac..4340f05c9 100644 --- a/src/nodes/index.js +++ b/src/nodes/index.js @@ -22,16 +22,18 @@ import Image from './Image' import PlainTextDocument from './PlainTextDocument' -import ListItem from './ListItem' import BulletList from './BulletList' +import TaskItem from './TaskItem' +import TaskList from './TaskList' import TrailingNode from './TrailingNode' import Heading from './Heading' export { Image, PlainTextDocument, - ListItem, BulletList, + TaskItem, + TaskList, TrailingNode, Heading, } |