Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@tiptap/react

Package Overview
Dependencies
Maintainers
5
Versions
237
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@tiptap/react - npm Package Compare versions

Comparing version 2.5.8 to 2.5.9

390

dist/index.js

@@ -129,13 +129,2 @@ import { BubbleMenuPlugin } from '@tiptap/extension-bubble-menu';

class Editor extends Editor$1 {
constructor() {
super(...arguments);
this.contentComponent = null;
}
}
var withSelector = {exports: {}};
var withSelector_production_min = {};
var shim = {exports: {}};

@@ -411,16 +400,21 @@

var hasRequiredShim;
if (process.env.NODE_ENV === 'production') {
shim.exports = requireUseSyncExternalStoreShim_production_min();
} else {
shim.exports = requireUseSyncExternalStoreShim_development();
}
function requireShim () {
if (hasRequiredShim) return shim.exports;
hasRequiredShim = 1;
var shimExports = shim.exports;
if (process.env.NODE_ENV === 'production') {
shim.exports = requireUseSyncExternalStoreShim_production_min();
} else {
shim.exports = requireUseSyncExternalStoreShim_development();
}
return shim.exports;
class Editor extends Editor$1 {
constructor() {
super(...arguments);
this.contentComponent = null;
}
}
var withSelector = {exports: {}};
var withSelector_production_min = {};
/**

@@ -441,3 +435,3 @@ * @license React

hasRequiredWithSelector_production_min = 1;
var h=React,n=requireShim();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;
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]);

@@ -478,3 +472,3 @@ u(function(){f.hasValue=!0;f.value=d;},[d]);w(d);return d};

var React$1 = React;
var shim = requireShim();
var shim = shimExports;

@@ -633,62 +627,66 @@ /**

*/
function makeEditorStateInstance(initialEditor) {
let transactionNumber = 0;
let lastTransactionNumber = 0;
let lastSnapshot = { editor: initialEditor, transactionNumber: 0 };
let editor = initialEditor;
const subscribers = new Set();
const editorInstance = {
/**
* Get the current editor instance.
*/
getSnapshot() {
if (transactionNumber === lastTransactionNumber) {
return lastSnapshot;
}
lastTransactionNumber = transactionNumber;
lastSnapshot = { editor, transactionNumber };
return lastSnapshot;
},
/**
* Always disable the editor on the server-side.
*/
getServerSnapshot() {
return { editor: null, transactionNumber: 0 };
},
/**
* Subscribe to the editor instance's changes.
*/
subscribe(callback) {
subscribers.add(callback);
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 () => {
subscribers.delete(callback);
currentEditor.off('transaction', fn);
};
},
/**
* Watch the editor instance for changes.
*/
watch(nextEditor) {
editor = nextEditor;
if (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 = () => {
transactionNumber += 1;
subscribers.forEach(callback => callback());
};
const currentEditor = editor;
currentEditor.on('transaction', fn);
return () => {
currentEditor.off('transaction', fn);
};
}
},
};
return editorInstance;
}
return undefined;
}
}
function useEditorState(options) {
const [editorInstance] = useState(() => makeEditorStateInstance(options.editor));
const [editorInstance] = useState(() => new EditorStateManager(options.editor));
// Using the `useSyncExternalStore` hook to sync the editor instance with the component state

@@ -698,3 +696,3 @@ const selectedState = withSelectorExports.useSyncExternalStoreWithSelector(editorInstance.subscribe, editorInstance.getSnapshot, editorInstance.getServerSnapshot, options.selector, options.equalityFn);

return editorInstance.watch(options.editor);
}, [options.editor]);
}, [options.editor, editorInstance]);
useDebugValue(selectedState);

@@ -708,21 +706,46 @@ return selectedState;

