@tiptap/vue-3
Version:
Vue components for tiptap
616 lines (605 loc) • 17.4 kB
JavaScript
"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.firstChild)) {
return;
}
const element = (0, import_vue2.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();
});
}
});
(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;
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
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 }) {
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() {
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 = {}) {
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 = (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 {
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 = (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.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
__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