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
|
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" />
|
||||||
|
|||||||
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
|
# 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
|
||||||
|
|||||||
@@ -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
|
// 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
|
const dom = this.view.dom as HTMLElement
|
||||||
dom.editor = this.proxy
|
dom.editor = this.proxy
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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 { 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 => {
|
||||||
|
|||||||
@@ -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 }]
|
||||||
})),
|
})),
|
||||||
|
|||||||
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 { 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),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ const ListItem = createNode({
|
|||||||
|
|
||||||
parseHTML() {
|
parseHTML() {
|
||||||
return [
|
return [
|
||||||
{ tag: 'li' },
|
{
|
||||||
|
tag: 'li',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
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 * 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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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