mirror of
https://github.com/ueberdosis/tiptap.git
synced 2025-06-08 01:53:04 +08:00
This commit is contained in:
parent
04bf24aed0
commit
7f24a6677b
8
.changeset/purple-bobcats-grab.md
Normal file
8
.changeset/purple-bobcats-grab.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
"@tiptap/react": patch
|
||||||
|
"@tiptap/vue-2": patch
|
||||||
|
"@tiptap/vue-3": patch
|
||||||
|
"@tiptap/core": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Update the Typescript types for NodeViews, bringing them inline with there actual implementation
|
6
.changeset/shaggy-dolphins-applaud.md
Normal file
6
.changeset/shaggy-dolphins-applaud.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@tiptap/vue-2": minor
|
||||||
|
"@tiptap/vue-3": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Vue NodeViews now listen for changes to selections and re-render when the selection is actually over the nodeview
|
@ -1,7 +1,7 @@
|
|||||||
import { keymap } from '@tiptap/pm/keymap'
|
import { keymap } from '@tiptap/pm/keymap'
|
||||||
import { Node as ProsemirrorNode, Schema } from '@tiptap/pm/model'
|
import { Schema } from '@tiptap/pm/model'
|
||||||
import { Plugin } from '@tiptap/pm/state'
|
import { Plugin } from '@tiptap/pm/state'
|
||||||
import { Decoration, EditorView } from '@tiptap/pm/view'
|
import { NodeViewConstructor } from '@tiptap/pm/view'
|
||||||
|
|
||||||
import type { Editor } from './Editor.js'
|
import type { Editor } from './Editor.js'
|
||||||
import { getAttributesFromExtensions } from './helpers/getAttributesFromExtensions.js'
|
import { getAttributesFromExtensions } from './helpers/getAttributesFromExtensions.js'
|
||||||
@ -288,21 +288,26 @@ export class ExtensionManager {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodeview = (
|
const nodeview: NodeViewConstructor = (
|
||||||
node: ProsemirrorNode,
|
node,
|
||||||
view: EditorView,
|
view,
|
||||||
getPos: (() => number) | boolean,
|
getPos,
|
||||||
decorations: Decoration[],
|
decorations,
|
||||||
|
innerDecorations,
|
||||||
) => {
|
) => {
|
||||||
const HTMLAttributes = getRenderedAttributes(node, extensionAttributes)
|
const HTMLAttributes = getRenderedAttributes(node, extensionAttributes)
|
||||||
|
|
||||||
return addNodeView()({
|
return addNodeView()({
|
||||||
editor,
|
// pass-through
|
||||||
node,
|
node,
|
||||||
getPos,
|
view,
|
||||||
|
getPos: getPos as () => number,
|
||||||
decorations,
|
decorations,
|
||||||
HTMLAttributes,
|
innerDecorations,
|
||||||
|
// tiptap-specific
|
||||||
|
editor,
|
||||||
extension,
|
extension,
|
||||||
|
HTMLAttributes,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
|
||||||
import { NodeSelection } from '@tiptap/pm/state'
|
import { NodeSelection } from '@tiptap/pm/state'
|
||||||
import { NodeView as ProseMirrorNodeView } from '@tiptap/pm/view'
|
import { NodeView as ProseMirrorNodeView } from '@tiptap/pm/view'
|
||||||
|
|
||||||
import { Editor as CoreEditor } from './Editor.js'
|
import { Editor as CoreEditor } from './Editor.js'
|
||||||
import { Node } from './Node.js'
|
|
||||||
import { DecorationWithType, NodeViewRendererOptions, NodeViewRendererProps } from './types.js'
|
import { DecorationWithType, NodeViewRendererOptions, NodeViewRendererProps } from './types.js'
|
||||||
import { isAndroid } from './utilities/isAndroid.js'
|
import { isAndroid } from './utilities/isAndroid.js'
|
||||||
import { isiOS } from './utilities/isiOS.js'
|
import { isiOS } from './utilities/isiOS.js'
|
||||||
@ -23,13 +21,19 @@ export class NodeView<
|
|||||||
|
|
||||||
options: Options
|
options: Options
|
||||||
|
|
||||||
extension: Node
|
extension: NodeViewRendererProps['extension']
|
||||||
|
|
||||||
node: ProseMirrorNode
|
node: NodeViewRendererProps['node']
|
||||||
|
|
||||||
decorations: DecorationWithType[]
|
decorations: NodeViewRendererProps['decorations']
|
||||||
|
|
||||||
getPos: any
|
innerDecorations: NodeViewRendererProps['innerDecorations']
|
||||||
|
|
||||||
|
view: NodeViewRendererProps['view']
|
||||||
|
|
||||||
|
getPos: NodeViewRendererProps['getPos']
|
||||||
|
|
||||||
|
HTMLAttributes: NodeViewRendererProps['HTMLAttributes']
|
||||||
|
|
||||||
isDragging = false
|
isDragging = false
|
||||||
|
|
||||||
@ -44,6 +48,9 @@ export class NodeView<
|
|||||||
this.extension = props.extension
|
this.extension = props.extension
|
||||||
this.node = props.node
|
this.node = props.node
|
||||||
this.decorations = props.decorations as DecorationWithType[]
|
this.decorations = props.decorations as DecorationWithType[]
|
||||||
|
this.innerDecorations = props.innerDecorations
|
||||||
|
this.view = props.view
|
||||||
|
this.HTMLAttributes = props.HTMLAttributes
|
||||||
this.getPos = props.getPos
|
this.getPos = props.getPos
|
||||||
this.mount()
|
this.mount()
|
||||||
}
|
}
|
||||||
@ -93,9 +100,14 @@ export class NodeView<
|
|||||||
|
|
||||||
event.dataTransfer?.setDragImage(this.dom, x, y)
|
event.dataTransfer?.setDragImage(this.dom, x, y)
|
||||||
|
|
||||||
|
const pos = this.getPos()
|
||||||
|
|
||||||
|
if (typeof pos !== 'number') {
|
||||||
|
return
|
||||||
|
}
|
||||||
// we need to tell ProseMirror that we want to move the whole node
|
// we need to tell ProseMirror that we want to move the whole node
|
||||||
// so we create a NodeSelection
|
// so we create a NodeSelection
|
||||||
const selection = NodeSelection.create(view.state.doc, this.getPos())
|
const selection = NodeSelection.create(view.state.doc, pos)
|
||||||
const transaction = view.state.tr.setSelection(selection)
|
const transaction = view.state.tr.setSelection(selection)
|
||||||
|
|
||||||
view.dispatch(transaction)
|
view.dispatch(transaction)
|
||||||
@ -197,6 +209,11 @@ export class NodeView<
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a DOM [mutation](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) or a selection change happens within the view.
|
||||||
|
* @return `false` if the editor should re-read the selection or re-parse the range around the mutation
|
||||||
|
* @return `true` if it can safely be ignored.
|
||||||
|
*/
|
||||||
ignoreMutation(mutation: MutationRecord | { type: 'selection'; target: Element }) {
|
ignoreMutation(mutation: MutationRecord | { type: 'selection'; target: Element }) {
|
||||||
if (!this.dom || !this.contentDOM) {
|
if (!this.dom || !this.contentDOM) {
|
||||||
return true
|
return true
|
||||||
@ -254,10 +271,17 @@ export class NodeView<
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAttributes(attributes: {}) {
|
/**
|
||||||
|
* Update the attributes of the prosemirror node.
|
||||||
|
*/
|
||||||
|
updateAttributes(attributes: Record<string, any>): void {
|
||||||
this.editor.commands.command(({ tr }) => {
|
this.editor.commands.command(({ tr }) => {
|
||||||
const pos = this.getPos()
|
const pos = this.getPos()
|
||||||
|
|
||||||
|
if (typeof pos !== 'number') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
tr.setNodeMarkup(pos, undefined, {
|
tr.setNodeMarkup(pos, undefined, {
|
||||||
...this.node.attrs,
|
...this.node.attrs,
|
||||||
...attributes,
|
...attributes,
|
||||||
@ -267,8 +291,15 @@ export class NodeView<
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the node.
|
||||||
|
*/
|
||||||
deleteNode(): void {
|
deleteNode(): void {
|
||||||
const from = this.getPos()
|
const from = this.getPos()
|
||||||
|
|
||||||
|
if (typeof from !== 'number') {
|
||||||
|
return
|
||||||
|
}
|
||||||
const to = from + this.node.nodeSize
|
const to = from + this.node.nodeSize
|
||||||
|
|
||||||
this.editor.commands.deleteRange({ from, to })
|
this.editor.commands.deleteRange({ from, to })
|
||||||
|
@ -6,7 +6,11 @@ import {
|
|||||||
} from '@tiptap/pm/model'
|
} from '@tiptap/pm/model'
|
||||||
import { EditorState, Transaction } from '@tiptap/pm/state'
|
import { EditorState, Transaction } from '@tiptap/pm/state'
|
||||||
import {
|
import {
|
||||||
Decoration, EditorProps, EditorView, NodeView,
|
Decoration,
|
||||||
|
EditorProps,
|
||||||
|
EditorView,
|
||||||
|
NodeView,
|
||||||
|
NodeViewConstructor,
|
||||||
} from '@tiptap/pm/view'
|
} from '@tiptap/pm/view'
|
||||||
|
|
||||||
import { Editor } from './Editor.js'
|
import { Editor } from './Editor.js'
|
||||||
@ -184,20 +188,21 @@ export type ValuesOf<T> = T[keyof T];
|
|||||||
|
|
||||||
export type KeysWithTypeOf<T, Type> = { [P in keyof T]: T[P] extends Type ? P : never }[keyof T];
|
export type KeysWithTypeOf<T, Type> = { [P in keyof T]: T[P] extends Type ? P : never }[keyof T];
|
||||||
|
|
||||||
|
export type Simplify<T> = { [KeyType in keyof T]: T[KeyType] } & {};
|
||||||
|
|
||||||
export type DecorationWithType = Decoration & {
|
export type DecorationWithType = Decoration & {
|
||||||
type: NodeType;
|
type: NodeType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NodeViewProps = {
|
export type NodeViewProps = Simplify<
|
||||||
editor: Editor;
|
Omit<NodeViewRendererProps, 'decorations'> & {
|
||||||
node: ProseMirrorNode;
|
// TODO this type is not technically correct, but it's the best we can do for now since prosemirror doesn't expose the type of decorations
|
||||||
decorations: DecorationWithType[];
|
decorations: readonly DecorationWithType[];
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
extension: Node;
|
updateAttributes: (attributes: Record<string, any>) => void;
|
||||||
getPos: () => number;
|
deleteNode: () => void;
|
||||||
updateAttributes: (attributes: Record<string, any>) => void;
|
}
|
||||||
deleteNode: () => void;
|
>;
|
||||||
};
|
|
||||||
|
|
||||||
export interface NodeViewRendererOptions {
|
export interface NodeViewRendererOptions {
|
||||||
stopEvent: ((props: { event: Event }) => boolean) | null;
|
stopEvent: ((props: { event: Event }) => boolean) | null;
|
||||||
@ -208,15 +213,19 @@ export interface NodeViewRendererOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type NodeViewRendererProps = {
|
export type NodeViewRendererProps = {
|
||||||
|
// pass-through from prosemirror
|
||||||
|
node: Parameters<NodeViewConstructor>[0];
|
||||||
|
view: Parameters<NodeViewConstructor>[1];
|
||||||
|
getPos: () => number; // TODO getPos was incorrectly typed before, change to `Parameters<NodeViewConstructor>[2];` in the next major version
|
||||||
|
decorations: Parameters<NodeViewConstructor>[3];
|
||||||
|
innerDecorations: Parameters<NodeViewConstructor>[4];
|
||||||
|
// tiptap-specific
|
||||||
editor: Editor;
|
editor: Editor;
|
||||||
node: ProseMirrorNode;
|
|
||||||
getPos: (() => number) | boolean;
|
|
||||||
HTMLAttributes: Record<string, any>;
|
|
||||||
decorations: Decoration[];
|
|
||||||
extension: Node;
|
extension: Node;
|
||||||
|
HTMLAttributes: Record<string, any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NodeViewRenderer = (props: NodeViewRendererProps) => NodeView | {};
|
export type NodeViewRenderer = (props: NodeViewRendererProps) => NodeView;
|
||||||
|
|
||||||
export type AnyCommands = Record<string, (...args: any[]) => Command>;
|
export type AnyCommands = Record<string, (...args: any[]) => Command>;
|
||||||
|
|
||||||
|
@ -154,6 +154,10 @@ export const TaskItem = Node.create<TaskItemOptions>({
|
|||||||
.focus(undefined, { scrollIntoView: false })
|
.focus(undefined, { scrollIntoView: false })
|
||||||
.command(({ tr }) => {
|
.command(({ tr }) => {
|
||||||
const position = getPos()
|
const position = getPos()
|
||||||
|
|
||||||
|
if (typeof position !== 'number') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
const currentNode = tr.doc.nodeAt(position)
|
const currentNode = tr.doc.nodeAt(position)
|
||||||
|
|
||||||
tr.setNodeMarkup(position, undefined, {
|
tr.setNodeMarkup(position, undefined, {
|
||||||
|
@ -6,56 +6,85 @@ import {
|
|||||||
NodeViewProps,
|
NodeViewProps,
|
||||||
NodeViewRenderer,
|
NodeViewRenderer,
|
||||||
NodeViewRendererOptions,
|
NodeViewRendererOptions,
|
||||||
NodeViewRendererProps,
|
|
||||||
} from '@tiptap/core'
|
} from '@tiptap/core'
|
||||||
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
import { Node, Node as ProseMirrorNode } from '@tiptap/pm/model'
|
||||||
import { Decoration, NodeView as ProseMirrorNodeView } from '@tiptap/pm/view'
|
import { Decoration, DecorationSource, NodeView as ProseMirrorNodeView } from '@tiptap/pm/view'
|
||||||
import React from 'react'
|
import React, { ComponentType } from 'react'
|
||||||
|
|
||||||
import { EditorWithContentComponent } from './Editor.js'
|
import { EditorWithContentComponent } from './Editor.js'
|
||||||
import { ReactRenderer } from './ReactRenderer.js'
|
import { ReactRenderer } from './ReactRenderer.js'
|
||||||
import { ReactNodeViewContext, ReactNodeViewContextProps } from './useReactNodeView.js'
|
import { ReactNodeViewContext, ReactNodeViewContextProps } from './useReactNodeView.js'
|
||||||
|
|
||||||
export interface ReactNodeViewRendererOptions extends NodeViewRendererOptions {
|
export interface ReactNodeViewRendererOptions extends NodeViewRendererOptions {
|
||||||
|
/**
|
||||||
|
* This function is called when the node view is updated.
|
||||||
|
* It allows you to compare the old node with the new node and decide if the component should update.
|
||||||
|
*/
|
||||||
update:
|
update:
|
||||||
| ((props: {
|
| ((props: {
|
||||||
oldNode: ProseMirrorNode
|
oldNode: ProseMirrorNode;
|
||||||
oldDecorations: Decoration[]
|
oldDecorations: readonly Decoration[];
|
||||||
newNode: ProseMirrorNode
|
oldInnerDecorations: DecorationSource;
|
||||||
newDecorations: Decoration[]
|
newNode: ProseMirrorNode;
|
||||||
updateProps: () => void
|
newDecorations: readonly Decoration[];
|
||||||
|
innerDecorations: DecorationSource;
|
||||||
|
updateProps: () => void;
|
||||||
}) => boolean)
|
}) => boolean)
|
||||||
| null
|
| null;
|
||||||
as?: string
|
/**
|
||||||
className?: string
|
* The tag name of the element wrapping the React component.
|
||||||
|
*/
|
||||||
|
as?: string;
|
||||||
|
/**
|
||||||
|
* The class name of the element wrapping the React component.
|
||||||
|
*/
|
||||||
|
className?: string;
|
||||||
|
/**
|
||||||
|
* Attributes that should be applied to the element wrapping the React component.
|
||||||
|
* If this is a function, it will be called each time the node view is updated.
|
||||||
|
* If this is an object, it will be applied once when the node view is mounted.
|
||||||
|
*/
|
||||||
attrs?:
|
attrs?:
|
||||||
| Record<string, string>
|
| Record<string, string>
|
||||||
| ((props: {
|
| ((props: {
|
||||||
node: ProseMirrorNode
|
node: ProseMirrorNode;
|
||||||
HTMLAttributes: Record<string, any>
|
HTMLAttributes: Record<string, any>;
|
||||||
}) => Record<string, string>)
|
}) => Record<string, string>);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ReactNodeView extends NodeView<
|
export class ReactNodeView<
|
||||||
React.FunctionComponent,
|
Component extends ComponentType<NodeViewProps> = ComponentType<NodeViewProps>,
|
||||||
Editor,
|
NodeEditor extends Editor = Editor,
|
||||||
ReactNodeViewRendererOptions
|
Options extends ReactNodeViewRendererOptions = ReactNodeViewRendererOptions,
|
||||||
> {
|
> extends NodeView<Component, NodeEditor, Options> {
|
||||||
renderer!: ReactRenderer
|
/**
|
||||||
|
* The renderer instance.
|
||||||
|
*/
|
||||||
|
renderer!: ReactRenderer<unknown, NodeViewProps>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The element that holds the rich-text content of the node.
|
||||||
|
*/
|
||||||
contentDOMElement!: HTMLElement | null
|
contentDOMElement!: HTMLElement | null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the React component.
|
||||||
|
* Called on initialization.
|
||||||
|
*/
|
||||||
mount() {
|
mount() {
|
||||||
const props: NodeViewProps = {
|
const props = {
|
||||||
editor: this.editor,
|
editor: this.editor,
|
||||||
node: this.node,
|
node: this.node,
|
||||||
decorations: this.decorations,
|
decorations: this.decorations as DecorationWithType[],
|
||||||
|
innerDecorations: this.innerDecorations,
|
||||||
|
view: this.view,
|
||||||
selected: false,
|
selected: false,
|
||||||
extension: this.extension,
|
extension: this.extension,
|
||||||
|
HTMLAttributes: this.HTMLAttributes,
|
||||||
getPos: () => this.getPos(),
|
getPos: () => this.getPos(),
|
||||||
updateAttributes: (attributes = {}) => this.updateAttributes(attributes),
|
updateAttributes: (attributes = {}) => this.updateAttributes(attributes),
|
||||||
deleteNode: () => this.deleteNode(),
|
deleteNode: () => this.deleteNode(),
|
||||||
}
|
} satisfies NodeViewProps
|
||||||
|
|
||||||
if (!(this.component as any).displayName) {
|
if (!(this.component as any).displayName) {
|
||||||
const capitalizeFirstChar = (string: string): string => {
|
const capitalizeFirstChar = (string: string): string => {
|
||||||
@ -75,13 +104,15 @@ class ReactNodeView extends NodeView<
|
|||||||
const Component = this.component
|
const Component = this.component
|
||||||
// For performance reasons, we memoize the provider component
|
// For performance reasons, we memoize the provider component
|
||||||
// And all of the things it requires are declared outside of the component, so it doesn't need to re-render
|
// And all of the things it requires are declared outside of the component, so it doesn't need to re-render
|
||||||
const ReactNodeViewProvider: React.FunctionComponent = React.memo(componentProps => {
|
const ReactNodeViewProvider: React.FunctionComponent<NodeViewProps> = React.memo(
|
||||||
return (
|
componentProps => {
|
||||||
<ReactNodeViewContext.Provider value={context}>
|
return (
|
||||||
{React.createElement(Component, componentProps)}
|
<ReactNodeViewContext.Provider value={context}>
|
||||||
</ReactNodeViewContext.Provider>
|
{React.createElement(Component, componentProps)}
|
||||||
)
|
</ReactNodeViewContext.Provider>
|
||||||
})
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
ReactNodeViewProvider.displayName = 'ReactNodeView'
|
ReactNodeViewProvider.displayName = 'ReactNodeView'
|
||||||
|
|
||||||
@ -121,6 +152,10 @@ class ReactNodeView extends NodeView<
|
|||||||
this.updateElementAttributes()
|
this.updateElementAttributes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the DOM element.
|
||||||
|
* This is the element that will be used to display the node view.
|
||||||
|
*/
|
||||||
get dom() {
|
get dom() {
|
||||||
if (
|
if (
|
||||||
this.renderer.element.firstElementChild
|
this.renderer.element.firstElementChild
|
||||||
@ -132,6 +167,10 @@ class ReactNodeView extends NodeView<
|
|||||||
return this.renderer.element as HTMLElement
|
return this.renderer.element as HTMLElement
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the content DOM element.
|
||||||
|
* This is the element that will be used to display the rich-text content of the node.
|
||||||
|
*/
|
||||||
get contentDOM() {
|
get contentDOM() {
|
||||||
if (this.node.isLeaf) {
|
if (this.node.isLeaf) {
|
||||||
return null
|
return null
|
||||||
@ -140,10 +179,19 @@ class ReactNodeView extends NodeView<
|
|||||||
return this.contentDOMElement
|
return this.contentDOMElement
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On editor selection update, check if the node is selected.
|
||||||
|
* If it is, call `selectNode`, otherwise call `deselectNode`.
|
||||||
|
*/
|
||||||
handleSelectionUpdate() {
|
handleSelectionUpdate() {
|
||||||
const { from, to } = this.editor.state.selection
|
const { from, to } = this.editor.state.selection
|
||||||
|
const pos = this.getPos()
|
||||||
|
|
||||||
if (from <= this.getPos() && to >= this.getPos() + this.node.nodeSize) {
|
if (typeof pos !== 'number') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from <= pos && to >= pos + this.node.nodeSize) {
|
||||||
if (this.renderer.props.selected) {
|
if (this.renderer.props.selected) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -158,8 +206,16 @@ class ReactNodeView extends NodeView<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update(node: ProseMirrorNode, decorations: DecorationWithType[]) {
|
/**
|
||||||
const updateProps = (props?: Record<string, any>) => {
|
* On update, update the React component.
|
||||||
|
* To prevent unnecessary updates, the `update` option can be used.
|
||||||
|
*/
|
||||||
|
update(
|
||||||
|
node: Node,
|
||||||
|
decorations: readonly Decoration[],
|
||||||
|
innerDecorations: DecorationSource,
|
||||||
|
): boolean {
|
||||||
|
const rerenderComponent = (props?: Record<string, any>) => {
|
||||||
this.renderer.updateProps(props)
|
this.renderer.updateProps(props)
|
||||||
if (typeof this.options.attrs === 'function') {
|
if (typeof this.options.attrs === 'function') {
|
||||||
this.updateElementAttributes()
|
this.updateElementAttributes()
|
||||||
@ -173,31 +229,44 @@ class ReactNodeView extends NodeView<
|
|||||||
if (typeof this.options.update === 'function') {
|
if (typeof this.options.update === 'function') {
|
||||||
const oldNode = this.node
|
const oldNode = this.node
|
||||||
const oldDecorations = this.decorations
|
const oldDecorations = this.decorations
|
||||||
|
const oldInnerDecorations = this.innerDecorations
|
||||||
|
|
||||||
this.node = node
|
this.node = node
|
||||||
this.decorations = decorations
|
this.decorations = decorations
|
||||||
|
this.innerDecorations = innerDecorations
|
||||||
|
|
||||||
return this.options.update({
|
return this.options.update({
|
||||||
oldNode,
|
oldNode,
|
||||||
oldDecorations,
|
oldDecorations,
|
||||||
newNode: node,
|
newNode: node,
|
||||||
newDecorations: decorations,
|
newDecorations: decorations,
|
||||||
updateProps: () => updateProps({ node, decorations }),
|
oldInnerDecorations,
|
||||||
|
innerDecorations,
|
||||||
|
updateProps: () => rerenderComponent({ node, decorations, innerDecorations }),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node === this.node && this.decorations === decorations) {
|
if (
|
||||||
|
node === this.node
|
||||||
|
&& this.decorations === decorations
|
||||||
|
&& this.innerDecorations === innerDecorations
|
||||||
|
) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
this.node = node
|
this.node = node
|
||||||
this.decorations = decorations
|
this.decorations = decorations
|
||||||
|
this.innerDecorations = innerDecorations
|
||||||
|
|
||||||
updateProps({ node, decorations })
|
rerenderComponent({ node, decorations, innerDecorations })
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select the node.
|
||||||
|
* Add the `selected` prop and the `ProseMirror-selectednode` class.
|
||||||
|
*/
|
||||||
selectNode() {
|
selectNode() {
|
||||||
this.renderer.updateProps({
|
this.renderer.updateProps({
|
||||||
selected: true,
|
selected: true,
|
||||||
@ -205,6 +274,10 @@ class ReactNodeView extends NodeView<
|
|||||||
this.renderer.element.classList.add('ProseMirror-selectednode')
|
this.renderer.element.classList.add('ProseMirror-selectednode')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deselect the node.
|
||||||
|
* Remove the `selected` prop and the `ProseMirror-selectednode` class.
|
||||||
|
*/
|
||||||
deselectNode() {
|
deselectNode() {
|
||||||
this.renderer.updateProps({
|
this.renderer.updateProps({
|
||||||
selected: false,
|
selected: false,
|
||||||
@ -212,12 +285,19 @@ class ReactNodeView extends NodeView<
|
|||||||
this.renderer.element.classList.remove('ProseMirror-selectednode')
|
this.renderer.element.classList.remove('ProseMirror-selectednode')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy the React component instance.
|
||||||
|
*/
|
||||||
destroy() {
|
destroy() {
|
||||||
this.renderer.destroy()
|
this.renderer.destroy()
|
||||||
this.editor.off('selectionUpdate', this.handleSelectionUpdate)
|
this.editor.off('selectionUpdate', this.handleSelectionUpdate)
|
||||||
this.contentDOMElement = null
|
this.contentDOMElement = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the attributes of the top-level element that holds the React component.
|
||||||
|
* Applying the attributes defined in the `attrs` option.
|
||||||
|
*/
|
||||||
updateElementAttributes() {
|
updateElementAttributes() {
|
||||||
if (this.options.attrs) {
|
if (this.options.attrs) {
|
||||||
let attrsObj: Record<string, string> = {}
|
let attrsObj: Record<string, string> = {}
|
||||||
@ -236,18 +316,21 @@ class ReactNodeView extends NodeView<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a React node view renderer.
|
||||||
|
*/
|
||||||
export function ReactNodeViewRenderer(
|
export function ReactNodeViewRenderer(
|
||||||
component: any,
|
component: ComponentType<NodeViewProps>,
|
||||||
options?: Partial<ReactNodeViewRendererOptions>,
|
options?: Partial<ReactNodeViewRendererOptions>,
|
||||||
): NodeViewRenderer {
|
): NodeViewRenderer {
|
||||||
return (props: NodeViewRendererProps) => {
|
return props => {
|
||||||
// try to get the parent component
|
// try to get the parent component
|
||||||
// this is important for vue devtools to show the component hierarchy correctly
|
// this is important for vue devtools to show the component hierarchy correctly
|
||||||
// maybe it’s `undefined` because <editor-content> isn’t rendered yet
|
// maybe it’s `undefined` because <editor-content> isn’t rendered yet
|
||||||
if (!(props.editor as EditorWithContentComponent).contentComponent) {
|
if (!(props.editor as EditorWithContentComponent).contentComponent) {
|
||||||
return {}
|
return {} as unknown as ProseMirrorNodeView
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ReactNodeView(component, props, options) as unknown as ProseMirrorNodeView
|
return new ReactNodeView(component, props, options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,7 +75,7 @@ type ComponentType<R, P> =
|
|||||||
* as: 'span',
|
* as: 'span',
|
||||||
* })
|
* })
|
||||||
*/
|
*/
|
||||||
export class ReactRenderer<R = unknown, P = unknown> {
|
export class ReactRenderer<R = unknown, P extends Record<string, any> = {}> {
|
||||||
id: string
|
id: string
|
||||||
|
|
||||||
editor: Editor
|
editor: Editor
|
||||||
@ -84,12 +84,15 @@ export class ReactRenderer<R = unknown, P = unknown> {
|
|||||||
|
|
||||||
element: Element
|
element: Element
|
||||||
|
|
||||||
props: Record<string, any>
|
props: P
|
||||||
|
|
||||||
reactElement: React.ReactNode
|
reactElement: React.ReactNode
|
||||||
|
|
||||||
ref: R | null = null
|
ref: R | null = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immediately creates element and renders the provided React component.
|
||||||
|
*/
|
||||||
constructor(component: ComponentType<R, P>, {
|
constructor(component: ComponentType<R, P>, {
|
||||||
editor,
|
editor,
|
||||||
props = {},
|
props = {},
|
||||||
@ -99,7 +102,7 @@ export class ReactRenderer<R = unknown, P = unknown> {
|
|||||||
this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString()
|
this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString()
|
||||||
this.component = component
|
this.component = component
|
||||||
this.editor = editor as EditorWithContentComponent
|
this.editor = editor as EditorWithContentComponent
|
||||||
this.props = props
|
this.props = props as P
|
||||||
this.element = document.createElement(as)
|
this.element = document.createElement(as)
|
||||||
this.element.classList.add('react-renderer')
|
this.element.classList.add('react-renderer')
|
||||||
|
|
||||||
@ -118,12 +121,16 @@ export class ReactRenderer<R = unknown, P = unknown> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the React component.
|
||||||
|
*/
|
||||||
render(): void {
|
render(): void {
|
||||||
const Component = this.component
|
const Component = this.component
|
||||||
const props = this.props
|
const props = this.props
|
||||||
const editor = this.editor as EditorWithContentComponent
|
const editor = this.editor as EditorWithContentComponent
|
||||||
|
|
||||||
if (isClassComponent(Component) || isForwardRefComponent(Component)) {
|
if (isClassComponent(Component) || isForwardRefComponent(Component)) {
|
||||||
|
// @ts-ignore This is a hack to make the ref work
|
||||||
props.ref = (ref: R) => {
|
props.ref = (ref: R) => {
|
||||||
this.ref = ref
|
this.ref = ref
|
||||||
}
|
}
|
||||||
@ -134,6 +141,9 @@ export class ReactRenderer<R = unknown, P = unknown> {
|
|||||||
editor?.contentComponent?.setRenderer(this.id, this)
|
editor?.contentComponent?.setRenderer(this.id, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-renders the React component with new props.
|
||||||
|
*/
|
||||||
updateProps(props: Record<string, any> = {}): void {
|
updateProps(props: Record<string, any> = {}): void {
|
||||||
this.props = {
|
this.props = {
|
||||||
...this.props,
|
...this.props,
|
||||||
@ -143,12 +153,18 @@ export class ReactRenderer<R = unknown, P = unknown> {
|
|||||||
this.render()
|
this.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy the React component.
|
||||||
|
*/
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
const editor = this.editor as EditorWithContentComponent
|
const editor = this.editor as EditorWithContentComponent
|
||||||
|
|
||||||
editor?.contentComponent?.removeRenderer(this.id)
|
editor?.contentComponent?.removeRenderer(this.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the attributes of the element that holds the React component.
|
||||||
|
*/
|
||||||
updateAttributes(attributes: Record<string, string>): void {
|
updateAttributes(attributes: Record<string, string>): void {
|
||||||
Object.keys(attributes).forEach(key => {
|
Object.keys(attributes).forEach(key => {
|
||||||
this.element.setAttribute(key, attributes[key])
|
this.element.setAttribute(key, attributes[key])
|
||||||
|
@ -4,10 +4,9 @@ import {
|
|||||||
NodeViewProps,
|
NodeViewProps,
|
||||||
NodeViewRenderer,
|
NodeViewRenderer,
|
||||||
NodeViewRendererOptions,
|
NodeViewRendererOptions,
|
||||||
NodeViewRendererProps,
|
|
||||||
} from '@tiptap/core'
|
} from '@tiptap/core'
|
||||||
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
||||||
import { Decoration, NodeView as ProseMirrorNodeView } from '@tiptap/pm/view'
|
import { Decoration, DecorationSource, NodeView as ProseMirrorNodeView } from '@tiptap/pm/view'
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import { VueConstructor } from 'vue/types/umd'
|
import { VueConstructor } from 'vue/types/umd'
|
||||||
import { booleanProp, functionProp, objectProp } from 'vue-ts-types'
|
import { booleanProp, functionProp, objectProp } from 'vue-ts-types'
|
||||||
@ -29,33 +28,38 @@ export const nodeViewProps = {
|
|||||||
export interface VueNodeViewRendererOptions extends NodeViewRendererOptions {
|
export interface VueNodeViewRendererOptions extends NodeViewRendererOptions {
|
||||||
update:
|
update:
|
||||||
| ((props: {
|
| ((props: {
|
||||||
oldNode: ProseMirrorNode
|
oldNode: ProseMirrorNode;
|
||||||
oldDecorations: Decoration[]
|
oldDecorations: readonly Decoration[];
|
||||||
newNode: ProseMirrorNode
|
oldInnerDecorations: DecorationSource;
|
||||||
newDecorations: Decoration[]
|
newNode: ProseMirrorNode;
|
||||||
updateProps: () => void
|
newDecorations: readonly Decoration[];
|
||||||
|
innerDecorations: DecorationSource;
|
||||||
|
updateProps: () => void;
|
||||||
}) => boolean)
|
}) => boolean)
|
||||||
| null
|
| null;
|
||||||
}
|
}
|
||||||
|
|
||||||
class VueNodeView extends NodeView<Vue | VueConstructor, Editor, VueNodeViewRendererOptions> {
|
class VueNodeView extends NodeView<Vue | VueConstructor, Editor, VueNodeViewRendererOptions> {
|
||||||
renderer!: VueRenderer
|
renderer!: VueRenderer
|
||||||
|
|
||||||
decorationClasses!: {
|
decorationClasses!: {
|
||||||
value: string
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
mount() {
|
mount() {
|
||||||
const props: NodeViewProps = {
|
const props = {
|
||||||
editor: this.editor,
|
editor: this.editor,
|
||||||
node: this.node,
|
node: this.node,
|
||||||
decorations: this.decorations,
|
decorations: this.decorations as DecorationWithType[],
|
||||||
|
innerDecorations: this.innerDecorations,
|
||||||
|
view: this.view,
|
||||||
selected: false,
|
selected: false,
|
||||||
extension: this.extension,
|
extension: this.extension,
|
||||||
|
HTMLAttributes: this.HTMLAttributes,
|
||||||
getPos: () => this.getPos(),
|
getPos: () => this.getPos(),
|
||||||
updateAttributes: (attributes = {}) => this.updateAttributes(attributes),
|
updateAttributes: (attributes = {}) => this.updateAttributes(attributes),
|
||||||
deleteNode: () => this.deleteNode(),
|
deleteNode: () => this.deleteNode(),
|
||||||
}
|
} satisfies NodeViewProps
|
||||||
|
|
||||||
const onDragStart = this.onDragStart.bind(this)
|
const onDragStart = this.onDragStart.bind(this)
|
||||||
|
|
||||||
@ -64,7 +68,7 @@ class VueNodeView extends NodeView<Vue | VueConstructor, Editor, VueNodeViewRend
|
|||||||
})
|
})
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const vue = this.editor.contentComponent?.$options._base ?? Vue // eslint-disable-line
|
const vue = this.editor.contentComponent?.$options._base ?? Vue; // eslint-disable-line
|
||||||
|
|
||||||
const Component = vue.extend(this.component).extend({
|
const Component = vue.extend(this.component).extend({
|
||||||
props: Object.keys(props),
|
props: Object.keys(props),
|
||||||
@ -76,12 +80,19 @@ class VueNodeView extends NodeView<Vue | VueConstructor, Editor, VueNodeViewRend
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.handleSelectionUpdate = this.handleSelectionUpdate.bind(this)
|
||||||
|
this.editor.on('selectionUpdate', this.handleSelectionUpdate)
|
||||||
|
|
||||||
this.renderer = new VueRenderer(Component, {
|
this.renderer = new VueRenderer(Component, {
|
||||||
parent: this.editor.contentComponent,
|
parent: this.editor.contentComponent,
|
||||||
propsData: props,
|
propsData: props,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the DOM element.
|
||||||
|
* This is the element that will be used to display the node view.
|
||||||
|
*/
|
||||||
get dom() {
|
get dom() {
|
||||||
if (!this.renderer.element.hasAttribute('data-node-view-wrapper')) {
|
if (!this.renderer.element.hasAttribute('data-node-view-wrapper')) {
|
||||||
throw Error('Please use the NodeViewWrapper component for your node view.')
|
throw Error('Please use the NodeViewWrapper component for your node view.')
|
||||||
@ -90,6 +101,10 @@ class VueNodeView extends NodeView<Vue | VueConstructor, Editor, VueNodeViewRend
|
|||||||
return this.renderer.element as HTMLElement
|
return this.renderer.element as HTMLElement
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the content DOM element.
|
||||||
|
* This is the element that will be used to display the rich-text content of the node.
|
||||||
|
*/
|
||||||
get contentDOM() {
|
get contentDOM() {
|
||||||
if (this.node.isLeaf) {
|
if (this.node.isLeaf) {
|
||||||
return null
|
return null
|
||||||
@ -100,8 +115,43 @@ class VueNodeView extends NodeView<Vue | VueConstructor, Editor, VueNodeViewRend
|
|||||||
return (contentElement || this.dom) as HTMLElement | null
|
return (contentElement || this.dom) as HTMLElement | null
|
||||||
}
|
}
|
||||||
|
|
||||||
update(node: ProseMirrorNode, decorations: DecorationWithType[]) {
|
/**
|
||||||
const updateProps = (props?: Record<string, any>) => {
|
* On editor selection update, check if the node is selected.
|
||||||
|
* If it is, call `selectNode`, otherwise call `deselectNode`.
|
||||||
|
*/
|
||||||
|
handleSelectionUpdate() {
|
||||||
|
const { from, to } = this.editor.state.selection
|
||||||
|
const pos = this.getPos()
|
||||||
|
|
||||||
|
if (typeof pos !== 'number') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from <= pos && to >= pos + this.node.nodeSize) {
|
||||||
|
if (this.renderer.ref.$props.selected) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectNode()
|
||||||
|
} else {
|
||||||
|
if (!this.renderer.ref.$props.selected) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.deselectNode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On update, update the React component.
|
||||||
|
* To prevent unnecessary updates, the `update` option can be used.
|
||||||
|
*/
|
||||||
|
update(
|
||||||
|
node: ProseMirrorNode,
|
||||||
|
decorations: readonly Decoration[],
|
||||||
|
innerDecorations: DecorationSource,
|
||||||
|
): boolean {
|
||||||
|
const rerenderComponent = (props?: Record<string, any>) => {
|
||||||
this.decorationClasses.value = this.getDecorationClasses()
|
this.decorationClasses.value = this.getDecorationClasses()
|
||||||
this.renderer.updateProps(props)
|
this.renderer.updateProps(props)
|
||||||
}
|
}
|
||||||
@ -109,16 +159,20 @@ class VueNodeView extends NodeView<Vue | VueConstructor, Editor, VueNodeViewRend
|
|||||||
if (typeof this.options.update === 'function') {
|
if (typeof this.options.update === 'function') {
|
||||||
const oldNode = this.node
|
const oldNode = this.node
|
||||||
const oldDecorations = this.decorations
|
const oldDecorations = this.decorations
|
||||||
|
const oldInnerDecorations = this.innerDecorations
|
||||||
|
|
||||||
this.node = node
|
this.node = node
|
||||||
this.decorations = decorations
|
this.decorations = decorations
|
||||||
|
this.innerDecorations = innerDecorations
|
||||||
|
|
||||||
return this.options.update({
|
return this.options.update({
|
||||||
oldNode,
|
oldNode,
|
||||||
oldDecorations,
|
oldDecorations,
|
||||||
newNode: node,
|
newNode: node,
|
||||||
newDecorations: decorations,
|
newDecorations: decorations,
|
||||||
updateProps: () => updateProps({ node, decorations }),
|
oldInnerDecorations,
|
||||||
|
innerDecorations,
|
||||||
|
updateProps: () => rerenderComponent({ node, decorations, innerDecorations }),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,18 +180,23 @@ class VueNodeView extends NodeView<Vue | VueConstructor, Editor, VueNodeViewRend
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node === this.node && this.decorations === decorations) {
|
if (node === this.node && this.decorations === decorations && this.innerDecorations === innerDecorations) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
this.node = node
|
this.node = node
|
||||||
this.decorations = decorations
|
this.decorations = decorations
|
||||||
|
this.innerDecorations = innerDecorations
|
||||||
|
|
||||||
updateProps({ node, decorations })
|
rerenderComponent({ node, decorations, innerDecorations })
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select the node.
|
||||||
|
* Add the `selected` prop and the `ProseMirror-selectednode` class.
|
||||||
|
*/
|
||||||
selectNode() {
|
selectNode() {
|
||||||
this.renderer.updateProps({
|
this.renderer.updateProps({
|
||||||
selected: true,
|
selected: true,
|
||||||
@ -145,6 +204,10 @@ class VueNodeView extends NodeView<Vue | VueConstructor, Editor, VueNodeViewRend
|
|||||||
this.renderer.element.classList.add('ProseMirror-selectednode')
|
this.renderer.element.classList.add('ProseMirror-selectednode')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deselect the node.
|
||||||
|
* Remove the `selected` prop and the `ProseMirror-selectednode` class.
|
||||||
|
*/
|
||||||
deselectNode() {
|
deselectNode() {
|
||||||
this.renderer.updateProps({
|
this.renderer.updateProps({
|
||||||
selected: false,
|
selected: false,
|
||||||
@ -164,6 +227,7 @@ class VueNodeView extends NodeView<Vue | VueConstructor, Editor, VueNodeViewRend
|
|||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.renderer.destroy()
|
this.renderer.destroy()
|
||||||
|
this.editor.off('selectionUpdate', this.handleSelectionUpdate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,14 +235,14 @@ export function VueNodeViewRenderer(
|
|||||||
component: Vue | VueConstructor,
|
component: Vue | VueConstructor,
|
||||||
options?: Partial<VueNodeViewRendererOptions>,
|
options?: Partial<VueNodeViewRendererOptions>,
|
||||||
): NodeViewRenderer {
|
): NodeViewRenderer {
|
||||||
return (props: NodeViewRendererProps) => {
|
return props => {
|
||||||
// try to get the parent component
|
// try to get the parent component
|
||||||
// this is important for vue devtools to show the component hierarchy correctly
|
// this is important for vue devtools to show the component hierarchy correctly
|
||||||
// maybe it’s `undefined` because <editor-content> isn’t rendered yet
|
// maybe it’s `undefined` because <editor-content> isn’t rendered yet
|
||||||
if (!(props.editor as Editor).contentComponent) {
|
if (!(props.editor as Editor).contentComponent) {
|
||||||
return {}
|
return {} as unknown as ProseMirrorNodeView
|
||||||
}
|
}
|
||||||
|
|
||||||
return new VueNodeView(component, props, options) as unknown as ProseMirrorNodeView
|
return new VueNodeView(component, props, options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,15 @@
|
|||||||
|
/* eslint-disable no-underscore-dangle */
|
||||||
import {
|
import {
|
||||||
DecorationWithType,
|
DecorationWithType,
|
||||||
NodeView,
|
NodeView,
|
||||||
NodeViewProps,
|
NodeViewProps,
|
||||||
NodeViewRenderer,
|
NodeViewRenderer,
|
||||||
NodeViewRendererOptions,
|
NodeViewRendererOptions,
|
||||||
NodeViewRendererProps,
|
|
||||||
} from '@tiptap/core'
|
} from '@tiptap/core'
|
||||||
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
||||||
import { Decoration, NodeView as ProseMirrorNodeView } from '@tiptap/pm/view'
|
import { Decoration, DecorationSource, NodeView as ProseMirrorNodeView } from '@tiptap/pm/view'
|
||||||
import {
|
import {
|
||||||
Component,
|
Component, defineComponent, PropType, provide, Ref, ref,
|
||||||
defineComponent,
|
|
||||||
PropType,
|
|
||||||
provide,
|
|
||||||
Ref,
|
|
||||||
ref,
|
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
|
|
||||||
import { Editor } from './Editor.js'
|
import { Editor } from './Editor.js'
|
||||||
@ -58,13 +53,15 @@ export const nodeViewProps = {
|
|||||||
export interface VueNodeViewRendererOptions extends NodeViewRendererOptions {
|
export interface VueNodeViewRendererOptions extends NodeViewRendererOptions {
|
||||||
update:
|
update:
|
||||||
| ((props: {
|
| ((props: {
|
||||||
oldNode: ProseMirrorNode
|
oldNode: ProseMirrorNode;
|
||||||
oldDecorations: Decoration[]
|
oldDecorations: readonly Decoration[];
|
||||||
newNode: ProseMirrorNode
|
oldInnerDecorations: DecorationSource;
|
||||||
newDecorations: Decoration[]
|
newNode: ProseMirrorNode;
|
||||||
updateProps: () => void
|
newDecorations: readonly Decoration[];
|
||||||
|
innerDecorations: DecorationSource;
|
||||||
|
updateProps: () => void;
|
||||||
}) => boolean)
|
}) => boolean)
|
||||||
| null
|
| null;
|
||||||
}
|
}
|
||||||
|
|
||||||
class VueNodeView extends NodeView<Component, Editor, VueNodeViewRendererOptions> {
|
class VueNodeView extends NodeView<Component, Editor, VueNodeViewRendererOptions> {
|
||||||
@ -73,16 +70,19 @@ class VueNodeView extends NodeView<Component, Editor, VueNodeViewRendererOptions
|
|||||||
decorationClasses!: Ref<string>
|
decorationClasses!: Ref<string>
|
||||||
|
|
||||||
mount() {
|
mount() {
|
||||||
const props: NodeViewProps = {
|
const props = {
|
||||||
editor: this.editor,
|
editor: this.editor,
|
||||||
node: this.node,
|
node: this.node,
|
||||||
decorations: this.decorations,
|
decorations: this.decorations as DecorationWithType[],
|
||||||
|
innerDecorations: this.innerDecorations,
|
||||||
|
view: this.view,
|
||||||
selected: false,
|
selected: false,
|
||||||
extension: this.extension,
|
extension: this.extension,
|
||||||
|
HTMLAttributes: this.HTMLAttributes,
|
||||||
getPos: () => this.getPos(),
|
getPos: () => this.getPos(),
|
||||||
updateAttributes: (attributes = {}) => this.updateAttributes(attributes),
|
updateAttributes: (attributes = {}) => this.updateAttributes(attributes),
|
||||||
deleteNode: () => this.deleteNode(),
|
deleteNode: () => this.deleteNode(),
|
||||||
}
|
} satisfies NodeViewProps
|
||||||
|
|
||||||
const onDragStart = this.onDragStart.bind(this)
|
const onDragStart = this.onDragStart.bind(this)
|
||||||
|
|
||||||
@ -117,12 +117,19 @@ class VueNodeView extends NodeView<Component, Editor, VueNodeViewRendererOptions
|
|||||||
__file: this.component.__file,
|
__file: this.component.__file,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.handleSelectionUpdate = this.handleSelectionUpdate.bind(this)
|
||||||
|
this.editor.on('selectionUpdate', this.handleSelectionUpdate)
|
||||||
|
|
||||||
this.renderer = new VueRenderer(extendedComponent, {
|
this.renderer = new VueRenderer(extendedComponent, {
|
||||||
editor: this.editor,
|
editor: this.editor,
|
||||||
props,
|
props,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the DOM element.
|
||||||
|
* This is the element that will be used to display the node view.
|
||||||
|
*/
|
||||||
get dom() {
|
get dom() {
|
||||||
if (!this.renderer.element || !this.renderer.element.hasAttribute('data-node-view-wrapper')) {
|
if (!this.renderer.element || !this.renderer.element.hasAttribute('data-node-view-wrapper')) {
|
||||||
throw Error('Please use the NodeViewWrapper component for your node view.')
|
throw Error('Please use the NodeViewWrapper component for your node view.')
|
||||||
@ -131,6 +138,10 @@ class VueNodeView extends NodeView<Component, Editor, VueNodeViewRendererOptions
|
|||||||
return this.renderer.element as HTMLElement
|
return this.renderer.element as HTMLElement
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the content DOM element.
|
||||||
|
* This is the element that will be used to display the rich-text content of the node.
|
||||||
|
*/
|
||||||
get contentDOM() {
|
get contentDOM() {
|
||||||
if (this.node.isLeaf) {
|
if (this.node.isLeaf) {
|
||||||
return null
|
return null
|
||||||
@ -139,8 +150,43 @@ class VueNodeView extends NodeView<Component, Editor, VueNodeViewRendererOptions
|
|||||||
return this.dom.querySelector('[data-node-view-content]') as HTMLElement | null
|
return this.dom.querySelector('[data-node-view-content]') as HTMLElement | null
|
||||||
}
|
}
|
||||||
|
|
||||||
update(node: ProseMirrorNode, decorations: DecorationWithType[]) {
|
/**
|
||||||
const updateProps = (props?: Record<string, any>) => {
|
* On editor selection update, check if the node is selected.
|
||||||
|
* If it is, call `selectNode`, otherwise call `deselectNode`.
|
||||||
|
*/
|
||||||
|
handleSelectionUpdate() {
|
||||||
|
const { from, to } = this.editor.state.selection
|
||||||
|
const pos = this.getPos()
|
||||||
|
|
||||||
|
if (typeof pos !== 'number') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from <= pos && to >= pos + this.node.nodeSize) {
|
||||||
|
if (this.renderer.props.selected) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectNode()
|
||||||
|
} else {
|
||||||
|
if (!this.renderer.props.selected) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.deselectNode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On update, update the React component.
|
||||||
|
* To prevent unnecessary updates, the `update` option can be used.
|
||||||
|
*/
|
||||||
|
update(
|
||||||
|
node: ProseMirrorNode,
|
||||||
|
decorations: readonly Decoration[],
|
||||||
|
innerDecorations: DecorationSource,
|
||||||
|
): boolean {
|
||||||
|
const rerenderComponent = (props?: Record<string, any>) => {
|
||||||
this.decorationClasses.value = this.getDecorationClasses()
|
this.decorationClasses.value = this.getDecorationClasses()
|
||||||
this.renderer.updateProps(props)
|
this.renderer.updateProps(props)
|
||||||
}
|
}
|
||||||
@ -148,16 +194,20 @@ class VueNodeView extends NodeView<Component, Editor, VueNodeViewRendererOptions
|
|||||||
if (typeof this.options.update === 'function') {
|
if (typeof this.options.update === 'function') {
|
||||||
const oldNode = this.node
|
const oldNode = this.node
|
||||||
const oldDecorations = this.decorations
|
const oldDecorations = this.decorations
|
||||||
|
const oldInnerDecorations = this.innerDecorations
|
||||||
|
|
||||||
this.node = node
|
this.node = node
|
||||||
this.decorations = decorations
|
this.decorations = decorations
|
||||||
|
this.innerDecorations = innerDecorations
|
||||||
|
|
||||||
return this.options.update({
|
return this.options.update({
|
||||||
oldNode,
|
oldNode,
|
||||||
oldDecorations,
|
oldDecorations,
|
||||||
newNode: node,
|
newNode: node,
|
||||||
newDecorations: decorations,
|
newDecorations: decorations,
|
||||||
updateProps: () => updateProps({ node, decorations }),
|
oldInnerDecorations,
|
||||||
|
innerDecorations,
|
||||||
|
updateProps: () => rerenderComponent({ node, decorations, innerDecorations }),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,18 +215,23 @@ class VueNodeView extends NodeView<Component, Editor, VueNodeViewRendererOptions
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node === this.node && this.decorations === decorations) {
|
if (node === this.node && this.decorations === decorations && this.innerDecorations === innerDecorations) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
this.node = node
|
this.node = node
|
||||||
this.decorations = decorations
|
this.decorations = decorations
|
||||||
|
this.innerDecorations = innerDecorations
|
||||||
|
|
||||||
updateProps({ node, decorations })
|
rerenderComponent({ node, decorations, innerDecorations })
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select the node.
|
||||||
|
* Add the `selected` prop and the `ProseMirror-selectednode` class.
|
||||||
|
*/
|
||||||
selectNode() {
|
selectNode() {
|
||||||
this.renderer.updateProps({
|
this.renderer.updateProps({
|
||||||
selected: true,
|
selected: true,
|
||||||
@ -186,6 +241,10 @@ class VueNodeView extends NodeView<Component, Editor, VueNodeViewRendererOptions
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deselect the node.
|
||||||
|
* Remove the `selected` prop and the `ProseMirror-selectednode` class.
|
||||||
|
*/
|
||||||
deselectNode() {
|
deselectNode() {
|
||||||
this.renderer.updateProps({
|
this.renderer.updateProps({
|
||||||
selected: false,
|
selected: false,
|
||||||
@ -207,26 +266,26 @@ class VueNodeView extends NodeView<Component, Editor, VueNodeViewRendererOptions
|
|||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.renderer.destroy()
|
this.renderer.destroy()
|
||||||
|
this.editor.off('selectionUpdate', this.handleSelectionUpdate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VueNodeViewRenderer(
|
export function VueNodeViewRenderer(
|
||||||
component: Component,
|
component: Component<NodeViewProps>,
|
||||||
options?: Partial<VueNodeViewRendererOptions>,
|
options?: Partial<VueNodeViewRendererOptions>,
|
||||||
): NodeViewRenderer {
|
): NodeViewRenderer {
|
||||||
return (props: NodeViewRendererProps) => {
|
return props => {
|
||||||
// try to get the parent component
|
// try to get the parent component
|
||||||
// this is important for vue devtools to show the component hierarchy correctly
|
// this is important for vue devtools to show the component hierarchy correctly
|
||||||
// maybe it’s `undefined` because <editor-content> isn’t rendered yet
|
// maybe it’s `undefined` because <editor-content> isn’t rendered yet
|
||||||
if (!(props.editor as Editor).contentComponent) {
|
if (!(props.editor as Editor).contentComponent) {
|
||||||
return {}
|
return {} as unknown as ProseMirrorNodeView
|
||||||
}
|
}
|
||||||
// check for class-component and normalize if neccessary
|
// check for class-component and normalize if neccessary
|
||||||
const normalizedComponent = typeof component === 'function' && '__vccOpts' in component
|
const normalizedComponent = typeof component === 'function' && '__vccOpts' in component
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
? (component.__vccOpts as Component)
|
||||||
? component.__vccOpts as Component
|
|
||||||
: component
|
: component
|
||||||
|
|
||||||
return new VueNodeView(normalizedComponent, props, options) as unknown as ProseMirrorNodeView
|
return new VueNodeView(normalizedComponent, props, options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user