diff --git a/packages/core/src/NodeView.ts b/packages/core/src/NodeView.ts new file mode 100644 index 00000000..9ec74b52 --- /dev/null +++ b/packages/core/src/NodeView.ts @@ -0,0 +1,180 @@ +import { Decoration, NodeView as ProseMirrorNodeView } from 'prosemirror-view' +import { NodeSelection } from 'prosemirror-state' +import { Node as ProseMirrorNode } from 'prosemirror-model' +import { Editor as CoreEditor } from './Editor' +import { Node } from './Node' +import { NodeViewRendererProps } from './types' + +interface NodeViewRendererOptions { + stopEvent: ((event: Event) => boolean) | null, + update: ((node: ProseMirrorNode, decorations: Decoration[]) => boolean) | null, +} + +export class NodeView implements ProseMirrorNodeView { + + component: Component + + editor: Editor + + extension: Node + + node: ProseMirrorNode + + decorations: Decoration[] + + getPos: any + + isDragging = false + + options: NodeViewRendererOptions = { + stopEvent: null, + update: null, + } + + constructor(component: Component, props: NodeViewRendererProps, options?: Partial) { + this.component = component + this.options = { ...this.options, ...options } + this.editor = props.editor as Editor + this.extension = props.extension + this.node = props.node + this.decorations = props.decorations + this.getPos = props.getPos + this.mount() + } + + mount() { + // eslint-disable-next-line + return + } + + get dom(): Element | null { + return null + } + + get contentDOM(): Element | null { + return null + } + + onDragStart(event: DragEvent) { + if (!this.dom) { + return + } + + const { view } = this.editor + const target = (event.target as HTMLElement) + + if (this.contentDOM?.contains(target)) { + return + } + + // sometimes `event.target` is not the `dom` element + event.dataTransfer?.setDragImage(this.dom, 0, 0) + + const selection = NodeSelection.create(view.state.doc, this.getPos()) + const transaction = view.state.tr.setSelection(selection) + + view.dispatch(transaction) + } + + stopEvent(event: Event) { + if (!this.dom) { + return false + } + + if (typeof this.options.stopEvent === 'function') { + return this.options.stopEvent(event) + } + + const target = (event.target as HTMLElement) + const isInElement = this.dom.contains(target) && !this.contentDOM?.contains(target) + + // ignore all events from child nodes + if (!isInElement) { + return false + } + + const { isEditable } = this.editor + const { isDragging } = this + const isDraggable = !!this.node.type.spec.draggable + const isSelectable = NodeSelection.isSelectable(this.node) + const isCopyEvent = event.type === 'copy' + const isPasteEvent = event.type === 'paste' + const isCutEvent = event.type === 'cut' + const isClickEvent = event.type === 'mousedown' + const isDragEvent = event.type.startsWith('drag') || event.type === 'drop' + + // ProseMirror tries to drag selectable nodes + // even if `draggable` is set to `false` + // this fix prevents that + if (!isDraggable && isSelectable && isDragEvent) { + event.preventDefault() + } + + if (isDraggable && isDragEvent && !isDragging) { + event.preventDefault() + return false + } + + // we have to store that dragging started + if (isDraggable && isEditable && !isDragging && isClickEvent) { + const dragHandle = target.closest('[data-drag-handle]') + const isValidDragHandle = dragHandle + && (this.dom === dragHandle || (this.dom.contains(dragHandle))) + + if (isValidDragHandle) { + this.isDragging = true + document.addEventListener('dragend', () => { + this.isDragging = false + }, { once: true }) + } + } + + // these events are handled by prosemirror + if ( + isDragging + || isCopyEvent + || isPasteEvent + || isCutEvent + || (isClickEvent && isSelectable) + ) { + return false + } + + return true + } + + ignoreMutation(mutation: MutationRecord | { type: 'selection', target: Element }) { + if (mutation.type === 'selection') { + if (this.node.isLeaf) { + return true + } + + return false + } + + if (!this.contentDOM) { + return true + } + + const contentDOMHasChanged = !this.contentDOM.contains(mutation.target) + || this.contentDOM === mutation.target + + return contentDOMHasChanged + } + + updateAttributes(attributes: {}) { + if (!this.editor.view.editable) { + return + } + + const { state } = this.editor.view + const pos = this.getPos() + const transaction = state.tr.setNodeMarkup(pos, undefined, { + ...this.node.attrs, + ...attributes, + }) + + this.editor.view.dispatch(transaction) + } + +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5b18b3be..e8b91e42 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -2,6 +2,7 @@ export * from './Editor' export * from './Extension' export * from './Node' export * from './Mark' +export * from './NodeView' export * from './types' export { default as nodeInputRule } from './inputRules/nodeInputRule' diff --git a/packages/react/src/ReactNodeViewRenderer.tsx b/packages/react/src/ReactNodeViewRenderer.tsx index be47a526..334b0a9c 100644 --- a/packages/react/src/ReactNodeViewRenderer.tsx +++ b/packages/react/src/ReactNodeViewRenderer.tsx @@ -1,7 +1,6 @@ import React, { useState, useEffect } from 'react' -import { Node, NodeViewRenderer, NodeViewRendererProps } from '@tiptap/core' -import { Decoration, NodeView } from 'prosemirror-view' -import { NodeSelection } from 'prosemirror-state' +import { NodeView, NodeViewRenderer, NodeViewRendererProps } from '@tiptap/core' +import { Decoration, NodeView as ProseMirrorNodeView } from 'prosemirror-view' import { Node as ProseMirrorNode } from 'prosemirror-model' import { Editor } from './Editor' import { ReactRenderer } from './ReactRenderer' @@ -12,54 +11,11 @@ interface ReactNodeViewRendererOptions { update: ((node: ProseMirrorNode, decorations: Decoration[]) => boolean) | null, } -class ReactNodeView implements NodeView { +class ReactNodeView extends NodeView { renderer!: ReactRenderer - editor: Editor - - extension!: Node - - node!: ProseMirrorNode - - decorations!: Decoration[] - - getPos!: any - - isDragging = false - - options: ReactNodeViewRendererOptions = { - stopEvent: null, - update: null, - } - - constructor(component: any, props: NodeViewRendererProps, options?: Partial) { - this.options = { ...this.options, ...options } - this.editor = props.editor as Editor - this.extension = props.extension - this.node = props.node - this.getPos = props.getPos - this.mount(component) - } - - onDragStart(event: DragEvent) { - const { view } = this.editor - const target = (event.target as HTMLElement) - - if (this.contentDOM?.contains(target)) { - return - } - - // sometimes `event.target` is not the `dom` element - event.dataTransfer?.setDragImage(this.dom, 0, 0) - - const selection = NodeSelection.create(view.state.doc, this.getPos()) - const transaction = view.state.tr.setSelection(selection) - - view.dispatch(transaction) - } - - mount(Component: any) { + mount() { const props = { editor: this.editor, node: this.node, @@ -70,12 +26,13 @@ class ReactNodeView implements NodeView { updateAttributes: (attributes = {}) => this.updateAttributes(attributes), } - if (!Component.displayName) { + if (!(this.component as any).displayName) { const capitalizeFirstChar = (string: string): string => { return string.charAt(0).toUpperCase() + string.substring(1) } - Component.displayName = capitalizeFirstChar(this.extension.config.name) + // @ts-ignore + this.component.displayName = capitalizeFirstChar(this.extension.config.name) } const ReactNodeView: React.FunctionComponent = (props) => { @@ -93,7 +50,7 @@ class ReactNodeView implements NodeView { return ( - + ) } @@ -122,92 +79,6 @@ class ReactNodeView implements NodeView { return contentElement || this.dom } - stopEvent(event: Event) { - if (typeof this.options.stopEvent === 'function') { - return this.options.stopEvent(event) - } - - const target = (event.target as HTMLElement) - const isInElement = this.dom.contains(target) && !this.contentDOM?.contains(target) - - // ignore all events from child nodes - if (!isInElement) { - return false - } - - const { isEditable } = this.editor - const { isDragging } = this - const isDraggable = !!this.node.type.spec.draggable - const isSelectable = NodeSelection.isSelectable(this.node) - const isCopyEvent = event.type === 'copy' - const isPasteEvent = event.type === 'paste' - const isCutEvent = event.type === 'cut' - const isClickEvent = event.type === 'mousedown' - const isDragEvent = event.type.startsWith('drag') || event.type === 'drop' - - // ProseMirror tries to drag selectable nodes - // even if `draggable` is set to `false` - // this fix prevents that - if (!isDraggable && isSelectable && isDragEvent) { - event.preventDefault() - } - - if (isDraggable && isDragEvent && !isDragging) { - event.preventDefault() - return false - } - - // we have to store that dragging started - if (isDraggable && isEditable && !isDragging && isClickEvent) { - const dragHandle = target.closest('[data-drag-handle]') - const isValidDragHandle = dragHandle - && (this.dom === dragHandle || (this.dom.contains(dragHandle))) - - if (isValidDragHandle) { - this.isDragging = true - document.addEventListener('dragend', () => { - this.isDragging = false - }, { once: true }) - } - } - - // these events are handled by prosemirror - if ( - isDragging - || isCopyEvent - || isPasteEvent - || isCutEvent - || (isClickEvent && isSelectable) - ) { - return false - } - - return true - } - - ignoreMutation(mutation: MutationRecord | { type: 'selection'; target: Element }) { - if (mutation.type === 'selection') { - if (this.node.isLeaf) { - return true - } - - return false - } - - if (!this.contentDOM) { - return true - } - - const contentDOMHasChanged = !this.contentDOM.contains(mutation.target) - || this.contentDOM === mutation.target - - return contentDOMHasChanged - } - - destroy() { - this.renderer.destroy() - } - update(node: ProseMirrorNode, decorations: Decoration[]) { if (typeof this.options.update === 'function') { return this.options.update(node, decorations) @@ -224,26 +95,10 @@ class ReactNodeView implements NodeView { this.node = node this.decorations = decorations this.renderer.updateProps({ node, decorations }) - this.renderer.render() return true } - updateAttributes(attributes: {}) { - if (!this.editor.view.editable) { - return - } - - const { state } = this.editor.view - const pos = this.getPos() - const transaction = state.tr.setNodeMarkup(pos, undefined, { - ...this.node.attrs, - ...attributes, - }) - - this.editor.view.dispatch(transaction) - } - selectNode() { this.renderer.updateProps({ selected: true, @@ -255,6 +110,10 @@ class ReactNodeView implements NodeView { selected: false, }) } + + destroy() { + this.renderer.destroy() + } } export function ReactNodeViewRenderer(component: any, options?: Partial): NodeViewRenderer { @@ -266,6 +125,6 @@ export function ReactNodeViewRenderer(component: any, options?: Partial boolean) | null, } -class VueNodeView implements NodeView { +class VueNodeView extends NodeView<(Vue | VueConstructor), Editor> { renderer!: VueRenderer - editor: Editor - - extension!: Node - - node!: ProseMirrorNode - - decorations!: Decoration[] - - getPos!: any - - isDragging = false - - options: VueNodeViewRendererOptions = { - stopEvent: null, - update: null, - } - - constructor(component: Vue | VueConstructor, props: NodeViewRendererProps, options?: Partial) { - this.options = { ...this.options, ...options } - this.editor = props.editor as Editor - this.extension = props.extension - this.node = props.node - this.getPos = props.getPos - this.mount(component) - } - - onDragStart(event: DragEvent) { - const { view } = this.editor - const target = (event.target as HTMLElement) - - if (this.contentDOM?.contains(target)) { - return - } - - // sometimes `event.target` is not the `dom` element - event.dataTransfer?.setDragImage(this.dom, 0, 0) - - const selection = NodeSelection.create(view.state.doc, this.getPos()) - const transaction = view.state.tr.setSelection(selection) - - view.dispatch(transaction) - } - - mount(component: Vue | VueConstructor) { + mount() { const props = { editor: this.editor, node: this.node, @@ -80,7 +36,7 @@ class VueNodeView implements NodeView { }) const Component = Vue - .extend(component) + .extend(this.component) .extend({ props: Object.keys(props), provide() { @@ -115,92 +71,6 @@ class VueNodeView implements NodeView { return contentElement || this.dom } - stopEvent(event: Event) { - if (typeof this.options.stopEvent === 'function') { - return this.options.stopEvent(event) - } - - const target = (event.target as HTMLElement) - const isInElement = this.dom.contains(target) && !this.contentDOM?.contains(target) - - // ignore all events from child nodes - if (!isInElement) { - return false - } - - const { isEditable } = this.editor - const { isDragging } = this - const isDraggable = !!this.node.type.spec.draggable - const isSelectable = NodeSelection.isSelectable(this.node) - const isCopyEvent = event.type === 'copy' - const isPasteEvent = event.type === 'paste' - const isCutEvent = event.type === 'cut' - const isClickEvent = event.type === 'mousedown' - const isDragEvent = event.type.startsWith('drag') || event.type === 'drop' - - // ProseMirror tries to drag selectable nodes - // even if `draggable` is set to `false` - // this fix prevents that - if (!isDraggable && isSelectable && isDragEvent) { - event.preventDefault() - } - - if (isDraggable && isDragEvent && !isDragging) { - event.preventDefault() - return false - } - - // we have to store that dragging started - if (isDraggable && isEditable && !isDragging && isClickEvent) { - const dragHandle = target.closest('[data-drag-handle]') - const isValidDragHandle = dragHandle - && (this.dom === dragHandle || (this.dom.contains(dragHandle))) - - if (isValidDragHandle) { - this.isDragging = true - document.addEventListener('dragend', () => { - this.isDragging = false - }, { once: true }) - } - } - - // these events are handled by prosemirror - if ( - isDragging - || isCopyEvent - || isPasteEvent - || isCutEvent - || (isClickEvent && isSelectable) - ) { - return false - } - - return true - } - - ignoreMutation(mutation: MutationRecord | { type: 'selection'; target: Element }) { - if (mutation.type === 'selection') { - if (this.node.isLeaf) { - return true - } - - return false - } - - if (!this.contentDOM) { - return true - } - - const contentDOMHasChanged = !this.contentDOM.contains(mutation.target) - || this.contentDOM === mutation.target - - return contentDOMHasChanged - } - - destroy() { - this.renderer.destroy() - } - update(node: ProseMirrorNode, decorations: Decoration[]) { if (typeof this.options.update === 'function') { return this.options.update(node, decorations) @@ -221,21 +91,6 @@ class VueNodeView implements NodeView { return true } - updateAttributes(attributes: {}) { - if (!this.editor.view.editable) { - return - } - - const { state } = this.editor.view - const pos = this.getPos() - const transaction = state.tr.setNodeMarkup(pos, undefined, { - ...this.node.attrs, - ...attributes, - }) - - this.editor.view.dispatch(transaction) - } - selectNode() { this.renderer.updateProps({ selected: true, @@ -248,6 +103,10 @@ class VueNodeView implements NodeView { }) } + destroy() { + this.renderer.destroy() + } + } export function VueNodeViewRenderer(component: Vue | VueConstructor, options?: Partial): NodeViewRenderer { @@ -259,6 +118,6 @@ export function VueNodeViewRenderer(component: Vue | VueConstructor, options?: P return {} } - return new VueNodeView(component, props, options) as NodeView + return new VueNodeView(component, props, options) as ProseMirrorNodeView } } diff --git a/packages/vue-3/src/VueNodeViewRenderer.ts b/packages/vue-3/src/VueNodeViewRenderer.ts index 175a68c3..21013db2 100644 --- a/packages/vue-3/src/VueNodeViewRenderer.ts +++ b/packages/vue-3/src/VueNodeViewRenderer.ts @@ -1,12 +1,11 @@ -import { Node, NodeViewRenderer, NodeViewRendererProps } from '@tiptap/core' +import { NodeView, NodeViewRenderer, NodeViewRendererProps } from '@tiptap/core' import { ref, provide, Component, defineComponent, } from 'vue' -import { Decoration, NodeView } from 'prosemirror-view' -import { NodeSelection } from 'prosemirror-state' +import { Decoration, NodeView as ProseMirrorNodeView } from 'prosemirror-view' import { Node as ProseMirrorNode } from 'prosemirror-model' import { Editor } from './Editor' import { VueRenderer } from './VueRenderer' @@ -16,54 +15,11 @@ interface VueNodeViewRendererOptions { update: ((node: ProseMirrorNode, decorations: Decoration[]) => boolean) | null, } -class VueNodeView implements NodeView { +class VueNodeView extends NodeView { renderer!: VueRenderer - editor: Editor - - extension!: Node - - node!: ProseMirrorNode - - decorations!: Decoration[] - - getPos!: any - - isDragging = false - - options: VueNodeViewRendererOptions = { - stopEvent: null, - update: null, - } - - constructor(component: Component, props: NodeViewRendererProps, options?: Partial) { - this.options = { ...this.options, ...options } - this.editor = props.editor as Editor - this.extension = props.extension - this.node = props.node - this.getPos = props.getPos - this.mount(component) - } - - onDragStart(event: DragEvent) { - const { view } = this.editor - const target = (event.target as HTMLElement) - - if (this.contentDOM?.contains(target)) { - return - } - - // sometimes `event.target` is not the `dom` element - event.dataTransfer?.setDragImage(this.dom, 0, 0) - - const selection = NodeSelection.create(view.state.doc, this.getPos()) - const transaction = view.state.tr.setSelection(selection) - - view.dispatch(transaction) - } - - mount(component: Component) { + mount() { const props = { editor: this.editor, node: this.node, @@ -82,13 +38,13 @@ class VueNodeView implements NodeView { }) const extendedComponent = defineComponent({ - extends: { ...component }, + extends: { ...this.component }, props: Object.keys(props), - setup() { + setup: () => { provide('onDragStart', onDragStart) provide('isEditable', isEditable) - return (component as any).setup?.() + return (this.component as any).setup?.() }, }) @@ -116,92 +72,6 @@ class VueNodeView implements NodeView { return contentElement || this.dom } - stopEvent(event: Event) { - if (typeof this.options.stopEvent === 'function') { - return this.options.stopEvent(event) - } - - const target = (event.target as HTMLElement) - const isInElement = this.dom.contains(target) && !this.contentDOM?.contains(target) - - // ignore all events from child nodes - if (!isInElement) { - return false - } - - const { isEditable } = this.editor - const { isDragging } = this - const isDraggable = !!this.node.type.spec.draggable - const isSelectable = NodeSelection.isSelectable(this.node) - const isCopyEvent = event.type === 'copy' - const isPasteEvent = event.type === 'paste' - const isCutEvent = event.type === 'cut' - const isClickEvent = event.type === 'mousedown' - const isDragEvent = event.type.startsWith('drag') || event.type === 'drop' - - // ProseMirror tries to drag selectable nodes - // even if `draggable` is set to `false` - // this fix prevents that - if (!isDraggable && isSelectable && isDragEvent) { - event.preventDefault() - } - - if (isDraggable && isDragEvent && !isDragging) { - event.preventDefault() - return false - } - - // we have to store that dragging started - if (isDraggable && isEditable && !isDragging && isClickEvent) { - const dragHandle = target.closest('[data-drag-handle]') - const isValidDragHandle = dragHandle - && (this.dom === dragHandle || (this.dom.contains(dragHandle))) - - if (isValidDragHandle) { - this.isDragging = true - document.addEventListener('dragend', () => { - this.isDragging = false - }, { once: true }) - } - } - - // these events are handled by prosemirror - if ( - isDragging - || isCopyEvent - || isPasteEvent - || isCutEvent - || (isClickEvent && isSelectable) - ) { - return false - } - - return true - } - - ignoreMutation(mutation: MutationRecord | { type: 'selection'; target: Element }) { - if (mutation.type === 'selection') { - if (this.node.isLeaf) { - return true - } - - return false - } - - if (!this.contentDOM) { - return true - } - - const contentDOMHasChanged = !this.contentDOM.contains(mutation.target) - || this.contentDOM === mutation.target - - return contentDOMHasChanged - } - - destroy() { - this.renderer.destroy() - } - update(node: ProseMirrorNode, decorations: Decoration[]) { if (typeof this.options.update === 'function') { return this.options.update(node, decorations) @@ -222,21 +92,6 @@ class VueNodeView implements NodeView { return true } - updateAttributes(attributes: {}) { - if (!this.editor.view.editable) { - return - } - - const { state } = this.editor.view - const pos = this.getPos() - const transaction = state.tr.setNodeMarkup(pos, undefined, { - ...this.node.attrs, - ...attributes, - }) - - this.editor.view.dispatch(transaction) - } - selectNode() { this.renderer.updateProps({ selected: true, @@ -249,6 +104,10 @@ class VueNodeView implements NodeView { }) } + destroy() { + this.renderer.destroy() + } + } export function VueNodeViewRenderer(component: Component, options?: Partial): NodeViewRenderer { @@ -260,6 +119,6 @@ export function VueNodeViewRenderer(component: Component, options?: Partial