/**
* Create a new editor instance. And attach event listeners.
* This class handles the creation, destruction, and re-creation of the editor instance.
*/
function createEditor(options) {
const editor = new Editor(options.current);
editor.on('beforeCreate', (...args) => { var _a, _b; return (_b = (_a = options.current).onBeforeCreate) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('blur', (...args) => { var _a, _b; return (_b = (_a = options.current).onBlur) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('create', (...args) => { var _a, _b; return (_b = (_a = options.current).onCreate) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('destroy', (...args) => { var _a, _b; return (_b = (_a = options.current).onDestroy) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('focus', (...args) => { var _a, _b; return (_b = (_a = options.current).onFocus) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('selectionUpdate', (...args) => { var _a, _b; return (_b = (_a = options.current).onSelectionUpdate) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('transaction', (...args) => { var _a, _b; return (_b = (_a = options.current).onTransaction) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('update', (...args) => { var _a, _b; return (_b = (_a = options.current).onUpdate) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('contentError', (...args) => { var _a, _b; return (_b = (_a = options.current).onContentError) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
return editor;
}
function useEditor(options = {}, deps = []) {
const mostRecentOptions = useRef(options);
const [editor, setEditor] = useState(() => {
if (options.immediatelyRender === undefined) {
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.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) {

@@ -741,51 +764,144 @@ // TODO in the next major release, we should throw an error here

// Default to immediately rendering when client-side rendering
return createEditor(mostRecentOptions);
return this.createEditor();
}
if (options.immediatelyRender && isSSR && isDev) {
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 (options.immediatelyRender) {
return createEditor(mostRecentOptions);
if (this.options.current.immediatelyRender) {
return this.createEditor();
}
return null;
});
const mostRecentEditor = useRef(editor);
mostRecentEditor.current = editor;
useDebugValue(editor);
// This effect will handle creating/updating the editor instance
useEffect(() => {
const destroyUnusedEditor = (editorInstance) => {
if (editorInstance) {
// We need to destroy the editor asynchronously to avoid memory leaks
// because the editor instance is still being used in the component.
setTimeout(() => {
// re-use the editor instance if it hasn't been replaced yet
// otherwise, asynchronously destroy the old editor instance
if (editorInstance !== mostRecentEditor.current && !editorInstance.isDestroyed) {
editorInstance.destroy();
}
});
}
/**
* Create a new editor instance. And attach event listeners.
*/
createEditor() {
const editor = new Editor(this.options.current);
// Always call the most recent version of the callback function by default
editor.on('beforeCreate', (...args) => { var _a, _b; return (_b = (_a = this.options.current).onBeforeCreate) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('blur', (...args) => { var _a, _b; return (_b = (_a = this.options.current).onBlur) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('create', (...args) => { var _a, _b; return (_b = (_a = this.options.current).onCreate) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('destroy', (...args) => { var _a, _b; return (_b = (_a = this.options.current).onDestroy) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('focus', (...args) => { var _a, _b; return (_b = (_a = this.options.current).onFocus) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('selectionUpdate', (...args) => { var _a, _b; return (_b = (_a = this.options.current).onSelectionUpdate) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('transaction', (...args) => { var _a, _b; return (_b = (_a = this.options.current).onTransaction) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('update', (...args) => { var _a, _b; return (_b = (_a = this.options.current).onUpdate) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('contentError', (...args) => { var _a, _b; return (_b = (_a = this.options.current).onContentError) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
// 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);
};
}
/**
* 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
// we can fast-path to update the editor options on the existing instance
this.editor.setOptions(this.options.current);
}
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();
};
};
let editorInstance = mostRecentEditor.current;
if (!editorInstance) {
editorInstance = createEditor(mostRecentOptions);
setEditor(editorInstance);
return () => destroyUnusedEditor(editorInstance);
}
/**
* 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 (!Array.isArray(deps) || deps.length === 0) {
// if the editor does exist & deps are empty, we don't need to re-initialize the editor
// we can fast-path to update the editor options on the existing instance
editorInstance.setOptions(options);
return () => destroyUnusedEditor(editorInstance);
if (this.editor && !this.editor.isDestroyed) {
// Destroy the editor instance if it exists
this.editor.destroy();
}
// We need to destroy the editor instance and re-initialize it
// when the deps array changes
editorInstance.destroy();
// the deps array is used to re-initialize the editor instance
editorInstance = createEditor(mostRecentOptions);
setEditor(editorInstance);
return () => destroyUnusedEditor(editorInstance);
}, deps);
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 a tick to see if the component is still mounted
this.scheduledDestructionTimeout = setTimeout(() => {
if (this.isComponentMounted && this.instanceId === currentInstanceId) {
// If still mounted on the next 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);
}
}
}, 0);
}
}
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

@@ -792,0 +908,0 @@ // This is legacy behavior that will be removed in future versions

@@ -128,13 +128,2 @@ (function (global, factory) {

class Editor extends core.Editor {
constructor() {
super(...arguments);
this.contentComponent = null;
}
}
var withSelector = {exports: {}};
var withSelector_production_min = {};
var shim = {exports: {}};

@@ -410,16 +399,21 @@

var hasRequiredShim;
if (process.env.NODE_ENV === 'production') {
shim.exports = requireUseSyncExternalStoreShim_production_min();
} else {
shim.exports = requireUseSyncExternalStoreShim_development();
}
function requireShim () {
if (hasRequiredShim) return shim.exports;
hasRequiredShim = 1;
var shimExports = shim.exports;
if (process.env.NODE_ENV === 'production') {
shim.exports = requireUseSyncExternalStoreShim_production_min();
} else {
shim.exports = requireUseSyncExternalStoreShim_development();
}
return shim.exports;
class Editor extends core.Editor {
constructor() {
super(...arguments);
this.contentComponent = null;
}
}
var withSelector = {exports: {}};
var withSelector_production_min = {};
/**

@@ -440,3 +434,3 @@ * @license React

hasRequiredWithSelector_production_min = 1;
var h=React,n=requireShim();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;
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]);

@@ -477,3 +471,3 @@ u(function(){f.hasValue=!0;f.value=d;},[d]);w(d);return d};

var React$1 = React;
var shim = requireShim();
var shim = shimExports;

@@ -632,62 +626,66 @@ /**

*/
function makeEditorStateInstance(initialEditor) {
let transactionNumber = 0;
let lastTransactionNumber = 0;
let lastSnapshot = { editor: initialEditor, transactionNumber: 0 };
let editor = initialEditor;
const subscribers = new Set();
const editorInstance = {
/**
* Get the current editor instance.
*/
getSnapshot() {
if (transactionNumber === lastTransactionNumber) {
return lastSnapshot;
}
lastTransactionNumber = transactionNumber;
lastSnapshot = { editor, transactionNumber };
return lastSnapshot;
},
/**
* Always disable the editor on the server-side.
*/
getServerSnapshot() {
return { editor: null, transactionNumber: 0 };
},
/**
* Subscribe to the editor instance's changes.
*/
subscribe(callback) {
subscribers.add(callback);
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 () => {
subscribers.delete(callback);
currentEditor.off('transaction', fn);
};
},
/**
* Watch the editor instance for changes.
*/
watch(nextEditor) {
editor = nextEditor;
if (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 = () => {
transactionNumber += 1;
subscribers.forEach(callback => callback());
};
const currentEditor = editor;
currentEditor.on('transaction', fn);
return () => {
currentEditor.off('transaction', fn);
};
}
},
};
return editorInstance;
}
return undefined;
}
}
function useEditorState(options) {
const [editorInstance] = React.useState(() => makeEditorStateInstance(options.editor));
const [editorInstance] = React.useState(() => new EditorStateManager(options.editor));
// Using the `useSyncExternalStore` hook to sync the editor instance with the component state

@@ -697,3 +695,3 @@ const selectedState = withSelectorExports.useSyncExternalStoreWithSelector(editorInstance.subscribe, editorInstance.getSnapshot, editorInstance.getServerSnapshot, options.selector, options.equalityFn);

return editorInstance.watch(options.editor);
}, [options.editor]);
}, [options.editor, editorInstance]);
React.useDebugValue(selectedState);

@@ -707,21 +705,46 @@ return selectedState;

/**
* Create a new editor instance. And attach event listeners.
* This class handles the creation, destruction, and re-creation of the editor instance.
*/
function createEditor(options) {
const editor = new Editor(options.current);
editor.on('beforeCreate', (...args) => { var _a, _b; return (_b = (_a = options.current).onBeforeCreate) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('blur', (...args) => { var _a, _b; return (_b = (_a = options.current).onBlur) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('create', (...args) => { var _a, _b; return (_b = (_a = options.current).onCreate) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('destroy', (...args) => { var _a, _b; return (_b = (_a = options.current).onDestroy) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('focus', (...args) => { var _a, _b; return (_b = (_a = options.current).onFocus) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('selectionUpdate', (...args) => { var _a, _b; return (_b = (_a = options.current).onSelectionUpdate) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('transaction', (...args) => { var _a, _b; return (_b = (_a = options.current).onTransaction) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('update', (...args) => { var _a, _b; return (_b = (_a = options.current).onUpdate) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('contentError', (...args) => { var _a, _b; return (_b = (_a = options.current).onContentError) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
return editor;
}
function useEditor(options = {}, deps = []) {
const mostRecentOptions = React.useRef(options);
const [editor, setEditor] = React.useState(() => {
if (options.immediatelyRender === undefined) {
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.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) {

@@ -740,51 +763,144 @@ // TODO in the next major release, we should throw an error here

// Default to immediately rendering when client-side rendering
return createEditor(mostRecentOptions);
return this.createEditor();
}
if (options.immediatelyRender && isSSR && isDev) {
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 (options.immediatelyRender) {
return createEditor(mostRecentOptions);
if (this.options.current.immediatelyRender) {
return this.createEditor();
}
return null;
});
const mostRecentEditor = React.useRef(editor);
mostRecentEditor.current = editor;
React.useDebugValue(editor);
// This effect will handle creating/updating the editor instance
React.useEffect(() => {
const destroyUnusedEditor = (editorInstance) => {
if (editorInstance) {
// We need to destroy the editor asynchronously to avoid memory leaks
// because the editor instance is still being used in the component.
setTimeout(() => {
// re-use the editor instance if it hasn't been replaced yet
// otherwise, asynchronously destroy the old editor instance
if (editorInstance !== mostRecentEditor.current && !editorInstance.isDestroyed) {
editorInstance.destroy();
}
});
}
/**
* Create a new editor instance. And attach event listeners.
*/
createEditor() {
const editor = new Editor(this.options.current);
// Always call the most recent version of the callback function by default
editor.on('beforeCreate', (...args) => { var _a, _b; return (_b = (_a = this.options.current).onBeforeCreate) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('blur', (...args) => { var _a, _b; return (_b = (_a = this.options.current).onBlur) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('create', (...args) => { var _a, _b; return (_b = (_a = this.options.current).onCreate) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('destroy', (...args) => { var _a, _b; return (_b = (_a = this.options.current).onDestroy) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('focus', (...args) => { var _a, _b; return (_b = (_a = this.options.current).onFocus) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('selectionUpdate', (...args) => { var _a, _b; return (_b = (_a = this.options.current).onSelectionUpdate) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('transaction', (...args) => { var _a, _b; return (_b = (_a = this.options.current).onTransaction) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('update', (...args) => { var _a, _b; return (_b = (_a = this.options.current).onUpdate) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
editor.on('contentError', (...args) => { var _a, _b; return (_b = (_a = this.options.current).onContentError) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); });
// 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);
};
}
/**
* 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
// we can fast-path to update the editor options on the existing instance
this.editor.setOptions(this.options.current);
}
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();
};
};
let editorInstance = mostRecentEditor.current;
if (!editorInstance) {
editorInstance = createEditor(mostRecentOptions);
setEditor(editorInstance);
return () => destroyUnusedEditor(editorInstance);
}
/**
* 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 (!Array.isArray(deps) || deps.length === 0) {
// if the editor does exist & deps are empty, we don't need to re-initialize the editor
// we can fast-path to update the editor options on the existing instance
editorInstance.setOptions(options);
return () => destroyUnusedEditor(editorInstance);
if (this.editor && !this.editor.isDestroyed) {
// Destroy the editor instance if it exists
this.editor.destroy();
}
// We need to destroy the editor instance and re-initialize it
// when the deps array changes
editorInstance.destroy();
// the deps array is used to re-initialize the editor instance
editorInstance = createEditor(mostRecentOptions);
setEditor(editorInstance);
return () => destroyUnusedEditor(editorInstance);
}, deps);
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 a tick to see if the component is still mounted
this.scheduledDestructionTimeout = setTimeout(() => {
if (this.isComponentMounted && this.instanceId === currentInstanceId) {
// If still mounted on the next 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);
}
}
}, 0);
}
}
function useEditor(options = {}, deps = []) {
const mostRecentOptions = React.useRef(options);
mostRecentOptions.current = options;
const [instanceManager] = React.useState(() => new EditorInstanceManager(mostRecentOptions));
const editor = shimExports.useSyncExternalStore(instanceManager.subscribe, instanceManager.getEditor, instanceManager.getServerSnapshot);
React.useDebugValue(editor);
// This effect will handle creating/updating the editor instance
// eslint-disable-next-line react-hooks/exhaustive-deps
React.useEffect(instanceManager.onRender(deps));
// The default behavior is to re-render on each transaction

@@ -791,0 +907,0 @@ // This is legacy behavior that will be removed in future versions

@@ -341,4 +341,4 @@ import { Plugin, Transaction } from '@tiptap/pm/state';

static create<O = any, S = any>(config?: Partial<ExtensionConfig<O, S>>): Extension<O, S>;
configure(options?: Partial<Options>): Extension<any, any>;
configure(options?: Partial<Options>): Extension<Options, Storage>;
extend<ExtendedOptions = Options, ExtendedStorage = Storage>(extendedConfig?: Partial<ExtensionConfig<ExtendedOptions, ExtendedStorage>>): Extension<ExtendedOptions, ExtendedStorage>;
}
import { Node as ProseMirrorNode } from '@tiptap/pm/model';
/**
* Returns true if the given node is empty.
* When `checkChildren` is true (default), it will also check if all children are empty.
* Returns true if the given prosemirror node is empty.
*/
export declare function isNodeEmpty(node: ProseMirrorNode, { checkChildren }?: {
checkChildren: boolean;
export declare function isNodeEmpty(node: ProseMirrorNode, { checkChildren, ignoreWhitespace, }?: {
/**
* When true (default), it will also check if all children are empty.
*/
checkChildren?: boolean;
/**
* When true, it will ignore whitespace when checking for emptiness.
*/
ignoreWhitespace?: boolean;
}): boolean;

@@ -445,3 +445,3 @@ import { DOMOutputSpec, Mark as ProseMirrorMark, MarkSpec, MarkType } from '@tiptap/pm/model';

static create<O = any, S = any>(config?: Partial<MarkConfig<O, S>>): Mark<O, S>;
configure(options?: Partial<Options>): Mark<any, any>;
configure(options?: Partial<Options>): Mark<Options, Storage>;
extend<ExtendedOptions = Options, ExtendedStorage = Storage>(extendedConfig?: Partial<MarkConfig<ExtendedOptions, ExtendedStorage>>): Mark<ExtendedOptions, ExtendedStorage>;

@@ -448,0 +448,0 @@ static handleExit({ editor, mark }: {

@@ -609,4 +609,4 @@ import { DOMOutputSpec, Node as ProseMirrorNode, NodeSpec, NodeType } from '@tiptap/pm/model';

static create<O = any, S = any>(config?: Partial<NodeConfig<O, S>>): Node<O, S>;
configure(options?: Partial<Options>): Node<any, any>;
configure(options?: Partial<Options>): Node<Options, Storage>;
extend<ExtendedOptions = Options, ExtendedStorage = Storage>(extendedConfig?: Partial<NodeConfig<ExtendedOptions, ExtendedStorage>>): Node<ExtendedOptions, ExtendedStorage>;
}
{
"name": "@tiptap/react",
"description": "React components for tiptap",
"version": "2.5.8",
"version": "2.5.9",
"homepage": "https://tiptap.dev",

@@ -32,4 +32,4 @@ "keywords": [

"dependencies": {
"@tiptap/extension-bubble-menu": "^2.5.8",
"@tiptap/extension-floating-menu": "^2.5.8",
"@tiptap/extension-bubble-menu": "^2.5.9",
"@tiptap/extension-floating-menu": "^2.5.9",
"@types/use-sync-external-store": "^0.0.6",

@@ -39,4 +39,4 @@ "use-sync-external-store": "^1.2.2"

"devDependencies": {
"@tiptap/core": "^2.5.8",
"@tiptap/pm": "^2.5.8",
"@tiptap/core": "^2.5.9",
"@tiptap/pm": "^2.5.9",
"@types/react": "^18.2.14",

@@ -48,4 +48,4 @@ "@types/react-dom": "^18.2.6",

"peerDependencies": {
"@tiptap/core": "^2.5.8",
"@tiptap/pm": "^2.5.8",
"@tiptap/core": "^2.5.9",
"@tiptap/pm": "^2.5.9",
"react": "^17.0.0 || ^18.0.0",

@@ -52,0 +52,0 @@ "react-dom": "^17.0.0 || ^18.0.0"

import { EditorOptions } from '@tiptap/core'
import {
DependencyList, MutableRefObject,
useDebugValue, useEffect, useRef, useState,
DependencyList,
MutableRefObject,
useDebugValue,
useEffect,
useRef,
useState,
} from 'react'
import { useSyncExternalStore } from 'use-sync-external-store/shim'

@@ -34,51 +39,65 @@ import { Editor } from './Editor.js'

/**
* Create a new editor instance. And attach event listeners.
* This class handles the creation, destruction, and re-creation of the editor instance.
*/
function createEditor(options: MutableRefObject<UseEditorOptions>): Editor {
const editor = new Editor(options.current)
class EditorInstanceManager {
/**
* The current editor instance.
*/
private editor: Editor | null = null
editor.on('beforeCreate', (...args) => options.current.onBeforeCreate?.(...args))
editor.on('blur', (...args) => options.current.onBlur?.(...args))
editor.on('create', (...args) => options.current.onCreate?.(...args))
editor.on('destroy', (...args) => options.current.onDestroy?.(...args))
editor.on('focus', (...args) => options.current.onFocus?.(...args))
editor.on('selectionUpdate', (...args) => options.current.onSelectionUpdate?.(...args))
editor.on('transaction', (...args) => options.current.onTransaction?.(...args))
editor.on('update', (...args) => options.current.onUpdate?.(...args))
editor.on('contentError', (...args) => options.current.onContentError?.(...args))
/**
* The most recent options to apply to the editor.
*/
private options: MutableRefObject<UseEditorOptions>
return editor
}
/**
* The subscriptions to notify when the editor instance
* has been created or destroyed.
*/
private subscriptions = new Set<() => void>()
/**
* This hook allows you to create an editor instance.
* @param options The editor options
* @param deps The dependencies to watch for changes
* @returns The editor instance
* @example const editor = useEditor({ extensions: [...] })
*/
export function useEditor(
options: UseEditorOptions & { immediatelyRender: true },
deps?: DependencyList
): Editor;
/**
* A timeout to destroy the editor if it was not mounted within a time frame.
*/
private scheduledDestructionTimeout: ReturnType<typeof setTimeout> | undefined
/**
* This hook allows you to create an editor instance.
* @param options The editor options
* @param deps The dependencies to watch for changes
* @returns The editor instance
* @example const editor = useEditor({ extensions: [...] })
*/
export function useEditor(
options?: UseEditorOptions,
deps?: DependencyList
): Editor | null;
/**
* Whether the editor has been mounted.
*/
private isComponentMounted = false
export function useEditor(
options: UseEditorOptions = {},
deps: DependencyList = [],
): Editor | null {
const mostRecentOptions = useRef(options)
const [editor, setEditor] = useState(() => {
if (options.immediatelyRender === undefined) {
/**
* The most recent dependencies array.
*/
private previousDeps: DependencyList | null = null
/**
* The unique instance ID. This is used to identify the editor instance. And will be re-generated for each new instance.
*/
public instanceId = ''
constructor(options: MutableRefObject<UseEditorOptions>) {
this.options = options
this.subscriptions = new Set<() => void>()
this.setEditor(this.getInitialEditor())
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)
}
private setEditor(editor: Editor | null) {
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())
}
private getInitialEditor() {
if (this.options.current.immediatelyRender === undefined) {
if (isSSR || isNext) {

@@ -101,6 +120,6 @@ // TODO in the next major release, we should throw an error here

// Default to immediately rendering when client-side rendering
return createEditor(mostRecentOptions)
return this.createEditor()
}
if (options.immediatelyRender && isSSR && isDev) {
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.

@@ -112,57 +131,192 @@ throw new Error(

if (options.immediatelyRender) {
return createEditor(mostRecentOptions)
if (this.options.current.immediatelyRender) {
return this.createEditor()
}
return null
})
const mostRecentEditor = useRef<Editor | null>(editor)
}
mostRecentEditor.current = editor
/**
* Create a new editor instance. And attach event listeners.
*/
private createEditor(): Editor {
const editor = new Editor(this.options.current)
useDebugValue(editor)
// Always call the most recent version of the callback function by default
editor.on('beforeCreate', (...args) => this.options.current.onBeforeCreate?.(...args))
editor.on('blur', (...args) => this.options.current.onBlur?.(...args))
editor.on('create', (...args) => this.options.current.onCreate?.(...args))
editor.on('destroy', (...args) => this.options.current.onDestroy?.(...args))
editor.on('focus', (...args) => this.options.current.onFocus?.(...args))
editor.on('selectionUpdate', (...args) => this.options.current.onSelectionUpdate?.(...args))
editor.on('transaction', (...args) => this.options.current.onTransaction?.(...args))
editor.on('update', (...args) => this.options.current.onUpdate?.(...args))
editor.on('contentError', (...args) => this.options.current.onContentError?.(...args))
// This effect will handle creating/updating the editor instance
useEffect(() => {
const destroyUnusedEditor = (editorInstance: Editor | null) => {
if (editorInstance) {
// We need to destroy the editor asynchronously to avoid memory leaks
// because the editor instance is still being used in the component.
// no need to keep track of the event listeners, they will be removed when the editor is destroyed
setTimeout(() => {
// re-use the editor instance if it hasn't been replaced yet
// otherwise, asynchronously destroy the old editor instance
if (editorInstance !== mostRecentEditor.current && !editorInstance.isDestroyed) {
editorInstance.destroy()
}
})
}
return editor
}
/**
* Get the current editor instance.
*/
getEditor(): Editor | null {
return this.editor
}
/**
* Always disable the editor on the server-side.
*/
getServerSnapshot(): null {
return null
}
/**
* Subscribe to the editor instance's changes.
*/
subscribe(onStoreChange: () => void) {
this.subscriptions.add(onStoreChange)
return () => {
this.subscriptions.delete(onStoreChange)
}
}
let editorInstance = mostRecentEditor.current
/**
* 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: DependencyList) {
// 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 (!editorInstance) {
editorInstance = createEditor(mostRecentOptions)
setEditor(editorInstance)
return () => destroyUnusedEditor(editorInstance)
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
// we can fast-path to update the editor options on the existing instance
this.editor.setOptions(this.options.current)
} 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()
}
}
}
if (!Array.isArray(deps) || deps.length === 0) {
// if the editor does exist & deps are empty, we don't need to re-initialize the editor
// we can fast-path to update the editor options on the existing instance
editorInstance.setOptions(options)
/**
* Recreate the editor instance if the dependencies have changed.
*/
private refreshEditorInstance(deps: DependencyList) {
return () => destroyUnusedEditor(editorInstance)
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
}
}
// We need to destroy the editor instance and re-initialize it
// when the deps array changes
editorInstance.destroy()
if (this.editor && !this.editor.isDestroyed) {
// Destroy the editor instance if it exists
this.editor.destroy()
}
// the deps array is used to re-initialize the editor instance
editorInstance = createEditor(mostRecentOptions)
setEditor(editorInstance)
return () => destroyUnusedEditor(editorInstance)
}, deps)
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.
*/
private scheduleDestroy() {
const currentInstanceId = this.instanceId
const currentEditor = this.editor
// Wait a tick to see if the component is still mounted
this.scheduledDestructionTimeout = setTimeout(() => {
if (this.isComponentMounted && this.instanceId === currentInstanceId) {
// If still mounted on the next 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)
}
}
}, 0)
}
}
/**
* This hook allows you to create an editor instance.
* @param options The editor options
* @param deps The dependencies to watch for changes
* @returns The editor instance
* @example const editor = useEditor({ extensions: [...] })
*/
export function useEditor(
options: UseEditorOptions & { immediatelyRender: true },
deps?: DependencyList
): Editor;
/**
* This hook allows you to create an editor instance.
* @param options The editor options
* @param deps The dependencies to watch for changes
* @returns The editor instance
* @example const editor = useEditor({ extensions: [...] })
*/
export function useEditor(options?: UseEditorOptions, deps?: DependencyList): Editor | null;
export function useEditor(
options: UseEditorOptions = {},
deps: DependencyList = [],
): Editor | null {
const mostRecentOptions = useRef(options)
mostRecentOptions.current = options
const [instanceManager] = useState(() => new EditorInstanceManager(mostRecentOptions))
const editor = 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

@@ -169,0 +323,0 @@ // This is legacy behavior that will be removed in future versions

@@ -33,64 +33,79 @@ import { useDebugValue, useEffect, useState } from 'react'

*/
function makeEditorStateInstance<TEditor extends Editor | null = Editor | null>(initialEditor: TEditor) {
let transactionNumber = 0
let lastTransactionNumber = 0
let lastSnapshot: EditorStateSnapshot<TEditor> = { editor: initialEditor, transactionNumber: 0 }
let editor = initialEditor
const subscribers = new Set<() => void>()
class EditorStateManager<TEditor extends Editor | null = Editor | null> {
private transactionNumber = 0
const editorInstance = {
/**
* Get the current editor instance.
*/
getSnapshot(): EditorStateSnapshot<TEditor> {
if (transactionNumber === lastTransactionNumber) {
return lastSnapshot
private lastTransactionNumber = 0
private lastSnapshot: EditorStateSnapshot<TEditor>
private editor: TEditor
private subscribers = new Set<() => void>()
constructor(initialEditor: TEditor) {
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(): EditorStateSnapshot<TEditor> {
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(): EditorStateSnapshot<null> {
return { editor: null, transactionNumber: 0 }
}
/**
* Subscribe to the editor instance's changes.
*/
subscribe(callback: () => void): () => void {
this.subscribers.add(callback)
return () => {
this.subscribers.delete(callback)
}
}
/**
* Watch the editor instance for changes.
*/
watch(nextEditor: Editor | null): undefined | (() => void) {
this.editor = nextEditor as TEditor
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())
}
lastTransactionNumber = transactionNumber
lastSnapshot = { editor, transactionNumber }
return lastSnapshot
},
/**
* Always disable the editor on the server-side.
*/
getServerSnapshot(): EditorStateSnapshot<null> {
return { editor: null, transactionNumber: 0 }
},
/**
* Subscribe to the editor instance's changes.
*/
subscribe(callback: () => void) {
subscribers.add(callback)
const currentEditor = this.editor
currentEditor.on('transaction', fn)
return () => {
subscribers.delete(callback)
currentEditor.off('transaction', fn)
}
},
/**
* Watch the editor instance for changes.
*/
watch(nextEditor: Editor | null) {
editor = nextEditor as TEditor
}
if (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 = () => {
transactionNumber += 1
subscribers.forEach(callback => callback())
}
const currentEditor = editor
currentEditor.on('transaction', fn)
return () => {
currentEditor.off('transaction', fn)
}
}
},
return undefined
}
return editorInstance
}

@@ -108,3 +123,3 @@

): TSelectorResult | null {
const [editorInstance] = useState(() => makeEditorStateInstance(options.editor))
const [editorInstance] = useState(() => new EditorStateManager(options.editor))

@@ -122,3 +137,3 @@ // Using the `useSyncExternalStore` hook to sync the editor instance with the component state

return editorInstance.watch(options.editor)
}, [options.editor])
}, [options.editor, editorInstance])

@@ -125,0 +140,0 @@ useDebugValue(selectedState)

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc