@ckeditor/ckeditor5-react
Version:
Official React component for CKEditor 5 – the best browser-based rich text editor.
1,415 lines (1,414 loc) • 49.4 kB
JavaScript
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);
import React, { useRef, useEffect, useCallback, useState, memo, forwardRef, useContext } from "react";
import PropTypes from "prop-types";
import { createDefer, once, uid, shallowCompareArrays, uniq, overwriteObject, overwriteArray, isSSR, loadCKEditorCloud } from "@ckeditor/ckeditor5-integrations-common";
import { loadCKEditorCloud as loadCKEditorCloud2 } from "@ckeditor/ckeditor5-integrations-common";
/**
* @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", 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 = 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 = useRef(false);
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 = useRef();
callbackRef.current = fn;
return 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 || (() => {
}));
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 onAddEditor = (_, editor) => {
editor.once("ready", onEditorStatusChange, { priority: "lowest" });
editor.once("destroy", onEditorStatusChange, { priority: "lowest" });
};
editors.on("add", onAddEditor);
return () => {
editors.off("add", onAddEditor);
};
}, [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 = useRef(null);
const [currentContextWatchdog, setCurrentContextWatchdog] = useState({
status: "initializing"
});
useEffect(() => {
if (isLayoutReady) {
initializeContextWatchdog();
} else {
setCurrentContextWatchdog({
status: "initializing"
});
}
}, [id, isLayoutReady]);
useEffect(() => () => {
if (currentContextWatchdog.status === "initialized") {
currentContextWatchdog.watchdog.destroy();
}
}, [currentContextWatchdog]);
useInitializedCKEditorsMap({
currentContextWatchdog,
onChangeInitializedEditors
});
function regenerateInitializationID() {
prevWatchdogInitializationIDRef.current = 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 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 <CKEditor> 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 <div> 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, 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 = 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 = useRef(null);
const [revision, setRevision] = 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 = useRef(null);
if (!shallowCompareArrays(prevDeps.current, deps)) {
prevDeps.current = [...deps];
fn();
}
};
/**
* @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 = useRef(props.semaphoreElement || null);
const semaphore = useLifeCycleSemaphoreSyncRef();
const editorRefs = {
watchdog: semaphore.createAttributeRef("watchdog"),
instance: semaphore.createAttributeRef("instance")
};
const context = useContext(ContextWatchdogContext);
const [roots, setRoots] = useState(() => Object.keys(props.data));
const [data, setData] = useState({ ...props.data });
const [attributes, setAttributes] = useState({ ...props.rootsAttributes });
const shouldUpdateEditor = 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);
};
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) => 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) => {
overwriteObject({ ...props.rootsAttributes }, attributes);
overwriteObject({ ...props.data }, data);
overwriteArray(Object.keys(props.data), roots);
return props.editor.create(initialData, config).then((editor) => {
const editorData = editor.getFullData();
overwriteObject({ ...editorData }, data);
overwriteObject({ ...editor.getRootsAttributes() }, attributes);
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 (data2, config) => {
const { onAfterDestroy } = props;
if (totalRestartsRef.current > 0 && onAfterDestroy && editorRefs.instance.current) {
onAfterDestroy(editorRefs.instance.current);
}
const instance = await _createEditor(data2, config);
if (totalRestartsRef.current > 0) {
semaphore.unsafeSetValue({
instance,
watchdog
});
setTimeout(() => {
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 = useCallback(
(newData) => {
semaphore.runAfterMount(() => {
shouldUpdateEditor.current = true;
setData(newData);
});
},
[setData]
);
const _externalSetAttributes = 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 hasModifiedData = dataKeys.some(
(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 = () => {
instance.data.set(data, { 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 (hasModifiedData) {
_updateEditorData();
}
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 = memo(forwardRef(({ id, semaphore, rootName }, ref) => {
const innerRef = useRef(null);
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 = forwardRef(({ editor }, ref) => {
const toolbarRef = useRef(null);
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 = useRef(false);
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] = useState({
status: "idle"
});
const unmountedRef = useIsUnmountedRef();
const prevExecutionUIDRef = useRef(null);
const asyncExecutor = useRefSafeCallback(async (...args) => {
if (unmountedRef.current || isSSR()) {
return null;
}
const currentExecutionUUID = 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 () => 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;
};
export {
CKEditor,
CKEditorContext,
loadCKEditorCloud2 as loadCKEditorCloud,
useCKEditorCloud,
useMultiRootEditor,
withCKEditorCloud
};
//# sourceMappingURL=index.js.map