UNPKG

@tiptap/vue-3

Version:

Vue components for tiptap

691 lines (680 loc) 20 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default")); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { Editor: () => Editor, EditorContent: () => EditorContent, MarkViewContent: () => MarkViewContent, NodeViewContent: () => NodeViewContent, NodeViewWrapper: () => NodeViewWrapper, VueMarkView: () => VueMarkView, VueMarkViewRenderer: () => VueMarkViewRenderer, VueNodeViewRenderer: () => VueNodeViewRenderer, VueRenderer: () => VueRenderer, markViewProps: () => markViewProps, nodeViewProps: () => nodeViewProps, useEditor: () => useEditor }); module.exports = __toCommonJS(index_exports); // src/Editor.ts var import_core = require("@tiptap/core"); var import_vue = require("vue"); function useDebouncedRef(value) { return (0, import_vue.customRef)((track, trigger) => { return { get() { track(); return value; }, set(newValue) { value = newValue; requestAnimationFrame(() => { requestAnimationFrame(() => { trigger(); }); }); } }; }); } var Editor = class extends import_core.Editor { 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 (0, import_vue.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 var import_vue2 = require("vue"); var EditorContent = (0, import_vue2.defineComponent)({ name: "EditorContent", props: { editor: { default: null, type: Object } }, setup(props) { const rootEl = (0, import_vue2.ref)(); const instance = (0, import_vue2.getCurrentInstance)(); (0, import_vue2.watchEffect)(() => { const editor = props.editor; if (editor && editor.options.element && rootEl.value) { (0, import_vue2.nextTick)(() => { var _a; if (!rootEl.value || !((_a = editor.view.dom) == null ? void 0 : _a.parentNode)) { return; } const element = (0, import_vue2.unref)(rootEl.value); rootEl.value.append(...editor.view.dom.parentNode.childNodes); 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(); }); } }); (0, import_vue2.onBeforeUnmount)(() => { const editor = props.editor; if (!editor) { return; } editor.contentComponent = null; editor.appContext = null; }); return { rootEl }; }, render() { return (0, import_vue2.h)("div", { ref: (el) => { this.rootEl = el; } }); } }); // src/NodeViewContent.ts var import_vue3 = require("vue"); var NodeViewContent = (0, import_vue3.defineComponent)({ name: "NodeViewContent", props: { as: { type: String, default: "div" } }, render() { return (0, import_vue3.h)(this.as, { style: { whiteSpace: "pre-wrap" }, "data-node-view-content": "" }); } }); // src/NodeViewWrapper.ts var import_vue4 = require("vue"); var NodeViewWrapper = (0, import_vue4.defineComponent)({ name: "NodeViewWrapper", props: { as: { type: String, default: "div" } }, inject: ["onDragStart", "decorationClasses"], render() { var _a, _b; return (0, import_vue4.h)( 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 var import_vue5 = require("vue"); var useEditor = (options = {}) => { const editor = (0, import_vue5.shallowRef)(); (0, import_vue5.onMounted)(() => { editor.value = new Editor(options); }); (0, import_vue5.onBeforeUnmount)(() => { var _a, _b, _c, _d; const nodes = (_b = (_a = editor.value) == null ? void 0 : _a.view.dom) == null ? void 0 : _b.parentNode; const newEl = nodes == null ? void 0 : nodes.cloneNode(true); (_c = nodes == null ? void 0 : nodes.parentNode) == null ? void 0 : _c.replaceChild(newEl, nodes); (_d = editor.value) == null ? void 0 : _d.destroy(); }); return editor; }; // src/VueMarkViewRenderer.ts var import_core2 = require("@tiptap/core"); var import_vue7 = require("vue"); // src/VueRenderer.ts var import_vue6 = require("vue"); var VueRenderer = class { constructor(component, { props = {}, editor }) { /** * Flag to track if the renderer has been destroyed, preventing queued or asynchronous renders from executing after teardown. */ this.destroyed = false; this.editor = editor; this.component = (0, import_vue6.markRaw)(component); this.el = document.createElement("div"); this.props = (0, import_vue6.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() { if (this.destroyed) { return this.renderedComponent; } let vNode = (0, import_vue6.h)(this.component, this.props); if (this.editor.appContext) { vNode.appContext = this.editor.appContext; } if (typeof document !== "undefined" && this.el) { (0, import_vue6.render)(vNode, this.el); } const destroy = () => { if (this.el) { (0, import_vue6.render)(null, this.el); } this.el = null; vNode = null; }; return { vNode, destroy, el: this.el ? this.el.firstElementChild : null }; } updateProps(props = {}) { if (this.destroyed) { return; } Object.entries(props).forEach(([key, value]) => { this.props[key] = value; }); this.renderComponent(); } destroy() { if (this.destroyed) { return; } this.destroyed = true; 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 = (0, import_vue7.defineComponent)({ name: "MarkViewContent", props: { as: { type: String, default: "span" } }, render() { return (0, import_vue7.h)(this.as, { style: { whiteSpace: "inherit" }, "data-mark-view-content": "" }); } }); var VueMarkView = class extends import_core2.MarkView { constructor(component, props, options) { super(component, props, options); const componentProps = { ...props, updateAttributes: this.updateAttributes.bind(this) }; const extendedComponent = (0, import_vue7.defineComponent)({ 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 = (0, import_vue7.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 var import_core3 = require("@tiptap/core"); var import_vue8 = require("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 import_core3.NodeView { constructor() { super(...arguments); /** * Callback registered with the per-editor position-update registry. * Stored so it can be unregistered in destroy(). */ this.positionCheckCallback = null; this.cachedExtensionWithSyncedStorage = null; } /** * Returns a proxy of the extension that redirects storage access to the editor's mutable storage. * This preserves the original prototype chain (instanceof checks, methods like configure/extend work). * Cached to avoid proxy creation on every update. */ get extensionWithSyncedStorage() { if (!this.cachedExtensionWithSyncedStorage) { const editor = this.editor; const extension = this.extension; this.cachedExtensionWithSyncedStorage = new Proxy(extension, { get(target, prop, receiver) { var _a; if (prop === "storage") { return (_a = editor.storage[extension.name]) != null ? _a : {}; } return Reflect.get(target, prop, receiver); } }); } return this.cachedExtensionWithSyncedStorage; } mount() { const props = { editor: this.editor, node: this.node, decorations: this.decorations, innerDecorations: this.innerDecorations, view: this.view, selected: false, extension: this.extensionWithSyncedStorage, HTMLAttributes: this.HTMLAttributes, getPos: () => this.getPos(), updateAttributes: (attributes = {}) => this.updateAttributes(attributes), deleteNode: () => this.deleteNode() }; const onDragStart = this.onDragStart.bind(this); this.decorationClasses = (0, import_vue8.ref)(this.getDecorationClasses()); const extendedComponent = (0, import_vue8.defineComponent)({ extends: { ...this.component }, props: Object.keys(props), template: this.component.template, setup: (reactiveProps) => { var _a, _b; (0, import_vue8.provide)("onDragStart", onDragStart); (0, import_vue8.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.currentPos = this.getPos(); this.positionCheckCallback = () => { if (!this.renderer) { return; } const newPos = this.getPos(); if (typeof newPos !== "number" || newPos === this.currentPos) { return; } this.currentPos = newPos; this.renderer.updateProps({ getPos: () => this.getPos() }); }; (0, import_core3.schedulePositionCheck)(this.editor, this.positionCheckCallback); 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; this.currentPos = this.getPos(); return this.options.update({ oldNode, oldDecorations, newNode: node, newDecorations: decorations, oldInnerDecorations, innerDecorations, updateProps: () => rerenderComponent({ node, decorations, innerDecorations, extension: this.extensionWithSyncedStorage }) }); } if (node.type !== this.node.type) { return false; } const newPos = this.getPos(); if (node === this.node && this.decorations === decorations && this.innerDecorations === innerDecorations) { if (newPos === this.currentPos) { return true; } this.currentPos = newPos; rerenderComponent({ node, decorations, innerDecorations, extension: this.extensionWithSyncedStorage, getPos: () => this.getPos() }); return true; } this.node = node; this.decorations = decorations; this.innerDecorations = innerDecorations; this.currentPos = newPos; rerenderComponent({ node, decorations, innerDecorations, extension: this.extensionWithSyncedStorage }); 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); if (this.positionCheckCallback) { (0, import_core3.cancelPositionCheck)(this.editor, this.positionCheckCallback); this.positionCheckCallback = null; } } }; 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 __reExport(index_exports, require("@tiptap/core"), module.exports); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Editor, EditorContent, MarkViewContent, NodeViewContent, NodeViewWrapper, VueMarkView, VueMarkViewRenderer, VueNodeViewRenderer, VueRenderer, markViewProps, nodeViewProps, useEditor, ...require("@tiptap/core") }); //# sourceMappingURL=index.cjs.map