diff options
author | Julius Härtl <jus@bitgrid.net> | 2019-11-29 21:36:43 +0300 |
---|---|---|
committer | Julius Härtl <jus@bitgrid.net> | 2019-12-02 10:39:20 +0300 |
commit | ffdf26ff3dcf4295b6f296881226b08afa90235d (patch) | |
tree | 6f1eb62960d5df5fc46d114a49dbb4ae5baa63f6 /src | |
parent | 10488dcbf791388a49adb0e0d9d42e8ed48c167d (diff) |
Implement checkbox support
Signed-off-by: Julius Härtl <jus@bitgrid.net>
Diffstat (limited to 'src')
-rw-r--r-- | src/EditorFactory.js | 5 | ||||
-rw-r--r-- | src/mixins/menubar.js | 8 | ||||
-rw-r--r-- | src/nodes/ListItem.js | 161 | ||||
-rw-r--r-- | src/nodes/index.js | 2 | ||||
-rw-r--r-- | src/tests/markdown.spec.js | 8 |
5 files changed, 181 insertions, 3 deletions
diff --git a/src/EditorFactory.js b/src/EditorFactory.js index b9acc42dd..9b37cb5c1 100644 --- a/src/EditorFactory.js +++ b/src/EditorFactory.js @@ -27,7 +27,6 @@ import { Link, BulletList, OrderedList, - ListItem, Blockquote, CodeBlock, CodeBlockHighlight, @@ -36,8 +35,9 @@ import { Placeholder, } from 'tiptap-extensions' import { Strong, Italic, Strike } from './marks' -import { Image, PlainTextDocument } from './nodes' +import { Image, PlainTextDocument, ListItem } from './nodes' import MarkdownIt from 'markdown-it' +import taskLists from 'markdown-it-task-lists' import { MarkdownSerializer, defaultMarkdownSerializer } from 'prosemirror-markdown' @@ -107,6 +107,7 @@ const createEditor = ({ content, onInit, onUpdate, extensions, enableRichEditing const markdownit = MarkdownIt('commonmark', { html: false, breaks: false }) .enable('strikethrough') + .use(taskLists, { enable: true, labelAfter: true }) const SerializeException = function(message) { this.message = message diff --git a/src/mixins/menubar.js b/src/mixins/menubar.js index 482a6e3ba..92b1a3c9b 100644 --- a/src/mixins/menubar.js +++ b/src/mixins/menubar.js @@ -124,7 +124,7 @@ export default [ class: 'icon-ul', isActive: (isActive) => isActive.bullet_list(), action: (command) => { - return command.bullet_list() + return command.bullet_list_item() }, }, { @@ -136,6 +136,12 @@ export default [ }, }, { + label: t('text', 'ToDo list'), + class: 'icon-checkmark', + isActive: (isActive) => isActive.list_item(), + action: (command) => command.todo_item(), + }, + { label: t('text', 'Blockquote'), class: 'icon-quote', isActive: (isActive) => isActive.blockquote(), diff --git a/src/nodes/ListItem.js b/src/nodes/ListItem.js new file mode 100644 index 000000000..864c69d04 --- /dev/null +++ b/src/nodes/ListItem.js @@ -0,0 +1,161 @@ +/* + * @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 { ListItem as TiptapListItem } from 'tiptap-extensions' +import { Plugin } from 'tiptap' +import { toggleList } from 'tiptap-commands' +import { findParentNode } from 'prosemirror-utils' + +const TYPES = { + BULLET: 0, + CHECKBOX: 1, +} + +export default class ListItem extends TiptapListItem { + + get defaultOptions() { + return { + nested: true, + } + } + + get schema() { + return { + attrs: { + done: { + default: false, + }, + type: { + default: TYPES.BULLET, + }, + }, + draggable: true, + content: 'paragraph block*', + toDOM: node => { + if (node.attrs.type === TYPES.BULLET) { + return ['li', 0] + } + const checkboxAttributes = { type: 'checkbox', class: 'checkbox' } + if (node.attrs.done) { + checkboxAttributes.checked = true + } + return [ + 'li', + [ + 'input', + checkboxAttributes, + ], + [ + 'label', + { class: 'checkbox-label' }, + ['div', { class: 'checkbox-wrapper' }, 0], + ], + ] + }, + parseDOM: [ + { + 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) + }, + } + } + + commands({ type, schema }) { + return { + 'bullet_list_item': () => { + return (state, dispatch, view) => { + return toggleList(schema.nodes.bullet_list, type)(state, dispatch, view) + } + }, + 'todo_item': () => { + return (state, dispatch, view) => { + const schema = state.schema + const selection = state.selection + const $from = selection.$from + const $to = selection.$to + const range = $from.blockRange($to) + const tr = state.tr + + if (!range) { + return false + } + + const parentList = findParentNode(function(node) { + return node.type === schema.nodes.list_item + })(selection) + + tr.setNodeMarkup(parentList.pos, schema.nodes.list_item, { type: parentList.node.attrs.type === TYPES.CHECKBOX ? TYPES.BULLET : TYPES.CHECKBOX }) + + if (dispatch) { + dispatch(tr) + } + + } + }, + } + } + + get plugins() { + return [ + new Plugin({ + props: { + handleClick: (view, pos, event) => { + const state = view.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 + } + + const parentList = findParentNode(function(node) { + return node.type === schema.nodes.list_item + })(selection) + + if (parentList.node.attrs.type !== TYPES.CHECKBOX) { + return + } + + const tr = state.tr + tr.setNodeMarkup(parentList.pos, schema.nodes.list_item, { done: !parentList.node.attrs.done, type: TYPES.CHECKBOX }) + view.dispatch(tr) + }, + }, + }), + ] + } + +} diff --git a/src/nodes/index.js b/src/nodes/index.js index e0eaf4882..c24428ee7 100644 --- a/src/nodes/index.js +++ b/src/nodes/index.js @@ -22,8 +22,10 @@ import Image from './Image' import PlainTextDocument from './PlainTextDocument' +import ListItem from './ListItem' export { Image, PlainTextDocument, + ListItem, } diff --git a/src/tests/markdown.spec.js b/src/tests/markdown.spec.js index 2c69aa9d3..354665b8a 100644 --- a/src/tests/markdown.spec.js +++ b/src/tests/markdown.spec.js @@ -95,4 +95,12 @@ describe('Markdown serializer from html', () => { test('images', () => { expect(markdownThroughEditorHtml('<img src="image" alt="description" />')).toBe('![description](image)') }) + test('checkboxes', () => { + expect(markdownThroughEditor('- [ ] asd')).toBe('* [ ] asd') + expect(markdownThroughEditor('- [X] asd')).toBe('* [x] asd') + expect(markdownThroughEditorHtml('<ul><li><input type="checkbox" checked /><label>foo</label></li></ul>')).toBe('* [x] foo') + expect(markdownThroughEditorHtml('<ul><li><input type="checkbox" /><label>test</label></li></ul>')).toBe('* [ ] test') + expect(markdownThroughEditorHtml('<ul><li><input type="checkbox" checked /><div><h2>Test</h2><p><strong>content</strong></p></div></li></ul>')).toBe('* [x] Test\n\n **content**') + expect(markdownThroughEditorHtml('<ul><li><input type="checkbox" checked /><p>Test</p><h1>Block level headline</h1></li></ul>')).toBe('* [x] Test\n\n # Block level headline') + }) }) |