@tiptap/react
Advanced tools
Comparing version 2.5.9 to 2.6.0
@@ -8,123 +8,2 @@ import { BubbleMenuPlugin } from '@tiptap/extension-bubble-menu'; | ||
const mergeRefs = (...refs) => { | ||
return (node) => { | ||
refs.forEach(ref => { | ||
if (typeof ref === 'function') { | ||
ref(node); | ||
} | ||
else if (ref) { | ||
ref.current = node; | ||
} | ||
}); | ||
}; | ||
}; | ||
const Portals = ({ renderers }) => { | ||
return (React.createElement(React.Fragment, null, Object.entries(renderers).map(([key, renderer]) => { | ||
return ReactDOM.createPortal(renderer.reactElement, renderer.element, key); | ||
}))); | ||
}; | ||
class PureEditorContent extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
this.editorContentRef = React.createRef(); | ||
this.initialized = false; | ||
this.state = { | ||
renderers: {}, | ||
}; | ||
} | ||
componentDidMount() { | ||
this.init(); | ||
} | ||
componentDidUpdate() { | ||
this.init(); | ||
} | ||
init() { | ||
const { editor } = this.props; | ||
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 = this; | ||
editor.createNodeViews(); | ||
this.initialized = true; | ||
} | ||
} | ||
maybeFlushSync(fn) { | ||
// Avoid calling flushSync until the editor is initialized. | ||
// Initialization happens during the componentDidMount or componentDidUpdate | ||
// lifecycle methods, and React doesn't allow calling flushSync from inside | ||
// a lifecycle method. | ||
if (this.initialized) { | ||
flushSync(fn); | ||
} | ||
else { | ||
fn(); | ||
} | ||
} | ||
setRenderer(id, renderer) { | ||
this.maybeFlushSync(() => { | ||
this.setState(({ renderers }) => ({ | ||
renderers: { | ||
...renderers, | ||
[id]: renderer, | ||
}, | ||
})); | ||
}); | ||
} | ||
removeRenderer(id) { | ||
this.maybeFlushSync(() => { | ||
this.setState(({ renderers }) => { | ||
const nextRenderers = { ...renderers }; | ||
delete nextRenderers[id]; | ||
return { renderers: nextRenderers }; | ||
}); | ||
}); | ||
} | ||
componentWillUnmount() { | ||
const { editor } = this.props; | ||
if (!editor) { | ||
return; | ||
} | ||
this.initialized = false; | ||
if (!editor.isDestroyed) { | ||
editor.view.setProps({ | ||
nodeViews: {}, | ||
}); | ||
} | ||
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 }), | ||
React.createElement(Portals, { renderers: this.state.renderers }))); | ||
} | ||
} | ||
// 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(); | ||
}, [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 shim = {exports: {}}; | ||
@@ -408,8 +287,157 @@ | ||
class Editor extends Editor$1 { | ||
constructor() { | ||
super(...arguments); | ||
this.contentComponent = null; | ||
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; | ||
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; | ||
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(); | ||
}, [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); | ||
@@ -774,13 +802,16 @@ var withSelector = {exports: {}}; | ||
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); }); | ||
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); }, | ||
}; | ||
const editor = new Editor$1(optionsToApply); | ||
// no need to keep track of the event listeners, they will be removed when the editor is destroyed | ||
@@ -977,2 +1008,9 @@ return editor; | ||
class Editor extends Editor$1 { | ||
constructor() { | ||
super(...arguments); | ||
this.contentComponent = null; | ||
} | ||
} | ||
const FloatingMenu = (props) => { | ||
@@ -1029,3 +1067,5 @@ const [element, setElement] = useState(null); | ||
const Tag = props.as || 'div'; | ||
return (React.createElement(Tag, { ...props, ref: ref, "data-node-view-wrapper": "", onDragStart: onDragStart, style: { | ||
return ( | ||
// @ts-ignore | ||
React.createElement(Tag, { ...props, ref: ref, "data-node-view-wrapper": "", onDragStart: onDragStart, style: { | ||
whiteSpace: 'normal', | ||
@@ -1084,3 +1124,12 @@ ...props.style, | ||
} | ||
this.render(); | ||
if (this.editor.isInitialized) { | ||
// On first render, we need to flush the render synchronously | ||
// Renders afterwards can be async, but this fixes a cursor positioning issue | ||
flushSync(() => { | ||
this.render(); | ||
}); | ||
} | ||
else { | ||
this.render(); | ||
} | ||
} | ||
@@ -1096,3 +1145,3 @@ render() { | ||
} | ||
this.reactElement = React.createElement(Component, { ...props }); | ||
this.reactElement = React.createElement(Component, props); | ||
(_b = (_a = this.editor) === null || _a === void 0 ? void 0 : _a.contentComponent) === null || _b === void 0 ? void 0 : _b.setRenderer(this.id, this); | ||
@@ -1131,14 +1180,15 @@ } | ||
} | ||
const ReactNodeViewProvider = componentProps => { | ||
const Component = this.component; | ||
const onDragStart = this.onDragStart.bind(this); | ||
const nodeViewContentRef = element => { | ||
if (element && this.contentDOMElement && element.firstChild !== this.contentDOMElement) { | ||
element.appendChild(this.contentDOMElement); | ||
} | ||
}; | ||
return (React.createElement(React.Fragment, null, | ||
React.createElement(ReactNodeViewContext.Provider, { value: { onDragStart, nodeViewContentRef } }, | ||
React.createElement(Component, { ...componentProps })))); | ||
const onDragStart = this.onDragStart.bind(this); | ||
const nodeViewContentRef = element => { | ||
if (element && this.contentDOMElement && element.firstChild !== this.contentDOMElement) { | ||
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 = React.memo(componentProps => { | ||
return (React.createElement(ReactNodeViewContext.Provider, { value: context }, React.createElement(Component, componentProps))); | ||
}); | ||
ReactNodeViewProvider.displayName = 'ReactNodeView'; | ||
@@ -1145,0 +1195,0 @@ if (this.node.isLeaf) { |
@@ -7,123 +7,2 @@ (function (global, factory) { | ||
const mergeRefs = (...refs) => { | ||
return (node) => { | ||
refs.forEach(ref => { | ||
if (typeof ref === 'function') { | ||
ref(node); | ||
} | ||
else if (ref) { | ||
ref.current = node; | ||
} | ||
}); | ||
}; | ||
}; | ||
const Portals = ({ renderers }) => { | ||
return (React.createElement(React.Fragment, null, Object.entries(renderers).map(([key, renderer]) => { | ||
return ReactDOM.createPortal(renderer.reactElement, renderer.element, key); | ||
}))); | ||
}; | ||
class PureEditorContent extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
this.editorContentRef = React.createRef(); | ||
this.initialized = false; | ||
this.state = { | ||
renderers: {}, | ||
}; | ||
} | ||
componentDidMount() { | ||
this.init(); | ||
} | ||
componentDidUpdate() { | ||
this.init(); | ||
} | ||
init() { | ||
const { editor } = this.props; | ||
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 = this; | ||
editor.createNodeViews(); | ||
this.initialized = true; | ||
} | ||
} | ||
maybeFlushSync(fn) { | ||
// Avoid calling flushSync until the editor is initialized. | ||
// Initialization happens during the componentDidMount or componentDidUpdate | ||
// lifecycle methods, and React doesn't allow calling flushSync from inside | ||
// a lifecycle method. | ||
if (this.initialized) { | ||
ReactDOM.flushSync(fn); | ||
} | ||
else { | ||
fn(); | ||
} | ||
} | ||
setRenderer(id, renderer) { | ||
this.maybeFlushSync(() => { | ||
this.setState(({ renderers }) => ({ | ||
renderers: { | ||
...renderers, | ||
[id]: renderer, | ||
}, | ||
})); | ||
}); | ||
} | ||
removeRenderer(id) { | ||
this.maybeFlushSync(() => { | ||
this.setState(({ renderers }) => { | ||
const nextRenderers = { ...renderers }; | ||
delete nextRenderers[id]; | ||
return { renderers: nextRenderers }; | ||
}); | ||
}); | ||
} | ||
componentWillUnmount() { | ||
const { editor } = this.props; | ||
if (!editor) { | ||
return; | ||
} | ||
this.initialized = false; | ||
if (!editor.isDestroyed) { | ||
editor.view.setProps({ | ||
nodeViews: {}, | ||
}); | ||
} | ||
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 }), | ||
React.createElement(Portals, { renderers: this.state.renderers }))); | ||
} | ||
} | ||
// EditorContent should be re-created whenever the Editor instance changes | ||
const EditorContentWithKey = React.forwardRef((props, ref) => { | ||
const key = React.useMemo(() => { | ||
return Math.floor(Math.random() * 0xFFFFFFFF).toString(); | ||
}, [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 shim = {exports: {}}; | ||
@@ -407,8 +286,157 @@ | ||
class Editor extends core.Editor { | ||
constructor() { | ||
super(...arguments); | ||
this.contentComponent = null; | ||
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; | ||
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; | ||
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 = React.forwardRef((props, ref) => { | ||
const key = React.useMemo(() => { | ||
return Math.floor(Math.random() * 0xffffffff).toString(); | ||
}, [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); | ||
@@ -773,13 +801,16 @@ var withSelector = {exports: {}}; | ||
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); }); | ||
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); }, | ||
}; | ||
const editor = new core.Editor(optionsToApply); | ||
// no need to keep track of the event listeners, they will be removed when the editor is destroyed | ||
@@ -976,2 +1007,9 @@ return editor; | ||
class Editor extends core.Editor { | ||
constructor() { | ||
super(...arguments); | ||
this.contentComponent = null; | ||
} | ||
} | ||
const FloatingMenu = (props) => { | ||
@@ -1028,3 +1066,5 @@ const [element, setElement] = React.useState(null); | ||
const Tag = props.as || 'div'; | ||
return (React.createElement(Tag, { ...props, ref: ref, "data-node-view-wrapper": "", onDragStart: onDragStart, style: { | ||
return ( | ||
// @ts-ignore | ||
React.createElement(Tag, { ...props, ref: ref, "data-node-view-wrapper": "", onDragStart: onDragStart, style: { | ||
whiteSpace: 'normal', | ||
@@ -1083,3 +1123,12 @@ ...props.style, | ||
} | ||
this.render(); | ||
if (this.editor.isInitialized) { | ||
// On first render, we need to flush the render synchronously | ||
// Renders afterwards can be async, but this fixes a cursor positioning issue | ||
ReactDOM.flushSync(() => { | ||
this.render(); | ||
}); | ||
} | ||
else { | ||
this.render(); | ||
} | ||
} | ||
@@ -1095,3 +1144,3 @@ render() { | ||
} | ||
this.reactElement = React.createElement(Component, { ...props }); | ||
this.reactElement = React.createElement(Component, props); | ||
(_b = (_a = this.editor) === null || _a === void 0 ? void 0 : _a.contentComponent) === null || _b === void 0 ? void 0 : _b.setRenderer(this.id, this); | ||
@@ -1130,14 +1179,15 @@ } | ||
} | ||
const ReactNodeViewProvider = componentProps => { | ||
const Component = this.component; | ||
const onDragStart = this.onDragStart.bind(this); | ||
const nodeViewContentRef = element => { | ||
if (element && this.contentDOMElement && element.firstChild !== this.contentDOMElement) { | ||
element.appendChild(this.contentDOMElement); | ||
} | ||
}; | ||
return (React.createElement(React.Fragment, null, | ||
React.createElement(ReactNodeViewContext.Provider, { value: { onDragStart, nodeViewContentRef } }, | ||
React.createElement(Component, { ...componentProps })))); | ||
const onDragStart = this.onDragStart.bind(this); | ||
const nodeViewContentRef = element => { | ||
if (element && this.contentDOMElement && element.firstChild !== this.contentDOMElement) { | ||
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 = React.memo(componentProps => { | ||
return (React.createElement(ReactNodeViewContext.Provider, { value: context }, React.createElement(Component, componentProps))); | ||
}); | ||
ReactNodeViewProvider.displayName = 'ReactNodeView'; | ||
@@ -1144,0 +1194,0 @@ if (this.node.isLeaf) { |
@@ -0,1 +1,2 @@ | ||
import type { Plugin, PluginKey } from '@tiptap/pm/state'; | ||
import { RawCommands } from '../types.js'; | ||
@@ -11,3 +12,3 @@ declare module '@tiptap/core' { | ||
*/ | ||
setMeta: (key: string, value: any) => ReturnType; | ||
setMeta: (key: string | Plugin | PluginKey, value: any) => ReturnType; | ||
}; | ||
@@ -14,0 +15,0 @@ } |
@@ -9,5 +9,6 @@ import { NodeType } from '@tiptap/pm/model'; | ||
* @param typeOrName The type or name of the node. | ||
* @param overrideAttrs The attributes to ensure on the new node. | ||
* @example editor.commands.splitListItem('listItem') | ||
*/ | ||
splitListItem: (typeOrName: string | NodeType) => ReturnType; | ||
splitListItem: (typeOrName: string | NodeType, overrideAttrs?: Record<string, any>) => ReturnType; | ||
}; | ||
@@ -14,0 +15,0 @@ } |
@@ -19,2 +19,6 @@ import { MarkType, NodeType, Schema } from '@tiptap/pm/model'; | ||
isFocused: boolean; | ||
/** | ||
* The editor is considered initialized after the `create` event has been emitted. | ||
*/ | ||
isInitialized: boolean; | ||
extensionStorage: Record<string, any>; | ||
@@ -21,0 +25,0 @@ options: EditorOptions; |
@@ -20,6 +20,6 @@ import { Plugin, Transaction } from '@tiptap/pm/state'; | ||
/** | ||
* The priority of your extension. The higher, the later it will be called | ||
* The priority of your extension. The higher, the earlier it will be called | ||
* and will take precedence over other extensions with a lower priority. | ||
* @default 1000 | ||
* @example 1001 | ||
* @default 100 | ||
* @example 101 | ||
*/ | ||
@@ -290,2 +290,3 @@ priority?: number; | ||
}, props: { | ||
editor: Editor; | ||
transaction: Transaction; | ||
@@ -292,0 +293,0 @@ }) => void) | null; |
@@ -20,6 +20,6 @@ import { DOMOutputSpec, Mark as ProseMirrorMark, MarkSpec, MarkType } from '@tiptap/pm/model'; | ||
/** | ||
* The priority of your extension. The higher, the later it will be called | ||
* The priority of your extension. The higher, the earlier it will be called | ||
* and will take precedence over other extensions with a lower priority. | ||
* @default 1000 | ||
* @example 1001 | ||
* @default 100 | ||
* @example 101 | ||
*/ | ||
@@ -300,2 +300,3 @@ priority?: number; | ||
}, props: { | ||
editor: Editor; | ||
transaction: Transaction; | ||
@@ -302,0 +303,0 @@ }) => void) | null; |
@@ -20,6 +20,6 @@ import { DOMOutputSpec, Node as ProseMirrorNode, NodeSpec, NodeType } from '@tiptap/pm/model'; | ||
/** | ||
* The priority of your extension. The higher, the later it will be called | ||
* The priority of your extension. The higher, the earlier it will be called | ||
* and will take precedence over other extensions with a lower priority. | ||
* @default 1000 | ||
* @example 1001 | ||
* @default 100 | ||
* @example 101 | ||
*/ | ||
@@ -301,2 +301,3 @@ priority?: number; | ||
}, props: { | ||
editor: Editor; | ||
transaction: Transaction; | ||
@@ -303,0 +304,0 @@ }) => void) | null; |
import { NodeType } from '@tiptap/pm/model'; | ||
import { PasteRule, PasteRuleFinder } from '../PasteRule.js'; | ||
import { ExtendedRegExpMatchArray } from '../types.js'; | ||
import { ExtendedRegExpMatchArray, JSONContent } from '../types.js'; | ||
/** | ||
@@ -13,2 +13,3 @@ * Build an paste rule that adds a node when the | ||
getAttributes?: Record<string, any> | ((match: ExtendedRegExpMatchArray, event: ClipboardEvent) => Record<string, any>) | false | null; | ||
getContent?: JSONContent[] | ((attrs: Record<string, any>) => JSONContent[]) | false | null; | ||
}): PasteRule; |
@@ -0,3 +1,3 @@ | ||
import { Editor } from '@tiptap/core'; | ||
import React, { ReactNode } from 'react'; | ||
import { Editor } from './Editor.js'; | ||
import { UseEditorOptions } from './useEditor.js'; | ||
@@ -4,0 +4,0 @@ export type EditorContextValue = { |
import { Editor as CoreEditor } from '@tiptap/core'; | ||
import React from 'react'; | ||
import { EditorContentProps, EditorContentState } from './EditorContent.js'; | ||
import { ReactRenderer } from './ReactRenderer.js'; | ||
type ContentComponent = React.Component<EditorContentProps, EditorContentState> & { | ||
type ContentComponent = { | ||
setRenderer(id: string, renderer: ReactRenderer): void; | ||
removeRenderer(id: string): void; | ||
subscribe: (callback: () => void) => () => void; | ||
getSnapshot: () => Record<string, React.ReactPortal>; | ||
getServerSnapshot: () => Record<string, React.ReactPortal>; | ||
}; | ||
@@ -9,0 +11,0 @@ export declare class Editor extends CoreEditor { |
import React, { ForwardedRef, HTMLProps } from 'react'; | ||
import { Editor } from './Editor.js'; | ||
import { ReactRenderer } from './ReactRenderer.js'; | ||
export interface EditorContentProps extends HTMLProps<HTMLDivElement> { | ||
@@ -8,8 +7,8 @@ editor: Editor | null; | ||
} | ||
export interface EditorContentState { | ||
renderers: Record<string, ReactRenderer>; | ||
} | ||
export declare class PureEditorContent extends React.Component<EditorContentProps, EditorContentState> { | ||
export declare class PureEditorContent extends React.Component<EditorContentProps, { | ||
hasContentComponentInitialized: boolean; | ||
}> { | ||
editorContentRef: React.RefObject<any>; | ||
initialized: boolean; | ||
unsubscribeToContentComponent?: () => void; | ||
constructor(props: EditorContentProps); | ||
@@ -19,5 +18,2 @@ componentDidMount(): void; | ||
init(): void; | ||
maybeFlushSync(fn: () => void): void; | ||
setRenderer(id: string, renderer: ReactRenderer): void; | ||
removeRenderer(id: string): void; | ||
componentWillUnmount(): void; | ||
@@ -24,0 +20,0 @@ render(): React.JSX.Element; |
@@ -1,4 +0,3 @@ | ||
import { EditorOptions } from '@tiptap/core'; | ||
import { type EditorOptions, Editor } from '@tiptap/core'; | ||
import { DependencyList } from 'react'; | ||
import { Editor } from './Editor.js'; | ||
/** | ||
@@ -5,0 +4,0 @@ * The options for the `useEditor` hook. |
@@ -1,2 +0,2 @@ | ||
import type { Editor } from './Editor.js'; | ||
import type { Editor } from '@tiptap/core'; | ||
export type EditorStateSnapshot<TEditor extends Editor | null = Editor | null> = { | ||
@@ -3,0 +3,0 @@ editor: TEditor; |
{ | ||
"name": "@tiptap/react", | ||
"description": "React components for tiptap", | ||
"version": "2.5.9", | ||
"version": "2.6.0", | ||
"homepage": "https://tiptap.dev", | ||
@@ -32,4 +32,4 @@ "keywords": [ | ||
"dependencies": { | ||
"@tiptap/extension-bubble-menu": "^2.5.9", | ||
"@tiptap/extension-floating-menu": "^2.5.9", | ||
"@tiptap/extension-bubble-menu": "^2.6.0", | ||
"@tiptap/extension-floating-menu": "^2.6.0", | ||
"@types/use-sync-external-store": "^0.0.6", | ||
@@ -39,4 +39,4 @@ "use-sync-external-store": "^1.2.2" | ||
"devDependencies": { | ||
"@tiptap/core": "^2.5.9", | ||
"@tiptap/pm": "^2.5.9", | ||
"@tiptap/core": "^2.6.0", | ||
"@tiptap/pm": "^2.6.0", | ||
"@types/react": "^18.2.14", | ||
@@ -48,4 +48,4 @@ "@types/react-dom": "^18.2.6", | ||
"peerDependencies": { | ||
"@tiptap/core": "^2.5.9", | ||
"@tiptap/pm": "^2.5.9", | ||
"@tiptap/core": "^2.6.0", | ||
"@tiptap/pm": "^2.6.0", | ||
"react": "^17.0.0 || ^18.0.0", | ||
@@ -52,0 +52,0 @@ "react-dom": "^17.0.0 || ^18.0.0" |
import { Editor as CoreEditor } from '@tiptap/core' | ||
import React from 'react' | ||
import { EditorContentProps, EditorContentState } from './EditorContent.js' | ||
import { ReactRenderer } from './ReactRenderer.js' | ||
type ContentComponent = React.Component<EditorContentProps, EditorContentState> & { | ||
type ContentComponent = { | ||
setRenderer(id: string, renderer: ReactRenderer): void; | ||
removeRenderer(id: string): void; | ||
subscribe: (callback: () => void) => () => void; | ||
getSnapshot: () => Record<string, React.ReactPortal>; | ||
getServerSnapshot: () => Record<string, React.ReactPortal>; | ||
} | ||
@@ -11,0 +13,0 @@ |
@@ -1,2 +0,2 @@ | ||
import { EditorOptions } from '@tiptap/core' | ||
import { type EditorOptions, Editor } from '@tiptap/core' | ||
import { | ||
@@ -12,3 +12,2 @@ DependencyList, | ||
import { Editor } from './Editor.js' | ||
import { useEditorState } from './useEditorState.js' | ||
@@ -141,15 +140,17 @@ | ||
private createEditor(): Editor { | ||
const editor = new Editor(this.options.current) | ||
const optionsToApply: Partial<EditorOptions> = { | ||
...this.options.current, | ||
// Always call the most recent version of the callback function by default | ||
onBeforeCreate: (...args) => this.options.current.onBeforeCreate?.(...args), | ||
onBlur: (...args) => this.options.current.onBlur?.(...args), | ||
onCreate: (...args) => this.options.current.onCreate?.(...args), | ||
onDestroy: (...args) => this.options.current.onDestroy?.(...args), | ||
onFocus: (...args) => this.options.current.onFocus?.(...args), | ||
onSelectionUpdate: (...args) => this.options.current.onSelectionUpdate?.(...args), | ||
onTransaction: (...args) => this.options.current.onTransaction?.(...args), | ||
onUpdate: (...args) => this.options.current.onUpdate?.(...args), | ||
onContentError: (...args) => this.options.current.onContentError?.(...args), | ||
} | ||
const editor = new Editor(optionsToApply) | ||
// 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)) | ||
// no need to keep track of the event listeners, they will be removed when the editor is destroyed | ||
@@ -221,3 +222,2 @@ | ||
private refreshEditorInstance(deps: DependencyList) { | ||
if (this.editor && !this.editor.isDestroyed) { | ||
@@ -224,0 +224,0 @@ // Editor instance already exists |
@@ -0,6 +1,5 @@ | ||
import type { Editor } from '@tiptap/core' | ||
import { useDebugValue, useEffect, useState } from 'react' | ||
import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector' | ||
import type { Editor } from './Editor.js' | ||
export type EditorStateSnapshot<TEditor extends Editor | null = Editor | null> = { | ||
@@ -7,0 +6,0 @@ editor: TEditor; |
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
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
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
634388
8987