diff options
-rw-r--r-- | css/prosemirror.scss | 18 | ||||
-rw-r--r-- | package-lock.json | 17 | ||||
-rw-r--r-- | package.json | 4 | ||||
-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 |
8 files changed, 213 insertions, 10 deletions
diff --git a/css/prosemirror.scss b/css/prosemirror.scss index c16cfe929..cd6c6ea2d 100644 --- a/css/prosemirror.scss +++ b/css/prosemirror.scss @@ -24,6 +24,21 @@ div.ProseMirror { font-size: 14px; } + li label.checkbox-label { + width: 100%; + display: flex; + margin-top: 10px; + + &:before { + position: relative; + top: 2px; + } + div.checkbox-wrapper { + & > p { + margin-top: -1px; + } + } + } p:first-child, h1:first-child, h2:first-child, @@ -127,7 +142,8 @@ div.ProseMirror { } ul, ol { - padding-left: 14px; + padding-left: 10px; + margin-left: 10px; } ul li { diff --git a/package-lock.json b/package-lock.json index 9348493c5..654ac0f04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11202,6 +11202,11 @@ "uc.micro": "^1.0.5" } }, + "markdown-it-task-lists": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz", + "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==" + }, "markdown-table": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-1.1.3.tgz", @@ -12893,9 +12898,9 @@ } }, "prosemirror-inputrules": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.0.4.tgz", - "integrity": "sha512-RhuBghqUgYWm8ai/P+k1lMl1ZGvt6Cs3Xeur8oN0L1Yy+Z5GmsTp3fT8RVl+vJeGkItEAxAit9Qh7yZxixX7rA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.1.2.tgz", + "integrity": "sha512-Ja5Z3BWestlHYGvtSGqyvxMeB8QEuBjlHM8YnKtLGUXMDp965qdDV4goV8lJb17kIWHk7e7JNj6Catuoa3302g==", "requires": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.0.0" @@ -12947,9 +12952,9 @@ } }, "prosemirror-schema-list": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.0.4.tgz", - "integrity": "sha512-7Y0b6FIG6ATnCcDSLrZfU9yIfOG5Yad3DMNZ9W7GGfMSzdIl0aHExrsIUgviJZjovO2jtLJVbxWGjMR3OrTupA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.1.2.tgz", + "integrity": "sha512-dgM9PwtM4twa5WsgSYMB+J8bwjnR43DAD3L9MsR9rKm/nZR5Y85xcjB7gusVMSsbQ2NomMZF03RE6No6mTnclQ==", "requires": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0" diff --git a/package.json b/package.json index b603317cf..e7f92e608 100644 --- a/package.json +++ b/package.json @@ -28,9 +28,13 @@ "escape-html": "^1.0.3", "highlight.js": "^9.16.2", "markdown-it": "^8.4.2", + "markdown-it-task-lists": "^2.1.1", "nextcloud-vue": "^0.12.7", "prosemirror-collab": "^1.2.2", + "prosemirror-inputrules": "^1.1.2", "prosemirror-markdown": "^1.4.2", + "prosemirror-schema-list": "^1.1.2", + "prosemirror-utils": "^0.9.6", "prosemirror-view": "^1.13.4", "tiptap": "^1.26.4", "tiptap-commands": "^1.12.3", 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') + }) }) |