diff --git a/docs/src/demos/Extensions/TaskList/index.vue b/docs/src/demos/Extensions/TaskList/index.vue
new file mode 100644
index 00000000..06d1f1be
--- /dev/null
+++ b/docs/src/demos/Extensions/TaskList/index.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
diff --git a/docs/src/docPages/api/extensions/image.md b/docs/src/docPages/api/extensions/image.md
index 3c463ea5..1c3c9132 100644
--- a/docs/src/docPages/api/extensions/image.md
+++ b/docs/src/docPages/api/extensions/image.md
@@ -9,8 +9,13 @@ npm install @tiptap/extension-image
yarn add @tiptap/extension-image
```
+## Settings
+| Option | Type | Default | Description |
+| ------ | ------- | ------- | ------------------------ |
+| inline | boolean | false | Renders the node inline. |
+
## Source code
[packages/extension-image/](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-image/)
## Usage
-
+
diff --git a/docs/src/docPages/api/extensions/task-list.md b/docs/src/docPages/api/extensions/task-list.md
new file mode 100644
index 00000000..3cc62c65
--- /dev/null
+++ b/docs/src/docPages/api/extensions/task-list.md
@@ -0,0 +1,31 @@
+# TaskList
+This extension enables you to use task lists in the editor. They are rendered as `
` HTML tags.
+
+## Installation
+::: warning Use with TaskItem
+The `TaskList` extension is intended to be used with the [`TaskItem`](/api/extensions/task-item) extension. Make sure to import that one too, otherwise you’ll get a SyntaxError.
+:::
+
+```bash
+# With npm
+npm install @tiptap/extension-task-list @tiptap/extension-task-item
+
+# Or: With Yarn
+yarn add @tiptap/extension-task-list @tiptap/extension-task-item
+```
+
+## Settings
+| Option | Type | Default | Description |
+| ------ | ------ | ------- | -------------------------------------------- |
+| class | string | – | Add a custom class to the rendered HTML tag. |
+
+## Commands
+| Command | Options | Description |
+| ----------- | ------- | --------------------- |
+| task_list | — | Toggle a task list. |
+
+## Source code
+[packages/extension-task-list/](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-task-list/)
+
+## Usage
+
diff --git a/docs/src/links.yaml b/docs/src/links.yaml
index 94fc555c..e52f8da1 100644
--- a/docs/src/links.yaml
+++ b/docs/src/links.yaml
@@ -158,6 +158,9 @@
# draft: true
- title: Strike
link: /api/extensions/strike
+ - title: TaskList
+ link: /api/extensions/task-list
+ draft: true
# - title: TableCell
# link: /api/extensions/table-cell
# draft: true
diff --git a/packages/core/src/Editor.ts b/packages/core/src/Editor.ts
index c49cd3de..238140df 100644
--- a/packages/core/src/Editor.ts
+++ b/packages/core/src/Editor.ts
@@ -75,8 +75,6 @@ declare module './Editor' {
@magicMethods
export class Editor extends EventEmitter {
- public renderer!: any
-
private proxy!: Editor
private commandManager!: CommandManager
@@ -265,10 +263,16 @@ export class Editor extends EventEmitter {
],
}),
dispatchTransaction: this.dispatchTransaction.bind(this),
+ })
+
+ // `editor.view` is not yet available at this time.
+ // Therefore we will add all node views directly afterwards.
+ this.view.setProps({
nodeViews: this.extensionManager.nodeViews,
})
- // store editor in dom element for better testing
+ // Let’s store the editor instance in the DOM element.
+ // So we’ll have access to it for tests.
const dom = this.view.dom as HTMLElement
dom.editor = this.proxy
}
diff --git a/packages/core/src/ExtensionManager.ts b/packages/core/src/ExtensionManager.ts
index 2da65cd5..23b91aac 100644
--- a/packages/core/src/ExtensionManager.ts
+++ b/packages/core/src/ExtensionManager.ts
@@ -1,14 +1,15 @@
import { Plugin } from 'prosemirror-state'
import { keymap } from 'prosemirror-keymap'
-// import { Schema, Node as ProsemirrorNode } from 'prosemirror-model'
+import { Schema, Node as ProsemirrorNode } from 'prosemirror-model'
import { inputRules } from 'prosemirror-inputrules'
-// import { EditorView, Decoration } from 'prosemirror-view'
-import { Schema } from 'prosemirror-model'
+import { EditorView, Decoration } from 'prosemirror-view'
import { Editor } from './Editor'
-// import capitalize from './utils/capitalize'
-import { Extensions } from './types'
+import { Extensions, NodeViewRenderer } from './types'
import getSchema from './utils/getSchema'
import getSchemaTypeByName from './utils/getSchemaTypeByName'
+import splitExtensions from './utils/splitExtensions'
+import getAttributesFromExtensions from './utils/getAttributesFromExtensions'
+import getRenderedAttributes from './utils/getRenderedAttributes'
export default class ExtensionManager {
@@ -98,36 +99,41 @@ export default class ExtensionManager {
}
get nodeViews() {
- // const { renderer: Renderer } = this.editor
+ const { editor } = this
+ const { nodeExtensions } = splitExtensions(this.extensions)
+ const allAttributes = getAttributesFromExtensions(this.extensions)
- // if (!Renderer || !Renderer.type) {
- // return {}
- // }
+ return Object.fromEntries(nodeExtensions
+ .filter(extension => !!extension.addNodeView)
+ .map(extension => {
+ const extensionAttributes = allAttributes.filter(attribute => attribute.type === extension.name)
+ const context = {
+ options: extension.options,
+ editor,
+ type: getSchemaTypeByName(extension.name, this.schema),
+ }
- // const prop = `to${capitalize(Renderer.type)}`
+ const renderer = extension.addNodeView?.bind(context)?.() as NodeViewRenderer
- // return collect(this.extensions)
- // .where('extensionType', 'node')
- // .filter((extension: any) => extension.schema()[prop])
- // .map((extension: any) => {
- // return (
- // node: ProsemirrorNode,
- // view: EditorView,
- // getPos: (() => number) | boolean,
- // decorations: Decoration[],
- // ) => {
- // return new Renderer(extension.schema()[prop], {
- // extension,
- // editor: this.editor,
- // node,
- // getPos,
- // decorations,
- // })
- // }
- // })
- // .all()
+ const nodeview = (
+ node: ProsemirrorNode,
+ view: EditorView,
+ getPos: (() => number) | boolean,
+ decorations: Decoration[],
+ ) => {
+ const attributes = getRenderedAttributes(node, extensionAttributes)
- return {}
+ return renderer({
+ editor,
+ node,
+ getPos,
+ decorations,
+ attributes,
+ })
+ }
+
+ return [extension.name, nodeview]
+ }))
}
}
diff --git a/packages/core/src/MarkExtension.ts b/packages/core/src/MarkExtension.ts
index 4627b681..da1d1f41 100644
--- a/packages/core/src/MarkExtension.ts
+++ b/packages/core/src/MarkExtension.ts
@@ -10,22 +10,22 @@ export interface MarkExtensionSpec extends Overwrit
/**
* Inclusive
*/
- inclusive?: MarkSpec['inclusive'],
+ inclusive?: MarkSpec['inclusive'] | ((this: { options: Options }) => MarkSpec['inclusive']),
/**
* Excludes
*/
- excludes?: MarkSpec['excludes'],
+ excludes?: MarkSpec['excludes'] | ((this: { options: Options }) => MarkSpec['excludes']),
/**
* Group
*/
- group?: MarkSpec['group'],
+ group?: MarkSpec['group'] | ((this: { options: Options }) => MarkSpec['group']),
/**
* Spanning
*/
- spanning?: MarkSpec['spanning'],
+ spanning?: MarkSpec['spanning'] | ((this: { options: Options }) => MarkSpec['spanning']),
/**
* Parse HTML
diff --git a/packages/core/src/NodeExtension.ts b/packages/core/src/NodeExtension.ts
index 76dba816..cfb304d9 100644
--- a/packages/core/src/NodeExtension.ts
+++ b/packages/core/src/NodeExtension.ts
@@ -3,7 +3,7 @@ import {
} from 'prosemirror-model'
import { Plugin } from 'prosemirror-state'
import { ExtensionSpec, defaultExtension } from './Extension'
-import { Attributes, Overwrite } from './types'
+import { Attributes, NodeViewRenderer, Overwrite } from './types'
import { Editor } from './Editor'
export interface NodeExtensionSpec extends Overwrite, {
@@ -12,55 +12,60 @@ export interface NodeExtensionSpec extends Overwrit
*/
topNode?: boolean,
+ /**
+ * List
+ */
+ list?: boolean,
+
/**
* Content
*/
- content?: NodeSpec['content'],
+ content?: NodeSpec['content'] | ((this: { options: Options }) => NodeSpec['content']),
/**
* Marks
*/
- marks?: NodeSpec['marks'],
+ marks?: NodeSpec['marks'] | ((this: { options: Options }) => NodeSpec['marks']),
/**
* Group
*/
- group?: NodeSpec['group'],
+ group?: NodeSpec['group'] | ((this: { options: Options }) => NodeSpec['group']),
/**
* Inline
*/
- inline?: NodeSpec['inline'],
+ inline?: NodeSpec['inline'] | ((this: { options: Options }) => NodeSpec['inline']),
/**
* Atom
*/
- atom?: NodeSpec['atom'],
+ atom?: NodeSpec['atom'] | ((this: { options: Options }) => NodeSpec['atom']),
/**
* Selectable
*/
- selectable?: NodeSpec['selectable'],
+ selectable?: NodeSpec['selectable'] | ((this: { options: Options }) => NodeSpec['selectable']),
/**
* Draggable
*/
- draggable?: NodeSpec['draggable'],
+ draggable?: NodeSpec['draggable'] | ((this: { options: Options }) => NodeSpec['draggable']),
/**
* Code
*/
- code?: NodeSpec['code'],
+ code?: NodeSpec['code'] | ((this: { options: Options }) => NodeSpec['code']),
/**
* Defining
*/
- defining?: NodeSpec['defining'],
+ defining?: NodeSpec['defining'] | ((this: { options: Options }) => NodeSpec['defining']),
/**
* Isolating
*/
- isolating?: NodeSpec['isolating'],
+ isolating?: NodeSpec['isolating'] | ((this: { options: Options }) => NodeSpec['isolating']),
/**
* Parse HTML
@@ -139,6 +144,11 @@ export interface NodeExtensionSpec extends Overwrit
editor: Editor,
type: NodeType,
}) => Plugin[],
+
+ /**
+ * Node View
+ */
+ addNodeView?: (() => NodeViewRenderer) | null,
}> {}
export type NodeExtension = Required & {
@@ -153,6 +163,7 @@ const defaultNode: NodeExtension = {
type: 'node',
name: 'node',
topNode: false,
+ list: false,
content: null,
marks: null,
group: null,
@@ -166,6 +177,7 @@ const defaultNode: NodeExtension = {
parseHTML: () => null,
renderHTML: null,
addAttributes: () => ({}),
+ addNodeView: null,
}
export function createNode(config: NodeExtensionSpec) {
diff --git a/packages/core/src/extensions/toggleList.ts b/packages/core/src/extensions/toggleList.ts
index 2f7a15d3..a0bc3288 100644
--- a/packages/core/src/extensions/toggleList.ts
+++ b/packages/core/src/extensions/toggleList.ts
@@ -1,23 +1,19 @@
import { wrapInList, liftListItem } from 'prosemirror-schema-list'
import { findParentNode } from 'prosemirror-utils'
-import { Node, NodeType, Schema } from 'prosemirror-model'
+import { NodeType } from 'prosemirror-model'
import { Command } from '../Editor'
import { createExtension } from '../Extension'
import getNodeType from '../utils/getNodeType'
-
-function isList(node: Node, schema: Schema) {
- return (node.type === schema.nodes.bullet_list
- || node.type === schema.nodes.ordered_list
- || node.type === schema.nodes.todo_list)
-}
+import isList from '../utils/isList'
export const ToggleList = createExtension({
addCommands() {
return {
toggleList: (listTypeOrName: string | NodeType, itemTypeOrName: string | NodeType): Command => ({ tr, state, dispatch }) => {
+ const { extensions } = this.editor.options
const listType = getNodeType(listTypeOrName, state.schema)
const itemType = getNodeType(itemTypeOrName, state.schema)
- const { schema, selection } = state
+ const { selection } = state
const { $from, $to } = selection
const range = $from.blockRange($to)
@@ -25,14 +21,14 @@ export const ToggleList = createExtension({
return false
}
- const parentList = findParentNode(node => isList(node, schema))(selection)
+ const parentList = findParentNode(node => isList(node.type.name, extensions))(selection)
if (range.depth >= 1 && parentList && range.depth - parentList.depth <= 1) {
if (parentList.node.type === listType) {
return liftListItem(itemType)(state, dispatch)
}
- if (isList(parentList.node, schema) && listType.validContent(parentList.node.content)) {
+ if (isList(parentList.node.type.name, extensions) && listType.validContent(parentList.node.content)) {
tr.setNodeMarkup(parentList.pos, listType)
return false
diff --git a/packages/core/src/style.ts b/packages/core/src/style.ts
index ce819062..e98e74b8 100644
--- a/packages/core/src/style.ts
+++ b/packages/core/src/style.ts
@@ -9,6 +9,14 @@ const style = `.ProseMirror {
font-variant-ligatures: none;
}
+.ProseMirror [contenteditable="false"] {
+ white-space: normal;
+}
+
+.ProseMirror [contenteditable="false"] [contenteditable="true"] {
+ white-space: pre-wrap;
+}
+
.ProseMirror pre {
white-space: pre-wrap;
}
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index 5533a4fd..e0f09679 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -1,6 +1,9 @@
+import { Node } from 'prosemirror-model'
+import { Decoration, NodeView } from 'prosemirror-view'
import { Extension } from './Extension'
import { NodeExtension } from './NodeExtension'
import { MarkExtension } from './MarkExtension'
+import Editor from '..'
export type Extensions = (Extension | NodeExtension | MarkExtension)[]
@@ -40,3 +43,13 @@ export type Overwrite = Pick> & U;
export type AnyObject = {
[key: string]: any
}
+
+export type NodeViewRendererProps = {
+ editor: Editor,
+ node: Node,
+ getPos: (() => number) | boolean,
+ decorations: Decoration[],
+ attributes: AnyObject,
+}
+
+export type NodeViewRenderer = (props: NodeViewRendererProps) => NodeView
diff --git a/packages/core/src/utils/callOrReturn.ts b/packages/core/src/utils/callOrReturn.ts
new file mode 100644
index 00000000..55e78e0d
--- /dev/null
+++ b/packages/core/src/utils/callOrReturn.ts
@@ -0,0 +1,17 @@
+/**
+ * Optionally calls `value` as a function.
+ * Otherwise it is returned directly.
+ * @param value Function or any value.
+ * @param context Optional context to bind to function.
+ */
+export default function callOrReturn(value: any, context?: any) {
+ if (typeof value === 'function') {
+ if (context) {
+ return value.bind(context)()
+ }
+
+ return value()
+ }
+
+ return value
+}
diff --git a/packages/core/src/utils/getRenderedAttributes.ts b/packages/core/src/utils/getRenderedAttributes.ts
index da92012d..48fd7021 100644
--- a/packages/core/src/utils/getRenderedAttributes.ts
+++ b/packages/core/src/utils/getRenderedAttributes.ts
@@ -1,8 +1,8 @@
import { Node, Mark } from 'prosemirror-model'
-import { ExtensionAttribute } from '../types'
+import { ExtensionAttribute, AnyObject } from '../types'
import mergeAttributes from './mergeAttributes'
-export default function getRenderedAttributes(nodeOrMark: Node | Mark, extensionAttributes: ExtensionAttribute[]): { [key: string]: any } {
+export default function getRenderedAttributes(nodeOrMark: Node | Mark, extensionAttributes: ExtensionAttribute[]): AnyObject {
return extensionAttributes
.filter(item => item.attribute.rendered)
.map(item => {
diff --git a/packages/core/src/utils/getSchema.ts b/packages/core/src/utils/getSchema.ts
index 817ed7a8..1a191f50 100644
--- a/packages/core/src/utils/getSchema.ts
+++ b/packages/core/src/utils/getSchema.ts
@@ -5,6 +5,7 @@ import getAttributesFromExtensions from './getAttributesFromExtensions'
import getRenderedAttributes from './getRenderedAttributes'
import isEmptyObject from './isEmptyObject'
import injectExtensionAttributesToParseRule from './injectExtensionAttributesToParseRule'
+import callOrReturn from './callOrReturn'
function cleanUpSchemaItem(data: T) {
return Object.fromEntries(Object.entries(data).filter(([key, value]) => {
@@ -25,16 +26,16 @@ export default function getSchema(extensions: Extensions): Schema {
const extensionAttributes = allAttributes.filter(attribute => attribute.type === extension.name)
const context = { options: extension.options }
const schema: NodeSpec = cleanUpSchemaItem({
- content: extension.content,
- marks: extension.marks,
- group: extension.group,
- inline: extension.inline,
- atom: extension.atom,
- selectable: extension.selectable,
- draggable: extension.draggable,
- code: extension.code,
- defining: extension.defining,
- isolating: extension.isolating,
+ content: callOrReturn(extension.content, context),
+ marks: callOrReturn(extension.marks, context),
+ group: callOrReturn(extension.group, context),
+ inline: callOrReturn(extension.inline, context),
+ atom: callOrReturn(extension.atom, context),
+ selectable: callOrReturn(extension.selectable, context),
+ draggable: callOrReturn(extension.draggable, context),
+ code: callOrReturn(extension.code, context),
+ defining: callOrReturn(extension.defining, context),
+ isolating: callOrReturn(extension.isolating, context),
attrs: Object.fromEntries(extensionAttributes.map(extensionAttribute => {
return [extensionAttribute.name, { default: extensionAttribute?.attribute?.default }]
})),
@@ -60,10 +61,10 @@ export default function getSchema(extensions: Extensions): Schema {
const extensionAttributes = allAttributes.filter(attribute => attribute.type === extension.name)
const context = { options: extension.options }
const schema: MarkSpec = cleanUpSchemaItem({
- inclusive: extension.inclusive,
- excludes: extension.excludes,
- group: extension.group,
- spanning: extension.spanning,
+ inclusive: callOrReturn(extension.inclusive, context),
+ excludes: callOrReturn(extension.excludes, context),
+ group: callOrReturn(extension.group, context),
+ spanning: callOrReturn(extension.spanning, context),
attrs: Object.fromEntries(extensionAttributes.map(extensionAttribute => {
return [extensionAttribute.name, { default: extensionAttribute?.attribute?.default }]
})),
diff --git a/packages/core/src/utils/isList.ts b/packages/core/src/utils/isList.ts
new file mode 100644
index 00000000..3a5a2305
--- /dev/null
+++ b/packages/core/src/utils/isList.ts
@@ -0,0 +1,13 @@
+import { Extensions } from '../types'
+import splitExtensions from './splitExtensions'
+
+export default function isList(name: string, extensions: Extensions) {
+ const { nodeExtensions } = splitExtensions(extensions)
+ const extension = nodeExtensions.find(item => item.name === name)
+
+ if (!extension) {
+ return false
+ }
+
+ return extension.list
+}
diff --git a/packages/extension-bullet-list/index.ts b/packages/extension-bullet-list/index.ts
index 45e6bc18..e116ca3c 100644
--- a/packages/extension-bullet-list/index.ts
+++ b/packages/extension-bullet-list/index.ts
@@ -1,13 +1,17 @@
import { Command, createNode } from '@tiptap/core'
import { wrappingInputRule } from 'prosemirror-inputrules'
+export const inputRegex = /^\s*([-+*])\s$/
+
const BulletList = createNode({
name: 'bullet_list',
- content: 'list_item+',
+ list: true,
group: 'block',
+ content: 'list_item+',
+
parseHTML() {
return [
{ tag: 'ul' },
@@ -34,7 +38,7 @@ const BulletList = createNode({
addInputRules() {
return [
- wrappingInputRule(/^\s*([-+*])\s$/, this.type),
+ wrappingInputRule(inputRegex, this.type),
]
},
})
diff --git a/packages/extension-image/index.ts b/packages/extension-image/index.ts
index 424cffc5..67e9c04a 100644
--- a/packages/extension-image/index.ts
+++ b/packages/extension-image/index.ts
@@ -1,13 +1,25 @@
import { Command, createNode, nodeInputRule } from '@tiptap/core'
+export interface ImageOptions {
+ inline: boolean,
+}
+
export const inputRegex = /!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/
const Image = createNode({
name: 'image',
- inline: true,
+ defaultOptions: {
+ inline: false,
+ },
- group: 'inline',
+ inline() {
+ return this.options.inline
+ },
+
+ group() {
+ return this.options.inline ? 'inline' : 'block'
+ },
draggable: true,
diff --git a/packages/extension-list-item/index.ts b/packages/extension-list-item/index.ts
index 3bfa852a..88cfca70 100644
--- a/packages/extension-list-item/index.ts
+++ b/packages/extension-list-item/index.ts
@@ -9,7 +9,9 @@ const ListItem = createNode({
parseHTML() {
return [
- { tag: 'li' },
+ {
+ tag: 'li',
+ },
]
},
diff --git a/packages/extension-ordered-list/index.ts b/packages/extension-ordered-list/index.ts
index 0b678728..e9d6e469 100644
--- a/packages/extension-ordered-list/index.ts
+++ b/packages/extension-ordered-list/index.ts
@@ -1,13 +1,17 @@
import { Command, createNode } from '@tiptap/core'
import { wrappingInputRule } from 'prosemirror-inputrules'
+export const inputRegex = /^(\d+)\.\s$/
+
const OrderedList = createNode({
name: 'ordered_list',
- content: 'list_item+',
+ list: true,
group: 'block',
+ content: 'list_item+',
+
addAttributes() {
return {
start: {
@@ -54,7 +58,7 @@ const OrderedList = createNode({
addInputRules() {
return [
wrappingInputRule(
- /^(\d+)\.\s$/,
+ inputRegex,
this.type,
match => ({ order: +match[1] }),
(match, node) => node.childCount + node.attrs.order === +match[1],
diff --git a/packages/extension-task-item/index.ts b/packages/extension-task-item/index.ts
new file mode 100644
index 00000000..ac64dceb
--- /dev/null
+++ b/packages/extension-task-item/index.ts
@@ -0,0 +1,120 @@
+import { createNode, mergeAttributes } from '@tiptap/core'
+import { wrappingInputRule } from 'prosemirror-inputrules'
+
+export const inputRegex = /^\s*(\[([ |x])\])\s$/
+
+export interface TaskItemOptions {
+ nested: boolean,
+}
+
+const TaskItem = createNode({
+ name: 'task_item',
+
+ content() {
+ return this.options.nested ? '(paragraph|task_list)+' : 'paragraph+'
+ },
+
+ defining: true,
+
+ defaultOptions: {
+ nested: false,
+ },
+
+ addAttributes() {
+ return {
+ checked: {
+ default: false,
+ parseHTML: element => ({
+ checked: element.getAttribute('data-checked') === 'true',
+ }),
+ renderHTML: attributes => ({
+ 'data-checked': attributes.checked,
+ }),
+ },
+ }
+ },
+
+ parseHTML() {
+ return [
+ {
+ tag: 'li[data-type="task_item"]',
+ priority: 51,
+ },
+ ]
+ },
+
+ renderHTML({ attributes }) {
+ return ['li', mergeAttributes(attributes, { 'data-type': 'task_item' }), 0]
+ },
+
+ addKeyboardShortcuts() {
+ const shortcuts = {
+ Enter: () => this.editor.splitListItem('task_item'),
+ }
+
+ if (!this.options.nested) {
+ return shortcuts
+ }
+
+ return {
+ ...shortcuts,
+ Tab: () => this.editor.sinkListItem('task_item'),
+ 'Shift-Tab': () => this.editor.liftListItem('task_item'),
+ }
+ },
+
+ addNodeView() {
+ return ({ attributes, getPos, editor }) => {
+ const { view } = editor
+ const listItem = document.createElement('li')
+ const checkbox = document.createElement('input')
+ const content = document.createElement('div')
+
+ checkbox.type = 'checkbox'
+ checkbox.addEventListener('change', event => {
+ const { checked } = event.target as any
+
+ if (typeof getPos === 'function') {
+ view.dispatch(view.state.tr.setNodeMarkup(getPos(), undefined, {
+ checked,
+ }))
+ }
+ })
+
+ if (attributes['data-checked'] === true) {
+ checkbox.setAttribute('checked', 'checked')
+ }
+
+ listItem.append(checkbox, content)
+
+ Object.entries(attributes).forEach(([key, value]) => {
+ listItem.setAttribute(key, value)
+ })
+
+ return {
+ dom: listItem,
+ contentDOM: content,
+ }
+ }
+ },
+
+ addInputRules() {
+ return [
+ wrappingInputRule(
+ inputRegex,
+ this.type,
+ match => ({
+ checked: match[match.length - 1] === 'x',
+ }),
+ ),
+ ]
+ },
+})
+
+export default TaskItem
+
+declare module '@tiptap/core/src/Editor' {
+ interface AllExtensions {
+ TaskItem: typeof TaskItem,
+ }
+}
diff --git a/packages/extension-task-item/package.json b/packages/extension-task-item/package.json
new file mode 100644
index 00000000..cb3e4b38
--- /dev/null
+++ b/packages/extension-task-item/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "@tiptap/extension-task-item",
+ "version": "1.0.0",
+ "source": "index.ts",
+ "main": "dist/tiptap-extension-task-item.js",
+ "umd:main": "dist/tiptap-extension-task-item.umd.js",
+ "module": "dist/tiptap-extension-task-item.mjs",
+ "unpkg": "dist/tiptap-extension-task-item.js",
+ "jsdelivr": "dist/tiptap-extension-task-item.js",
+ "files": [
+ "src",
+ "dist"
+ ],
+ "peerDependencies": {
+ "@tiptap/core": "2.x"
+ }
+}
diff --git a/packages/extension-task-list/index.ts b/packages/extension-task-list/index.ts
new file mode 100644
index 00000000..bd73d1e2
--- /dev/null
+++ b/packages/extension-task-list/index.ts
@@ -0,0 +1,40 @@
+import { Command, createNode, mergeAttributes } from '@tiptap/core'
+
+const TaskList = createNode({
+ name: 'task_list',
+
+ list: true,
+
+ group: 'block',
+
+ content: 'task_item+',
+
+ parseHTML() {
+ return [
+ {
+ tag: 'ul[data-type="task_list"]',
+ priority: 51,
+ },
+ ]
+ },
+
+ renderHTML({ attributes }) {
+ return ['ul', mergeAttributes(attributes, { 'data-type': 'task_list' }), 0]
+ },
+
+ addCommands() {
+ return {
+ taskList: (): Command => ({ commands }) => {
+ return commands.toggleList('task_list', 'task_item')
+ },
+ }
+ },
+})
+
+export default TaskList
+
+declare module '@tiptap/core/src/Editor' {
+ interface AllExtensions {
+ TaskList: typeof TaskList,
+ }
+}
diff --git a/packages/extension-task-list/package.json b/packages/extension-task-list/package.json
new file mode 100644
index 00000000..50add09a
--- /dev/null
+++ b/packages/extension-task-list/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "@tiptap/extension-task-list",
+ "version": "1.0.0",
+ "source": "index.ts",
+ "main": "dist/tiptap-extension-task-list.js",
+ "umd:main": "dist/tiptap-extension-task-list.umd.js",
+ "module": "dist/tiptap-extension-task-list.mjs",
+ "unpkg": "dist/tiptap-extension-task-list.js",
+ "jsdelivr": "dist/tiptap-extension-task-list.js",
+ "files": [
+ "src",
+ "dist"
+ ],
+ "peerDependencies": {
+ "@tiptap/core": "2.x"
+ }
+}
diff --git a/packages/vue/index.ts b/packages/vue/index.ts
index 79050a05..80cb8873 100644
--- a/packages/vue/index.ts
+++ b/packages/vue/index.ts
@@ -1,9 +1,3 @@
-import { Editor as CoreEditor } from '@tiptap/core'
-import Renderer from './src/Renderer'
-
export * from '@tiptap/core'
+export { default as VueRenderer } from './src/VueRenderer'
export { default as EditorContent } from './src/components/EditorContent'
-
-export class Editor extends CoreEditor {
- renderer = Renderer
-}
diff --git a/packages/vue/src/Renderer.ts b/packages/vue/src/Renderer.ts
deleted file mode 100644
index 67d3e6ec..00000000
--- a/packages/vue/src/Renderer.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import Vue from 'vue'
-import { ComponentRenderer } from '@tiptap/core'
-
-export default class Renderer extends ComponentRenderer {
-
- static type = 'vue'
-
- vm!: Vue
-
- constructor(component: Vue) {
- super()
- this.mount(component)
- }
-
- mount(component: Vue) {
- const Component = Vue.extend(component)
-
- this.vm = new Component({
- // parent: this.parent,
- // propsData: props,
- }).$mount()
- }
-
- get dom() {
- return this.vm.$el
- }
-
- get contentDOM() {
- return this.vm.$refs.content
- }
-
-}
diff --git a/packages/vue/src/VueRenderer.ts b/packages/vue/src/VueRenderer.ts
new file mode 100644
index 00000000..9bfd525c
--- /dev/null
+++ b/packages/vue/src/VueRenderer.ts
@@ -0,0 +1,42 @@
+import { NodeViewRendererProps } from '@tiptap/core'
+import { NodeView } from 'prosemirror-view'
+import Vue from 'vue'
+import { VueConstructor } from 'vue/types/umd'
+
+class VueNodeView implements NodeView {
+
+ vm!: Vue
+
+ constructor(component: Vue | VueConstructor, props: NodeViewRendererProps) {
+ // const { node, editor, getPos } = props
+ // const { view } = editor
+
+ this.mount(component)
+ }
+
+ mount(component: Vue | VueConstructor) {
+ const Component = Vue.extend(component)
+
+ this.vm = new Component({
+ // parent: this.parent,
+ // propsData: props,
+ }).$mount()
+ }
+
+ get dom() {
+ return this.vm.$el
+ }
+
+ get contentDOM() {
+ return this.vm.$refs.content as Element
+ }
+
+ stopEvent() {
+ return true
+ }
+
+}
+
+export default function VueRenderer(component: Vue | VueConstructor) {
+ return (props: NodeViewRendererProps) => new VueNodeView(component, props) as NodeView
+}