Merge branch 'feature/node-views' into main
This commit is contained in:
53
docs/src/demos/Extensions/TaskList/index.vue
Normal file
53
docs/src/demos/Extensions/TaskList/index.vue
Normal 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>
|
||||
@@ -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
|
||||
<demo name="Extensions/Image" highlight="12,30" />
|
||||
<demo name="Extensions/Image" />
|
||||
|
||||
31
docs/src/docPages/api/extensions/task-list.md
Normal file
31
docs/src/docPages/api/extensions/task-list.md
Normal 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 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
|
||||
<demo name="Extensions/TaskList" />
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
}))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,22 +10,22 @@ export interface MarkExtensionSpec<Options = {}, Commands = {}> 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
|
||||
|
||||
@@ -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<Options = {}, Commands = {}> extends Overwrite<ExtensionSpec<Options, Commands>, {
|
||||
@@ -12,55 +12,60 @@ export interface NodeExtensionSpec<Options = {}, Commands = {}> 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<Options = {}, Commands = {}> extends Overwrit
|
||||
editor: Editor,
|
||||
type: NodeType,
|
||||
}) => Plugin[],
|
||||
|
||||
/**
|
||||
* Node View
|
||||
*/
|
||||
addNodeView?: (() => NodeViewRenderer) | null,
|
||||
}> {}
|
||||
|
||||
export type NodeExtension = Required<Omit<NodeExtensionSpec, 'defaultOptions'> & {
|
||||
@@ -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<Options extends {}, Commands extends {}>(config: NodeExtensionSpec<Options, Commands>) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<T, U> = Pick<T, Diff<keyof T, keyof U>> & 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
|
||||
|
||||
17
packages/core/src/utils/callOrReturn.ts
Normal file
17
packages/core/src/utils/callOrReturn.ts
Normal 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
|
||||
}
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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<T>(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 }]
|
||||
})),
|
||||
|
||||
13
packages/core/src/utils/isList.ts
Normal file
13
packages/core/src/utils/isList.ts
Normal 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
|
||||
}
|
||||
@@ -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),
|
||||
]
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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: <ImageOptions>{
|
||||
inline: false,
|
||||
},
|
||||
|
||||
group: 'inline',
|
||||
inline() {
|
||||
return this.options.inline
|
||||
},
|
||||
|
||||
group() {
|
||||
return this.options.inline ? 'inline' : 'block'
|
||||
},
|
||||
|
||||
draggable: true,
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ const ListItem = createNode({
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{ tag: 'li' },
|
||||
{
|
||||
tag: 'li',
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
120
packages/extension-task-item/index.ts
Normal file
120
packages/extension-task-item/index.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
17
packages/extension-task-item/package.json
Normal file
17
packages/extension-task-item/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
40
packages/extension-task-list/index.ts
Normal file
40
packages/extension-task-list/index.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
17
packages/extension-task-list/package.json
Normal file
17
packages/extension-task-list/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
42
packages/vue/src/VueRenderer.ts
Normal file
42
packages/vue/src/VueRenderer.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user