1131 lines
35 KiB
JavaScript
1131 lines
35 KiB
JavaScript
// src/Context.tsx
|
|
import { createContext, useContext, useMemo } from "react";
|
|
|
|
// src/EditorContent.tsx
|
|
import React, { forwardRef } from "react";
|
|
import ReactDOM from "react-dom";
|
|
import { useSyncExternalStore } from "use-sync-external-store/shim/index.js";
|
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
var mergeRefs = (...refs) => {
|
|
return (node) => {
|
|
refs.forEach((ref) => {
|
|
if (typeof ref === "function") {
|
|
ref(node);
|
|
} else if (ref) {
|
|
;
|
|
ref.current = node;
|
|
}
|
|
});
|
|
};
|
|
};
|
|
var Portals = ({ contentComponent }) => {
|
|
const renderers = useSyncExternalStore(
|
|
contentComponent.subscribe,
|
|
contentComponent.getSnapshot,
|
|
contentComponent.getServerSnapshot
|
|
);
|
|
return /* @__PURE__ */ jsx(Fragment, { children: Object.values(renderers) });
|
|
};
|
|
function getInstance() {
|
|
const subscribers = /* @__PURE__ */ new Set();
|
|
let renderers = {};
|
|
return {
|
|
/**
|
|
* Subscribe to the editor instance's changes.
|
|
*/
|
|
subscribe(callback) {
|
|
subscribers.add(callback);
|
|
return () => {
|
|
subscribers.delete(callback);
|
|
};
|
|
},
|
|
getSnapshot() {
|
|
return renderers;
|
|
},
|
|
getServerSnapshot() {
|
|
return renderers;
|
|
},
|
|
/**
|
|
* Adds a new NodeView Renderer to the editor.
|
|
*/
|
|
setRenderer(id, renderer) {
|
|
renderers = {
|
|
...renderers,
|
|
[id]: ReactDOM.createPortal(renderer.reactElement, renderer.element, id)
|
|
};
|
|
subscribers.forEach((subscriber) => subscriber());
|
|
},
|
|
/**
|
|
* Removes a NodeView Renderer from the editor.
|
|
*/
|
|
removeRenderer(id) {
|
|
const nextRenderers = { ...renderers };
|
|
delete nextRenderers[id];
|
|
renderers = nextRenderers;
|
|
subscribers.forEach((subscriber) => subscriber());
|
|
}
|
|
};
|
|
}
|
|
var PureEditorContent = class extends React.Component {
|
|
constructor(props) {
|
|
var _a;
|
|
super(props);
|
|
this.editorContentRef = React.createRef();
|
|
this.initialized = false;
|
|
this.state = {
|
|
hasContentComponentInitialized: Boolean((_a = props.editor) == null ? void 0 : _a.contentComponent)
|
|
};
|
|
}
|
|
componentDidMount() {
|
|
this.init();
|
|
}
|
|
componentDidUpdate() {
|
|
this.init();
|
|
}
|
|
init() {
|
|
var _a;
|
|
const editor = this.props.editor;
|
|
if (editor && !editor.isDestroyed && ((_a = editor.view.dom) == null ? void 0 : _a.parentNode)) {
|
|
if (editor.contentComponent) {
|
|
return;
|
|
}
|
|
const element = this.editorContentRef.current;
|
|
element.append(...editor.view.dom.parentNode.childNodes);
|
|
editor.setOptions({
|
|
element
|
|
});
|
|
editor.contentComponent = getInstance();
|
|
if (!this.state.hasContentComponentInitialized) {
|
|
this.unsubscribeToContentComponent = editor.contentComponent.subscribe(() => {
|
|
this.setState((prevState) => {
|
|
if (!prevState.hasContentComponentInitialized) {
|
|
return {
|
|
hasContentComponentInitialized: true
|
|
};
|
|
}
|
|
return prevState;
|
|
});
|
|
if (this.unsubscribeToContentComponent) {
|
|
this.unsubscribeToContentComponent();
|
|
}
|
|
});
|
|
}
|
|
editor.createNodeViews();
|
|
this.initialized = true;
|
|
}
|
|
}
|
|
componentWillUnmount() {
|
|
var _a;
|
|
const editor = this.props.editor;
|
|
if (!editor) {
|
|
return;
|
|
}
|
|
this.initialized = false;
|
|
if (!editor.isDestroyed) {
|
|
editor.view.setProps({
|
|
nodeViews: {}
|
|
});
|
|
}
|
|
if (this.unsubscribeToContentComponent) {
|
|
this.unsubscribeToContentComponent();
|
|
}
|
|
editor.contentComponent = null;
|
|
try {
|
|
if (!((_a = editor.view.dom) == null ? void 0 : _a.parentNode)) {
|
|
return;
|
|
}
|
|
const newElement = document.createElement("div");
|
|
newElement.append(...editor.view.dom.parentNode.childNodes);
|
|
editor.setOptions({
|
|
element: newElement
|
|
});
|
|
} catch {
|
|
}
|
|
}
|
|
render() {
|
|
const { editor, innerRef, ...rest } = this.props;
|
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
/* @__PURE__ */ jsx("div", { ref: mergeRefs(innerRef, this.editorContentRef), ...rest }),
|
|
(editor == null ? void 0 : editor.contentComponent) && /* @__PURE__ */ jsx(Portals, { contentComponent: editor.contentComponent })
|
|
] });
|
|
}
|
|
};
|
|
var EditorContentWithKey = forwardRef(
|
|
(props, ref) => {
|
|
const key = React.useMemo(() => {
|
|
return Math.floor(Math.random() * 4294967295).toString();
|
|
}, [props.editor]);
|
|
return React.createElement(PureEditorContent, {
|
|
key,
|
|
innerRef: ref,
|
|
...props
|
|
});
|
|
}
|
|
);
|
|
var EditorContent = React.memo(EditorContentWithKey);
|
|
|
|
// src/useEditor.ts
|
|
import { Editor } from "@tiptap/core";
|
|
import { useDebugValue as useDebugValue2, useEffect as useEffect2, useRef, useState as useState2 } from "react";
|
|
import { useSyncExternalStore as useSyncExternalStore2 } from "use-sync-external-store/shim/index.js";
|
|
|
|
// src/useEditorState.ts
|
|
import { deepEqual } from "fast-equals";
|
|
import { useDebugValue, useEffect, useLayoutEffect, useState } from "react";
|
|
import { useSyncExternalStoreWithSelector } from "use-sync-external-store/shim/with-selector.js";
|
|
var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
|
|
var EditorStateManager = class {
|
|
constructor(initialEditor) {
|
|
this.transactionNumber = 0;
|
|
this.lastTransactionNumber = 0;
|
|
this.subscribers = /* @__PURE__ */ new Set();
|
|
this.editor = initialEditor;
|
|
this.lastSnapshot = { editor: initialEditor, transactionNumber: 0 };
|
|
this.getSnapshot = this.getSnapshot.bind(this);
|
|
this.getServerSnapshot = this.getServerSnapshot.bind(this);
|
|
this.watch = this.watch.bind(this);
|
|
this.subscribe = this.subscribe.bind(this);
|
|
}
|
|
/**
|
|
* Get the current editor instance.
|
|
*/
|
|
getSnapshot() {
|
|
if (this.transactionNumber === this.lastTransactionNumber) {
|
|
return this.lastSnapshot;
|
|
}
|
|
this.lastTransactionNumber = this.transactionNumber;
|
|
this.lastSnapshot = { editor: this.editor, transactionNumber: this.transactionNumber };
|
|
return this.lastSnapshot;
|
|
}
|
|
/**
|
|
* Always disable the editor on the server-side.
|
|
*/
|
|
getServerSnapshot() {
|
|
return { editor: null, transactionNumber: 0 };
|
|
}
|
|
/**
|
|
* Subscribe to the editor instance's changes.
|
|
*/
|
|
subscribe(callback) {
|
|
this.subscribers.add(callback);
|
|
return () => {
|
|
this.subscribers.delete(callback);
|
|
};
|
|
}
|
|
/**
|
|
* Watch the editor instance for changes.
|
|
*/
|
|
watch(nextEditor) {
|
|
this.editor = nextEditor;
|
|
if (this.editor) {
|
|
const fn = () => {
|
|
this.transactionNumber += 1;
|
|
this.subscribers.forEach((callback) => callback());
|
|
};
|
|
const currentEditor = this.editor;
|
|
currentEditor.on("transaction", fn);
|
|
return () => {
|
|
currentEditor.off("transaction", fn);
|
|
};
|
|
}
|
|
return void 0;
|
|
}
|
|
};
|
|
function useEditorState(options) {
|
|
var _a;
|
|
const [editorStateManager] = useState(() => new EditorStateManager(options.editor));
|
|
const selectedState = useSyncExternalStoreWithSelector(
|
|
editorStateManager.subscribe,
|
|
editorStateManager.getSnapshot,
|
|
editorStateManager.getServerSnapshot,
|
|
options.selector,
|
|
(_a = options.equalityFn) != null ? _a : deepEqual
|
|
);
|
|
useIsomorphicLayoutEffect(() => {
|
|
return editorStateManager.watch(options.editor);
|
|
}, [options.editor, editorStateManager]);
|
|
useDebugValue(selectedState);
|
|
return selectedState;
|
|
}
|
|
|
|
// src/useEditor.ts
|
|
var isDev = process.env.NODE_ENV !== "production";
|
|
var isSSR = typeof window === "undefined";
|
|
var isNext = isSSR || Boolean(typeof window !== "undefined" && window.next);
|
|
var EditorInstanceManager = class _EditorInstanceManager {
|
|
constructor(options) {
|
|
/**
|
|
* The current editor instance.
|
|
*/
|
|
this.editor = null;
|
|
/**
|
|
* The subscriptions to notify when the editor instance
|
|
* has been created or destroyed.
|
|
*/
|
|
this.subscriptions = /* @__PURE__ */ new Set();
|
|
/**
|
|
* Whether the editor has been mounted.
|
|
*/
|
|
this.isComponentMounted = false;
|
|
/**
|
|
* The most recent dependencies array.
|
|
*/
|
|
this.previousDeps = null;
|
|
/**
|
|
* The unique instance ID. This is used to identify the editor instance. And will be re-generated for each new instance.
|
|
*/
|
|
this.instanceId = "";
|
|
this.options = options;
|
|
this.subscriptions = /* @__PURE__ */ new Set();
|
|
this.setEditor(this.getInitialEditor());
|
|
this.scheduleDestroy();
|
|
this.getEditor = this.getEditor.bind(this);
|
|
this.getServerSnapshot = this.getServerSnapshot.bind(this);
|
|
this.subscribe = this.subscribe.bind(this);
|
|
this.refreshEditorInstance = this.refreshEditorInstance.bind(this);
|
|
this.scheduleDestroy = this.scheduleDestroy.bind(this);
|
|
this.onRender = this.onRender.bind(this);
|
|
this.createEditor = this.createEditor.bind(this);
|
|
}
|
|
setEditor(editor) {
|
|
this.editor = editor;
|
|
this.instanceId = Math.random().toString(36).slice(2, 9);
|
|
this.subscriptions.forEach((cb) => cb());
|
|
}
|
|
getInitialEditor() {
|
|
if (this.options.current.immediatelyRender === void 0) {
|
|
if (isSSR || isNext) {
|
|
if (isDev) {
|
|
throw new Error(
|
|
"Tiptap Error: SSR has been detected, please set `immediatelyRender` explicitly to `false` to avoid hydration mismatches."
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
return this.createEditor();
|
|
}
|
|
if (this.options.current.immediatelyRender && isSSR && isDev) {
|
|
throw new Error(
|
|
"Tiptap Error: SSR has been detected, and `immediatelyRender` has been set to `true` this is an unsupported configuration that may result in errors, explicitly set `immediatelyRender` to `false` to avoid hydration mismatches."
|
|
);
|
|
}
|
|
if (this.options.current.immediatelyRender) {
|
|
return this.createEditor();
|
|
}
|
|
return null;
|
|
}
|
|
/**
|
|
* Create a new editor instance. And attach event listeners.
|
|
*/
|
|
createEditor() {
|
|
const optionsToApply = {
|
|
...this.options.current,
|
|
// Always call the most recent version of the callback function by default
|
|
onBeforeCreate: (...args) => {
|
|
var _a, _b;
|
|
return (_b = (_a = this.options.current).onBeforeCreate) == null ? void 0 : _b.call(_a, ...args);
|
|
},
|
|
onBlur: (...args) => {
|
|
var _a, _b;
|
|
return (_b = (_a = this.options.current).onBlur) == null ? void 0 : _b.call(_a, ...args);
|
|
},
|
|
onCreate: (...args) => {
|
|
var _a, _b;
|
|
return (_b = (_a = this.options.current).onCreate) == null ? void 0 : _b.call(_a, ...args);
|
|
},
|
|
onDestroy: (...args) => {
|
|
var _a, _b;
|
|
return (_b = (_a = this.options.current).onDestroy) == null ? void 0 : _b.call(_a, ...args);
|
|
},
|
|
onFocus: (...args) => {
|
|
var _a, _b;
|
|
return (_b = (_a = this.options.current).onFocus) == null ? void 0 : _b.call(_a, ...args);
|
|
},
|
|
onSelectionUpdate: (...args) => {
|
|
var _a, _b;
|
|
return (_b = (_a = this.options.current).onSelectionUpdate) == null ? void 0 : _b.call(_a, ...args);
|
|
},
|
|
onTransaction: (...args) => {
|
|
var _a, _b;
|
|
return (_b = (_a = this.options.current).onTransaction) == null ? void 0 : _b.call(_a, ...args);
|
|
},
|
|
onUpdate: (...args) => {
|
|
var _a, _b;
|
|
return (_b = (_a = this.options.current).onUpdate) == null ? void 0 : _b.call(_a, ...args);
|
|
},
|
|
onContentError: (...args) => {
|
|
var _a, _b;
|
|
return (_b = (_a = this.options.current).onContentError) == null ? void 0 : _b.call(_a, ...args);
|
|
},
|
|
onDrop: (...args) => {
|
|
var _a, _b;
|
|
return (_b = (_a = this.options.current).onDrop) == null ? void 0 : _b.call(_a, ...args);
|
|
},
|
|
onPaste: (...args) => {
|
|
var _a, _b;
|
|
return (_b = (_a = this.options.current).onPaste) == null ? void 0 : _b.call(_a, ...args);
|
|
},
|
|
onDelete: (...args) => {
|
|
var _a, _b;
|
|
return (_b = (_a = this.options.current).onDelete) == null ? void 0 : _b.call(_a, ...args);
|
|
}
|
|
};
|
|
const editor = new Editor(optionsToApply);
|
|
return editor;
|
|
}
|
|
/**
|
|
* Get the current editor instance.
|
|
*/
|
|
getEditor() {
|
|
return this.editor;
|
|
}
|
|
/**
|
|
* Always disable the editor on the server-side.
|
|
*/
|
|
getServerSnapshot() {
|
|
return null;
|
|
}
|
|
/**
|
|
* Subscribe to the editor instance's changes.
|
|
*/
|
|
subscribe(onStoreChange) {
|
|
this.subscriptions.add(onStoreChange);
|
|
return () => {
|
|
this.subscriptions.delete(onStoreChange);
|
|
};
|
|
}
|
|
static compareOptions(a, b) {
|
|
return Object.keys(a).every((key) => {
|
|
if ([
|
|
"onCreate",
|
|
"onBeforeCreate",
|
|
"onDestroy",
|
|
"onUpdate",
|
|
"onTransaction",
|
|
"onFocus",
|
|
"onBlur",
|
|
"onSelectionUpdate",
|
|
"onContentError",
|
|
"onDrop",
|
|
"onPaste"
|
|
].includes(key)) {
|
|
return true;
|
|
}
|
|
if (key === "extensions" && a.extensions && b.extensions) {
|
|
if (a.extensions.length !== b.extensions.length) {
|
|
return false;
|
|
}
|
|
return a.extensions.every((extension, index) => {
|
|
var _a;
|
|
if (extension !== ((_a = b.extensions) == null ? void 0 : _a[index])) {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
if (a[key] !== b[key]) {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
/**
|
|
* On each render, we will create, update, or destroy the editor instance.
|
|
* @param deps The dependencies to watch for changes
|
|
* @returns A cleanup function
|
|
*/
|
|
onRender(deps) {
|
|
return () => {
|
|
this.isComponentMounted = true;
|
|
clearTimeout(this.scheduledDestructionTimeout);
|
|
if (this.editor && !this.editor.isDestroyed && deps.length === 0) {
|
|
if (!_EditorInstanceManager.compareOptions(this.options.current, this.editor.options)) {
|
|
this.editor.setOptions({
|
|
...this.options.current,
|
|
editable: this.editor.isEditable
|
|
});
|
|
}
|
|
} else {
|
|
this.refreshEditorInstance(deps);
|
|
}
|
|
return () => {
|
|
this.isComponentMounted = false;
|
|
this.scheduleDestroy();
|
|
};
|
|
};
|
|
}
|
|
/**
|
|
* Recreate the editor instance if the dependencies have changed.
|
|
*/
|
|
refreshEditorInstance(deps) {
|
|
if (this.editor && !this.editor.isDestroyed) {
|
|
if (this.previousDeps === null) {
|
|
this.previousDeps = deps;
|
|
return;
|
|
}
|
|
const depsAreEqual = this.previousDeps.length === deps.length && this.previousDeps.every((dep, index) => dep === deps[index]);
|
|
if (depsAreEqual) {
|
|
return;
|
|
}
|
|
}
|
|
if (this.editor && !this.editor.isDestroyed) {
|
|
this.editor.destroy();
|
|
}
|
|
this.setEditor(this.createEditor());
|
|
this.previousDeps = deps;
|
|
}
|
|
/**
|
|
* Schedule the destruction of the editor instance.
|
|
* This will only destroy the editor if it was not mounted on the next tick.
|
|
* This is to avoid destroying the editor instance when it's actually still mounted.
|
|
*/
|
|
scheduleDestroy() {
|
|
const currentInstanceId = this.instanceId;
|
|
const currentEditor = this.editor;
|
|
this.scheduledDestructionTimeout = setTimeout(() => {
|
|
if (this.isComponentMounted && this.instanceId === currentInstanceId) {
|
|
if (currentEditor) {
|
|
currentEditor.setOptions(this.options.current);
|
|
}
|
|
return;
|
|
}
|
|
if (currentEditor && !currentEditor.isDestroyed) {
|
|
currentEditor.destroy();
|
|
if (this.instanceId === currentInstanceId) {
|
|
this.setEditor(null);
|
|
}
|
|
}
|
|
}, 1);
|
|
}
|
|
};
|
|
function useEditor(options = {}, deps = []) {
|
|
const mostRecentOptions = useRef(options);
|
|
mostRecentOptions.current = options;
|
|
const [instanceManager] = useState2(() => new EditorInstanceManager(mostRecentOptions));
|
|
const editor = useSyncExternalStore2(
|
|
instanceManager.subscribe,
|
|
instanceManager.getEditor,
|
|
instanceManager.getServerSnapshot
|
|
);
|
|
useDebugValue2(editor);
|
|
useEffect2(instanceManager.onRender(deps));
|
|
useEditorState({
|
|
editor,
|
|
selector: ({ transactionNumber }) => {
|
|
if (options.shouldRerenderOnTransaction === false || options.shouldRerenderOnTransaction === void 0) {
|
|
return null;
|
|
}
|
|
if (options.immediatelyRender && transactionNumber === 0) {
|
|
return 0;
|
|
}
|
|
return transactionNumber + 1;
|
|
}
|
|
});
|
|
return editor;
|
|
}
|
|
|
|
// src/Context.tsx
|
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
var EditorContext = createContext({
|
|
editor: null
|
|
});
|
|
var EditorConsumer = EditorContext.Consumer;
|
|
var useCurrentEditor = () => useContext(EditorContext);
|
|
function EditorProvider({
|
|
children,
|
|
slotAfter,
|
|
slotBefore,
|
|
editorContainerProps = {},
|
|
...editorOptions
|
|
}) {
|
|
const editor = useEditor(editorOptions);
|
|
const contextValue = useMemo(() => ({ editor }), [editor]);
|
|
if (!editor) {
|
|
return null;
|
|
}
|
|
return /* @__PURE__ */ jsxs2(EditorContext.Provider, { value: contextValue, children: [
|
|
slotBefore,
|
|
/* @__PURE__ */ jsx2(EditorConsumer, { children: ({ editor: currentEditor }) => /* @__PURE__ */ jsx2(EditorContent, { editor: currentEditor, ...editorContainerProps }) }),
|
|
children,
|
|
slotAfter
|
|
] });
|
|
}
|
|
|
|
// src/useReactNodeView.ts
|
|
import { createContext as createContext2, createElement, useContext as useContext2 } from "react";
|
|
var ReactNodeViewContext = createContext2({
|
|
onDragStart: () => {
|
|
},
|
|
nodeViewContentChildren: void 0,
|
|
nodeViewContentRef: () => {
|
|
}
|
|
});
|
|
var ReactNodeViewContentProvider = ({ children, content }) => {
|
|
return createElement(ReactNodeViewContext.Provider, { value: { nodeViewContentChildren: content } }, children);
|
|
};
|
|
var useReactNodeView = () => useContext2(ReactNodeViewContext);
|
|
|
|
// src/NodeViewContent.tsx
|
|
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
function NodeViewContent({
|
|
as: Tag = "div",
|
|
...props
|
|
}) {
|
|
const { nodeViewContentRef, nodeViewContentChildren } = useReactNodeView();
|
|
return (
|
|
// @ts-ignore
|
|
/* @__PURE__ */ jsx3(
|
|
Tag,
|
|
{
|
|
...props,
|
|
ref: nodeViewContentRef,
|
|
"data-node-view-content": "",
|
|
style: {
|
|
whiteSpace: "pre-wrap",
|
|
...props.style
|
|
},
|
|
children: nodeViewContentChildren
|
|
}
|
|
)
|
|
);
|
|
}
|
|
|
|
// src/NodeViewWrapper.tsx
|
|
import React3 from "react";
|
|
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
var NodeViewWrapper = React3.forwardRef((props, ref) => {
|
|
const { onDragStart } = useReactNodeView();
|
|
const Tag = props.as || "div";
|
|
return (
|
|
// @ts-ignore
|
|
/* @__PURE__ */ jsx4(
|
|
Tag,
|
|
{
|
|
...props,
|
|
ref,
|
|
"data-node-view-wrapper": "",
|
|
onDragStart,
|
|
style: {
|
|
whiteSpace: "normal",
|
|
...props.style
|
|
}
|
|
}
|
|
)
|
|
);
|
|
});
|
|
|
|
// src/ReactMarkViewRenderer.tsx
|
|
import { MarkView } from "@tiptap/core";
|
|
import React4 from "react";
|
|
|
|
// src/ReactRenderer.tsx
|
|
import { version as reactVersion } from "react";
|
|
import { flushSync } from "react-dom";
|
|
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
function isClassComponent(Component) {
|
|
return !!(typeof Component === "function" && Component.prototype && Component.prototype.isReactComponent);
|
|
}
|
|
function isForwardRefComponent(Component) {
|
|
return !!(typeof Component === "object" && Component.$$typeof && (Component.$$typeof.toString() === "Symbol(react.forward_ref)" || Component.$$typeof.description === "react.forward_ref"));
|
|
}
|
|
function isMemoComponent(Component) {
|
|
return !!(typeof Component === "object" && Component.$$typeof && (Component.$$typeof.toString() === "Symbol(react.memo)" || Component.$$typeof.description === "react.memo"));
|
|
}
|
|
function canReceiveRef(Component) {
|
|
if (isClassComponent(Component)) {
|
|
return true;
|
|
}
|
|
if (isForwardRefComponent(Component)) {
|
|
return true;
|
|
}
|
|
if (isMemoComponent(Component)) {
|
|
const wrappedComponent = Component.type;
|
|
if (wrappedComponent) {
|
|
return isClassComponent(wrappedComponent) || isForwardRefComponent(wrappedComponent);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function isReact19Plus() {
|
|
try {
|
|
if (reactVersion) {
|
|
const majorVersion = parseInt(reactVersion.split(".")[0], 10);
|
|
return majorVersion >= 19;
|
|
}
|
|
} catch {
|
|
}
|
|
return false;
|
|
}
|
|
var ReactRenderer = class {
|
|
/**
|
|
* Immediately creates element and renders the provided React component.
|
|
*/
|
|
constructor(component, { editor, props = {}, as = "div", className = "" }) {
|
|
this.ref = null;
|
|
/**
|
|
* Flag to track if the renderer has been destroyed, preventing queued or asynchronous renders from executing after teardown.
|
|
*/
|
|
this.destroyed = false;
|
|
this.id = Math.floor(Math.random() * 4294967295).toString();
|
|
this.component = component;
|
|
this.editor = editor;
|
|
this.props = props;
|
|
this.element = document.createElement(as);
|
|
this.element.classList.add("react-renderer");
|
|
if (className) {
|
|
this.element.classList.add(...className.split(" "));
|
|
}
|
|
if (this.editor.isInitialized) {
|
|
flushSync(() => {
|
|
this.render();
|
|
});
|
|
} else {
|
|
queueMicrotask(() => {
|
|
if (this.destroyed) {
|
|
return;
|
|
}
|
|
this.render();
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
* Render the React component.
|
|
*/
|
|
render() {
|
|
var _a;
|
|
if (this.destroyed) {
|
|
return;
|
|
}
|
|
const Component = this.component;
|
|
const props = this.props;
|
|
const editor = this.editor;
|
|
const isReact19 = isReact19Plus();
|
|
const componentCanReceiveRef = canReceiveRef(Component);
|
|
const elementProps = { ...props };
|
|
if (elementProps.ref && !(isReact19 || componentCanReceiveRef)) {
|
|
delete elementProps.ref;
|
|
}
|
|
if (!elementProps.ref && (isReact19 || componentCanReceiveRef)) {
|
|
elementProps.ref = (ref) => {
|
|
this.ref = ref;
|
|
};
|
|
}
|
|
this.reactElement = /* @__PURE__ */ jsx5(Component, { ...elementProps });
|
|
(_a = editor == null ? void 0 : editor.contentComponent) == null ? void 0 : _a.setRenderer(this.id, this);
|
|
}
|
|
/**
|
|
* Re-renders the React component with new props.
|
|
*/
|
|
updateProps(props = {}) {
|
|
if (this.destroyed) {
|
|
return;
|
|
}
|
|
this.props = {
|
|
...this.props,
|
|
...props
|
|
};
|
|
this.render();
|
|
}
|
|
/**
|
|
* Destroy the React component.
|
|
*/
|
|
destroy() {
|
|
var _a;
|
|
this.destroyed = true;
|
|
const editor = this.editor;
|
|
(_a = editor == null ? void 0 : editor.contentComponent) == null ? void 0 : _a.removeRenderer(this.id);
|
|
try {
|
|
if (this.element && this.element.parentNode) {
|
|
this.element.parentNode.removeChild(this.element);
|
|
}
|
|
} catch {
|
|
}
|
|
}
|
|
/**
|
|
* Update the attributes of the element that holds the React component.
|
|
*/
|
|
updateAttributes(attributes) {
|
|
Object.keys(attributes).forEach((key) => {
|
|
this.element.setAttribute(key, attributes[key]);
|
|
});
|
|
}
|
|
};
|
|
|
|
// src/ReactMarkViewRenderer.tsx
|
|
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
var ReactMarkViewContext = React4.createContext({
|
|
markViewContentRef: () => {
|
|
}
|
|
});
|
|
var MarkViewContent = (props) => {
|
|
const { as: Tag = "span", ...rest } = props;
|
|
const { markViewContentRef } = React4.useContext(ReactMarkViewContext);
|
|
return (
|
|
// @ts-ignore
|
|
/* @__PURE__ */ jsx6(Tag, { ...rest, ref: markViewContentRef, "data-mark-view-content": "" })
|
|
);
|
|
};
|
|
var ReactMarkView = class extends MarkView {
|
|
constructor(component, props, options) {
|
|
super(component, props, options);
|
|
const { as = "span", attrs, className = "" } = options || {};
|
|
const componentProps = { ...props, updateAttributes: this.updateAttributes.bind(this) };
|
|
this.contentDOMElement = document.createElement("span");
|
|
const markViewContentRef = (el) => {
|
|
if (el && !el.contains(this.contentDOMElement)) {
|
|
el.appendChild(this.contentDOMElement);
|
|
}
|
|
};
|
|
const context = {
|
|
markViewContentRef
|
|
};
|
|
const ReactMarkViewProvider = React4.memo((componentProps2) => {
|
|
return /* @__PURE__ */ jsx6(ReactMarkViewContext.Provider, { value: context, children: React4.createElement(component, componentProps2) });
|
|
});
|
|
ReactMarkViewProvider.displayName = "ReactMarkView";
|
|
this.renderer = new ReactRenderer(ReactMarkViewProvider, {
|
|
editor: props.editor,
|
|
props: componentProps,
|
|
as,
|
|
className: `mark-${props.mark.type.name} ${className}`.trim()
|
|
});
|
|
if (attrs) {
|
|
this.renderer.updateAttributes(attrs);
|
|
}
|
|
}
|
|
get dom() {
|
|
return this.renderer.element;
|
|
}
|
|
get contentDOM() {
|
|
return this.contentDOMElement;
|
|
}
|
|
};
|
|
function ReactMarkViewRenderer(component, options = {}) {
|
|
return (props) => new ReactMarkView(component, props, options);
|
|
}
|
|
|
|
// src/ReactNodeViewRenderer.tsx
|
|
import { getRenderedAttributes, NodeView } from "@tiptap/core";
|
|
import { createElement as createElement2, createRef, memo } from "react";
|
|
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
var ReactNodeView = class extends NodeView {
|
|
constructor(component, props, options) {
|
|
super(component, props, options);
|
|
/**
|
|
* The requestAnimationFrame ID used for selection updates.
|
|
*/
|
|
this.selectionRafId = null;
|
|
this.cachedExtensionWithSyncedStorage = null;
|
|
if (!this.node.isLeaf) {
|
|
if (this.options.contentDOMElementTag) {
|
|
this.contentDOMElement = document.createElement(this.options.contentDOMElementTag);
|
|
} else {
|
|
this.contentDOMElement = document.createElement(this.node.isInline ? "span" : "div");
|
|
}
|
|
this.contentDOMElement.dataset.nodeViewContentReact = "";
|
|
this.contentDOMElement.dataset.nodeViewWrapper = "";
|
|
this.contentDOMElement.style.whiteSpace = "inherit";
|
|
const contentTarget = this.dom.querySelector("[data-node-view-content]");
|
|
if (!contentTarget) {
|
|
return;
|
|
}
|
|
contentTarget.appendChild(this.contentDOMElement);
|
|
}
|
|
}
|
|
/**
|
|
* 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;
|
|
}
|
|
/**
|
|
* Setup the React component.
|
|
* Called on initialization.
|
|
*/
|
|
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(),
|
|
ref: createRef()
|
|
};
|
|
if (!this.component.displayName) {
|
|
const capitalizeFirstChar = (string) => {
|
|
return string.charAt(0).toUpperCase() + string.substring(1);
|
|
};
|
|
this.component.displayName = capitalizeFirstChar(this.extension.name);
|
|
}
|
|
const onDragStart = this.onDragStart.bind(this);
|
|
const nodeViewContentRef = (element) => {
|
|
if (element && this.contentDOMElement && element.firstChild !== this.contentDOMElement) {
|
|
if (element.hasAttribute("data-node-view-wrapper")) {
|
|
element.removeAttribute("data-node-view-wrapper");
|
|
}
|
|
element.appendChild(this.contentDOMElement);
|
|
}
|
|
};
|
|
const context = { onDragStart, nodeViewContentRef };
|
|
const Component = this.component;
|
|
const ReactNodeViewProvider = memo((componentProps) => {
|
|
return /* @__PURE__ */ jsx7(ReactNodeViewContext.Provider, { value: context, children: createElement2(Component, componentProps) });
|
|
});
|
|
ReactNodeViewProvider.displayName = "ReactNodeView";
|
|
let as = this.node.isInline ? "span" : "div";
|
|
if (this.options.as) {
|
|
as = this.options.as;
|
|
}
|
|
const { className = "" } = this.options;
|
|
this.handleSelectionUpdate = this.handleSelectionUpdate.bind(this);
|
|
this.renderer = new ReactRenderer(ReactNodeViewProvider, {
|
|
editor: this.editor,
|
|
props,
|
|
as,
|
|
className: `node-${this.node.type.name} ${className}`.trim()
|
|
});
|
|
this.editor.on("selectionUpdate", this.handleSelectionUpdate);
|
|
this.updateElementAttributes();
|
|
}
|
|
/**
|
|
* Return the DOM element.
|
|
* This is the element that will be used to display the node view.
|
|
*/
|
|
get dom() {
|
|
var _a;
|
|
if (this.renderer.element.firstElementChild && !((_a = this.renderer.element.firstElementChild) == null ? void 0 : _a.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.contentDOMElement;
|
|
}
|
|
/**
|
|
* On editor selection update, check if the node is selected.
|
|
* If it is, call `selectNode`, otherwise call `deselectNode`.
|
|
*/
|
|
handleSelectionUpdate() {
|
|
if (this.selectionRafId) {
|
|
cancelAnimationFrame(this.selectionRafId);
|
|
this.selectionRafId = null;
|
|
}
|
|
this.selectionRafId = requestAnimationFrame(() => {
|
|
this.selectionRafId = null;
|
|
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.renderer.updateProps(props);
|
|
if (typeof this.options.attrs === "function") {
|
|
this.updateElementAttributes();
|
|
}
|
|
};
|
|
if (node.type !== this.node.type) {
|
|
return false;
|
|
}
|
|
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 === 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
|
|
});
|
|
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
|
|
});
|
|
this.renderer.element.classList.remove("ProseMirror-selectednode");
|
|
}
|
|
/**
|
|
* Destroy the React component instance.
|
|
*/
|
|
destroy() {
|
|
this.renderer.destroy();
|
|
this.editor.off("selectionUpdate", this.handleSelectionUpdate);
|
|
this.contentDOMElement = null;
|
|
if (this.selectionRafId) {
|
|
cancelAnimationFrame(this.selectionRafId);
|
|
this.selectionRafId = 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);
|
|
}
|
|
}
|
|
};
|
|
function ReactNodeViewRenderer(component, options) {
|
|
return (props) => {
|
|
if (!props.editor.contentComponent) {
|
|
return {};
|
|
}
|
|
return new ReactNodeView(component, props, options);
|
|
};
|
|
}
|
|
|
|
// src/Tiptap.tsx
|
|
import { createContext as createContext3, useContext as useContext3, useMemo as useMemo2 } from "react";
|
|
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
var TiptapContext = createContext3({
|
|
get editor() {
|
|
throw new Error("useTiptap must be used within a <Tiptap> provider");
|
|
}
|
|
});
|
|
TiptapContext.displayName = "TiptapContext";
|
|
var useTiptap = () => useContext3(TiptapContext);
|
|
function useTiptapState(selector, equalityFn) {
|
|
const { editor } = useTiptap();
|
|
return useEditorState({
|
|
editor,
|
|
selector,
|
|
equalityFn
|
|
});
|
|
}
|
|
function TiptapWrapper({ editor, instance, children }) {
|
|
const resolvedEditor = editor != null ? editor : instance;
|
|
if (!resolvedEditor) {
|
|
throw new Error("Tiptap: An editor instance is required. Pass a non-null `editor` prop.");
|
|
}
|
|
const tiptapContextValue = useMemo2(() => ({ editor: resolvedEditor }), [resolvedEditor]);
|
|
const legacyContextValue = useMemo2(() => ({ editor: resolvedEditor }), [resolvedEditor]);
|
|
return /* @__PURE__ */ jsx8(EditorContext.Provider, { value: legacyContextValue, children: /* @__PURE__ */ jsx8(TiptapContext.Provider, { value: tiptapContextValue, children }) });
|
|
}
|
|
TiptapWrapper.displayName = "Tiptap";
|
|
function TiptapContent({ ...rest }) {
|
|
const { editor } = useTiptap();
|
|
return /* @__PURE__ */ jsx8(EditorContent, { editor, ...rest });
|
|
}
|
|
TiptapContent.displayName = "Tiptap.Content";
|
|
var Tiptap = Object.assign(TiptapWrapper, {
|
|
/**
|
|
* The Tiptap Content component that renders the EditorContent with the editor instance from the context.
|
|
* @see TiptapContent
|
|
*/
|
|
Content: TiptapContent
|
|
});
|
|
|
|
// src/index.ts
|
|
export * from "@tiptap/core";
|
|
export {
|
|
EditorConsumer,
|
|
EditorContent,
|
|
EditorContext,
|
|
EditorProvider,
|
|
MarkViewContent,
|
|
NodeViewContent,
|
|
NodeViewWrapper,
|
|
PureEditorContent,
|
|
ReactMarkView,
|
|
ReactMarkViewContext,
|
|
ReactMarkViewRenderer,
|
|
ReactNodeView,
|
|
ReactNodeViewContentProvider,
|
|
ReactNodeViewContext,
|
|
ReactNodeViewRenderer,
|
|
ReactRenderer,
|
|
Tiptap,
|
|
TiptapContent,
|
|
TiptapContext,
|
|
TiptapWrapper,
|
|
useCurrentEditor,
|
|
useEditor,
|
|
useEditorState,
|
|
useReactNodeView,
|
|
useTiptap,
|
|
useTiptapState
|
|
};
|
|
//# sourceMappingURL=index.js.map
|