1- import { Chain , NamedChain } from '@ephox/agar' ;
21import { Fun , Optional } from '@ephox/katamari' ;
32import { SugarElement , SugarNode } from '@ephox/sugar' ;
43import * as React from 'react' ;
54import * as ReactDOM from 'react-dom' ;
6- import { Editor , IAllProps , IProps } from '../../../main/ts/components/Editor' ;
5+ import { Editor , IAllProps , IProps , Version } from '../../../main/ts/components/Editor' ;
76import { Editor as TinyMCEEditor } from 'tinymce' ;
7+ import { before , context } from '@ephox/bedrock-client' ;
8+ import { VersionLoader } from '@tinymce/miniature' ;
9+
10+ // @ts -expect-error Remove when dispose polyfill is not needed
11+ Symbol . dispose ??= Symbol ( 'Symbol.dispose' ) ;
12+ // @ts -expect-error Remove when dispose polyfill is not needed
13+ Symbol . asyncDispose ??= Symbol ( 'Symbol.asyncDispose' ) ;
814
915export interface Context {
1016 DOMNode : HTMLElement ;
@@ -18,78 +24,78 @@ const getRoot = () => Optional.from(document.getElementById('root')).getOrThunk(
1824 document . body . appendChild ( root ) ;
1925 return root ;
2026} ) ;
27+ export interface ReactEditorContext extends Context , Disposable {
28+ reRender ( props : IAllProps ) : Promise < void > ;
29+ remove ( ) : void ;
30+ }
2131
22- const cRender = ( props : Partial < IAllProps > ) => Chain . async < unknown , Context > ( ( _ , next , die ) => {
32+ export const render = async ( props : Partial < IAllProps > = { } , container : HTMLElement = getRoot ( ) ) : Promise < ReactEditorContext > => {
2333 const originalInit = props . init || { } ;
2434 const originalSetup = originalInit . setup || Fun . noop ;
2535 const ref = React . createRef < Editor > ( ) ;
2636
27- const init : IProps [ 'init' ] = {
28- ...originalInit ,
29- setup : ( editor ) => {
30- originalSetup ( editor ) ;
37+ const ctx = await new Promise < Context > ( ( resolve , reject ) => {
38+ const init : IProps [ 'init' ] = {
39+ ...originalInit ,
40+ setup : ( editor ) => {
41+ originalSetup ( editor ) ;
3142
32- editor . on ( 'SkinLoaded' , ( ) => {
33- setTimeout ( ( ) => {
34- Optional . from ( ref . current )
35- . map ( ReactDOM . findDOMNode )
36- . bind ( Optional . from )
37- . map ( SugarElement . fromDom )
38- . filter ( SugarNode . isHTMLElement )
39- . map ( ( val ) => val . dom )
40- . fold ( ( ) => die ( 'Could not find DOMNode' ) , ( DOMNode ) => {
41- next ( {
42- ref,
43- editor,
44- DOMNode
43+ editor . on ( 'SkinLoaded' , ( ) => {
44+ setTimeout ( ( ) => {
45+ Optional . from ( ref . current )
46+ . map ( ReactDOM . findDOMNode )
47+ . bind ( Optional . from )
48+ . map ( SugarElement . fromDom )
49+ . filter ( SugarNode . isHTMLElement )
50+ . map ( ( val ) => val . dom )
51+ . fold ( ( ) => reject ( 'Could not find DOMNode' ) , ( DOMNode ) => {
52+ resolve ( {
53+ ref,
54+ editor,
55+ DOMNode,
56+ } ) ;
4557 } ) ;
46- } ) ;
47- } , 0 ) ;
48- } ) ;
49- }
50- } ;
58+ } , 0 ) ;
59+ } ) ;
60+ }
61+ } ;
5162
52- /**
63+ /**
5364 * NOTE: TinyMCE will manipulate the DOM directly and this may cause issues with React's virtual DOM getting
5465 * out of sync. The official fix for this is wrap everything (textarea + editor) in an element. As far as React
5566 * is concerned, the wrapper always only has a single child, thus ensuring that React doesn’t have a reason to
5667 * touch the nodes created by TinyMCE. Since this only seems to be an issue when rendering TinyMCE 4 directly
5768 * into a root and a fix would be a breaking change, let's just wrap the editor in a <div> here for now.
5869 */
59- ReactDOM . render ( < div > < Editor ref = { ref } apiKey = 'no-api-key' { ...props } init = { init } /> </ div > , getRoot ( ) ) ;
60- } ) ;
61-
62- // By rendering the Editor into the same root, React will perform a diff and update.
63- const cReRender = ( props : Partial < IAllProps > ) => Chain . op < Context > ( ( context ) => {
64- ReactDOM . render ( < div > < Editor apiKey = 'no-api-key' ref = { context . ref } { ...props } /> </ div > , getRoot ( ) ) ;
65- } ) ;
70+ ReactDOM . render ( < div > < Editor ref = { ref } apiKey = 'no-api-key' { ...props } init = { init } /> </ div > , container ) ;
71+ } ) ;
6672
67- const cRemove = Chain . op ( ( _ ) => {
68- ReactDOM . unmountComponentAtNode ( getRoot ( ) ) ;
69- } ) ;
73+ const remove = ( ) => {
74+ ReactDOM . unmountComponentAtNode ( container ) ;
75+ } ;
7076
71- const cNamedChainDirect = ( name : keyof Context ) => NamedChain . direct (
72- NamedChain . inputName ( ) ,
73- Chain . mapper ( ( res : Context ) => res [ name ] ) ,
74- name
75- ) ;
77+ return {
78+ ...ctx ,
79+ /** By rendering the Editor into the same root, React will perform a diff and update. */
80+ reRender : ( newProps : IAllProps ) => new Promise < void > ( ( resolve ) =>
81+ ReactDOM . render ( < div > < Editor apiKey = 'no-api-key' ref = { ctx . ref } { ...newProps } /> </ div > , container , resolve )
82+ ) ,
83+ remove,
84+ [ Symbol . dispose ] : remove
85+ } ;
86+ } ;
7687
77- const cDOMNode = ( chain : Chain < Context [ 'DOMNode' ] , unknown > ) : Chain < Context , Context > => NamedChain . asChain < Context > ( [
78- cNamedChainDirect ( 'DOMNode' ) ,
79- NamedChain . read ( 'DOMNode' , chain ) ,
80- NamedChain . outputInput
81- ] ) ;
88+ type RenderWithVersion = (
89+ props : Omit < IAllProps , 'cloudChannel' | 'tinymceScriptSrc' > ,
90+ container ?: HTMLElement | HTMLDivElement
91+ ) => Promise < ReactEditorContext > ;
8292
83- const cEditor = ( chain : Chain < Context [ 'editor' ] , unknown > ) : Chain < Context , Context > => NamedChain . asChain < Context > ( [
84- cNamedChainDirect ( 'editor' ) ,
85- NamedChain . read ( 'editor' , chain ) ,
86- NamedChain . outputInput
87- ] ) ;
93+ export const withVersion = ( version : Version , fn : ( render : RenderWithVersion ) => void ) : void => {
94+ context ( `TinyMCE ( ${ version } )` , ( ) => {
95+ before ( async ( ) => {
96+ await VersionLoader . pLoadVersion ( version ) ;
97+ } ) ;
8898
89- export {
90- cRender ,
91- cReRender ,
92- cRemove ,
93- cDOMNode ,
94- cEditor
99+ fn ( render as RenderWithVersion ) ;
100+ } ) ;
95101} ;
0 commit comments