src/config.js
/**
* @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @module utils/config
*/
import { isPlainObject, isElement, cloneDeepWith } from 'lodash-es';
/**
* Handles a configuration dictionary.
*/
export default class Config {
/**
* Creates an instance of the {@link ~Config} class.
*
* @param {Object} [configurations] The initial configurations to be set. Usually, provided by the user.
* @param {Object} [defaultConfigurations] The default configurations. Usually, provided by the system.
*/
constructor( configurations, defaultConfigurations ) {
/**
* Store for the whole configuration.
*
* @private
* @member {Object}
*/
this._config = {};
// 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 configuration values.
*
* It accepts both a name/value pair or an object, which properties and values will be used to set
* configurations.
*
* It also accepts setting a "deep configuration" by using dots in the name. For example, `'resize.width'` sets
* the value for the `width` configuration in the `resize` subset.
*
* config.set( 'width', 500 );
* config.set( 'toolbar.collapsed', true );
*
* // Equivalent to:
* config.set( {
* width: 500
* toolbar: {
* collapsed: true
* }
* } );
*
* Passing an object as the value will amend the configuration, not replace it.
*
* config.set( 'toolbar', {
* collapsed: true,
* } );
*
* config.set( 'toolbar', {
* color: 'red',
* } );
*
* config.get( 'toolbar.collapsed' ); // true
* config.get( 'toolbar.color' ); // 'red'
*
* @param {String|Object} 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.
*/
set( name, value ) {
this._setToTarget( this._config, name, value );
}
/**
* Does exactly the same as {@link #set} with one exception – passed configuration extends
* existing one, but does not overwrite already defined values.
*
* This method is supposed to be called by plugin developers to setup plugin's configurations. It would be
* rarely used for other needs.
*
* @param {String|Object} 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.
*/
define( name, value ) {
const isDefine = true;
this._setToTarget( this._config, name, value, isDefine );
}
/**
* Gets the value for a configuration entry.
*
* config.get( 'name' );
*
* Deep configurations can be retrieved by separating each part with a dot.
*
* config.get( 'toolbar.collapsed' );
*
* @param {String} 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.
*
* @returns {Iterable.<String>}
*/
* names() {
for ( const name of Object.keys( this._config ) ) {
yield name;
}
}
/**
* Saves passed configuration to the specified target (nested object).
*
* @private
* @param {Object} target Nested config object.
* @param {String|Object} 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 {Boolean} [isDefine=false] 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 ] = {};
}
// 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 ] = {};
}
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).
*
* @private
* @param {Object} source level of nested object.
* @param {String} 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.
*
* @private
* @param {Object} target Nested config object.
* @param {Object} configuration Configuration data set
* @param {Boolean} [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.
// @param {*} source Source configuration
// @returns {*} Cloned configuration value.
function cloneConfig( source ) {
return cloneDeepWith( source, leaveDOMReferences );
}
// A customized function for cloneDeepWith.
// It will leave references to DOM Elements instead of cloning them.
//
// @param {*} value
// @returns {Element|undefined}
function leaveDOMReferences( value ) {
return isElement( value ) ? value : undefined;
}