Merge branch 'feature/node-views' into main

This commit is contained in:
Philipp Kühn
2020-10-30 16:58:13 +01:00
26 changed files with 504 additions and 122 deletions

View File

@@ -0,0 +1,53 @@
<template>
<div v-if="editor">
<button @click="editor.chain().focus().taskList().run()" :class="{ 'is-active': editor.isActive('task_list') }">
task list
</button>
<editor-content :editor="editor" />
</div>
</template>
<script>
import { Editor } from '@tiptap/core'
import { EditorContent } from '@tiptap/vue'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import TaskList from '@tiptap/extension-task-list'
import TaskItem from '@tiptap/extension-task-item'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [
Document(),
Paragraph(),
Text(),
TaskList(),
TaskItem(),
],
content: `
<ul data-type="task_list">
<li data-type="task_item" data-checked="true">A list item</li>
<li data-type="task_item" data-checked="false">And another one</li>
</ul>
`,
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>

View File

@@ -9,8 +9,13 @@ npm install @tiptap/extension-image
yarn add @tiptap/extension-image yarn add @tiptap/extension-image
``` ```
## Settings
| Option | Type | Default | Description |
| ------ | ------- | ------- | ------------------------ |
| inline | boolean | false | Renders the node inline. |
## Source code ## Source code
[packages/extension-image/](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-image/) [packages/extension-image/](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-image/)
## Usage ## Usage
<demo name="Extensions/Image" highlight="12,30" /> <demo name="Extensions/Image" />

View File

@@ -0,0 +1,31 @@
# TaskList
This extension enables you to use task lists in the editor. They are rendered as `<ul>` 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 youll 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
<demo name="Extensions/TaskList" />

View File

@@ -158,6 +158,9 @@
# draft: true # draft: true
- title: Strike - title: Strike
link: /api/extensions/strike link: /api/extensions/strike
- title: TaskList
link: /api/extensions/task-list
draft: true
# - title: TableCell # - title: TableCell
# link: /api/extensions/table-cell # link: /api/extensions/table-cell
# draft: true # draft: true

View File

@@ -75,8 +75,6 @@ declare module './Editor' {
@magicMethods @magicMethods
export class Editor extends EventEmitter { export class Editor extends EventEmitter {
public renderer!: any
private proxy!: Editor private proxy!: Editor
private commandManager!: CommandManager private commandManager!: CommandManager
@@ -265,10 +263,16 @@ export class Editor extends EventEmitter {
], ],
}), }),
dispatchTransaction: this.dispatchTransaction.bind(this), 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, nodeViews: this.extensionManager.nodeViews,
}) })
// store editor in dom element for better testing // Lets store the editor instance in the DOM element.
// So well have access to it for tests.
const dom = this.view.dom as HTMLElement const dom = this.view.dom as HTMLElement
dom.editor = this.proxy dom.editor = this.proxy
} }

View File

@@ -1,14 +1,15 @@
import { Plugin } from 'prosemirror-state' import { Plugin } from 'prosemirror-state'
import { keymap } from 'prosemirror-keymap' 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 { inputRules } from 'prosemirror-inputrules'
// import { EditorView, Decoration } from 'prosemirror-view' import { EditorView, Decoration } from 'prosemirror-view'
import { Schema } from 'prosemirror-model'
import { Editor } from './Editor' import { Editor } from './Editor'
// import capitalize from './utils/capitalize' import { Extensions, NodeViewRenderer } from './types'
import { Extensions } from './types'
import getSchema from './utils/getSchema' import getSchema from './utils/getSchema'
import getSchemaTypeByName from './utils/getSchemaTypeByName' import getSchemaTypeByName from './utils/getSchemaTypeByName'
import splitExtensions from './utils/splitExtensions'
import getAttributesFromExtensions from './utils/getAttributesFromExtensions'
import getRenderedAttributes from './utils/getRenderedAttributes'
export default class ExtensionManager { export default class ExtensionManager {
@@ -98,36 +99,41 @@ export default class ExtensionManager {
} }
get nodeViews() { 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 Object.fromEntries(nodeExtensions
// return {} .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) const nodeview = (
// .where('extensionType', 'node') node: ProsemirrorNode,
// .filter((extension: any) => extension.schema()[prop]) view: EditorView,
// .map((extension: any) => { getPos: (() => number) | boolean,
// return ( decorations: Decoration[],
// node: ProsemirrorNode, ) => {
// view: EditorView, const attributes = getRenderedAttributes(node, extensionAttributes)
// getPos: (() => number) | boolean,
// decorations: Decoration[],
// ) => {
// return new Renderer(extension.schema()[prop], {
// extension,
// editor: this.editor,
// node,
// getPos,
// decorations,
// })
// }
// })
// .all()
return {} return renderer({
editor,
node,
getPos,
decorations,
attributes,
})
}
return [extension.name, nodeview]
}))
} }
} }

View File

@@ -10,22 +10,22 @@ export interface MarkExtensionSpec<Options = {}, Commands = {}> extends Overwrit
/** /**
* Inclusive * Inclusive
*/ */
inclusive?: MarkSpec['inclusive'], inclusive?: MarkSpec['inclusive'] | ((this: { options: Options }) => MarkSpec['inclusive']),
/** /**
* Excludes * Excludes
*/ */
excludes?: MarkSpec['excludes'], excludes?: MarkSpec['excludes'] | ((this: { options: Options }) => MarkSpec['excludes']),
/** /**
* Group * Group
*/ */
group?: MarkSpec['group'], group?: MarkSpec['group'] | ((this: { options: Options }) => MarkSpec['group']),
/** /**
* Spanning * Spanning
*/ */
spanning?: MarkSpec['spanning'], spanning?: MarkSpec['spanning'] | ((this: { options: Options }) => MarkSpec['spanning']),
/** /**
* Parse HTML * Parse HTML

View File

@@ -3,7 +3,7 @@ import {
} from 'prosemirror-model' } from 'prosemirror-model'
import { Plugin } from 'prosemirror-state' import { Plugin } from 'prosemirror-state'
import { ExtensionSpec, defaultExtension } from './Extension' import { ExtensionSpec, defaultExtension } from './Extension'
import { Attributes, Overwrite } from './types' import { Attributes, NodeViewRenderer, Overwrite } from './types'
import { Editor } from './Editor' import { Editor } from './Editor'
export interface NodeExtensionSpec<Options = {}, Commands = {}> extends Overwrite<ExtensionSpec<Options, Commands>, { export interface NodeExtensionSpec<Options = {}, Commands = {}> extends Overwrite<ExtensionSpec<Options, Commands>, {
@@ -12,55 +12,60 @@ export interface NodeExtensionSpec<Options = {}, Commands = {}> extends Overwrit
*/ */
topNode?: boolean, topNode?: boolean,
/**
* List
*/
list?: boolean,
/** /**
* Content * Content
*/ */
content?: NodeSpec['content'], content?: NodeSpec['content'] | ((this: { options: Options }) => NodeSpec['content']),
/** /**
* Marks * Marks
*/ */
marks?: NodeSpec['marks'], marks?: NodeSpec['marks'] | ((this: { options: Options }) => NodeSpec['marks']),
/** /**
* Group * Group
*/ */
group?: NodeSpec['group'], group?: NodeSpec['group'] | ((this: { options: Options }) => NodeSpec['group']),
/** /**
* Inline * Inline
*/ */
inline?: NodeSpec['inline'], inline?: NodeSpec['inline'] | ((this: { options: Options }) => NodeSpec['inline']),
/** /**
* Atom * Atom
*/ */
atom?: NodeSpec['atom'], atom?: NodeSpec['atom'] | ((this: { options: Options }) => NodeSpec['atom']),
/** /**
* Selectable * Selectable
*/ */
selectable?: NodeSpec['selectable'], selectable?: NodeSpec['selectable'] | ((this: { options: Options }) => NodeSpec['selectable']),
/** /**
* Draggable * Draggable
*/ */
draggable?: NodeSpec['draggable'], draggable?: NodeSpec['draggable'] | ((this: { options: Options }) => NodeSpec['draggable']),
/** /**
* Code * Code
*/ */
code?: NodeSpec['code'], code?: NodeSpec['code'] | ((this: { options: Options }) => NodeSpec['code']),
/** /**
* Defining * Defining
*/ */
defining?: NodeSpec['defining'], defining?: NodeSpec['defining'] | ((this: { options: Options }) => NodeSpec['defining']),
/** /**
* Isolating * Isolating
*/ */
isolating?: NodeSpec['isolating'], isolating?: NodeSpec['isolating'] | ((this: { options: Options }) => NodeSpec['isolating']),
/** /**
* Parse HTML * Parse HTML
@@ -139,6 +144,11 @@ export interface NodeExtensionSpec<Options = {}, Commands = {}> extends Overwrit
editor: Editor, editor: Editor,
type: NodeType, type: NodeType,
}) => Plugin[], }) => Plugin[],
/**
* Node View
*/
addNodeView?: (() => NodeViewRenderer) | null,
}> {} }> {}
export type NodeExtension = Required<Omit<NodeExtensionSpec, 'defaultOptions'> & { export type NodeExtension = Required<Omit<NodeExtensionSpec, 'defaultOptions'> & {
@@ -153,6 +163,7 @@ const defaultNode: NodeExtension = {
type: 'node', type: 'node',
name: 'node', name: 'node',
topNode: false, topNode: false,
list: false,
content: null, content: null,
marks: null, marks: null,
group: null, group: null,
@@ -166,6 +177,7 @@ const defaultNode: NodeExtension = {
parseHTML: () => null, parseHTML: () => null,
renderHTML: null, renderHTML: null,
addAttributes: () => ({}), addAttributes: () => ({}),
addNodeView: null,
} }
export function createNode<Options extends {}, Commands extends {}>(config: NodeExtensionSpec<Options, Commands>) { export function createNode<Options extends {}, Commands extends {}>(config: NodeExtensionSpec<Options, Commands>) {

View File

@@ -1,23 +1,19 @@
import { wrapInList, liftListItem } from 'prosemirror-schema-list' import { wrapInList, liftListItem } from 'prosemirror-schema-list'
import { findParentNode } from 'prosemirror-utils' import { findParentNode } from 'prosemirror-utils'
import { Node, NodeType, Schema } from 'prosemirror-model' import { NodeType } from 'prosemirror-model'
import { Command } from '../Editor' import { Command } from '../Editor'
import { createExtension } from '../Extension' import { createExtension } from '../Extension'
import getNodeType from '../utils/getNodeType' import getNodeType from '../utils/getNodeType'
import isList from '../utils/isList'
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)
}
export const ToggleList = createExtension({ export const ToggleList = createExtension({
addCommands() { addCommands() {
return { return {
toggleList: (listTypeOrName: string | NodeType, itemTypeOrName: string | NodeType): Command => ({ tr, state, dispatch }) => { toggleList: (listTypeOrName: string | NodeType, itemTypeOrName: string | NodeType): Command => ({ tr, state, dispatch }) => {
const { extensions } = this.editor.options
const listType = getNodeType(listTypeOrName, state.schema) const listType = getNodeType(listTypeOrName, state.schema)
const itemType = getNodeType(itemTypeOrName, state.schema) const itemType = getNodeType(itemTypeOrName, state.schema)
const { schema, selection } = state const { selection } = state
const { $from, $to } = selection const { $from, $to } = selection
const range = $from.blockRange($to) const range = $from.blockRange($to)
@@ -25,14 +21,14 @@ export const ToggleList = createExtension({
return false 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 (range.depth >= 1 && parentList && range.depth - parentList.depth <= 1) {
if (parentList.node.type === listType) { if (parentList.node.type === listType) {
return liftListItem(itemType)(state, dispatch) 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) tr.setNodeMarkup(parentList.pos, listType)
return false return false

View File

@@ -9,6 +9,14 @@ const style = `.ProseMirror {
font-variant-ligatures: none; font-variant-ligatures: none;
} }
.ProseMirror [contenteditable="false"] {
white-space: normal;
}
.ProseMirror [contenteditable="false"] [contenteditable="true"] {
white-space: pre-wrap;
}
.ProseMirror pre { .ProseMirror pre {
white-space: pre-wrap; white-space: pre-wrap;
} }

View File

@@ -1,6 +1,9 @@
import { Node } from 'prosemirror-model'
import { Decoration, NodeView } from 'prosemirror-view'
import { Extension } from './Extension' import { Extension } from './Extension'
import { NodeExtension } from './NodeExtension' import { NodeExtension } from './NodeExtension'
import { MarkExtension } from './MarkExtension' import { MarkExtension } from './MarkExtension'
import Editor from '..'
export type Extensions = (Extension | NodeExtension | MarkExtension)[] export type Extensions = (Extension | NodeExtension | MarkExtension)[]
@@ -40,3 +43,13 @@ export type Overwrite<T, U> = Pick<T, Diff<keyof T, keyof U>> & U;
export type AnyObject = { export type AnyObject = {
[key: string]: any [key: string]: any
} }
export type NodeViewRendererProps = {
editor: Editor,
node: Node,
getPos: (() => number) | boolean,
decorations: Decoration[],
attributes: AnyObject,
}
export type NodeViewRenderer = (props: NodeViewRendererProps) => NodeView

View File

@@ -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
}

View File

@@ -1,8 +1,8 @@
import { Node, Mark } from 'prosemirror-model' import { Node, Mark } from 'prosemirror-model'
import { ExtensionAttribute } from '../types' import { ExtensionAttribute, AnyObject } from '../types'
import mergeAttributes from './mergeAttributes' 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 return extensionAttributes
.filter(item => item.attribute.rendered) .filter(item => item.attribute.rendered)
.map(item => { .map(item => {

View File

@@ -5,6 +5,7 @@ import getAttributesFromExtensions from './getAttributesFromExtensions'
import getRenderedAttributes from './getRenderedAttributes' import getRenderedAttributes from './getRenderedAttributes'
import isEmptyObject from './isEmptyObject' import isEmptyObject from './isEmptyObject'
import injectExtensionAttributesToParseRule from './injectExtensionAttributesToParseRule' import injectExtensionAttributesToParseRule from './injectExtensionAttributesToParseRule'
import callOrReturn from './callOrReturn'
function cleanUpSchemaItem<T>(data: T) { function cleanUpSchemaItem<T>(data: T) {
return Object.fromEntries(Object.entries(data).filter(([key, value]) => { 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 extensionAttributes = allAttributes.filter(attribute => attribute.type === extension.name)
const context = { options: extension.options } const context = { options: extension.options }
const schema: NodeSpec = cleanUpSchemaItem({ const schema: NodeSpec = cleanUpSchemaItem({
content: extension.content, content: callOrReturn(extension.content, context),
marks: extension.marks, marks: callOrReturn(extension.marks, context),
group: extension.group, group: callOrReturn(extension.group, context),
inline: extension.inline, inline: callOrReturn(extension.inline, context),
atom: extension.atom, atom: callOrReturn(extension.atom, context),
selectable: extension.selectable, selectable: callOrReturn(extension.selectable, context),
draggable: extension.draggable, draggable: callOrReturn(extension.draggable, context),
code: extension.code, code: callOrReturn(extension.code, context),
defining: extension.defining, defining: callOrReturn(extension.defining, context),
isolating: extension.isolating, isolating: callOrReturn(extension.isolating, context),
attrs: Object.fromEntries(extensionAttributes.map(extensionAttribute => { attrs: Object.fromEntries(extensionAttributes.map(extensionAttribute => {
return [extensionAttribute.name, { default: extensionAttribute?.attribute?.default }] 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 extensionAttributes = allAttributes.filter(attribute => attribute.type === extension.name)
const context = { options: extension.options } const context = { options: extension.options }
const schema: MarkSpec = cleanUpSchemaItem({ const schema: MarkSpec = cleanUpSchemaItem({
inclusive: extension.inclusive, inclusive: callOrReturn(extension.inclusive, context),
excludes: extension.excludes, excludes: callOrReturn(extension.excludes, context),
group: extension.group, group: callOrReturn(extension.group, context),
spanning: extension.spanning, spanning: callOrReturn(extension.spanning, context),
attrs: Object.fromEntries(extensionAttributes.map(extensionAttribute => { attrs: Object.fromEntries(extensionAttributes.map(extensionAttribute => {
return [extensionAttribute.name, { default: extensionAttribute?.attribute?.default }] return [extensionAttribute.name, { default: extensionAttribute?.attribute?.default }]
})), })),

View File

@@ -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
}

View File

@@ -1,13 +1,17 @@
import { Command, createNode } from '@tiptap/core' import { Command, createNode } from '@tiptap/core'
import { wrappingInputRule } from 'prosemirror-inputrules' import { wrappingInputRule } from 'prosemirror-inputrules'
export const inputRegex = /^\s*([-+*])\s$/
const BulletList = createNode({ const BulletList = createNode({
name: 'bullet_list', name: 'bullet_list',
content: 'list_item+', list: true,
group: 'block', group: 'block',
content: 'list_item+',
parseHTML() { parseHTML() {
return [ return [
{ tag: 'ul' }, { tag: 'ul' },
@@ -34,7 +38,7 @@ const BulletList = createNode({
addInputRules() { addInputRules() {
return [ return [
wrappingInputRule(/^\s*([-+*])\s$/, this.type), wrappingInputRule(inputRegex, this.type),
] ]
}, },
}) })

View File

@@ -1,13 +1,25 @@
import { Command, createNode, nodeInputRule } from '@tiptap/core' import { Command, createNode, nodeInputRule } from '@tiptap/core'
export interface ImageOptions {
inline: boolean,
}
export const inputRegex = /!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/ export const inputRegex = /!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/
const Image = createNode({ const Image = createNode({
name: 'image', name: 'image',
inline: true, defaultOptions: <ImageOptions>{
inline: false,
},
group: 'inline', inline() {
return this.options.inline
},
group() {
return this.options.inline ? 'inline' : 'block'
},
draggable: true, draggable: true,

View File

@@ -9,7 +9,9 @@ const ListItem = createNode({
parseHTML() { parseHTML() {
return [ return [
{ tag: 'li' }, {
tag: 'li',
},
] ]
}, },

View File

@@ -1,13 +1,17 @@
import { Command, createNode } from '@tiptap/core' import { Command, createNode } from '@tiptap/core'
import { wrappingInputRule } from 'prosemirror-inputrules' import { wrappingInputRule } from 'prosemirror-inputrules'
export const inputRegex = /^(\d+)\.\s$/
const OrderedList = createNode({ const OrderedList = createNode({
name: 'ordered_list', name: 'ordered_list',
content: 'list_item+', list: true,
group: 'block', group: 'block',
content: 'list_item+',
addAttributes() { addAttributes() {
return { return {
start: { start: {
@@ -54,7 +58,7 @@ const OrderedList = createNode({
addInputRules() { addInputRules() {
return [ return [
wrappingInputRule( wrappingInputRule(
/^(\d+)\.\s$/, inputRegex,
this.type, this.type,
match => ({ order: +match[1] }), match => ({ order: +match[1] }),
(match, node) => node.childCount + node.attrs.order === +match[1], (match, node) => node.childCount + node.attrs.order === +match[1],

View File

@@ -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: <TaskItemOptions>{
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,
}
}

View File

@@ -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"
}
}

View File

@@ -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,
}
}

View File

@@ -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"
}
}

View File

@@ -1,9 +1,3 @@
import { Editor as CoreEditor } from '@tiptap/core'
import Renderer from './src/Renderer'
export * from '@tiptap/core' export * from '@tiptap/core'
export { default as VueRenderer } from './src/VueRenderer'
export { default as EditorContent } from './src/components/EditorContent' export { default as EditorContent } from './src/components/EditorContent'
export class Editor extends CoreEditor {
renderer = Renderer
}

View File

@@ -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
}
}

View File

@@ -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
}