1598 lines
60 KiB
JavaScript
1598 lines
60 KiB
JavaScript
import { BubbleMenuPlugin } from '@tiptap/extension-bubble-menu';
|
||
import React, { forwardRef, useState, useDebugValue, useLayoutEffect, useEffect, useRef, createContext, useContext, version, createRef, memo, createElement } from 'react';
|
||
import ReactDOM, { flushSync } from 'react-dom';
|
||
import { Editor, NodeView, getRenderedAttributes } from '@tiptap/core';
|
||
export * from '@tiptap/core';
|
||
import { FloatingMenuPlugin } from '@tiptap/extension-floating-menu';
|
||
|
||
function getDefaultExportFromCjs (x) {
|
||
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
||
}
|
||
|
||
var shim = {exports: {}};
|
||
|
||
var useSyncExternalStoreShim_production_min = {};
|
||
|
||
/**
|
||
* @license React
|
||
* use-sync-external-store-shim.production.min.js
|
||
*
|
||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||
*
|
||
* This source code is licensed under the MIT license found in the
|
||
* LICENSE file in the root directory of this source tree.
|
||
*/
|
||
|
||
var hasRequiredUseSyncExternalStoreShim_production_min;
|
||
|
||
function requireUseSyncExternalStoreShim_production_min () {
|
||
if (hasRequiredUseSyncExternalStoreShim_production_min) return useSyncExternalStoreShim_production_min;
|
||
hasRequiredUseSyncExternalStoreShim_production_min = 1;
|
||
var e=React;function h(a,b){return a===b&&(0!==a||1/a===1/b)||a!==a&&b!==b}var k="function"===typeof Object.is?Object.is:h,l=e.useState,m=e.useEffect,n=e.useLayoutEffect,p=e.useDebugValue;function q(a,b){var d=b(),f=l({inst:{value:d,getSnapshot:b}}),c=f[0].inst,g=f[1];n(function(){c.value=d;c.getSnapshot=b;r(c)&&g({inst:c});},[a,d,b]);m(function(){r(c)&&g({inst:c});return a(function(){r(c)&&g({inst:c});})},[a]);p(d);return d}
|
||
function r(a){var b=a.getSnapshot;a=a.value;try{var d=b();return !k(a,d)}catch(f){return !0}}function t(a,b){return b()}var u="undefined"===typeof window||"undefined"===typeof window.document||"undefined"===typeof window.document.createElement?t:q;useSyncExternalStoreShim_production_min.useSyncExternalStore=void 0!==e.useSyncExternalStore?e.useSyncExternalStore:u;
|
||
return useSyncExternalStoreShim_production_min;
|
||
}
|
||
|
||
var useSyncExternalStoreShim_development = {};
|
||
|
||
/**
|
||
* @license React
|
||
* use-sync-external-store-shim.development.js
|
||
*
|
||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||
*
|
||
* This source code is licensed under the MIT license found in the
|
||
* LICENSE file in the root directory of this source tree.
|
||
*/
|
||
|
||
var hasRequiredUseSyncExternalStoreShim_development;
|
||
|
||
function requireUseSyncExternalStoreShim_development () {
|
||
if (hasRequiredUseSyncExternalStoreShim_development) return useSyncExternalStoreShim_development;
|
||
hasRequiredUseSyncExternalStoreShim_development = 1;
|
||
|
||
if (process.env.NODE_ENV !== "production") {
|
||
(function() {
|
||
|
||
/* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */
|
||
if (
|
||
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' &&
|
||
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart ===
|
||
'function'
|
||
) {
|
||
__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error());
|
||
}
|
||
var React$1 = React;
|
||
|
||
var ReactSharedInternals = React$1.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
|
||
|
||
function error(format) {
|
||
{
|
||
{
|
||
for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
|
||
args[_key2 - 1] = arguments[_key2];
|
||
}
|
||
|
||
printWarning('error', format, args);
|
||
}
|
||
}
|
||
}
|
||
|
||
function printWarning(level, format, args) {
|
||
// When changing this logic, you might want to also
|
||
// update consoleWithStackDev.www.js as well.
|
||
{
|
||
var ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;
|
||
var stack = ReactDebugCurrentFrame.getStackAddendum();
|
||
|
||
if (stack !== '') {
|
||
format += '%s';
|
||
args = args.concat([stack]);
|
||
} // eslint-disable-next-line react-internal/safe-string-coercion
|
||
|
||
|
||
var argsWithFormat = args.map(function (item) {
|
||
return String(item);
|
||
}); // Careful: RN currently depends on this prefix
|
||
|
||
argsWithFormat.unshift('Warning: ' + format); // We intentionally don't use spread (or .apply) directly because it
|
||
// breaks IE9: https://github.com/facebook/react/issues/13610
|
||
// eslint-disable-next-line react-internal/no-production-logging
|
||
|
||
Function.prototype.apply.call(console[level], console, argsWithFormat);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* inlined Object.is polyfill to avoid requiring consumers ship their own
|
||
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
|
||
*/
|
||
function is(x, y) {
|
||
return x === y && (x !== 0 || 1 / x === 1 / y) || x !== x && y !== y // eslint-disable-line no-self-compare
|
||
;
|
||
}
|
||
|
||
var objectIs = typeof Object.is === 'function' ? Object.is : is;
|
||
|
||
// dispatch for CommonJS interop named imports.
|
||
|
||
var useState = React$1.useState,
|
||
useEffect = React$1.useEffect,
|
||
useLayoutEffect = React$1.useLayoutEffect,
|
||
useDebugValue = React$1.useDebugValue;
|
||
var didWarnOld18Alpha = false;
|
||
var didWarnUncachedGetSnapshot = false; // Disclaimer: This shim breaks many of the rules of React, and only works
|
||
// because of a very particular set of implementation details and assumptions
|
||
// -- change any one of them and it will break. The most important assumption
|
||
// is that updates are always synchronous, because concurrent rendering is
|
||
// only available in versions of React that also have a built-in
|
||
// useSyncExternalStore API. And we only use this shim when the built-in API
|
||
// does not exist.
|
||
//
|
||
// Do not assume that the clever hacks used by this hook also work in general.
|
||
// The point of this shim is to replace the need for hacks by other libraries.
|
||
|
||
function useSyncExternalStore(subscribe, getSnapshot, // Note: The shim does not use getServerSnapshot, because pre-18 versions of
|
||
// React do not expose a way to check if we're hydrating. So users of the shim
|
||
// will need to track that themselves and return the correct value
|
||
// from `getSnapshot`.
|
||
getServerSnapshot) {
|
||
{
|
||
if (!didWarnOld18Alpha) {
|
||
if (React$1.startTransition !== undefined) {
|
||
didWarnOld18Alpha = true;
|
||
|
||
error('You are using an outdated, pre-release alpha of React 18 that ' + 'does not support useSyncExternalStore. The ' + 'use-sync-external-store shim will not work correctly. Upgrade ' + 'to a newer pre-release.');
|
||
}
|
||
}
|
||
} // Read the current snapshot from the store on every render. Again, this
|
||
// breaks the rules of React, and only works here because of specific
|
||
// implementation details, most importantly that updates are
|
||
// always synchronous.
|
||
|
||
|
||
var value = getSnapshot();
|
||
|
||
{
|
||
if (!didWarnUncachedGetSnapshot) {
|
||
var cachedValue = getSnapshot();
|
||
|
||
if (!objectIs(value, cachedValue)) {
|
||
error('The result of getSnapshot should be cached to avoid an infinite loop');
|
||
|
||
didWarnUncachedGetSnapshot = true;
|
||
}
|
||
}
|
||
} // Because updates are synchronous, we don't queue them. Instead we force a
|
||
// re-render whenever the subscribed state changes by updating an some
|
||
// arbitrary useState hook. Then, during render, we call getSnapshot to read
|
||
// the current value.
|
||
//
|
||
// Because we don't actually use the state returned by the useState hook, we
|
||
// can save a bit of memory by storing other stuff in that slot.
|
||
//
|
||
// To implement the early bailout, we need to track some things on a mutable
|
||
// object. Usually, we would put that in a useRef hook, but we can stash it in
|
||
// our useState hook instead.
|
||
//
|
||
// To force a re-render, we call forceUpdate({inst}). That works because the
|
||
// new object always fails an equality check.
|
||
|
||
|
||
var _useState = useState({
|
||
inst: {
|
||
value: value,
|
||
getSnapshot: getSnapshot
|
||
}
|
||
}),
|
||
inst = _useState[0].inst,
|
||
forceUpdate = _useState[1]; // Track the latest getSnapshot function with a ref. This needs to be updated
|
||
// in the layout phase so we can access it during the tearing check that
|
||
// happens on subscribe.
|
||
|
||
|
||
useLayoutEffect(function () {
|
||
inst.value = value;
|
||
inst.getSnapshot = getSnapshot; // Whenever getSnapshot or subscribe changes, we need to check in the
|
||
// commit phase if there was an interleaved mutation. In concurrent mode
|
||
// this can happen all the time, but even in synchronous mode, an earlier
|
||
// effect may have mutated the store.
|
||
|
||
if (checkIfSnapshotChanged(inst)) {
|
||
// Force a re-render.
|
||
forceUpdate({
|
||
inst: inst
|
||
});
|
||
}
|
||
}, [subscribe, value, getSnapshot]);
|
||
useEffect(function () {
|
||
// Check for changes right before subscribing. Subsequent changes will be
|
||
// detected in the subscription handler.
|
||
if (checkIfSnapshotChanged(inst)) {
|
||
// Force a re-render.
|
||
forceUpdate({
|
||
inst: inst
|
||
});
|
||
}
|
||
|
||
var handleStoreChange = function () {
|
||
// TODO: Because there is no cross-renderer API for batching updates, it's
|
||
// up to the consumer of this library to wrap their subscription event
|
||
// with unstable_batchedUpdates. Should we try to detect when this isn't
|
||
// the case and print a warning in development?
|
||
// The store changed. Check if the snapshot changed since the last time we
|
||
// read from the store.
|
||
if (checkIfSnapshotChanged(inst)) {
|
||
// Force a re-render.
|
||
forceUpdate({
|
||
inst: inst
|
||
});
|
||
}
|
||
}; // Subscribe to the store and return a clean-up function.
|
||
|
||
|
||
return subscribe(handleStoreChange);
|
||
}, [subscribe]);
|
||
useDebugValue(value);
|
||
return value;
|
||
}
|
||
|
||
function checkIfSnapshotChanged(inst) {
|
||
var latestGetSnapshot = inst.getSnapshot;
|
||
var prevValue = inst.value;
|
||
|
||
try {
|
||
var nextValue = latestGetSnapshot();
|
||
return !objectIs(prevValue, nextValue);
|
||
} catch (error) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
function useSyncExternalStore$1(subscribe, getSnapshot, getServerSnapshot) {
|
||
// Note: The shim does not use getServerSnapshot, because pre-18 versions of
|
||
// React do not expose a way to check if we're hydrating. So users of the shim
|
||
// will need to track that themselves and return the correct value
|
||
// from `getSnapshot`.
|
||
return getSnapshot();
|
||
}
|
||
|
||
var canUseDOM = !!(typeof window !== 'undefined' && typeof window.document !== 'undefined' && typeof window.document.createElement !== 'undefined');
|
||
|
||
var isServerEnvironment = !canUseDOM;
|
||
|
||
var shim = isServerEnvironment ? useSyncExternalStore$1 : useSyncExternalStore;
|
||
var useSyncExternalStore$2 = React$1.useSyncExternalStore !== undefined ? React$1.useSyncExternalStore : shim;
|
||
|
||
useSyncExternalStoreShim_development.useSyncExternalStore = useSyncExternalStore$2;
|
||
/* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */
|
||
if (
|
||
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' &&
|
||
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop ===
|
||
'function'
|
||
) {
|
||
__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(new Error());
|
||
}
|
||
|
||
})();
|
||
}
|
||
return useSyncExternalStoreShim_development;
|
||
}
|
||
|
||
if (process.env.NODE_ENV === 'production') {
|
||
shim.exports = requireUseSyncExternalStoreShim_production_min();
|
||
} else {
|
||
shim.exports = requireUseSyncExternalStoreShim_development();
|
||
}
|
||
|
||
var shimExports = shim.exports;
|
||
|
||
const mergeRefs = (...refs) => {
|
||
return (node) => {
|
||
refs.forEach(ref => {
|
||
if (typeof ref === 'function') {
|
||
ref(node);
|
||
}
|
||
else if (ref) {
|
||
ref.current = node;
|
||
}
|
||
});
|
||
};
|
||
};
|
||
/**
|
||
* This component renders all of the editor's node views.
|
||
*/
|
||
const Portals = ({ contentComponent, }) => {
|
||
// For performance reasons, we render the node view portals on state changes only
|
||
const renderers = shimExports.useSyncExternalStore(contentComponent.subscribe, contentComponent.getSnapshot, contentComponent.getServerSnapshot);
|
||
// This allows us to directly render the portals without any additional wrapper
|
||
return (React.createElement(React.Fragment, null, Object.values(renderers)));
|
||
};
|
||
function getInstance() {
|
||
const subscribers = 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());
|
||
},
|
||
};
|
||
}
|
||
class PureEditorContent extends React.Component {
|
||
constructor(props) {
|
||
var _a;
|
||
super(props);
|
||
this.editorContentRef = React.createRef();
|
||
this.initialized = false;
|
||
this.state = {
|
||
hasContentComponentInitialized: Boolean((_a = props.editor) === null || _a === void 0 ? void 0 : _a.contentComponent),
|
||
};
|
||
}
|
||
componentDidMount() {
|
||
this.init();
|
||
}
|
||
componentDidUpdate() {
|
||
this.init();
|
||
}
|
||
init() {
|
||
const editor = this.props.editor;
|
||
if (editor && !editor.isDestroyed && editor.options.element) {
|
||
if (editor.contentComponent) {
|
||
return;
|
||
}
|
||
const element = this.editorContentRef.current;
|
||
element.append(...editor.options.element.childNodes);
|
||
editor.setOptions({
|
||
element,
|
||
});
|
||
editor.contentComponent = getInstance();
|
||
// Has the content component been initialized?
|
||
if (!this.state.hasContentComponentInitialized) {
|
||
// Subscribe to the content component
|
||
this.unsubscribeToContentComponent = editor.contentComponent.subscribe(() => {
|
||
this.setState(prevState => {
|
||
if (!prevState.hasContentComponentInitialized) {
|
||
return {
|
||
hasContentComponentInitialized: true,
|
||
};
|
||
}
|
||
return prevState;
|
||
});
|
||
// Unsubscribe to previous content component
|
||
if (this.unsubscribeToContentComponent) {
|
||
this.unsubscribeToContentComponent();
|
||
}
|
||
});
|
||
}
|
||
editor.createNodeViews();
|
||
this.initialized = true;
|
||
}
|
||
}
|
||
componentWillUnmount() {
|
||
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;
|
||
if (!editor.options.element.firstChild) {
|
||
return;
|
||
}
|
||
const newElement = document.createElement('div');
|
||
newElement.append(...editor.options.element.childNodes);
|
||
editor.setOptions({
|
||
element: newElement,
|
||
});
|
||
}
|
||
render() {
|
||
const { editor, innerRef, ...rest } = this.props;
|
||
return (React.createElement(React.Fragment, null,
|
||
React.createElement("div", { ref: mergeRefs(innerRef, this.editorContentRef), ...rest }),
|
||
(editor === null || editor === void 0 ? void 0 : editor.contentComponent) && React.createElement(Portals, { contentComponent: editor.contentComponent })));
|
||
}
|
||
}
|
||
// EditorContent should be re-created whenever the Editor instance changes
|
||
const EditorContentWithKey = forwardRef((props, ref) => {
|
||
const key = React.useMemo(() => {
|
||
return Math.floor(Math.random() * 0xffffffff).toString();
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [props.editor]);
|
||
// Can't use JSX here because it conflicts with the type definition of Vue's JSX, so use createElement
|
||
return React.createElement(PureEditorContent, {
|
||
key,
|
||
innerRef: ref,
|
||
...props,
|
||
});
|
||
});
|
||
const EditorContent = React.memo(EditorContentWithKey);
|
||
|
||
var react = function equal(a, b) {
|
||
if (a === b) return true;
|
||
|
||
if (a && b && typeof a == 'object' && typeof b == 'object') {
|
||
if (a.constructor !== b.constructor) return false;
|
||
|
||
var length, i, keys;
|
||
if (Array.isArray(a)) {
|
||
length = a.length;
|
||
if (length != b.length) return false;
|
||
for (i = length; i-- !== 0;)
|
||
if (!equal(a[i], b[i])) return false;
|
||
return true;
|
||
}
|
||
|
||
|
||
if ((a instanceof Map) && (b instanceof Map)) {
|
||
if (a.size !== b.size) return false;
|
||
for (i of a.entries())
|
||
if (!b.has(i[0])) return false;
|
||
for (i of a.entries())
|
||
if (!equal(i[1], b.get(i[0]))) return false;
|
||
return true;
|
||
}
|
||
|
||
if ((a instanceof Set) && (b instanceof Set)) {
|
||
if (a.size !== b.size) return false;
|
||
for (i of a.entries())
|
||
if (!b.has(i[0])) return false;
|
||
return true;
|
||
}
|
||
|
||
if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) {
|
||
length = a.length;
|
||
if (length != b.length) return false;
|
||
for (i = length; i-- !== 0;)
|
||
if (a[i] !== b[i]) return false;
|
||
return true;
|
||
}
|
||
|
||
|
||
if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags;
|
||
if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf();
|
||
if (a.toString !== Object.prototype.toString) return a.toString() === b.toString();
|
||
|
||
keys = Object.keys(a);
|
||
length = keys.length;
|
||
if (length !== Object.keys(b).length) return false;
|
||
|
||
for (i = length; i-- !== 0;)
|
||
if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false;
|
||
|
||
for (i = length; i-- !== 0;) {
|
||
var key = keys[i];
|
||
|
||
if (key === '_owner' && a.$$typeof) {
|
||
// React-specific: avoid traversing React elements' _owner.
|
||
// _owner contains circular references
|
||
// and is not needed when comparing the actual elements (and not their owners)
|
||
continue;
|
||
}
|
||
|
||
if (!equal(a[key], b[key])) return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
// true if both NaN, false otherwise
|
||
return a!==a && b!==b;
|
||
};
|
||
|
||
var deepEqual = /*@__PURE__*/getDefaultExportFromCjs(react);
|
||
|
||
var withSelector = {exports: {}};
|
||
|
||
var withSelector_production_min = {};
|
||
|
||
/**
|
||
* @license React
|
||
* use-sync-external-store-shim/with-selector.production.min.js
|
||
*
|
||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||
*
|
||
* This source code is licensed under the MIT license found in the
|
||
* LICENSE file in the root directory of this source tree.
|
||
*/
|
||
|
||
var hasRequiredWithSelector_production_min;
|
||
|
||
function requireWithSelector_production_min () {
|
||
if (hasRequiredWithSelector_production_min) return withSelector_production_min;
|
||
hasRequiredWithSelector_production_min = 1;
|
||
var h=React,n=shimExports;function p(a,b){return a===b&&(0!==a||1/a===1/b)||a!==a&&b!==b}var q="function"===typeof Object.is?Object.is:p,r=n.useSyncExternalStore,t=h.useRef,u=h.useEffect,v=h.useMemo,w=h.useDebugValue;
|
||
withSelector_production_min.useSyncExternalStoreWithSelector=function(a,b,e,l,g){var c=t(null);if(null===c.current){var f={hasValue:!1,value:null};c.current=f;}else f=c.current;c=v(function(){function a(a){if(!c){c=!0;d=a;a=l(a);if(void 0!==g&&f.hasValue){var b=f.value;if(g(b,a))return k=b}return k=a}b=k;if(q(d,a))return b;var e=l(a);if(void 0!==g&&g(b,e))return b;d=a;return k=e}var c=!1,d,k,m=void 0===e?null:e;return [function(){return a(b())},null===m?void 0:function(){return a(m())}]},[b,e,l,g]);var d=r(a,c[0],c[1]);
|
||
u(function(){f.hasValue=!0;f.value=d;},[d]);w(d);return d};
|
||
return withSelector_production_min;
|
||
}
|
||
|
||
var withSelector_development = {};
|
||
|
||
/**
|
||
* @license React
|
||
* use-sync-external-store-shim/with-selector.development.js
|
||
*
|
||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||
*
|
||
* This source code is licensed under the MIT license found in the
|
||
* LICENSE file in the root directory of this source tree.
|
||
*/
|
||
|
||
var hasRequiredWithSelector_development;
|
||
|
||
function requireWithSelector_development () {
|
||
if (hasRequiredWithSelector_development) return withSelector_development;
|
||
hasRequiredWithSelector_development = 1;
|
||
|
||
if (process.env.NODE_ENV !== "production") {
|
||
(function() {
|
||
|
||
/* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */
|
||
if (
|
||
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' &&
|
||
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart ===
|
||
'function'
|
||
) {
|
||
__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error());
|
||
}
|
||
var React$1 = React;
|
||
var shim = shimExports;
|
||
|
||
/**
|
||
* inlined Object.is polyfill to avoid requiring consumers ship their own
|
||
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
|
||
*/
|
||
function is(x, y) {
|
||
return x === y && (x !== 0 || 1 / x === 1 / y) || x !== x && y !== y // eslint-disable-line no-self-compare
|
||
;
|
||
}
|
||
|
||
var objectIs = typeof Object.is === 'function' ? Object.is : is;
|
||
|
||
var useSyncExternalStore = shim.useSyncExternalStore;
|
||
|
||
// for CommonJS interop.
|
||
|
||
var useRef = React$1.useRef,
|
||
useEffect = React$1.useEffect,
|
||
useMemo = React$1.useMemo,
|
||
useDebugValue = React$1.useDebugValue; // Same as useSyncExternalStore, but supports selector and isEqual arguments.
|
||
|
||
function useSyncExternalStoreWithSelector(subscribe, getSnapshot, getServerSnapshot, selector, isEqual) {
|
||
// Use this to track the rendered snapshot.
|
||
var instRef = useRef(null);
|
||
var inst;
|
||
|
||
if (instRef.current === null) {
|
||
inst = {
|
||
hasValue: false,
|
||
value: null
|
||
};
|
||
instRef.current = inst;
|
||
} else {
|
||
inst = instRef.current;
|
||
}
|
||
|
||
var _useMemo = useMemo(function () {
|
||
// Track the memoized state using closure variables that are local to this
|
||
// memoized instance of a getSnapshot function. Intentionally not using a
|
||
// useRef hook, because that state would be shared across all concurrent
|
||
// copies of the hook/component.
|
||
var hasMemo = false;
|
||
var memoizedSnapshot;
|
||
var memoizedSelection;
|
||
|
||
var memoizedSelector = function (nextSnapshot) {
|
||
if (!hasMemo) {
|
||
// The first time the hook is called, there is no memoized result.
|
||
hasMemo = true;
|
||
memoizedSnapshot = nextSnapshot;
|
||
|
||
var _nextSelection = selector(nextSnapshot);
|
||
|
||
if (isEqual !== undefined) {
|
||
// Even if the selector has changed, the currently rendered selection
|
||
// may be equal to the new selection. We should attempt to reuse the
|
||
// current value if possible, to preserve downstream memoizations.
|
||
if (inst.hasValue) {
|
||
var currentSelection = inst.value;
|
||
|
||
if (isEqual(currentSelection, _nextSelection)) {
|
||
memoizedSelection = currentSelection;
|
||
return currentSelection;
|
||
}
|
||
}
|
||
}
|
||
|
||
memoizedSelection = _nextSelection;
|
||
return _nextSelection;
|
||
} // We may be able to reuse the previous invocation's result.
|
||
|
||
|
||
// We may be able to reuse the previous invocation's result.
|
||
var prevSnapshot = memoizedSnapshot;
|
||
var prevSelection = memoizedSelection;
|
||
|
||
if (objectIs(prevSnapshot, nextSnapshot)) {
|
||
// The snapshot is the same as last time. Reuse the previous selection.
|
||
return prevSelection;
|
||
} // The snapshot has changed, so we need to compute a new selection.
|
||
|
||
|
||
// The snapshot has changed, so we need to compute a new selection.
|
||
var nextSelection = selector(nextSnapshot); // If a custom isEqual function is provided, use that to check if the data
|
||
// has changed. If it hasn't, return the previous selection. That signals
|
||
// to React that the selections are conceptually equal, and we can bail
|
||
// out of rendering.
|
||
|
||
// If a custom isEqual function is provided, use that to check if the data
|
||
// has changed. If it hasn't, return the previous selection. That signals
|
||
// to React that the selections are conceptually equal, and we can bail
|
||
// out of rendering.
|
||
if (isEqual !== undefined && isEqual(prevSelection, nextSelection)) {
|
||
return prevSelection;
|
||
}
|
||
|
||
memoizedSnapshot = nextSnapshot;
|
||
memoizedSelection = nextSelection;
|
||
return nextSelection;
|
||
}; // Assigning this to a constant so that Flow knows it can't change.
|
||
|
||
|
||
// Assigning this to a constant so that Flow knows it can't change.
|
||
var maybeGetServerSnapshot = getServerSnapshot === undefined ? null : getServerSnapshot;
|
||
|
||
var getSnapshotWithSelector = function () {
|
||
return memoizedSelector(getSnapshot());
|
||
};
|
||
|
||
var getServerSnapshotWithSelector = maybeGetServerSnapshot === null ? undefined : function () {
|
||
return memoizedSelector(maybeGetServerSnapshot());
|
||
};
|
||
return [getSnapshotWithSelector, getServerSnapshotWithSelector];
|
||
}, [getSnapshot, getServerSnapshot, selector, isEqual]),
|
||
getSelection = _useMemo[0],
|
||
getServerSelection = _useMemo[1];
|
||
|
||
var value = useSyncExternalStore(subscribe, getSelection, getServerSelection);
|
||
useEffect(function () {
|
||
inst.hasValue = true;
|
||
inst.value = value;
|
||
}, [value]);
|
||
useDebugValue(value);
|
||
return value;
|
||
}
|
||
|
||
withSelector_development.useSyncExternalStoreWithSelector = useSyncExternalStoreWithSelector;
|
||
/* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */
|
||
if (
|
||
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' &&
|
||
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop ===
|
||
'function'
|
||
) {
|
||
__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(new Error());
|
||
}
|
||
|
||
})();
|
||
}
|
||
return withSelector_development;
|
||
}
|
||
|
||
if (process.env.NODE_ENV === 'production') {
|
||
withSelector.exports = requireWithSelector_production_min();
|
||
} else {
|
||
withSelector.exports = requireWithSelector_development();
|
||
}
|
||
|
||
var withSelectorExports = withSelector.exports;
|
||
|
||
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
||
/**
|
||
* To synchronize the editor instance with the component state,
|
||
* we need to create a separate instance that is not affected by the component re-renders.
|
||
*/
|
||
class EditorStateManager {
|
||
constructor(initialEditor) {
|
||
this.transactionNumber = 0;
|
||
this.lastTransactionNumber = 0;
|
||
this.subscribers = 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) {
|
||
/**
|
||
* This will force a re-render when the editor state changes.
|
||
* This is to support things like `editor.can().toggleBold()` in components that `useEditor`.
|
||
* This could be more efficient, but it's a good trade-off for now.
|
||
*/
|
||
const fn = () => {
|
||
this.transactionNumber += 1;
|
||
this.subscribers.forEach(callback => callback());
|
||
};
|
||
const currentEditor = this.editor;
|
||
currentEditor.on('transaction', fn);
|
||
return () => {
|
||
currentEditor.off('transaction', fn);
|
||
};
|
||
}
|
||
return undefined;
|
||
}
|
||
}
|
||
/**
|
||
* This hook allows you to watch for changes on the editor instance.
|
||
* It will allow you to select a part of the editor state and re-render the component when it changes.
|
||
* @example
|
||
* ```tsx
|
||
* const editor = useEditor({...options})
|
||
* const { currentSelection } = useEditorState({
|
||
* editor,
|
||
* selector: snapshot => ({ currentSelection: snapshot.editor.state.selection }),
|
||
* })
|
||
*/
|
||
function useEditorState(options) {
|
||
var _a;
|
||
const [editorStateManager] = useState(() => new EditorStateManager(options.editor));
|
||
// Using the `useSyncExternalStore` hook to sync the editor instance with the component state
|
||
const selectedState = withSelectorExports.useSyncExternalStoreWithSelector(editorStateManager.subscribe, editorStateManager.getSnapshot, editorStateManager.getServerSnapshot, options.selector, (_a = options.equalityFn) !== null && _a !== void 0 ? _a : deepEqual);
|
||
useIsomorphicLayoutEffect(() => {
|
||
return editorStateManager.watch(options.editor);
|
||
}, [options.editor, editorStateManager]);
|
||
useDebugValue(selectedState);
|
||
return selectedState;
|
||
}
|
||
|
||
const isDev = process.env.NODE_ENV !== 'production';
|
||
const isSSR = typeof window === 'undefined';
|
||
const isNext = isSSR || Boolean(typeof window !== 'undefined' && window.next);
|
||
/**
|
||
* This class handles the creation, destruction, and re-creation of the editor instance.
|
||
*/
|
||
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 = 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 = 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);
|
||
// Notify all subscribers that the editor instance has been created
|
||
this.subscriptions.forEach(cb => cb());
|
||
}
|
||
getInitialEditor() {
|
||
if (this.options.current.immediatelyRender === undefined) {
|
||
if (isSSR || isNext) {
|
||
// TODO in the next major release, we should throw an error here
|
||
if (isDev) {
|
||
/**
|
||
* Throw an error in development, to make sure the developer is aware that tiptap cannot be SSR'd
|
||
* and that they need to set `immediatelyRender` to `false` to avoid hydration mismatches.
|
||
*/
|
||
console.warn('Tiptap Error: SSR has been detected, please set `immediatelyRender` explicitly to `false` to avoid hydration mismatches.');
|
||
}
|
||
// Best faith effort in production, run the code in the legacy mode to avoid hydration mismatches and errors in production
|
||
return null;
|
||
}
|
||
// Default to immediately rendering when client-side rendering
|
||
return this.createEditor();
|
||
}
|
||
if (this.options.current.immediatelyRender && isSSR && isDev) {
|
||
// Warn in development, to make sure the developer is aware that tiptap cannot be SSR'd, set `immediatelyRender` to `false` to avoid hydration mismatches.
|
||
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 || _b === void 0 ? void 0 : _b.call(_a, ...args); },
|
||
onBlur: (...args) => { var _a, _b; return (_b = (_a = this.options.current).onBlur) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); },
|
||
onCreate: (...args) => { var _a, _b; return (_b = (_a = this.options.current).onCreate) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); },
|
||
onDestroy: (...args) => { var _a, _b; return (_b = (_a = this.options.current).onDestroy) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); },
|
||
onFocus: (...args) => { var _a, _b; return (_b = (_a = this.options.current).onFocus) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); },
|
||
onSelectionUpdate: (...args) => { var _a, _b; return (_b = (_a = this.options.current).onSelectionUpdate) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); },
|
||
onTransaction: (...args) => { var _a, _b; return (_b = (_a = this.options.current).onTransaction) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); },
|
||
onUpdate: (...args) => { var _a, _b; return (_b = (_a = this.options.current).onUpdate) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); },
|
||
onContentError: (...args) => { var _a, _b; return (_b = (_a = this.options.current).onContentError) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); },
|
||
onDrop: (...args) => { var _a, _b; return (_b = (_a = this.options.current).onDrop) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); },
|
||
onPaste: (...args) => { var _a, _b; return (_b = (_a = this.options.current).onPaste) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); },
|
||
};
|
||
const editor = new Editor(optionsToApply);
|
||
// no need to keep track of the event listeners, they will be removed when the editor is destroyed
|
||
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)) {
|
||
// we don't want to compare callbacks, they are always different and only registered once
|
||
return true;
|
||
}
|
||
// We often encourage putting extensions inlined in the options object, so we will do a slightly deeper comparison here
|
||
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 || _a === void 0 ? void 0 : _a[index])) {
|
||
return false;
|
||
}
|
||
return true;
|
||
});
|
||
}
|
||
if (a[key] !== b[key]) {
|
||
// if any of the options have changed, we should update the editor options
|
||
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) {
|
||
// The returned callback will run on each render
|
||
return () => {
|
||
this.isComponentMounted = true;
|
||
// Cleanup any scheduled destructions, since we are currently rendering
|
||
clearTimeout(this.scheduledDestructionTimeout);
|
||
if (this.editor && !this.editor.isDestroyed && deps.length === 0) {
|
||
// if the editor does exist & deps are empty, we don't need to re-initialize the editor generally
|
||
if (!EditorInstanceManager.compareOptions(this.options.current, this.editor.options)) {
|
||
// But, the options are different, so we need to update the editor options
|
||
// Still, this is faster than re-creating the editor
|
||
this.editor.setOptions({
|
||
...this.options.current,
|
||
editable: this.editor.isEditable,
|
||
});
|
||
}
|
||
}
|
||
else {
|
||
// When the editor:
|
||
// - does not yet exist
|
||
// - is destroyed
|
||
// - the deps array changes
|
||
// We need to destroy the editor instance and re-initialize it
|
||
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) {
|
||
// Editor instance already exists
|
||
if (this.previousDeps === null) {
|
||
// If lastDeps has not yet been initialized, reuse the current editor instance
|
||
this.previousDeps = deps;
|
||
return;
|
||
}
|
||
const depsAreEqual = this.previousDeps.length === deps.length
|
||
&& this.previousDeps.every((dep, index) => dep === deps[index]);
|
||
if (depsAreEqual) {
|
||
// deps exist and are equal, no need to recreate
|
||
return;
|
||
}
|
||
}
|
||
if (this.editor && !this.editor.isDestroyed) {
|
||
// Destroy the editor instance if it exists
|
||
this.editor.destroy();
|
||
}
|
||
this.setEditor(this.createEditor());
|
||
// Update the lastDeps to the current deps
|
||
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;
|
||
// Wait two ticks to see if the component is still mounted
|
||
this.scheduledDestructionTimeout = setTimeout(() => {
|
||
if (this.isComponentMounted && this.instanceId === currentInstanceId) {
|
||
// If still mounted on the following tick, with the same instanceId, do not destroy the editor
|
||
if (currentEditor) {
|
||
// just re-apply options as they might have changed
|
||
currentEditor.setOptions(this.options.current);
|
||
}
|
||
return;
|
||
}
|
||
if (currentEditor && !currentEditor.isDestroyed) {
|
||
currentEditor.destroy();
|
||
if (this.instanceId === currentInstanceId) {
|
||
this.setEditor(null);
|
||
}
|
||
}
|
||
// This allows the effect to run again between ticks
|
||
// which may save us from having to re-create the editor
|
||
}, 1);
|
||
}
|
||
}
|
||
function useEditor(options = {}, deps = []) {
|
||
const mostRecentOptions = useRef(options);
|
||
mostRecentOptions.current = options;
|
||
const [instanceManager] = useState(() => new EditorInstanceManager(mostRecentOptions));
|
||
const editor = shimExports.useSyncExternalStore(instanceManager.subscribe, instanceManager.getEditor, instanceManager.getServerSnapshot);
|
||
useDebugValue(editor);
|
||
// This effect will handle creating/updating the editor instance
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
useEffect(instanceManager.onRender(deps));
|
||
// The default behavior is to re-render on each transaction
|
||
// This is legacy behavior that will be removed in future versions
|
||
useEditorState({
|
||
editor,
|
||
selector: ({ transactionNumber }) => {
|
||
if (options.shouldRerenderOnTransaction === false) {
|
||
// This will prevent the editor from re-rendering on each transaction
|
||
return null;
|
||
}
|
||
// This will avoid re-rendering on the first transaction when `immediatelyRender` is set to `true`
|
||
if (options.immediatelyRender && transactionNumber === 0) {
|
||
return 0;
|
||
}
|
||
return transactionNumber + 1;
|
||
},
|
||
});
|
||
return editor;
|
||
}
|
||
|
||
const EditorContext = createContext({
|
||
editor: null,
|
||
});
|
||
const EditorConsumer = EditorContext.Consumer;
|
||
/**
|
||
* A hook to get the current editor instance.
|
||
*/
|
||
const useCurrentEditor = () => useContext(EditorContext);
|
||
/**
|
||
* This is the provider component for the editor.
|
||
* It allows the editor to be accessible across the entire component tree
|
||
* with `useCurrentEditor`.
|
||
*/
|
||
function EditorProvider({ children, slotAfter, slotBefore, editorContainerProps = {}, ...editorOptions }) {
|
||
const editor = useEditor(editorOptions);
|
||
if (!editor) {
|
||
return null;
|
||
}
|
||
return (React.createElement(EditorContext.Provider, { value: { editor } },
|
||
slotBefore,
|
||
React.createElement(EditorConsumer, null, ({ editor: currentEditor }) => (React.createElement(EditorContent, { editor: currentEditor, ...editorContainerProps }))),
|
||
children,
|
||
slotAfter));
|
||
}
|
||
|
||
const BubbleMenu = (props) => {
|
||
const [element, setElement] = useState(null);
|
||
const { editor: currentEditor } = useCurrentEditor();
|
||
useEffect(() => {
|
||
var _a;
|
||
if (!element) {
|
||
return;
|
||
}
|
||
if (((_a = props.editor) === null || _a === void 0 ? void 0 : _a.isDestroyed) || (currentEditor === null || currentEditor === void 0 ? void 0 : currentEditor.isDestroyed)) {
|
||
return;
|
||
}
|
||
const { pluginKey = 'bubbleMenu', editor, tippyOptions = {}, updateDelay, shouldShow = null, } = props;
|
||
const menuEditor = editor || currentEditor;
|
||
if (!menuEditor) {
|
||
console.warn('BubbleMenu component is not rendered inside of an editor component or does not have editor prop.');
|
||
return;
|
||
}
|
||
const plugin = BubbleMenuPlugin({
|
||
updateDelay,
|
||
editor: menuEditor,
|
||
element,
|
||
pluginKey,
|
||
shouldShow,
|
||
tippyOptions,
|
||
});
|
||
menuEditor.registerPlugin(plugin);
|
||
return () => { menuEditor.unregisterPlugin(pluginKey); };
|
||
}, [props.editor, currentEditor, element]);
|
||
return (React.createElement("div", { ref: setElement, className: props.className, style: { visibility: 'hidden' } }, props.children));
|
||
};
|
||
|
||
const FloatingMenu = (props) => {
|
||
const [element, setElement] = useState(null);
|
||
const { editor: currentEditor } = useCurrentEditor();
|
||
useEffect(() => {
|
||
var _a;
|
||
if (!element) {
|
||
return;
|
||
}
|
||
if (((_a = props.editor) === null || _a === void 0 ? void 0 : _a.isDestroyed) || (currentEditor === null || currentEditor === void 0 ? void 0 : currentEditor.isDestroyed)) {
|
||
return;
|
||
}
|
||
const { pluginKey = 'floatingMenu', editor, tippyOptions = {}, shouldShow = null, } = props;
|
||
const menuEditor = editor || currentEditor;
|
||
if (!menuEditor) {
|
||
console.warn('FloatingMenu component is not rendered inside of an editor component or does not have editor prop.');
|
||
return;
|
||
}
|
||
const plugin = FloatingMenuPlugin({
|
||
pluginKey,
|
||
editor: menuEditor,
|
||
element,
|
||
tippyOptions,
|
||
shouldShow,
|
||
});
|
||
menuEditor.registerPlugin(plugin);
|
||
return () => { menuEditor.unregisterPlugin(pluginKey); };
|
||
}, [
|
||
props.editor,
|
||
currentEditor,
|
||
element,
|
||
]);
|
||
return (React.createElement("div", { ref: setElement, className: props.className, style: { visibility: 'hidden' } }, props.children));
|
||
};
|
||
|
||
const ReactNodeViewContext = createContext({
|
||
onDragStart: undefined,
|
||
});
|
||
const useReactNodeView = () => useContext(ReactNodeViewContext);
|
||
|
||
const NodeViewContent = props => {
|
||
const Tag = props.as || 'div';
|
||
const { nodeViewContentRef } = useReactNodeView();
|
||
return (
|
||
// @ts-ignore
|
||
React.createElement(Tag, { ...props, ref: nodeViewContentRef, "data-node-view-content": "", style: {
|
||
whiteSpace: 'pre-wrap',
|
||
...props.style,
|
||
} }));
|
||
};
|
||
|
||
const NodeViewWrapper = React.forwardRef((props, ref) => {
|
||
const { onDragStart } = useReactNodeView();
|
||
const Tag = props.as || 'div';
|
||
return (
|
||
// @ts-ignore
|
||
React.createElement(Tag, { ...props, ref: ref, "data-node-view-wrapper": "", onDragStart: onDragStart, style: {
|
||
whiteSpace: 'normal',
|
||
...props.style,
|
||
} }));
|
||
});
|
||
|
||
/**
|
||
* Check if a component is a class component.
|
||
* @param Component
|
||
* @returns {boolean}
|
||
*/
|
||
function isClassComponent(Component) {
|
||
return !!(typeof Component === 'function'
|
||
&& Component.prototype
|
||
&& Component.prototype.isReactComponent);
|
||
}
|
||
/**
|
||
* Check if a component is a forward ref component.
|
||
* @param Component
|
||
* @returns {boolean}
|
||
*/
|
||
function isForwardRefComponent(Component) {
|
||
return !!(typeof Component === 'object'
|
||
&& Component.$$typeof
|
||
&& (Component.$$typeof.toString() === 'Symbol(react.forward_ref)'
|
||
|| Component.$$typeof.description === 'react.forward_ref'));
|
||
}
|
||
/**
|
||
* Check if a component is a memoized component.
|
||
* @param Component
|
||
* @returns {boolean}
|
||
*/
|
||
function isMemoComponent(Component) {
|
||
return !!(typeof Component === 'object'
|
||
&& Component.$$typeof
|
||
&& (Component.$$typeof.toString() === 'Symbol(react.memo)' || Component.$$typeof.description === 'react.memo'));
|
||
}
|
||
/**
|
||
* Check if a component can safely receive a ref prop.
|
||
* This includes class components, forwardRef components, and memoized components
|
||
* that wrap forwardRef or class components.
|
||
* @param Component
|
||
* @returns {boolean}
|
||
*/
|
||
function canReceiveRef(Component) {
|
||
// Check if it's a class component
|
||
if (isClassComponent(Component)) {
|
||
return true;
|
||
}
|
||
// Check if it's a forwardRef component
|
||
if (isForwardRefComponent(Component)) {
|
||
return true;
|
||
}
|
||
// Check if it's a memoized component
|
||
if (isMemoComponent(Component)) {
|
||
// For memoized components, check the wrapped component
|
||
const wrappedComponent = Component.type;
|
||
if (wrappedComponent) {
|
||
return isClassComponent(wrappedComponent) || isForwardRefComponent(wrappedComponent);
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
/**
|
||
* Check if we're running React 19+ by detecting if function components support ref props
|
||
* @returns {boolean}
|
||
*/
|
||
function isReact19Plus() {
|
||
// React 19 is detected by checking React version if available
|
||
// In practice, we'll use a more conservative approach and assume React 18 behavior
|
||
// unless we can definitively detect React 19
|
||
try {
|
||
// @ts-ignore
|
||
if (version) {
|
||
const majorVersion = parseInt(version.split('.')[0], 10);
|
||
return majorVersion >= 19;
|
||
}
|
||
}
|
||
catch {
|
||
// Fallback to React 18 behavior if we can't determine version
|
||
}
|
||
return false;
|
||
}
|
||
/**
|
||
* The ReactRenderer class. It's responsible for rendering React components inside the editor.
|
||
* @example
|
||
* new ReactRenderer(MyComponent, {
|
||
* editor,
|
||
* props: {
|
||
* foo: 'bar',
|
||
* },
|
||
* as: 'span',
|
||
* })
|
||
*/
|
||
class ReactRenderer {
|
||
/**
|
||
* Immediately creates element and renders the provided React component.
|
||
*/
|
||
constructor(component, { editor, props = {}, as = 'div', className = '', }) {
|
||
this.ref = null;
|
||
this.id = Math.floor(Math.random() * 0xFFFFFFFF).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 the editor is already initialized, we will need to
|
||
// synchronously render the component to ensure it renders
|
||
// together with Prosemirror's rendering.
|
||
if (this.editor.isInitialized) {
|
||
flushSync(() => {
|
||
this.render();
|
||
});
|
||
}
|
||
else {
|
||
queueMicrotask(() => {
|
||
this.render();
|
||
});
|
||
}
|
||
}
|
||
/**
|
||
* Render the React component.
|
||
*/
|
||
render() {
|
||
var _a;
|
||
const Component = this.component;
|
||
const props = this.props;
|
||
const editor = this.editor;
|
||
// Handle ref forwarding with React 18/19 compatibility
|
||
const isReact19 = isReact19Plus();
|
||
const componentCanReceiveRef = canReceiveRef(Component);
|
||
const elementProps = { ...props };
|
||
// Always remove ref if the component cannot receive it (unless React 19+)
|
||
if (elementProps.ref && !(isReact19 || componentCanReceiveRef)) {
|
||
delete elementProps.ref;
|
||
}
|
||
// Only assign our own ref if allowed
|
||
if (!elementProps.ref && (isReact19 || componentCanReceiveRef)) {
|
||
// @ts-ignore - Setting ref prop for compatible components
|
||
elementProps.ref = (ref) => {
|
||
this.ref = ref;
|
||
};
|
||
}
|
||
this.reactElement = React.createElement(Component, { ...elementProps });
|
||
(_a = editor === null || editor === void 0 ? void 0 : editor.contentComponent) === null || _a === void 0 ? void 0 : _a.setRenderer(this.id, this);
|
||
}
|
||
/**
|
||
* Re-renders the React component with new props.
|
||
*/
|
||
updateProps(props = {}) {
|
||
this.props = {
|
||
...this.props,
|
||
...props,
|
||
};
|
||
this.render();
|
||
}
|
||
/**
|
||
* Destroy the React component.
|
||
*/
|
||
destroy() {
|
||
var _a;
|
||
const editor = this.editor;
|
||
(_a = editor === null || editor === void 0 ? void 0 : editor.contentComponent) === null || _a === void 0 ? void 0 : _a.removeRenderer(this.id);
|
||
}
|
||
/**
|
||
* Update the attributes of the element that holds the React component.
|
||
*/
|
||
updateAttributes(attributes) {
|
||
Object.keys(attributes).forEach(key => {
|
||
this.element.setAttribute(key, attributes[key]);
|
||
});
|
||
}
|
||
}
|
||
|
||
class ReactNodeView extends NodeView {
|
||
constructor(component, props, options) {
|
||
super(component, props, options);
|
||
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 = '';
|
||
// For some reason the whiteSpace prop is not inherited properly in Chrome and Safari
|
||
// With this fix it seems to work fine
|
||
// See: https://github.com/ueberdosis/tiptap/issues/1197
|
||
this.contentDOMElement.style.whiteSpace = 'inherit';
|
||
const contentTarget = this.dom.querySelector('[data-node-view-content]');
|
||
if (!contentTarget) {
|
||
return;
|
||
}
|
||
contentTarget.appendChild(this.contentDOMElement);
|
||
}
|
||
}
|
||
/**
|
||
* 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.extension,
|
||
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) {
|
||
// remove the nodeViewWrapper attribute from the element
|
||
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;
|
||
// For performance reasons, we memoize the provider component
|
||
// And all of the things it requires are declared outside of the component, so it doesn't need to re-render
|
||
const ReactNodeViewProvider = memo(componentProps => {
|
||
return (React.createElement(ReactNodeViewContext.Provider, { value: context }, createElement(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 || _a === void 0 ? 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() {
|
||
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 }),
|
||
});
|
||
}
|
||
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,
|
||
});
|
||
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;
|
||
}
|
||
/**
|
||
* 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);
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* Create a React node view renderer.
|
||
*/
|
||
function ReactNodeViewRenderer(component, options) {
|
||
return props => {
|
||
// try to get the parent component
|
||
// this is important for vue devtools to show the component hierarchy correctly
|
||
// maybe it’s `undefined` because <editor-content> isn’t rendered yet
|
||
if (!props.editor.contentComponent) {
|
||
return {};
|
||
}
|
||
return new ReactNodeView(component, props, options);
|
||
};
|
||
}
|
||
|
||
export { BubbleMenu, EditorConsumer, EditorContent, EditorContext, EditorProvider, FloatingMenu, NodeViewContent, NodeViewWrapper, PureEditorContent, ReactNodeView, ReactNodeViewContext, ReactNodeViewRenderer, ReactRenderer, useCurrentEditor, useEditor, useEditorState, useReactNodeView };
|
||
//# sourceMappingURL=index.js.map
|