import composeRefs from '@seznam/compose-react-refs' ; import { Dispatch , ReactNode , Ref , SetStateAction } from 'react' ; import { AnyExtension , ErrorConstant , Framework , FrameworkOptions , FrameworkProps , GetSchema , invariant , isArray , object , shouldUseDomEnvironment , STATE_OVERRIDE , UpdateStateProps , } from '@remirror/core' ; import type { EditorState } from '@remirror/pm/state' ; import type { EditorView } from '@remirror/pm/view' ; import { ReactPlaceholderExtension } from '@remirror/preset-react' ; import { createEditorView , RemirrorSSR } from '@remirror/react-ssr' ; import { addKeyToElement } from '@remirror/react-utils' ; import type { GetRootPropsConfig , ReactFrameworkOutput , RefKeyRootProps } from './react-types' ; export class ReactFramework < Extension extends AnyExtension > extends Framework < Extension , ReactFrameworkProps < Extension > , ReactFrameworkOutput < Extension > > { /**
     * Whether to render the client immediately.
     */ #getShouldRenderClient : ( ) => boolean | undefined ; /**
     * Update the should render client state input.
     */ #setShouldRenderClient : SetShouldRenderClient ; /**
     * Stores the Prosemirror EditorView dom element.
     */ #editorRef ? : HTMLElement ; /**
     * Used when suppressHydrationWarning is true to determine when it's okay to
     * render the client content.
     */ private get shouldRenderClient ( ) : boolean | undefined { return this . #getShouldRenderClient ( ) ; } /**
     * Keep track of whether the get root props has been called during the most recent render.
     */ private rootPropsConfig = { called : false , count : 0 , } ; get name ( ) { return 'react' as const ; } constructor ( props ) { super ( props ) ; const { getShouldRenderClient , setShouldRenderClient } = props ; this . #getShouldRenderClient = getShouldRenderClient ; this . #setShouldRenderClient = setShouldRenderClient ; if ( this . manager . view ) { this . manager . view . setProps ( { state : this . manager . view . state , dispatchTransaction : this . dispatchTransaction , attributes : ( ) => this . getAttributes ( ) , editable : ( ) => this . props . editable ?? true , } ) ; return ; } this . manager . getExtension ( ReactPlaceholderExtension ) . setOptions ( { placeholder : this . props . placeholder ?? '' } ) ; } /**
     * This is called to update props on every render so that values don't become stale.
     */ update ( props ) { super . update ( props ) ; const { getShouldRenderClient , setShouldRenderClient } = props ; this . #getShouldRenderClient = getShouldRenderClient ; this . #setShouldRenderClient = setShouldRenderClient ; return this ; } /**
     * Create the prosemirror editor view.
     */ protected createView ( state ) { return createEditorView < GetSchema < Extension > > ( undefined , { state , dispatchTransaction : this . dispatchTransaction , attributes : ( ) => this . getAttributes ( ) , editable : ( ) => this . props . editable ?? true , } , this . manager . settings . forceEnvironment ) ; } /**
     * The external `getRootProps` that is used to spread props onto a desired
     * holder element for the prosemirror view.
     */ private readonly getRootProps = < RefKey extends string = 'ref' > ( options ? ) => { return this . internalGetRootProps ( options , null ) ; } ; /**
     * Creates the props that should be spread on the root element inside which
     * the prosemirror instance will be rendered.
     *
     * TODO - this is useless - REFACTOR
     */ private readonly internalGetRootProps = < RefKey extends string = 'ref' > ( options ? , children ? ) : RefKeyRootProps < RefKey > => { this . rootPropsConfig . called = true ; const { refKey : refKey = 'ref' , ref , ... config } = options ?? object < GetRootPropsConfig < RefKey > > ( ) ; return { [ refKey ] : composeRefs ( ref as Ref < HTMLElement > , this . onRef ) , key : this . uid , ... config , children : this . renderChildren ( children ) , } as any ; } ; /**
     * Stores the Prosemirror editor dom instance for this component using `refs`.
     */ private readonly onRef : Ref < HTMLElement > = ( element ) => { if ( ! element ) { return ; } this . rootPropsConfig . count += 1 ; invariant ( this . rootPropsConfig . count <= 1 , { code : ErrorConstant . REACT_GET_ROOT_PROPS , message : `Called ${ this . rootPropsConfig . count } times` , } ) ; this . #editorRef = element ; this . onRefLoad ( ) ; } ; /**
     * Updates the state either by calling `onChange` when it exists or
     * directly setting the internal state via a `setState` call.
     */ protected updateState ( { state , ... rest } ) { const { triggerChange = true , tr , transactions } = rest ; if ( this . props . state ) { const { onChange } = this . props ; invariant ( onChange , { code : ErrorConstant . REACT_CONTROLLED , message : 'You are required to provide the `onChange` handler when creating a controlled editor.' , } ) ; invariant ( triggerChange , { code : ErrorConstant . REACT_CONTROLLED , message : 'Controlled editors do not support `clearContent` or `setContent` where `triggerChange` is `true`. Update the `state` prop instead.' , } ) ; if ( ! this . previousStateOverride ) { this . previousStateOverride = this . getState ( ) ; } this . onChange ( { state , tr } ) ; return ; } if ( ! tr && ! transactions ) { state = state . apply ( state . tr . setMeta ( STATE_OVERRIDE , { } ) ) ; } this . view . updateState ( state ) ; if ( triggerChange ) { this . onChange ( { state , tr } ) ; } this . manager . onStateUpdate ( { previousState : this . previousState , state , tr , transactions } ) ; } /**
     * Update the controlled state when the value changes and notify the extension
     * of this update.
     */ updateControlledState ( state , previousState ? ) { this . previousStateOverride = previousState ; state = state . apply ( state . tr . setMeta ( STATE_OVERRIDE , { } ) ) ; this . view . updateState ( state ) ; this . manager . onStateUpdate ( { previousState : this . previousState , state } ) ; this . previousStateOverride = undefined ; } /**
     * Adds the prosemirror view to the dom in the position specified via the
     * component props.
     */ private addProsemirrorViewToDom ( element , viewDom ) { if ( this . props . insertPosition === 'start' ) { element . insertBefore ( viewDom , element . firstChild ) ; } else { element . append ( viewDom ) ; } } /**
     * Called once the container dom node (`this.editorRef`) has been initialized
     * after the component mounts.
     *
     * This method handles the cases where the dom is not focused.
     */ private onRefLoad ( ) { invariant ( this . #editorRef , { code : ErrorConstant . REACT_EDITOR_VIEW , message : 'Something went wrong when initializing the text editor. Please check your setup.' , } ) ; const { autoFocus } = this . props ; this . addProsemirrorViewToDom ( this . #editorRef , this . view . dom ) ; if ( autoFocus ) { this . focus ( autoFocus ) ; } this . onChange ( ) ; this . addFocusListeners ( ) ; } onMount ( ) { const { suppressHydrationWarning } = this . props ; if ( suppressHydrationWarning ) { this . #setShouldRenderClient ( true ) ; } } /**
     * Called for every update of the props and state.
     */ onUpdate ( ) { if ( this . view && this . #editorRef ) { this . view . setProps ( { ... this . view . props , editable : ( ) => this . props . editable ?? true } ) ; } } /**
     * Get the framework output.
     */ get frameworkOutput ( ) : ReactFrameworkOutput < Extension > { return { ... this . baseOutput , getRootProps : this . getRootProps , portalContainer : this . manager . store . portalContainer , renderSsr : this . renderSsr , } ; } param children private renderChildren ( child = null ) { const { insertPosition = 'end' , suppressHydrationWarning } = this . props ; const children = isArray ( child ) ? child : [ child ] ; if ( shouldUseDomEnvironment ( this . manager . settings . forceEnvironment ) && ( ! suppressHydrationWarning || this . shouldRenderClient ) ) { return children ; } const ssrElement = this . renderSsr ( ) ; return ( insertPosition === 'start' ? [ ssrElement , ... children ] : [ ... children , ssrElement ] ) . map ( addKeyToElement ) ; } /**
     * Return a JSX Element to be used within the ssr rendering phase.
     */ renderSsr = ( ) : ReactNode => { const { suppressHydrationWarning , editable } = this . props ; if ( shouldUseDomEnvironment ( this . manager . settings . forceEnvironment ) && ( ! suppressHydrationWarning || this . shouldRenderClient ) ) { return null ; } return ( < RemirrorSSR attributes = { this . getAttributes ( true ) } state = { this . getState ( ) } manager = { this . manager } editable = { editable ?? true } />); } ; /**
     * Reset the called status of `getRootProps`.
     */ resetRender ( ) { this . rootPropsConfig . called = false ; this . rootPropsConfig . count = 0 ; } } export interface ReactFrameworkProps < Extension extends AnyExtension > extends FrameworkProps < Extension > { remarks state ? : EditorState < GetSchema < Extension > > | null ; remarks This is a potential solution for those who require server side rendering.
     *
     * While on the server the prosemirror document is transformed into a react
     * component so that it can be rendered. The moment it enters the DOM
     * environment prosemirror takes over control of the root element. The problem
     * is that this will always see this hydration warning on the client:
     *
     * `Warning: Did not expect server HTML to contain a <div> in <div>.`
     *
     * Setting this to true removes the warning at the cost of a slightly slower
     * start up time. It uses the two pass solution mentioned in the react docs.
     * See  https .
     *
     * For ease of use this prop copies the name used by react for DOM Elements.
     * See  {@link
     * https://reactjs.org/docs/dom-elements.html#suppresshydrationwarning.
 suppressHydrationWarning ? : boolean ; default insertPosition ? : 'start' | 'end' ; /**
     * The placeholder to set for the editor.
     */ placeholder ? : string ; } /**
 * The options that are passed into the [[`ReactFramework`]] constructor.
 */ export interface ReactFrameworkOptions < Extension extends AnyExtension > extends FrameworkOptions < Extension , ReactFrameworkProps < Extension > > { getShouldRenderClient : ( ) => boolean | undefined ; setShouldRenderClient : SetShouldRenderClient ; } export type SetShouldRenderClient = Dispatch < SetStateAction < boolean | undefined > > ;