UNPKG

@ckeditor/ckeditor5-utils

Version:

Miscellaneous utilities used by CKEditor 5.

168 lines (167 loc) 6.51 kB
/** * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options */ /** * @module utils/config */ import { isPlainObject, isElement, cloneDeepWith } from 'es-toolkit/compat'; /** * Handles a configuration dictionary. * * @typeParam Cfg A type of the configuration dictionary. */ export default class Config { /** * Store for the whole configuration. */ _config; /** * Creates an instance of the {@link ~Config} class. * * @param configurations The initial configurations to be set. Usually, provided by the user. * @param defaultConfigurations The default configurations. Usually, provided by the system. */ constructor(configurations, defaultConfigurations) { this._config = Object.create(null); // Set default configuration. if (defaultConfigurations) { // Clone the configuration to make sure that the properties will not be shared // between editors and make the watchdog feature work correctly. this.define(cloneConfig(defaultConfigurations)); } // Set initial configuration. if (configurations) { this._setObjectToTarget(this._config, configurations); } } set(name, value) { this._setToTarget(this._config, name, value); } define(name, value) { const isDefine = true; this._setToTarget(this._config, name, value, isDefine); } /** * Gets the value for a configuration entry. * * ```ts * config.get( 'name' ); * ``` * * Deep configurations can be retrieved by separating each part with a dot. * * ```ts * config.get( 'toolbar.collapsed' ); * ``` * * @param name The configuration name. Configuration names are case-sensitive. * @returns The configuration value or `undefined` if the configuration entry was not found. */ get(name) { return this._getFromSource(this._config, name); } /** * Iterates over all top level configuration names. */ *names() { for (const name of Object.keys(this._config)) { yield name; } } /** * Saves passed configuration to the specified target (nested object). * * @param target Nested config object. * @param name The configuration name or an object from which take properties as * configuration entries. Configuration names are case-sensitive. * @param value The configuration value. Used if a name is passed. * @param isDefine Define if passed configuration should overwrite existing one. */ _setToTarget(target, name, value, isDefine = false) { // In case of an object, iterate through it and call `_setToTarget` again for each property. if (isPlainObject(name)) { this._setObjectToTarget(target, name, isDefine); return; } // The configuration name should be split into parts if it has dots. E.g. `resize.width` -> [`resize`, `width`]. const parts = name.split('.'); // Take the name of the configuration out of the parts. E.g. `resize.width` -> `width`. name = parts.pop(); // Iterate over parts to check if currently stored configuration has proper structure. for (const part of parts) { // If there is no object for specified part then create one. if (!isPlainObject(target[part])) { target[part] = Object.create(null); } // Nested object becomes a target. target = target[part]; } // In case of value is an object. if (isPlainObject(value)) { // We take care of proper config structure. if (!isPlainObject(target[name])) { target[name] = Object.create(null); } target = target[name]; // And iterate through this object calling `_setToTarget` again for each property. this._setObjectToTarget(target, value, isDefine); return; } // Do nothing if we are defining configuration for non empty name. if (isDefine && typeof target[name] != 'undefined') { return; } target[name] = value; } /** * Get specified configuration from specified source (nested object). * * @param source level of nested object. * @param name The configuration name. Configuration names are case-sensitive. * @returns The configuration value or `undefined` if the configuration entry was not found. */ _getFromSource(source, name) { // The configuration name should be split into parts if it has dots. E.g. `resize.width` -> [`resize`, `width`]. const parts = name.split('.'); // Take the name of the configuration out of the parts. E.g. `resize.width` -> `width`. name = parts.pop(); // Iterate over parts to check if currently stored configuration has proper structure. for (const part of parts) { if (!isPlainObject(source[part])) { source = null; break; } // Nested object becomes a source. source = source[part]; } // Always returns undefined for non existing configuration. return source ? cloneConfig(source[name]) : undefined; } /** * Iterates through passed object and calls {@link #_setToTarget} method with object key and value for each property. * * @param target Nested config object. * @param configuration Configuration data set * @param isDefine Defines if passed configuration is default configuration or not. */ _setObjectToTarget(target, configuration, isDefine) { Object.keys(configuration).forEach(key => { this._setToTarget(target, key, configuration[key], isDefine); }); } } /** * Clones configuration object or value. */ function cloneConfig(source) { return cloneDeepWith(source, leaveItemReferences); } /** * A customized function for cloneDeepWith. * In case if it's a DOM Element it will leave references to DOM Elements instead of cloning them. * If it's a function it will leave reference to actuall function. */ function leaveItemReferences(value) { return isElement(value) || typeof value === 'function' ? value : undefined; }