(function(global, factory) { typeof exports === "object" && typeof module !== "undefined" ? factory(exports, require("react"), require("prop-types"), require("@ckeditor/ckeditor5-integrations-common")) : typeof define === "function" && define.amd ? define(["exports", "react", "prop-types", "@ckeditor/ckeditor5-integrations-common"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.CKEDITOR_REACT = {}, global.React, global.PropTypes, global.CKEDITOR_INTEGRATIONS_COMMON)); })(this, function(exports2, React, PropTypes, ckeditor5IntegrationsCommon) { "use strict";var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); /** * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ const _LifeCycleElementSemaphore = class _LifeCycleElementSemaphore { constructor(element, lifecycle) { /** * This should define async methods for initializing and destroying the editor. * Essentially, it's an async version of basic React lifecycle methods like `componentDidMount`, `componentWillUnmount`. * * * Result of {@link LifeCycleAsyncOperators#mount} method is passed to {@link LifeCycleAsyncOperators#unmount} as an argument. */ __publicField(this, "_lifecycle"); /** * This is the element instance that the editor uses for mounting. This element should contain the `ckeditorInstance` member * once the editor has been successfully mounted to it. The semaphore ensures that a new instance of the editor, which will * be assigned to this element by the {@link #_lifecycle:mount} method, will always be initialized after the successful * destruction of the underlying `ckeditorInstance` that was previously mounted on this element. */ __publicField(this, "_element"); /** * This is the lock mechanism utilized by the {@link #lock} and {@link #release} methods. * * * If the editor is not yet mounted and is awaiting mounting (for instance, when another editor is * occupying the element), then it is null. * * * When the editor is mounted on the element, this variable holds an unresolved promise that will be * resolved after the editor is destroyed. * * * Once the editor is destroyed (and it was previously mounted), the promise is resolved. */ __publicField(this, "_releaseLock", null); /** * This is the result of the {@link #_lifecycle:mount} function. This value should be reset to `null` * once the semaphore is released. It is utilized to store certain data that must be removed following * the destruction of the editor. This data may include the editor's instance, the assigned watchdog, * or handles for additional window listeners. */ __publicField(this, "_value", null); /** * This is a list of callbacks that are triggered if the semaphore {@link #_lifecycle:mount} method executes successfully. * It is utilized in scenarios where we need to assign certain properties to an editor that is currently in the process of mounting. * An instance of such usage could be two-way binding. We aim to prevent the loss of all `setData` calls if the editor has not * yet been mounted, therefore these calls will be executed immediately following the completion of the mounting process. */ __publicField(this, "_afterMountCallbacks", []); /** * This represents the actual mounting state of the semaphore. It is primarily used by the {@link #release} method to * determine whether the initialization of the editor should be skipped or, if the editor is already initialized, the editor * should be destroyed. * * * If `destroyedBeforeInitialization` is true, then the {@link #release} method was invoked before the editor began to mount. * This often occurs in strict mode when we assign a promise to the {@link LifeCycleEditorElementSemaphore#_semaphores} map * and the assigned `mount` callback has not yet been called. In this scenario, it is safe to skip the initialization of the editor * and simply release the semaphore. * * * If `mountingInProgress` is a Promise, then the {@link #release} method was invoked after the initialization of the editor and the editor must be destroyed before the semaphore is released. */ __publicField(this, "_state", { destroyedBeforeInitialization: false, mountingInProgress: null }); /** * Inverse of {@link #_lock} method that tries to destroy attached editor. * * * If editor is being already attached to element (or is in attaching process) then after fully initialization of editor * destroy is performed and semaphore is released. The {@link #_lifecycle} unmount method is called. * * * If editor is being destroyed before initialization then it does nothing but sets `destroyedBeforeInitialization` flag that * will be later checked by {@link #_lock} method in initialization. The {@link #_lifecycle} unmount method is not called. * * *Important note:* * * It’s really important to keep this method *sync*. If we make this method *async*, it won’t work well because * it will cause problems when we’re trying to set up the {@link LifeCycleEditorElementSemaphore#_semaphores} map entries. */ __publicField(this, "release", ckeditor5IntegrationsCommon.once(() => { const { _releaseLock, _state, _element, _lifecycle } = this; if (_state.mountingInProgress) { _state.mountingInProgress.then(() => _lifecycle.unmount({ element: _element, // Mount result might be overridden by watchdog during restart so use instance variable. mountResult: this.value })).catch((error) => { console.error("Semaphore unmounting error:", error); }).then(_releaseLock.resolve).then(() => { this._value = null; }); } else { _state.destroyedBeforeInitialization = true; _releaseLock.resolve(); } })); this._element = element; this._lifecycle = lifecycle; this._lock(); } /** * Getter for {@link #_value}. */ get value() { return this._value; } /** * Occasionally, the Watchdog restarts the editor instance, resulting in a new instance being assigned to the semaphore. * In terms of race conditions, it's generally safer to simply override the semaphore value rather than recreating it * with a different one. */ unsafeSetValue(value) { this._value = value; this._afterMountCallbacks.forEach((callback) => callback(value)); this._afterMountCallbacks = []; } /** * This registers a callback that will be triggered after the editor has been successfully mounted. * * * If the editor is already mounted, the callback will be executed immediately. * * If the editor is in the process of mounting, the callback will be executed upon successful mounting. * * If the editor is never mounted, the passed callback will not be executed. * * If an exception is thrown within the callback, it will be re-thrown in the semaphore. */ runAfterMount(callback) { const { _value, _afterMountCallbacks } = this; if (_value) { callback(_value); } else { _afterMountCallbacks.push(callback); } } /** * This method is used to inform other components that the {@link #_element} will be used by the editor, * which is initialized by the {@link #_lifecycle} methods. * * * If an editor is already present on the provided element, the initialization of the current one * will be postponed until the previous one is destroyed. * * * If the element is empty and does not have an editor attached to it, the currently locked editor will * be mounted immediately. * * After the successful initialization of the editor and the assignment of the {@link #_value} member, * the `onReady` lifecycle method is called. * * *Important note:* * * It’s really important to keep this method *sync*. If we make this method *async*, it won’t work well because * it will cause problems when we’re trying to set up the {@link LifeCycleEditorElementSemaphore#_semaphores} map entries. */ _lock() { const { _semaphores } = _LifeCycleElementSemaphore; const { _state, _element, _lifecycle } = this; const prevElementSemaphore = _semaphores.get(_element) || Promise.resolve(null); const releaseLock = ckeditor5IntegrationsCommon.createDefer(); this._releaseLock = releaseLock; const newElementSemaphore = prevElementSemaphore.then(() => { if (_state.destroyedBeforeInitialization) { return Promise.resolve(void 0); } _state.mountingInProgress = _lifecycle.mount().then((mountResult) => { if (mountResult) { this.unsafeSetValue(mountResult); } return mountResult; }); return _state.mountingInProgress; }).then(async (mountResult) => { if (mountResult && _lifecycle.afterMount) { await _lifecycle.afterMount({ element: _element, mountResult }); } }).then(() => releaseLock.promise).catch((error) => { console.error("Semaphore mounting error:", error); }).then(() => { if (_semaphores.get(_element) === newElementSemaphore) { _semaphores.delete(_element); } }); _semaphores.set(_element, newElementSemaphore); } }; /** * This is a map of elements associated with promises. It informs the semaphore that the underlying HTML element, used as a key, * is currently in use by another editor. Each element is assigned a promise, which allows for the easy chaining of new * editor instances on an element that is already in use by another instance. The process works as follows: * * 1. If an element is being used by an editor, then the initialization of a new editor * instance is chained using the `.then()` method of the Promise. * * 2. If the editor associated with the underlying element is destroyed, then `Promise.resolve()` is called * and the previously assigned `.then()` editor callback is executed. * * @see {@link #lock} for more detailed information on the implementation. */ __publicField(_LifeCycleElementSemaphore, "_semaphores", /* @__PURE__ */ new Map()); let LifeCycleElementSemaphore = _LifeCycleElementSemaphore; /** * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ const ReactContextMetadataKey = "$__CKEditorReactContextMetadata"; function withCKEditorReactContextMetadata(metadata, config) { return { ...config, [ReactContextMetadataKey]: metadata }; } function tryExtractCKEditorReactContextMetadata(object) { return object.get(ReactContextMetadataKey); } /** * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ const useIsMountedRef = () => { const mountedRef = React.useRef(false); React.useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); return mountedRef; }; /** * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ const useRefSafeCallback = (fn) => { const callbackRef = React.useRef(); callbackRef.current = fn; return React.useCallback( (...args) => callbackRef.current(...args), [] ); }; /** * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ const useInitializedCKEditorsMap = ({ currentContextWatchdog, onChangeInitializedEditors }) => { const onChangeInitializedEditorsSafe = useRefSafeCallback(onChangeInitializedEditors || (() => { })); React.useEffect(() => { var _a; if (currentContextWatchdog.status !== "initialized") { return; } const { watchdog } = currentContextWatchdog; const editors = (_a = watchdog == null ? void 0 : watchdog.context) == null ? void 0 : _a.editors; if (!editors) { return; } const getInitializedContextEditors = () => [...editors].reduce( (map, editor) => { var _a2; if (editor.state !== "ready") { return map; } const metadata = tryExtractCKEditorReactContextMetadata(editor.config); const nameOrId = (_a2 = metadata == null ? void 0 : metadata.name) != null ? _a2 : editor.id; map[nameOrId] = { instance: editor, metadata }; return map; }, /* @__PURE__ */ Object.create({}) // Prevent the prototype pollution. ); const onEditorStatusChange = () => { onChangeInitializedEditorsSafe( getInitializedContextEditors(), watchdog ); }; const trackEditorLifecycle = (editor) => { editor.once("ready", onEditorStatusChange, { priority: "lowest" }); editor.once("destroy", onEditorStatusChange, { priority: "lowest" }); }; const onAddEditorToCollection = (_, editor) => { trackEditorLifecycle(editor); }; editors.forEach(trackEditorLifecycle); editors.on("add", onAddEditorToCollection); if (Array.from(editors).some((editor) => editor.state === "ready")) { onEditorStatusChange(); } return () => { editors.off("add", onAddEditorToCollection); }; }, [currentContextWatchdog]); }; /** * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ const ContextWatchdogContext = React.createContext(null); const CKEditorContext = (props) => { const { id, context, watchdogConfig, children, config, onReady, contextWatchdog: ContextWatchdogConstructor, isLayoutReady = true, onChangeInitializedEditors, onError = (error, details) => console.error(error, details) } = props; const isMountedRef = useIsMountedRef(); const prevWatchdogInitializationIDRef = React.useRef(null); const [currentContextWatchdog, setCurrentContextWatchdog] = React.useState({ status: "initializing" }); React.useEffect(() => { if (isLayoutReady) { initializeContextWatchdog(); } else { setCurrentContextWatchdog({ status: "initializing" }); } }, [id, isLayoutReady]); React.useEffect(() => () => { if (currentContextWatchdog.status === "initialized") { currentContextWatchdog.watchdog.destroy(); } }, [currentContextWatchdog]); useInitializedCKEditorsMap({ currentContextWatchdog, onChangeInitializedEditors }); function regenerateInitializationID() { prevWatchdogInitializationIDRef.current = ckeditor5IntegrationsCommon.uid(); return prevWatchdogInitializationIDRef.current; } function canUpdateState(initializationID) { return prevWatchdogInitializationIDRef.current === initializationID && isMountedRef.current; } function initializeContextWatchdog() { const watchdogInitializationID = regenerateInitializationID(); const contextWatchdog = new ContextWatchdogConstructor(context, watchdogConfig); contextWatchdog.on("error", (_, errorEvent) => { /* istanbul ignore else -- @preserve */ if (canUpdateState(watchdogInitializationID)) { onError(errorEvent.error, { phase: "runtime", willContextRestart: errorEvent.causesRestart }); } }); contextWatchdog.on("stateChange", () => { if (onReady && contextWatchdog.state === "ready" && canUpdateState(watchdogInitializationID)) { onReady( contextWatchdog.context, contextWatchdog ); } }); contextWatchdog.create(config).then(() => { if (canUpdateState(watchdogInitializationID)) { setCurrentContextWatchdog({ status: "initialized", watchdog: contextWatchdog }); } else { contextWatchdog.destroy(); } }).catch((error) => { if (canUpdateState(watchdogInitializationID)) { onError(error, { phase: "initialization", willContextRestart: false }); setCurrentContextWatchdog({ status: "error", error }); } }); return contextWatchdog; } return /* @__PURE__ */ React.createElement(ContextWatchdogContext.Provider, { value: currentContextWatchdog }, children); }; const isContextWatchdogValue = (obj) => !!obj && typeof obj === "object" && "status" in obj && ["initializing", "initialized", "error"].includes(obj.status); const isContextWatchdogValueWithStatus = (status) => (obj) => isContextWatchdogValue(obj) && obj.status === status; const isContextWatchdogInitializing = isContextWatchdogValueWithStatus("initializing"); const isContextWatchdogReadyToUse = (obj) => isContextWatchdogValueWithStatus("initialized")(obj) && obj.watchdog.state === "ready"; /** * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ const ReactIntegrationUsageDataPlugin = ckeditor5IntegrationsCommon.createIntegrationUsageDataPlugin( "react", { version: "9.4.0", frameworkVersion: React.version } ); /** * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ function appendAllIntegrationPluginsToConfig(editorConfig) { if (ckeditor5IntegrationsCommon.isCKEditorFreeLicense(editorConfig.licenseKey)) { return editorConfig; } return ckeditor5IntegrationsCommon.appendExtraPluginsToEditorConfig(editorConfig, [ /** * This part of the code is not executed in open-source implementations using a GPL key. * It only runs when a specific license key is provided. If you are uncertain whether * this applies to your installation, please contact our support team. */ ReactIntegrationUsageDataPlugin ]); } /** * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ const REACT_INTEGRATION_READ_ONLY_LOCK_ID$1 = "Lock from React integration (@ckeditor/ckeditor5-react)"; class CKEditor extends React.Component { constructor(props) { super(props); /** * After mounting the editor, the variable will contain a reference to the created editor. * @see: https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html */ __publicField(this, "domContainer", React.createRef()); /** * Unlocks element in editor semaphore after destroy editor instance. */ __publicField(this, "editorSemaphore", null); this._checkVersion(); } /** * Checks if the CKEditor version used in the application is compatible with the component. */ _checkVersion() { const { CKEDITOR_VERSION } = window; if (!CKEDITOR_VERSION) { return console.warn('Cannot find the "CKEDITOR_VERSION" in the "window" scope.'); } const [major] = CKEDITOR_VERSION.split(".").map(Number); if (major >= 42 || CKEDITOR_VERSION.startsWith("0.0.0")) { return; } console.warn("The component requires using CKEditor 5 in version 42+ or nightly build."); } get _semaphoreValue() { const { editorSemaphore } = this; return editorSemaphore ? editorSemaphore.value : null; } /** * An watchdog instance. */ get watchdog() { const { _semaphoreValue } = this; return _semaphoreValue ? _semaphoreValue.watchdog : null; } /** * An editor instance. */ get editor() { const { _semaphoreValue } = this; return _semaphoreValue ? _semaphoreValue.instance : null; } /** * The CKEditor component should not be updated by React itself. * However, if the component identifier changes, the whole structure should be created once again. */ shouldComponentUpdate(nextProps) { const { props, editorSemaphore } = this; if (nextProps.id !== props.id) { return true; } if (nextProps.disableWatchdog !== props.disableWatchdog) { return true; } if (editorSemaphore) { editorSemaphore.runAfterMount(({ instance }) => { if (this._shouldUpdateEditorData(props, nextProps, instance)) { instance.data.set(nextProps.data); } }); if ("disabled" in nextProps) { editorSemaphore.runAfterMount(({ instance }) => { if (nextProps.disabled) { instance.enableReadOnlyMode(REACT_INTEGRATION_READ_ONLY_LOCK_ID$1); } else { instance.disableReadOnlyMode(REACT_INTEGRATION_READ_ONLY_LOCK_ID$1); } }); } } return false; } /** * Initialize the editor when the component is mounted. */ componentDidMount() { if (!isContextWatchdogInitializing(this.context)) { this._initLifeCycleSemaphore(); } } /** * Re-render the entire component once again. The old editor will be destroyed and the new one will be created. */ componentDidUpdate() { if (!isContextWatchdogInitializing(this.context)) { this._initLifeCycleSemaphore(); } } /** * Destroy the editor before unmounting the component. */ componentWillUnmount() { this._unlockLifeCycleSemaphore(); } /** * Async destroy attached editor and unlock element semaphore. */ _unlockLifeCycleSemaphore() { if (this.editorSemaphore) { this.editorSemaphore.release(); this.editorSemaphore = null; } } /** * Unlocks previous editor semaphore and creates new one.. */ _initLifeCycleSemaphore() { this._unlockLifeCycleSemaphore(); this.editorSemaphore = new LifeCycleElementSemaphore(this.domContainer.current, { mount: async () => this._initializeEditor(), afterMount: ({ mountResult }) => { const { onReady } = this.props; if (onReady && this.domContainer.current !== null) { onReady(mountResult.instance); } }, unmount: async ({ element, mountResult }) => { const { onAfterDestroy } = this.props; try { await this._destroyEditor(mountResult); element.innerHTML = ""; } finally { if (onAfterDestroy) { onAfterDestroy(mountResult.instance); } } } }); } /** * Render a
element which will be replaced by CKEditor. */ render() { return /* @__PURE__ */ React.createElement("div", { ref: this.domContainer }); } /** * Initializes the editor by creating a proper watchdog and initializing it with the editor's configuration. */ async _initializeEditor() { if (this.props.disableWatchdog) { const instance = await this._createEditor(this.domContainer.current, this._getConfig()); return { instance, watchdog: null }; } const watchdog = (() => { if (isContextWatchdogReadyToUse(this.context)) { return new EditorWatchdogAdapter(this.context.watchdog); } return new this.props.editor.EditorWatchdog(this.props.editor, this.props.watchdogConfig); })(); const totalRestartsRef = { current: 0 }; watchdog.setCreator(async (el, config) => { var _a; const { editorSemaphore } = this; const { onAfterDestroy } = this.props; if (totalRestartsRef.current > 0 && onAfterDestroy && ((_a = editorSemaphore == null ? void 0 : editorSemaphore.value) == null ? void 0 : _a.instance)) { onAfterDestroy(editorSemaphore.value.instance); } const instance = await this._createEditor(el, config); if (editorSemaphore && totalRestartsRef.current > 0) { editorSemaphore.unsafeSetValue({ instance, watchdog }); setTimeout(() => { if (this.props.onReady) { this.props.onReady(watchdog.editor); } }); } totalRestartsRef.current++; return instance; }); watchdog.on("error", (_, { error, causesRestart }) => { const onError = this.props.onError || console.error; onError(error, { phase: "runtime", willEditorRestart: causesRestart }); }); await watchdog.create(this.domContainer.current, this._getConfig()).catch((error) => { const onError = this.props.onError || console.error; onError(error, { phase: "initialization", willEditorRestart: false }); }); return { watchdog, instance: watchdog.editor }; } /** * Creates an editor from the element and configuration. * * @param element The source element. * @param config CKEditor 5 editor configuration. */ _createEditor(element, config) { const { contextItemMetadata } = this.props; if (contextItemMetadata) { config = withCKEditorReactContextMetadata(contextItemMetadata, config); } return this.props.editor.create( element, appendAllIntegrationPluginsToConfig(config) ).then((editor) => { if ("disabled" in this.props) { /* istanbul ignore else -- @preserve */ if (this.props.disabled) { editor.enableReadOnlyMode(REACT_INTEGRATION_READ_ONLY_LOCK_ID$1); } } const modelDocument = editor.model.document; const viewDocument = editor.editing.view.document; modelDocument.on("change:data", (event) => { /* istanbul ignore else -- @preserve */ if (this.props.onChange) { this.props.onChange(event, editor); } }); viewDocument.on("focus", (event) => { /* istanbul ignore else -- @preserve */ if (this.props.onFocus) { this.props.onFocus(event, editor); } }); viewDocument.on("blur", (event) => { /* istanbul ignore else -- @preserve */ if (this.props.onBlur) { this.props.onBlur(event, editor); } }); return editor; }); } /** * Destroys the editor by destroying the watchdog. */ async _destroyEditor(initializeResult) { const { watchdog, instance } = initializeResult; return new Promise((resolve, reject) => { /* istanbul ignore next -- @preserve */ setTimeout(async () => { try { if (watchdog) { await watchdog.destroy(); return resolve(); } if (instance) { await instance.destroy(); return resolve(); } resolve(); } catch (e) { console.error(e); reject(e); } }); }); } /** * Returns true when the editor should be updated. * * @param prevProps Previous react's properties. * @param nextProps React's properties. * @param editor Current editor instance. */ _shouldUpdateEditorData(prevProps, nextProps, editor) { if (prevProps.data === nextProps.data) { return false; } if (editor.data.get() === nextProps.data) { return false; } return true; } /** * Returns the editor configuration. */ _getConfig() { const config = this.props.config || {}; if (this.props.data && config.initialData) { console.warn( "Editor data should be provided either using `config.initialData` or `content` property. The config value takes precedence over `content` property and will be used when both are specified." ); } return { ...config, initialData: config.initialData || this.props.data || "" }; } } __publicField(CKEditor, "contextType", ContextWatchdogContext); // Properties definition. __publicField(CKEditor, "propTypes", { editor: PropTypes.func.isRequired, data: PropTypes.string, config: PropTypes.object, disableWatchdog: PropTypes.bool, watchdogConfig: PropTypes.object, onChange: PropTypes.func, onReady: PropTypes.func, onFocus: PropTypes.func, onBlur: PropTypes.func, onError: PropTypes.func, disabled: PropTypes.bool, id: PropTypes.any }); class EditorWatchdogAdapter { /** * @param contextWatchdog The context watchdog instance that will be wrapped into editor watchdog API. */ constructor(contextWatchdog) { /** * The context watchdog instance that will be wrapped into editor watchdog API. */ __publicField(this, "_contextWatchdog"); /** * A unique id for the adapter to distinguish editor items when using the context watchdog API. */ __publicField(this, "_id"); /** * A watchdog's editor creator function. */ __publicField(this, "_creator"); this._contextWatchdog = contextWatchdog; this._id = ckeditor5IntegrationsCommon.uid(); } /** * @param creator A watchdog's editor creator function. */ setCreator(creator) { this._creator = creator; } /** * Adds an editor configuration to the context watchdog registry. Creates an instance of it. * * @param sourceElementOrData A source element or data for the new editor. * @param config CKEditor 5 editor config. */ create(sourceElementOrData, config) { return this._contextWatchdog.add({ sourceElementOrData, config, creator: this._creator, id: this._id, type: "editor" }); } /** * Creates a listener that is attached to context watchdog's item and run when the context watchdog fires. * Currently works only for the `error` event. */ on(_, callback) { this._contextWatchdog.on("itemError", (_2, { itemId, error }) => { if (itemId === this._id) { callback(null, { error, causesRestart: void 0 }); } }); } destroy() { if (this._contextWatchdog.state === "ready") { return this._contextWatchdog.remove(this._id); } return Promise.resolve(); } /** * An editor instance. */ get editor() { return this._contextWatchdog.getItem(this._id); } } /** * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ const useLifeCycleSemaphoreSyncRef = () => { const semaphoreRef = React.useRef(null); const [revision, setRevision] = React.useState(() => Date.now()); const refresh = () => { setRevision(Date.now()); }; const release = (rerender = true) => { if (semaphoreRef.current) { semaphoreRef.current.release(); semaphoreRef.current = null; } if (rerender) { setRevision(Date.now()); } }; const unsafeSetValue = (value) => { var _a; (_a = semaphoreRef.current) == null ? void 0 : _a.unsafeSetValue(value); refresh(); }; const runAfterMount = (callback) => { if (semaphoreRef.current) { semaphoreRef.current.runAfterMount(callback); } }; const replace = (newSemaphore) => { release(false); semaphoreRef.current = newSemaphore(); refresh(); runAfterMount(refresh); }; const createAttributeRef = (key) => ({ get current() { if (!semaphoreRef.current || !semaphoreRef.current.value) { return null; } return semaphoreRef.current.value[key]; } }); return { get current() { return semaphoreRef.current; }, revision, createAttributeRef, unsafeSetValue, release, replace, runAfterMount }; }; /** * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ function mergeRefs(...refs) { return (value) => { refs.forEach((ref) => { if (typeof ref === "function") { ref(value); } else if (ref != null) { ref.current = value; } }); }; } /** * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ const useInstantEffect = (fn, deps) => { const [prevDeps, setDeps] = React.useState(null); if (!ckeditor5IntegrationsCommon.shallowCompareArrays(prevDeps, deps)) { fn(); setDeps([...deps]); } }; /** * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ const useInstantEditorEffect = (semaphore, fn, deps) => { useInstantEffect(() => { if (semaphore) { semaphore.runAfterMount(fn); } }, [semaphore, ...deps]); }; /** * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ const REACT_INTEGRATION_READ_ONLY_LOCK_ID = "Lock from React integration (@ckeditor/ckeditor5-react)"; const useMultiRootEditor = (props) => { const semaphoreElementRef = React.useRef(props.semaphoreElement || null); const semaphore = useLifeCycleSemaphoreSyncRef(); const editorRefs = { watchdog: semaphore.createAttributeRef("watchdog"), instance: semaphore.createAttributeRef("instance") }; const context = React.useContext(ContextWatchdogContext); const [roots, setRoots] = React.useState(() => Object.keys(props.data)); const [data, setData] = React.useState({ ...props.data }); const [attributes, setAttributes] = React.useState({ ...props.rootsAttributes }); const shouldUpdateEditor = React.useRef(true); const forceAssignFakeEditableElements = () => { const editor = editorRefs.instance.current; if (!editor) { return; } const initializeEditableWithFakeElement = (editable) => { if (editable.name && !editor.editing.view.getDomRoot(editable.name)) { editor.editing.view.attachDomRoot(document.createElement("div"), editable.name); } }; Object.values(editor.ui.view.editables).forEach(initializeEditableWithFakeElement); }; React.useEffect(() => { const semaphoreElement = semaphoreElementRef.current; if (context && !isContextWatchdogReadyToUse(context)) { return; } if (!semaphoreElement || props.isLayoutReady === false) { return; } semaphore.replace(() => new LifeCycleElementSemaphore(semaphoreElement, { mount: _initializeEditor, afterMount: ({ mountResult }) => { const { onReady } = props; if (onReady && semaphoreElementRef.current !== null) { onReady(mountResult.instance); } }, unmount: async ({ element, mountResult }) => { const { onAfterDestroy } = props; try { await _destroyEditor(mountResult); element.innerHTML = ""; } finally { if (onAfterDestroy) { onAfterDestroy(mountResult.instance); } } } })); return () => { forceAssignFakeEditableElements(); semaphore.release(false); }; }, [props.id, props.isLayoutReady, context == null ? void 0 : context.status]); const _getConfig = () => { const config = props.config || {}; if (props.data && config.initialData) { console.warn( "Editor data should be provided either using `config.initialData` or `data` property. The config value takes precedence over `data` property and will be used when both are specified." ); } return { ...config, rootsAttributes: attributes }; }; const onChangeData = useRefSafeCallback((editor, event) => { const modelDocument = editor.model.document; if (!props.disableTwoWayDataBinding) { const newData = {}; const newAttributes = {}; modelDocument.differ.getChanges().forEach((change) => { let root; /* istanbul ignore else -- @preserve */ if (change.type == "insert" || change.type == "remove") { root = change.position.root; } else { root = change.range.root; } if (!root.isAttached()) { return; } const { rootName } = root; newData[rootName] = editor.getData({ rootName }); }); modelDocument.differ.getChangedRoots().forEach((changedRoot) => { if (changedRoot.state) { if (newData[changedRoot.name] !== void 0) { delete newData[changedRoot.name]; } return; } const rootName = changedRoot.name; newAttributes[rootName] = editor.getRootAttributes(rootName); }); if (Object.keys(newData).length) { setData((previousData) => ({ ...previousData, ...newData })); } if (Object.keys(newAttributes).length) { setAttributes((previousAttributes) => ({ ...previousAttributes, ...newAttributes })); } } /* istanbul ignore else -- @preserve */ if (props.onChange) { props.onChange(event, editor); } }); const onAddRoot = useRefSafeCallback((editor, _evt, root) => { const rootName = root.rootName; if (!props.disableTwoWayDataBinding) { setData( (previousData) => ({ ...previousData, [rootName]: editor.getData({ rootName }) }) ); setAttributes( (previousAttributes) => ({ ...previousAttributes, [rootName]: editor.getRootAttributes(rootName) }) ); } setRoots((prevRoots) => ckeditor5IntegrationsCommon.uniq([...prevRoots, root.rootName])); }); const onDetachRoot = useRefSafeCallback((_editor, _evt, root) => { const rootName = root.rootName; if (!props.disableTwoWayDataBinding) { setData((previousData) => { const { [rootName]: _, ...newData } = previousData; return { ...newData }; }); setAttributes((previousAttributes) => { const { [rootName]: _, ...newAttributes } = previousAttributes; return { ...newAttributes }; }); } setRoots((prevRoots) => prevRoots.filter((root2) => root2 !== rootName)); }); const _createEditor = useRefSafeCallback((initialData, config) => { ckeditor5IntegrationsCommon.overwriteObject({ ...props.rootsAttributes }, attributes); ckeditor5IntegrationsCommon.overwriteObject({ ...props.data }, data); ckeditor5IntegrationsCommon.overwriteArray(Object.keys(props.data), roots); return props.editor.create( initialData, appendAllIntegrationPluginsToConfig(config) ).then((editor) => { const editorData = editor.getFullData(); ckeditor5IntegrationsCommon.overwriteObject({ ...editorData }, data); ckeditor5IntegrationsCommon.overwriteObject({ ...editor.getRootsAttributes() }, attributes); ckeditor5IntegrationsCommon.overwriteArray(Object.keys(editorData), roots); if (props.disabled) { /* istanbul ignore else -- @preserve */ editor.enableReadOnlyMode(REACT_INTEGRATION_READ_ONLY_LOCK_ID); } const modelDocument = editor.model.document; const viewDocument = editor.editing.view.document; modelDocument.on("change:data", (evt) => onChangeData(editor, evt)); editor.on("addRoot", (evt, root) => onAddRoot(editor, evt, root)); editor.on("detachRoot", (evt, root) => onDetachRoot(editor, evt, root)); viewDocument.on("focus", (event) => { /* istanbul ignore else -- @preserve */ if (props.onFocus) { props.onFocus(event, editor); } }); viewDocument.on("blur", (event) => { /* istanbul ignore else -- @preserve */ if (props.onBlur) { props.onBlur(event, editor); } }); return editor; }); }); const _destroyEditor = (initializeResult) => { const { watchdog, instance } = initializeResult; return new Promise((resolve, reject) => { /* istanbul ignore next -- @preserve */ setTimeout(async () => { try { if (watchdog) { await watchdog.destroy(); return resolve(); } if (instance) { await instance.destroy(); return resolve(); } resolve(); } catch (e) { console.error(e); reject(e); } }); }); }; const _initializeEditor = async () => { if (props.disableWatchdog) { const instance = await _createEditor(props.data, _getConfig()); return { instance, watchdog: null }; } const watchdog = (() => { if (isContextWatchdogReadyToUse(context)) { return new EditorWatchdogAdapter(context.watchdog); } return new props.editor.EditorWatchdog(props.editor, props.watchdogConfig); })(); const totalRestartsRef = { current: 0 }; watchdog.setCreator(async (_, config) => { const { onAfterDestroy } = props; if (totalRestartsRef.current > 0 && onAfterDestroy && editorRefs.instance.current) { onAfterDestroy(editorRefs.instance.current); } const instance = await _createEditor(data, config); if (totalRestartsRef.current > 0) { semaphore.unsafeSetValue({ instance, watchdog }); setTimeout(() => { /* istanbul ignore next -- @preserve */ if (props.onReady) { props.onReady(watchdog.editor); } }); } totalRestartsRef.current++; return instance; }); watchdog.on("error", (_, { error, causesRestart }) => { const onError = props.onError || console.error; onError(error, { phase: "runtime", willEditorRestart: causesRestart }); }); await watchdog.create(data, _getConfig()).catch((error) => { const onError = props.onError || console.error; onError(error, { phase: "initialization", willEditorRestart: false }); throw error; }); return { watchdog, instance: watchdog.editor }; }; const _getStateDiff = (previousState, newState) => { const previousStateKeys = Object.keys(previousState); const newStateKeys = Object.keys(newState); return { addedKeys: newStateKeys.filter((key) => !previousStateKeys.includes(key)), removedKeys: previousStateKeys.filter((key) => !newStateKeys.includes(key)) }; }; const _externalSetData = React.useCallback( (newData) => { semaphore.runAfterMount(() => { shouldUpdateEditor.current = true; setData(newData); }); }, [setData] ); const _externalSetAttributes = React.useCallback( (newAttributes) => { semaphore.runAfterMount(() => { shouldUpdateEditor.current = true; setAttributes(newAttributes); }); }, [setAttributes] ); const toolbarElement = /* @__PURE__ */ React.createElement( EditorToolbarWrapper, { ref: semaphoreElementRef, editor: editorRefs.instance.current } ); useInstantEditorEffect(semaphore.current, ({ instance }) => { if (props.disabled) { instance.enableReadOnlyMode(REACT_INTEGRATION_READ_ONLY_LOCK_ID); } else { instance.disableReadOnlyMode(REACT_INTEGRATION_READ_ONLY_LOCK_ID); } }, [props.disabled]); useInstantEditorEffect(semaphore.current, ({ instance }) => { if (shouldUpdateEditor.current) { shouldUpdateEditor.current = false; const dataKeys = Object.keys(data); const attributesKeys = Object.keys(attributes); if (!dataKeys.every((key) => attributesKeys.includes(key))) { console.error("`data` and `attributes` objects must have the same keys (roots)."); throw new Error("`data` and `attributes` objects must have the same keys (roots)."); } const editorData = instance.getFullData(); const editorAttributes = instance.getRootsAttributes(); const { addedKeys: newRoots, removedKeys: removedRoots } = _getStateDiff( editorData, data || /* istanbul ignore next -- @preserve: It should never happen, data should be always filled. */ {} ); const modifiedRoots = dataKeys.filter( (rootName) => editorData[rootName] !== void 0 && JSON.stringify(editorData[rootName]) !== JSON.stringify(data[rootName]) ); const rootsWithChangedAttributes = attributesKeys.filter((rootName) => JSON.stringify(editorAttributes[rootName]) !== JSON.stringify(attributes[rootName])); const _handleNewRoots = (roots2) => { roots2.forEach((rootName) => { instance.addRoot(rootName, { data: data[rootName] || "", attributes: (attributes == null ? void 0 : attributes[rootName]) || /* istanbul ignore next -- @preserve: attributes should be in sync with root keys */ {}, isUndoable: true }); }); }; const _handleRemovedRoots = (roots2) => { roots2.forEach((rootName) => { instance.detachRoot(rootName, true); }); }; const _updateEditorData = (roots2) => { const dataToUpdate = roots2.reduce( (result, rootName) => ({ ...result, [rootName]: data[rootName] }), /* @__PURE__ */ Object.create(null) ); instance.data.set(dataToUpdate, { suppressErrorInCollaboration: true }); }; const _updateEditorAttributes = (writer, roots2) => { roots2.forEach((rootName) => { Object.keys(attributes[rootName]).forEach((attr) => { instance.registerRootAttribute(attr); }); writer.clearAttributes(instance.model.document.getRoot(rootName)); writer.setAttributes(attributes[rootName], instance.model.document.getRoot(rootName)); }); }; setTimeout(() => { instance.model.change((writer) => { _handleNewRoots(newRoots); _handleRemovedRoots(removedRoots); if (modifiedRoots.length) { _updateEditorData(modifiedRoots); } if (rootsWithChangedAttributes.length) { _updateEditorAttributes(writer, rootsWithChangedAttributes); } }); }); } }, [data, attributes]); const editableElements = roots.map( (rootName) => /* @__PURE__ */ React.createElement( EditorEditable, { key: rootName, id: rootName, rootName, semaphore } ) ); return { editor: editorRefs.instance.current, editableElements, toolbarElement, data, setData: _externalSetData, attributes, setAttributes: _externalSetAttributes }; }; const EditorEditable = React.memo(React.forwardRef(({ id, semaphore, rootName }, ref) => { const innerRef = React.useRef(null); React.useEffect(() => { let editable; let editor; semaphore.runAfterMount(({ instance }) => { if (!innerRef.current) { return; } editor = instance; const { ui, model } = editor; const root = model.document.getRoot(rootName); if (root && editor.ui.getEditableElement(rootName)) { editor.detachEditable(root); } editable = ui.view.createEditable(rootName, innerRef.current); ui.addEditable(editable); instance.editing.view.forceRender(); }); return () => { if (editor && editor.state !== "destroyed" && innerRef.current) { const root = editor.model.document.getRoot(rootName); /* istanbul ignore else -- @preserve */ if (root) { editor.detachEditable(root); } } }; }, [semaphore.revision]); return /* @__PURE__ */ React.createElement( "div", { key: semaphore.revision, id, ref: mergeRefs(ref, innerRef) } ); })); EditorEditable.displayName = "EditorEditable"; const EditorToolbarWrapper = React.forwardRef(({ editor }, ref) => { const toolbarRef = React.useRef(null); React.useEffect(() => { const toolbarContainer = toolbarRef.current; if (!editor || !toolbarContainer) { return void 0; } const element = editor.ui.view.toolbar.element; toolbarContainer.appendChild(element); return () => { if (toolbarContainer.contains(element)) { toolbarContainer.removeChild(element); } }; }, [editor && editor.id]); return /* @__PURE__ */ React.createElement("div", { ref: mergeRefs(toolbarRef, ref) }); }); EditorToolbarWrapper.displayName = "EditorToolbarWrapper"; /** * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ const useIsUnmountedRef = () => { const mountedRef = React.useRef(false); React.useEffect(() => { mountedRef.current = false; return () => { mountedRef.current = true; }; }, []); return mountedRef; }; /** * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ const useAsyncCallback = (callback) => { const [asyncState, setAsyncState] = React.useState({ status: "idle" }); const unmountedRef = useIsUnmountedRef(); const prevExecutionUIDRef = React.useRef(null); const asyncExecutor = useRefSafeCallback(async (...args) => { if (unmountedRef.current || ckeditor5IntegrationsCommon.isSSR()) { return null; } const currentExecutionUUID = ckeditor5IntegrationsCommon.uid(); prevExecutionUIDRef.current = currentExecutionUUID; try { if (asyncState.status !== "loading") { setAsyncState({ status: "loading" }); } const result = await callback(...args); if (!unmountedRef.current && prevExecutionUIDRef.current === currentExecutionUUID) { setAsyncState({ status: "success", data: result }); } return result; } catch (error) { console.error(error); if (!unmountedRef.current && prevExecutionUIDRef.current === currentExecutionUUID) { setAsyncState({ status: "error", error }); } } return null; }); return [asyncExecutor, asyncState]; }; /** * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ const useAsyncValue = (callback, deps) => { const [asyncCallback, asyncState] = useAsyncCallback(callback); useInstantEffect(asyncCallback, deps); if (asyncState.status === "idle") { return { status: "loading" }; } return asyncState; }; /** * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ function useCKEditorCloud(config) { const serializedConfigKey = JSON.stringify(config); const result = useAsyncValue( async () => ckeditor5IntegrationsCommon.loadCKEditorCloud(config), [serializedConfigKey] ); if (result.status === "success") { return { ...result.data, status: "success" }; } return result; } /** * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ const withCKEditorCloud = (config) => (WrappedComponent) => { const ComponentWithCKEditorCloud = (props) => { var _a, _b; const ckeditorCloudResult = useCKEditorCloud(config.cloud); switch (ckeditorCloudResult.status) { case "error": if (!config.renderError) { return "Unable to load CKEditor Cloud data!"; } return config.renderError(ckeditorCloudResult.error); case "success": return /* @__PURE__ */ React.createElement(WrappedComponent, { ...props, cloud: ckeditorCloudResult }); default: return (_b = (_a = config.renderLoader) == null ? void 0 : _a.call(config)) != null ? _b : null; } }; ComponentWithCKEditorCloud.displayName = "ComponentWithCKEditorCloud"; return ComponentWithCKEditorCloud; }; Object.defineProperty(exports2, "loadCKEditorCloud", { enumerable: true, get: () => ckeditor5IntegrationsCommon.loadCKEditorCloud }); exports2.CKEditor = CKEditor; exports2.CKEditorContext = CKEditorContext; exports2.useCKEditorCloud = useCKEditorCloud; exports2.useMultiRootEditor = useMultiRootEditor; exports2.withCKEditorCloud = withCKEditorCloud; Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" }); }); //# sourceMappingURL=index.umd.cjs.map