svelte-tiptap
Version:
Svelte components for tiptap v2
162 lines (161 loc) • 6.29 kB
JavaScript
import { NodeView, Editor, getRenderedAttributes } from '@tiptap/core';
import { getAllContexts, mount } from 'svelte';
import SvelteRenderer from './SvelteRenderer';
import { TIPTAP_NODE_VIEW } from './context';
import { invariant } from './utils';
import { SvelteMap } from 'svelte/reactivity';
class SvelteNodeView extends NodeView {
mount() {
const Component = this.component;
const props = $state({
editor: this.editor,
node: this.node,
decorations: this.decorations,
innerDecorations: this.innerDecorations,
view: this.view,
selected: false,
extension: this.extension,
HTMLAttributes: this.HTMLAttributes,
getPos: () => this.getPos(),
updateAttributes: (attributes = {}) => this.updateAttributes(attributes),
deleteNode: () => this.deleteNode(),
});
this.contentDOMElement = this.node.isLeaf ? null : document.createElement(this.node.isInline ? 'span' : 'div');
if (this.contentDOMElement) {
// For some reason the whiteSpace prop is not inherited properly in Chrome and Safari
// With this fix it seems to work fine
// See: https://github.com/ueberdosis/tiptap/issues/1197
this.contentDOMElement.style.whiteSpace = 'inherit';
}
const context = this.options.context || new SvelteMap();
context.set(TIPTAP_NODE_VIEW, {
onDragStart: this.onDragStart.bind(this),
});
const as = this.options.as ?? (this.node.isInline ? 'span' : 'div');
const target = document.createElement(as);
target.classList.add(`node-${this.node.type.name}`);
this.handleSelectionUpdate = this.handleSelectionUpdate.bind(this);
this.editor.on('selectionUpdate', this.handleSelectionUpdate);
const svelteComponent = mount(Component, { target, props, context });
this.renderer = new SvelteRenderer(svelteComponent, {
element: target,
props,
});
this.appendContendDom();
this.updateElementAttributes();
}
appendContendDom() {
const contentElement = this.dom.querySelector('[data-node-view-content]');
if (this.contentDOMElement && contentElement && !contentElement.contains(this.contentDOMElement)) {
contentElement.appendChild(this.contentDOMElement);
}
}
get dom() {
invariant(this.renderer.dom.firstElementChild?.hasAttribute('data-node-view-wrapper'), 'Please use the NodeViewWrapper component for your node view.');
return this.renderer.dom;
}
get contentDOM() {
if (this.node.isLeaf) {
return null;
}
return this.contentDOMElement;
}
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();
}
}
update(node, decorations, innerDecorations) {
const updateProps = (props) => {
this.renderer.updateProps(props);
if (typeof this.options.attrs === 'function') {
this.updateElementAttributes();
}
};
if (typeof this.options.update === 'function') {
const oldNode = this.node;
const oldDecorations = this.decorations;
const oldInnerDecorations = this.innerDecorations;
this.node = node;
this.decorations = decorations;
this.innerDecorations = innerDecorations;
return this.options.update({
oldNode,
oldDecorations,
oldInnerDecorations,
newNode: node,
newDecorations: decorations,
newInnerDecorations: innerDecorations,
updateProps: () => updateProps({
node,
decorations: decorations,
innerDecorations,
}),
});
}
if (node.type !== this.node.type) {
return false;
}
if (node === this.node && this.decorations === decorations && this.innerDecorations === innerDecorations) {
return true;
}
this.node = node;
this.decorations = decorations;
this.innerDecorations = innerDecorations;
updateProps({
node,
decorations: decorations,
innerDecorations,
});
return true;
}
selectNode() {
this.renderer.updateProps({ selected: true });
this.renderer.dom.classList.add('ProseMirror-selectednode');
}
deselectNode() {
this.renderer.updateProps({ selected: false });
this.renderer.dom.classList.remove('ProseMirror-selectednode');
}
destroy() {
this.renderer.destroy();
this.editor.off('selectionUpdate', this.handleSelectionUpdate);
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() {
if (this.options.attrs) {
let attrsObj = {};
if (typeof this.options.attrs === 'function') {
const extensionAttributes = this.editor.extensionManager.attributes;
const HTMLAttributes = getRenderedAttributes(this.node, extensionAttributes);
attrsObj = this.options.attrs({ node: this.node, HTMLAttributes });
}
else {
attrsObj = this.options.attrs;
}
this.renderer.updateAttributes(attrsObj);
}
}
}
const SvelteNodeViewRenderer = (component, options) => {
return (props) => new SvelteNodeView(component, props, options);
};
export default SvelteNodeViewRenderer;