tiptap/packages/vue-3/src/VueNodeViewRenderer.ts
2021-03-05 11:58:52 +01:00

268 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Node, NodeViewRenderer, NodeViewRendererProps } from '@tiptap/core'
import {
ref,
provide,
Component,
defineComponent,
} from 'vue'
import { Decoration, NodeView } from 'prosemirror-view'
import { NodeSelection } from 'prosemirror-state'
import { Node as ProseMirrorNode } from 'prosemirror-model'
import { Editor } from './Editor'
import { VueRenderer } from './VueRenderer'
interface VueNodeViewRendererOptions {
stopEvent: ((event: Event) => boolean) | null,
update: ((node: ProseMirrorNode, decorations: Decoration[]) => boolean) | null,
}
class VueNodeView implements 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<VueNodeViewRendererOptions>) {
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) {
const props = {
editor: this.editor,
node: this.node,
decorations: this.decorations,
selected: false,
extension: this.extension,
getPos: () => this.getPos(),
updateAttributes: (attributes = {}) => this.updateAttributes(attributes),
}
const onDragStart = this.onDragStart.bind(this)
const isEditable = ref(this.editor.isEditable)
this.editor.on('viewUpdate', () => {
isEditable.value = this.editor.isEditable
})
const extendedComponent = defineComponent({
extends: { ...component },
props: Object.keys(props),
setup() {
provide('onDragStart', onDragStart)
provide('isEditable', isEditable)
return (component as any).setup?.()
},
})
this.renderer = new VueRenderer(extendedComponent, {
editor: this.editor,
props,
})
}
get dom() {
if (!this.renderer.element.hasAttribute('data-node-view-wrapper')) {
throw Error('Please use the NodeViewWrapper component for your node view.')
}
return this.renderer.element
}
get contentDOM() {
const hasContent = !this.node.type.isAtom
if (!hasContent) {
return null
}
const contentElement = this.dom.querySelector('[data-node-view-content]')
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)
}
if (node.type !== this.node.type) {
return false
}
if (node === this.node && this.decorations === decorations) {
return true
}
this.node = node
this.decorations = decorations
this.renderer.updateProps({ node, decorations })
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,
})
}
deselectNode() {
this.renderer.updateProps({
selected: false,
})
}
}
export function VueNodeViewRenderer(component: Component, options?: Partial<VueNodeViewRendererOptions>): NodeViewRenderer {
return (props: NodeViewRendererProps) => {
// try to get the parent component
// this is important for vue devtools to show the component hierarchy correctly
// maybe its `undefined` because <editor-content> isnt rendered yet
if (!(props.editor as Editor).contentComponent) {
return {}
}
return new VueNodeView(component, props, options) as NodeView
}
}