UNPKG

@tiptap/vue-3

Version:

Vue components for tiptap

576 lines (567 loc) 15.5 kB
// src/Editor.ts import { Editor as CoreEditor } from "@tiptap/core"; import { customRef, markRaw } from "vue"; function useDebouncedRef(value) { return customRef((track, trigger) => { return { get() { track(); return value; }, set(newValue) { value = newValue; requestAnimationFrame(() => { requestAnimationFrame(() => { trigger(); }); }); } }; }); } var Editor = class extends CoreEditor { constructor(options = {}) { super(options); this.contentComponent = null; this.appContext = null; this.reactiveState = useDebouncedRef(this.view.state); this.reactiveExtensionStorage = useDebouncedRef(this.extensionStorage); this.on("beforeTransaction", ({ nextState }) => { this.reactiveState.value = nextState; this.reactiveExtensionStorage.value = this.extensionStorage; }); return markRaw(this); } get state() { return this.reactiveState ? this.reactiveState.value : this.view.state; } get storage() { return this.reactiveExtensionStorage ? this.reactiveExtensionStorage.value : super.storage; } /** * Register a ProseMirror plugin. */ registerPlugin(plugin, handlePlugins) { const nextState = super.registerPlugin(plugin, handlePlugins); if (this.reactiveState) { this.reactiveState.value = nextState; } return nextState; } /** * Unregister a ProseMirror plugin. */ unregisterPlugin(nameOrPluginKey) { const nextState = super.unregisterPlugin(nameOrPluginKey); if (this.reactiveState && nextState) { this.reactiveState.value = nextState; } return nextState; } }; // src/EditorContent.ts import { defineComponent, getCurrentInstance, h, nextTick, onBeforeUnmount, ref, unref, watchEffect } from "vue"; var EditorContent = defineComponent({ name: "EditorContent", props: { editor: { default: null, type: Object } }, setup(props) { const rootEl = ref(); const instance = getCurrentInstance(); watchEffect(() => { const editor = props.editor; if (editor && editor.options.element && rootEl.value) { nextTick(() => { var _a; if (!rootEl.value || !((_a = editor.view.dom) == null ? void 0 : _a.firstChild)) { return; } const element = unref(rootEl.value); rootEl.value.append(editor.view.dom); editor.contentComponent = instance.ctx._; if (instance) { editor.appContext = { ...instance.appContext, // Vue internally uses prototype chain to forward/shadow injects across the entire component chain // so don't use object spread operator or 'Object.assign' and just set `provides` as is on editor's appContext // @ts-expect-error forward instance's 'provides' into appContext provides: instance.provides }; } editor.setOptions({ element }); editor.createNodeViews(); }); } }); onBeforeUnmount(() => { const editor = props.editor; if (!editor) { return; } editor.contentComponent = null; editor.appContext = null; }); return { rootEl }; }, render() { return h("div", { ref: (el) => { this.rootEl = el; } }); } }); // src/NodeViewContent.ts import { defineComponent as defineComponent2, h as h2 } from "vue"; var NodeViewContent = defineComponent2({ name: "NodeViewContent", props: { as: { type: String, default: "div" } }, render() { return h2(this.as, { style: { whiteSpace: "pre-wrap" }, "data-node-view-content": "" }); } }); // src/NodeViewWrapper.ts import { defineComponent as defineComponent3, h as h3 } from "vue"; var NodeViewWrapper = defineComponent3({ name: "NodeViewWrapper", props: { as: { type: String, default: "div" } }, inject: ["onDragStart", "decorationClasses"], render() { var _a, _b; return h3( this.as, { // @ts-ignore class: this.decorationClasses, style: { whiteSpace: "normal" }, "data-node-view-wrapper": "", // @ts-ignore (https://github.com/vuejs/vue-next/issues/3031) onDragstart: this.onDragStart }, (_b = (_a = this.$slots).default) == null ? void 0 : _b.call(_a) ); } }); // src/useEditor.ts import { onBeforeUnmount as onBeforeUnmount2, onMounted, shallowRef } from "vue"; var useEditor = (options = {}) => { const editor = shallowRef(); onMounted(() => { editor.value = new Editor(options); }); onBeforeUnmount2(() => { var _a, _b, _c; const nodes = (_a = editor.value) == null ? void 0 : _a.view.dom; const newEl = nodes == null ? void 0 : nodes.cloneNode(true); (_b = nodes == null ? void 0 : nodes.parentNode) == null ? void 0 : _b.replaceChild(newEl, nodes); (_c = editor.value) == null ? void 0 : _c.destroy(); }); return editor; }; // src/VueMarkViewRenderer.ts import { MarkView } from "@tiptap/core"; import { defineComponent as defineComponent4, h as h5, toRaw } from "vue"; // src/VueRenderer.ts import { h as h4, markRaw as markRaw2, reactive, render } from "vue"; var VueRenderer = class { constructor(component, { props = {}, editor }) { this.editor = editor; this.component = markRaw2(component); this.el = document.createElement("div"); this.props = reactive(props); this.renderedComponent = this.renderComponent(); } get element() { return this.renderedComponent.el; } get ref() { var _a, _b, _c, _d; if ((_b = (_a = this.renderedComponent.vNode) == null ? void 0 : _a.component) == null ? void 0 : _b.exposed) { return this.renderedComponent.vNode.component.exposed; } return (_d = (_c = this.renderedComponent.vNode) == null ? void 0 : _c.component) == null ? void 0 : _d.proxy; } renderComponent() { let vNode = h4(this.component, this.props); if (this.editor.appContext) { vNode.appContext = this.editor.appContext; } if (typeof document !== "undefined" && this.el) { render(vNode, this.el); } const destroy = () => { if (this.el) { render(null, this.el); } this.el = null; vNode = null; }; return { vNode, destroy, el: this.el ? this.el.firstElementChild : null }; } updateProps(props = {}) { Object.entries(props).forEach(([key, value]) => { this.props[key] = value; }); this.renderComponent(); } destroy() { this.renderedComponent.destroy(); } }; // src/VueMarkViewRenderer.ts var markViewProps = { editor: { type: Object, required: true }, mark: { type: Object, required: true }, extension: { type: Object, required: true }, inline: { type: Boolean, required: true }, view: { type: Object, required: true }, updateAttributes: { type: Function, required: true }, HTMLAttributes: { type: Object, required: true } }; var MarkViewContent = defineComponent4({ name: "MarkViewContent", props: { as: { type: String, default: "span" } }, render() { return h5(this.as, { style: { whiteSpace: "inherit" }, "data-mark-view-content": "" }); } }); var VueMarkView = class extends MarkView { constructor(component, props, options) { super(component, props, options); const componentProps = { ...props, updateAttributes: this.updateAttributes.bind(this) }; const extendedComponent = defineComponent4({ extends: { ...component }, props: Object.keys(componentProps), template: this.component.template, setup: (reactiveProps) => { var _a; return (_a = component.setup) == null ? void 0 : _a.call(component, reactiveProps, { expose: () => void 0 }); }, // Add support for scoped styles __scopeId: component.__scopeId, __cssModules: component.__cssModules, __name: component.__name, __file: component.__file }); this.renderer = new VueRenderer(extendedComponent, { editor: this.editor, props: componentProps }); } get dom() { return this.renderer.element; } get contentDOM() { return this.dom.querySelector("[data-mark-view-content]"); } updateAttributes(attrs) { const unproxiedMark = toRaw(this.mark); super.updateAttributes(attrs, unproxiedMark); } destroy() { this.renderer.destroy(); } }; function VueMarkViewRenderer(component, options = {}) { return (props) => { if (!props.editor.contentComponent) { return {}; } return new VueMarkView(component, props, options); }; } // src/VueNodeViewRenderer.ts import { NodeView } from "@tiptap/core"; import { defineComponent as defineComponent5, provide, ref as ref2 } from "vue"; var nodeViewProps = { editor: { type: Object, required: true }, node: { type: Object, required: true }, decorations: { type: Object, required: true }, selected: { type: Boolean, required: true }, extension: { type: Object, required: true }, getPos: { type: Function, required: true }, updateAttributes: { type: Function, required: true }, deleteNode: { type: Function, required: true }, view: { type: Object, required: true }, innerDecorations: { type: Object, required: true }, HTMLAttributes: { type: Object, required: true } }; var VueNodeView = class extends NodeView { mount() { const props = { 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() }; const onDragStart = this.onDragStart.bind(this); this.decorationClasses = ref2(this.getDecorationClasses()); const extendedComponent = defineComponent5({ extends: { ...this.component }, props: Object.keys(props), template: this.component.template, setup: (reactiveProps) => { var _a, _b; provide("onDragStart", onDragStart); provide("decorationClasses", this.decorationClasses); return (_b = (_a = this.component).setup) == null ? void 0 : _b.call(_a, reactiveProps, { expose: () => void 0 }); }, // add support for scoped styles // @ts-ignore // eslint-disable-next-line __scopeId: this.component.__scopeId, // add support for CSS Modules // @ts-ignore // eslint-disable-next-line __cssModules: this.component.__cssModules, // add support for vue devtools // @ts-ignore // eslint-disable-next-line __name: this.component.__name, // @ts-ignore // eslint-disable-next-line __file: this.component.__file }); this.handleSelectionUpdate = this.handleSelectionUpdate.bind(this); this.editor.on("selectionUpdate", this.handleSelectionUpdate); this.renderer = new VueRenderer(extendedComponent, { editor: this.editor, props }); } /** * Return the DOM element. * This is the element that will be used to display the node view. */ get dom() { if (!this.renderer.element || !this.renderer.element.hasAttribute("data-node-view-wrapper")) { throw Error("Please use the NodeViewWrapper component for your node view."); } return this.renderer.element; } /** * Return the content DOM element. * This is the element that will be used to display the rich-text content of the node. */ get contentDOM() { if (this.node.isLeaf) { return null; } return this.dom.querySelector("[data-node-view-content]"); } /** * 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, decorations, innerDecorations) { const rerenderComponent = (props) => { this.decorationClasses.value = this.getDecorationClasses(); this.renderer.updateProps(props); }; 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, newNode: node, newDecorations: decorations, oldInnerDecorations, innerDecorations, updateProps: () => rerenderComponent({ node, 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; rerenderComponent({ node, decorations, innerDecorations }); return true; } /** * Select the node. * Add the `selected` prop and the `ProseMirror-selectednode` class. */ selectNode() { this.renderer.updateProps({ selected: true }); if (this.renderer.element) { this.renderer.element.classList.add("ProseMirror-selectednode"); } } /** * Deselect the node. * Remove the `selected` prop and the `ProseMirror-selectednode` class. */ deselectNode() { this.renderer.updateProps({ selected: false }); if (this.renderer.element) { this.renderer.element.classList.remove("ProseMirror-selectednode"); } } getDecorationClasses() { return this.decorations.flatMap((item) => item.type.attrs.class).join(" "); } destroy() { this.renderer.destroy(); this.editor.off("selectionUpdate", this.handleSelectionUpdate); } }; function VueNodeViewRenderer(component, options) { return (props) => { if (!props.editor.contentComponent) { return {}; } const normalizedComponent = typeof component === "function" && "__vccOpts" in component ? component.__vccOpts : component; return new VueNodeView(normalizedComponent, props, options); }; } // src/index.ts export * from "@tiptap/core"; export { Editor, EditorContent, MarkViewContent, NodeViewContent, NodeViewWrapper, VueMarkView, VueMarkViewRenderer, VueNodeViewRenderer, VueRenderer, markViewProps, nodeViewProps, useEditor }; //# sourceMappingURL=index.js.map