import type { TestLogs } from '@ephox/agar'; import { Arr, Fun, FutureResult, Global, Id, type Optional, Result, Type } from '@ephox/katamari'; import { Attribute, DomEvent, Insert, Remove, SelectorFilter, SugarBody, SugarElement, SugarHead, SugarShadowDom } from '@ephox/sugar'; import type { Editor } from '../alien/EditorTypes'; import { detectTinymceBaseUrl } from './Urls'; export type SuccessCallback = (v?: any, logs?: TestLogs) => void; export type FailureCallback = (err: Error | string, logs?: TestLogs) => void; export type RunCallback = (editor: any, success: SuccessCallback, failure: FailureCallback) => void; interface Callbacks { preInit: (tinymce: any, settings: Record) => void; run: RunCallback; success: SuccessCallback; failure: FailureCallback; } const createTarget = (inline: boolean) => SugarElement.fromTag(inline ? 'div' : 'textarea'); const removeTinymceElements = () => { // NOTE: Don't remove the link/scripts added, as those are part of the global tinymce which we don't clean up const elements = Arr.flatten([ // Some older versions of tinymce leaves elements behind in the dom SelectorFilter.all('.mce-notification,.mce-window,#mce-modal-block'), // TinyMCE leaves inline editor content_styles in the dom SelectorFilter.children(SugarElement.fromDom(document.head), 'style') ]); Arr.each(elements, Remove.remove); }; const loadScript = (url: string): FutureResult => FutureResult.nu((resolve) => { const script = SugarElement.fromTag('script'); Attribute.set(script, 'referrerpolicy', 'origin'); Attribute.set(script, 'src', url); const onLoad = DomEvent.bind(script, 'load', () => { onLoad.unbind(); onError.unbind(); resolve(Result.value(url)); }); const onError = DomEvent.bind(script, 'error', () => { onLoad.unbind(); onError.unbind(); resolve(Result.error(new Error('Failed to load script: ' + url))); }); Insert.append(SugarHead.head(), script); }); const setup = (callbacks: Callbacks, settings: Record, elementOpt: Optional>): void => { const target = elementOpt.getOrThunk(() => createTarget(settings.inline)); const randomId = Id.generate('tiny-loader'); Attribute.set(target, 'id', randomId); if (!SugarBody.inBody(target)) { Insert.append(SugarBody.body(), target); } const teardown = () => { Global.tinymce.remove(); Remove.remove(target); removeTinymceElements(); }; // Agar v. ??? supports logging const onSuccess = (v?: any, logs?: TestLogs) => { teardown(); // We may want to continue the logs for multiple editor // loads in the same test callbacks.success(v, logs); }; // Agar v. ??? supports logging const onFailure = (errU: unknown, logs?: TestLogs) => { const err = Type.isString(errU) || errU instanceof Error ? errU : String(errU); // eslint-disable-next-line no-console console.log('Tiny Loader error: ', err); // Do no teardown so that the failed test still shows the editor. Important for selection callbacks.failure(err, logs); }; const settingsSetup = settings.setup !== undefined ? settings.setup : Fun.noop; const run = () => { const tinymce = Global.tinymce; callbacks.preInit(tinymce, settings); const targetSettings = SugarShadowDom.isInShadowRoot(target) ? ({ target: target.dom }) : ({ selector: '#' + randomId }); try { Promise.resolve(tinymce.init({ promotion: false, license_key: 'gpl', ...settings, ...targetSettings, setup: (editor: Editor) => { // Execute the setup called by the test. settingsSetup(editor); editor.once('SkinLoaded', () => { setTimeout(() => { try { callbacks.run(editor, onSuccess, onFailure); } catch (e: any) { onFailure(e); } }, 100); }); editor.once('SkinLoadError', (e) => { callbacks.failure(e.message); }); } })).catch(onFailure); } catch (err: unknown) { onFailure(err); } }; if (!Global.tinymce) { // Attempt to load TinyMCE if it's not available loadScript(detectTinymceBaseUrl(settings) + '/tinymce.js').get((result) => { result.fold(() => callbacks.failure('Failed to find a global tinymce instance'), run); }); } else { run(); } }; export { setup, loadScript };