UNPKG

@ckeditor/ckeditor5-react

Version:

Official React component for CKEditor 5 – the best browser-based rich text editor.

162 lines (161 loc) 9.86 kB
/** * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ /** * This class is utilized to pause the initialization of an editor when another instance is already present on a specified element. * It is engineered to address the following issues: * * * Rapid changes in component properties often lead to the re-initialization of the editor, which can trigger * the `editor-source-element-already-used` exception. This occurs because the editor is still in the process of initializing * when the component decides to destroy it. This semaphore waits for the editor to fully initialize before destroying it, thereby * allowing a new instance of the editor to be attached to the specified element. * * * Rapid mounting and unmounting in strict mode frequently results in the `editor-source-element-already-used` exception * being thrown by the editor. This is due to React reusing the underlying DOM element during the mounting and unmounting of components * (especially if the same component is being mounted and unmounted). Consequently, a race condition arises. The first render begins to * attach the editor (in async mode), and shortly thereafter, it is destroyed and a new instance of the component is initialized. * This semaphore, by utilizing a static semaphores promises map, retains information about whether the element is used by a previous * instance of the editor and resumes execution when it is freed. * * * The process involves starting up many editors that are no longer needed and are immediately removed in the following rerenders. * This can cause the editor’s initialization performance to slow down. The initialization of the editor is skipped when numerous * rerenders occur within a short time-frame while using this semaphore. An example of this could be a situation with 4 rerenders * occurring within a 10ms period. This semaphore will likely batch these calls, and instead of initializing 4 editors, only 2 will be * initialized (the first and the last one). */ export declare class LifeCycleElementSemaphore<R> { /** * 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. */ private static readonly _semaphores; /** * 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. */ private readonly _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. */ private readonly _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. */ private _releaseLock; /** * 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. */ private _value; /** * 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. */ private _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. */ private _state; constructor(element: HTMLElement, lifecycle: LifeCycleAsyncOperators<R>); /** * Getter for {@link #_value}. */ get value(): R | null; /** * 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: R): void; /** * 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: LifeCycleAfterMountCallback<R>): void; /** * 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. */ private _lock; /** * 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. */ readonly release: () => void; } export type LifeCycleAfterMountCallback<R> = (mountResult: R) => void; type LifeCyclePostMountAttrs<R> = { element: HTMLElement; mountResult: R; }; export type LifeCycleAsyncOperators<R> = { mount: () => Promise<R>; afterMount?: (result: LifeCyclePostMountAttrs<R>) => Promise<void> | void; unmount: (result: LifeCyclePostMountAttrs<R>) => Promise<void>; }; export {};