615 lines
17 KiB
JavaScript
615 lines
17 KiB
JavaScript
// 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.parentNode)) {
|
|
return;
|
|
}
|
|
const element = 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();
|
|
});
|
|
}
|
|
});
|
|
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, _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
|
|
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 }) {
|
|
/**
|
|
* 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 = 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() {
|
|
if (this.destroyed) {
|
|
return this.renderedComponent;
|
|
}
|
|
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 = {}) {
|
|
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 = 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 {
|
|
constructor() {
|
|
super(...arguments);
|
|
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 = 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, extension: this.extensionWithSyncedStorage })
|
|
});
|
|
}
|
|
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, 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);
|
|
}
|
|
};
|
|
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
|