2021-03-17 04:55:40 +08:00
|
|
|
|
import { Node as ProseMirrorNode } from 'prosemirror-model'
|
2022-06-08 20:10:25 +08:00
|
|
|
|
import { NodeSelection } from 'prosemirror-state'
|
|
|
|
|
import { Decoration, NodeView as ProseMirrorNodeView } from 'prosemirror-view'
|
|
|
|
|
|
2021-03-17 04:55:40 +08:00
|
|
|
|
import { Editor as CoreEditor } from './Editor'
|
|
|
|
|
import { Node } from './Node'
|
2022-06-08 20:10:25 +08:00
|
|
|
|
import { NodeViewRendererOptions, NodeViewRendererProps } from './types'
|
2021-12-06 19:00:09 +08:00
|
|
|
|
import { isiOS } from './utilities/isiOS'
|
2021-03-17 04:55:40 +08:00
|
|
|
|
|
2021-07-27 18:26:24 +08:00
|
|
|
|
export class NodeView<
|
|
|
|
|
Component,
|
|
|
|
|
Editor extends CoreEditor = CoreEditor,
|
|
|
|
|
Options extends NodeViewRendererOptions = NodeViewRendererOptions,
|
|
|
|
|
> implements ProseMirrorNodeView {
|
2021-03-17 04:55:40 +08:00
|
|
|
|
|
|
|
|
|
component: Component
|
|
|
|
|
|
|
|
|
|
editor: Editor
|
|
|
|
|
|
2021-07-27 18:26:24 +08:00
|
|
|
|
options: Options
|
|
|
|
|
|
2021-03-17 04:55:40 +08:00
|
|
|
|
extension: Node
|
|
|
|
|
|
|
|
|
|
node: ProseMirrorNode
|
|
|
|
|
|
|
|
|
|
decorations: Decoration[]
|
|
|
|
|
|
|
|
|
|
getPos: any
|
|
|
|
|
|
|
|
|
|
isDragging = false
|
|
|
|
|
|
2021-07-27 18:26:24 +08:00
|
|
|
|
constructor(component: Component, props: NodeViewRendererProps, options?: Partial<Options>) {
|
2021-03-17 04:55:40 +08:00
|
|
|
|
this.component = component
|
|
|
|
|
this.editor = props.editor as Editor
|
2021-07-27 18:26:24 +08:00
|
|
|
|
this.options = {
|
|
|
|
|
stopEvent: null,
|
|
|
|
|
ignoreMutation: null,
|
|
|
|
|
...options,
|
|
|
|
|
} as Options
|
2021-03-17 04:55:40 +08:00
|
|
|
|
this.extension = props.extension
|
|
|
|
|
this.node = props.node
|
|
|
|
|
this.decorations = props.decorations
|
|
|
|
|
this.getPos = props.getPos
|
|
|
|
|
this.mount()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mount() {
|
|
|
|
|
// eslint-disable-next-line
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-20 17:45:37 +08:00
|
|
|
|
get dom(): HTMLElement {
|
|
|
|
|
return this.editor.view.dom as HTMLElement
|
2021-03-17 04:55:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
2022-06-20 17:45:37 +08:00
|
|
|
|
get contentDOM(): HTMLElement | null {
|
2021-03-17 04:55:40 +08:00
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onDragStart(event: DragEvent) {
|
|
|
|
|
const { view } = this.editor
|
|
|
|
|
const target = (event.target as HTMLElement)
|
|
|
|
|
|
2021-04-20 23:18:59 +08:00
|
|
|
|
// get the drag handle element
|
|
|
|
|
// `closest` is not available for text nodes so we may have to use its parent
|
|
|
|
|
const dragHandle = target.nodeType === 3
|
|
|
|
|
? target.parentElement?.closest('[data-drag-handle]')
|
|
|
|
|
: target.closest('[data-drag-handle]')
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
!this.dom
|
|
|
|
|
|| this.contentDOM?.contains(target)
|
|
|
|
|
|| !dragHandle
|
|
|
|
|
) {
|
2021-03-17 04:55:40 +08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-20 23:18:59 +08:00
|
|
|
|
let x = 0
|
|
|
|
|
let y = 0
|
|
|
|
|
|
|
|
|
|
// calculate offset for drag element if we use a different drag handle element
|
|
|
|
|
if (this.dom !== dragHandle) {
|
|
|
|
|
const domBox = this.dom.getBoundingClientRect()
|
|
|
|
|
const handleBox = dragHandle.getBoundingClientRect()
|
|
|
|
|
|
2022-05-09 16:03:13 +08:00
|
|
|
|
// In React, we have to go through nativeEvent to reach offsetX/offsetY.
|
|
|
|
|
const offsetX = event.offsetX ?? (event as any).nativeEvent?.offsetX
|
|
|
|
|
const offsetY = event.offsetY ?? (event as any).nativeEvent?.offsetY
|
2022-05-11 01:51:52 +08:00
|
|
|
|
|
2022-05-09 16:03:13 +08:00
|
|
|
|
x = handleBox.x - domBox.x + offsetX
|
|
|
|
|
y = handleBox.y - domBox.y + offsetY
|
2021-04-20 23:18:59 +08:00
|
|
|
|
}
|
2021-04-09 05:53:47 +08:00
|
|
|
|
|
|
|
|
|
event.dataTransfer?.setDragImage(this.dom, x, y)
|
2021-03-17 04:55:40 +08:00
|
|
|
|
|
2021-04-20 23:18:59 +08:00
|
|
|
|
// we need to tell ProseMirror that we want to move the whole node
|
|
|
|
|
// so we create a NodeSelection
|
2021-03-17 04:55:40 +08:00
|
|
|
|
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') {
|
2021-07-27 18:26:24 +08:00
|
|
|
|
return this.options.stopEvent({ event })
|
2021-03-17 04:55:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const target = (event.target as HTMLElement)
|
|
|
|
|
const isInElement = this.dom.contains(target) && !this.contentDOM?.contains(target)
|
|
|
|
|
|
2021-03-18 20:54:48 +08:00
|
|
|
|
// any event from child nodes should be handled by ProseMirror
|
2021-03-17 04:55:40 +08:00
|
|
|
|
if (!isInElement) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-23 03:27:58 +08:00
|
|
|
|
const isDropEvent = event.type === 'drop'
|
2021-03-18 20:54:48 +08:00
|
|
|
|
const isInput = ['INPUT', 'BUTTON', 'SELECT', 'TEXTAREA'].includes(target.tagName)
|
2021-04-04 15:50:36 +08:00
|
|
|
|
|| target.isContentEditable
|
2021-03-18 20:54:48 +08:00
|
|
|
|
|
|
|
|
|
// any input event within node views should be ignored by ProseMirror
|
2021-10-23 03:27:58 +08:00
|
|
|
|
if (isInput && !isDropEvent) {
|
2021-03-18 20:54:48 +08:00
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-17 04:55:40 +08:00
|
|
|
|
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'
|
2021-10-23 03:27:58 +08:00
|
|
|
|
const isDragEvent = event.type.startsWith('drag')
|
2021-03-17 04:55:40 +08:00
|
|
|
|
|
|
|
|
|
// 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
|
2021-03-18 20:54:48 +08:00
|
|
|
|
|
2021-03-17 04:55:40 +08:00
|
|
|
|
document.addEventListener('dragend', () => {
|
|
|
|
|
this.isDragging = false
|
|
|
|
|
}, { once: true })
|
2021-03-18 20:54:48 +08:00
|
|
|
|
|
|
|
|
|
document.addEventListener('mouseup', () => {
|
|
|
|
|
this.isDragging = false
|
|
|
|
|
}, { once: true })
|
2021-03-17 04:55:40 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// these events are handled by prosemirror
|
|
|
|
|
if (
|
|
|
|
|
isDragging
|
2021-10-23 03:27:58 +08:00
|
|
|
|
|| isDropEvent
|
2021-03-17 04:55:40 +08:00
|
|
|
|
|| isCopyEvent
|
|
|
|
|
|| isPasteEvent
|
|
|
|
|
|| isCutEvent
|
|
|
|
|
|| (isClickEvent && isSelectable)
|
|
|
|
|
) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ignoreMutation(mutation: MutationRecord | { type: 'selection', target: Element }) {
|
2021-04-28 03:07:12 +08:00
|
|
|
|
if (!this.dom || !this.contentDOM) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-27 00:44:02 +08:00
|
|
|
|
if (typeof this.options.ignoreMutation === 'function') {
|
2021-07-27 18:26:24 +08:00
|
|
|
|
return this.options.ignoreMutation({ mutation })
|
2021-07-27 00:44:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-04-28 03:07:12 +08:00
|
|
|
|
// a leaf/atom node is like a black box for ProseMirror
|
|
|
|
|
// and should be fully handled by the node view
|
2021-06-14 21:56:24 +08:00
|
|
|
|
if (this.node.isLeaf || this.node.isAtom) {
|
2021-04-28 03:07:12 +08:00
|
|
|
|
return true
|
|
|
|
|
}
|
2021-03-17 04:55:40 +08:00
|
|
|
|
|
2021-04-28 03:07:12 +08:00
|
|
|
|
// ProseMirror should handle any selections
|
|
|
|
|
if (mutation.type === 'selection') {
|
2021-03-17 04:55:40 +08:00
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-28 03:07:12 +08:00
|
|
|
|
// try to prevent a bug on iOS that will break node views on enter
|
|
|
|
|
// this is because ProseMirror can’t preventDispatch on enter
|
|
|
|
|
// this will lead to a re-render of the node view on enter
|
|
|
|
|
// see: https://github.com/ueberdosis/tiptap/issues/1214
|
2021-11-18 19:38:02 +08:00
|
|
|
|
if (
|
|
|
|
|
this.dom.contains(mutation.target)
|
|
|
|
|
&& mutation.type === 'childList'
|
|
|
|
|
&& isiOS()
|
|
|
|
|
&& this.editor.isFocused
|
|
|
|
|
) {
|
2021-04-28 03:07:12 +08:00
|
|
|
|
const changedNodes = [
|
|
|
|
|
...Array.from(mutation.addedNodes),
|
|
|
|
|
...Array.from(mutation.removedNodes),
|
|
|
|
|
] as HTMLElement[]
|
|
|
|
|
|
|
|
|
|
// we’ll check if every changed node is contentEditable
|
|
|
|
|
// to make sure it’s probably mutated by ProseMirror
|
|
|
|
|
if (changedNodes.every(node => node.isContentEditable)) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// we will allow mutation contentDOM with attributes
|
|
|
|
|
// so we can for example adding classes within our node view
|
|
|
|
|
if (this.contentDOM === mutation.target && mutation.type === 'attributes') {
|
2021-03-17 04:55:40 +08:00
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-28 03:07:12 +08:00
|
|
|
|
// ProseMirror should handle any changes within contentDOM
|
|
|
|
|
if (this.contentDOM.contains(mutation.target)) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2021-03-17 04:55:40 +08:00
|
|
|
|
|
2021-04-28 03:07:12 +08:00
|
|
|
|
return true
|
2021-03-17 04:55:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateAttributes(attributes: {}) {
|
2021-06-14 21:35:43 +08:00
|
|
|
|
this.editor.commands.command(({ tr }) => {
|
|
|
|
|
const pos = this.getPos()
|
|
|
|
|
|
|
|
|
|
tr.setNodeMarkup(pos, undefined, {
|
|
|
|
|
...this.node.attrs,
|
|
|
|
|
...attributes,
|
|
|
|
|
})
|
2021-03-17 04:55:40 +08:00
|
|
|
|
|
2021-06-14 21:35:43 +08:00
|
|
|
|
return true
|
|
|
|
|
})
|
2021-03-17 04:55:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-19 06:01:49 +08:00
|
|
|
|
deleteNode(): void {
|
|
|
|
|
const from = this.getPos()
|
|
|
|
|
const to = from + this.node.nodeSize
|
|
|
|
|
|
|
|
|
|
this.editor.commands.deleteRange({ from, to })
|
|
|
|
|
}
|
2021-03-17 04:55:40 +08:00
|
|
|
|
}
|