Skip to content

Commit

Permalink
INT-3297: Refactor to not use any agar chaining and simplified contex…
Browse files Browse the repository at this point in the history
…t usage
  • Loading branch information
danoaky-tiny committed Apr 10, 2024
1 parent b84b572 commit 6b3c1e0
Showing 1 changed file with 70 additions and 61 deletions.
131 changes: 70 additions & 61 deletions src/test/ts/alien/Loader.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { Chain, NamedChain } from '@ephox/agar';
import { Fun, Optional } from '@ephox/katamari';
import { SugarElement, SugarNode } from '@ephox/sugar';
import { Arr, Fun, Global, Optional, Strings } from '@ephox/katamari';
import { Attribute, Remove, SelectorFilter, SugarElement, SugarNode } from '@ephox/sugar';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Editor, IAllProps, IProps } from '../../../main/ts/components/Editor';
import { Editor as TinyMCEEditor } from 'tinymce';
import { ScriptLoader } from 'src/main/ts/ScriptLoader2';

// @ts-expect-error Remove when dispose polyfill is not needed
Symbol.dispose ??= Symbol('Symbol.dispose');
// @ts-expect-error Remove when dispose polyfill is not needed
Symbol.asyncDispose ??= Symbol('Symbol.asyncDispose');

export interface Context {
DOMNode: HTMLElement;
editor: TinyMCEEditor;
ref: React.RefObject<Editor>;
version: string;
}

const getRoot = () => Optional.from(document.getElementById('root')).getOrThunk(() => {
Expand All @@ -18,78 +24,81 @@ const getRoot = () => Optional.from(document.getElementById('root')).getOrThunk(
document.body.appendChild(root);
return root;
});
export interface ReactEditorContext extends Context, Disposable {
reRender(props: IAllProps): Promise<void>;
remove(): void;
}

const deleteTinymce = () => {
ScriptLoader.reinitialize();

delete Global.tinymce;
delete Global.tinyMCE;
const hasTinymceUri = (attrName: string) => (elm: SugarElement<Element>) =>
Attribute.getOpt(elm, attrName).exists((src) => Strings.contains(src, 'tinymce'));

const elements = Arr.flatten([
Arr.filter(SelectorFilter.all('script'), hasTinymceUri('src')),
Arr.filter(SelectorFilter.all('link'), hasTinymceUri('href')),
]);

const cRender = (props: Partial<IAllProps>) => Chain.async<unknown, Context>((_, next, die) => {
Arr.each(elements, Remove.remove);
};

export const render = async (props: Partial<IAllProps> = {}, container: HTMLElement = getRoot()): Promise<ReactEditorContext> => {
const originalInit = props.init || {};
const originalSetup = originalInit.setup || Fun.noop;
const ref = React.createRef<Editor>();

const init: IProps['init'] = {
...originalInit,
setup: (editor) => {
originalSetup(editor);
const context = await new Promise<Context>((resolve, reject) => {
const init: IProps['init'] = {
...originalInit,
setup: (editor) => {
originalSetup(editor);

editor.on('SkinLoaded', () => {
setTimeout(() => {
Optional.from(ref.current)
.map(ReactDOM.findDOMNode)
.bind(Optional.from)
.map(SugarElement.fromDom)
.filter(SugarNode.isHTMLElement)
.map((val) => val.dom)
.fold(() => die('Could not find DOMNode'), (DOMNode) => {
next({
ref,
editor,
DOMNode
editor.on('SkinLoaded', () => {
setTimeout(() => {
Optional.from(ref.current)
.map(ReactDOM.findDOMNode)
.bind(Optional.from)
.map(SugarElement.fromDom)
.filter(SugarNode.isHTMLElement)
.map((val) => val.dom)
.fold(() => reject('Could not find DOMNode'), (DOMNode) => {
resolve({
ref,
editor,
DOMNode,
version: Global.tinymce.majorVersion
});
});
});
}, 0);
});
}
};
}, 0);
});
}
};

/**
/**
* NOTE: TinyMCE will manipulate the DOM directly and this may cause issues with React's virtual DOM getting
* out of sync. The official fix for this is wrap everything (textarea + editor) in an element. As far as React
* is concerned, the wrapper always only has a single child, thus ensuring that React doesn’t have a reason to
* touch the nodes created by TinyMCE. Since this only seems to be an issue when rendering TinyMCE 4 directly
* into a root and a fix would be a breaking change, let's just wrap the editor in a <div> here for now.
*/
ReactDOM.render(<div><Editor ref={ref} apiKey='no-api-key' {...props} init={init} /></div>, getRoot());
});
ReactDOM.render(<div><Editor ref={ref} apiKey='no-api-key' {...props} init={init} /></div>, container);
});

// By rendering the Editor into the same root, React will perform a diff and update.
const cReRender = (props: Partial<IAllProps>) => Chain.op<Context>((context) => {
ReactDOM.render(<div><Editor apiKey='no-api-key' ref={context.ref} {...props} /></div>, getRoot());
});

const cRemove = Chain.op((_) => {
ReactDOM.unmountComponentAtNode(getRoot());
});

const cNamedChainDirect = (name: keyof Context) => NamedChain.direct(
NamedChain.inputName(),
Chain.mapper((res: Context) => res[name]),
name
);

const cDOMNode = (chain: Chain<Context['DOMNode'], unknown>): Chain<Context, Context> => NamedChain.asChain<Context>([
cNamedChainDirect('DOMNode'),
NamedChain.read('DOMNode', chain),
NamedChain.outputInput
]);

const cEditor = (chain: Chain<Context['editor'], unknown>): Chain<Context, Context> => NamedChain.asChain<Context>([
cNamedChainDirect('editor'),
NamedChain.read('editor', chain),
NamedChain.outputInput
]);
const remove = () => {
ReactDOM.unmountComponentAtNode(container);
deleteTinymce();
};

export {
cRender,
cReRender,
cRemove,
cDOMNode,
cEditor
return {
...context,
/** By rendering the Editor into the same root, React will perform a diff and update. */
reRender: (newProps: IAllProps) => new Promise<void>((resolve) =>
ReactDOM.render(<div><Editor apiKey='no-api-key' ref={context.ref} {...newProps} /></div>, container, resolve)
),
remove,
[Symbol.dispose]: remove
};
};

0 comments on commit 6b3c1e0

Please sign in to comment.