config.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /**
  6. * @module utils/config
  7. */
  8. import { isPlainObject, isElement, cloneDeepWith } from 'lodash-es';
  9. /**
  10. * Handles a configuration dictionary.
  11. */
  12. export default class Config {
  13. /**
  14. * Creates an instance of the {@link ~Config} class.
  15. *
  16. * @param {Object} [configurations] The initial configurations to be set. Usually, provided by the user.
  17. * @param {Object} [defaultConfigurations] The default configurations. Usually, provided by the system.
  18. */
  19. constructor( configurations, defaultConfigurations ) {
  20. /**
  21. * Store for the whole configuration.
  22. *
  23. * @private
  24. * @member {Object}
  25. */
  26. this._config = {};
  27. // Set default configuration.
  28. if ( defaultConfigurations ) {
  29. this.define( defaultConfigurations );
  30. }
  31. // Set initial configuration.
  32. if ( configurations ) {
  33. this._setObjectToTarget( this._config, configurations );
  34. }
  35. }
  36. /**
  37. * Set configuration values.
  38. *
  39. * It accepts both a name/value pair or an object, which properties and values will be used to set
  40. * configurations.
  41. *
  42. * It also accepts setting a "deep configuration" by using dots in the name. For example, `'resize.width'` sets
  43. * the value for the `width` configuration in the `resize` subset.
  44. *
  45. * config.set( 'width', 500 );
  46. * config.set( 'toolbar.collapsed', true );
  47. *
  48. * // Equivalent to:
  49. * config.set( {
  50. * width: 500
  51. * toolbar: {
  52. * collapsed: true
  53. * }
  54. * } );
  55. *
  56. * Passing an object as the value will amend the configuration, not replace it.
  57. *
  58. * config.set( 'toolbar', {
  59. * collapsed: true,
  60. * } );
  61. *
  62. * config.set( 'toolbar', {
  63. * color: 'red',
  64. * } );
  65. *
  66. * config.get( 'toolbar.collapsed' ); // true
  67. * config.get( 'toolbar.color' ); // 'red'
  68. *
  69. * @param {String|Object} name The configuration name or an object from which take properties as
  70. * configuration entries. Configuration names are case-sensitive.
  71. * @param {*} value The configuration value. Used if a name is passed.
  72. */
  73. set( name, value ) {
  74. this._setToTarget( this._config, name, value );
  75. }
  76. /**
  77. * Does exactly the same as {@link #set} with one exception – passed configuration extends
  78. * existing one, but does not overwrite already defined values.
  79. *
  80. * This method is supposed to be called by plugin developers to setup plugin's configurations. It would be
  81. * rarely used for other needs.
  82. *
  83. * @param {String|Object} name The configuration name or an object from which take properties as
  84. * configuration entries. Configuration names are case-sensitive.
  85. * @param {*} value The configuration value. Used if a name is passed.
  86. */
  87. define( name, value ) {
  88. const isDefine = true;
  89. this._setToTarget( this._config, name, value, isDefine );
  90. }
  91. /**
  92. * Gets the value for a configuration entry.
  93. *
  94. * config.get( 'name' );
  95. *
  96. * Deep configurations can be retrieved by separating each part with a dot.
  97. *
  98. * config.get( 'toolbar.collapsed' );
  99. *
  100. * @param {String} name The configuration name. Configuration names are case-sensitive.
  101. * @returns {*} The configuration value or `undefined` if the configuration entry was not found.
  102. */
  103. get( name ) {
  104. return this._getFromSource( this._config, name );
  105. }
  106. /**
  107. * Iterates over all top level configuration names.
  108. *
  109. * @returns {Iterable.<String>}
  110. */
  111. * names() {
  112. for ( const name of Object.keys( this._config ) ) {
  113. yield name;
  114. }
  115. }
  116. /**
  117. * Saves passed configuration to the specified target (nested object).
  118. *
  119. * @private
  120. * @param {Object} target Nested config object.
  121. * @param {String|Object} name The configuration name or an object from which take properties as
  122. * configuration entries. Configuration names are case-sensitive.
  123. * @param {*} value The configuration value. Used if a name is passed.
  124. * @param {Boolean} [isDefine=false] Define if passed configuration should overwrite existing one.
  125. */
  126. _setToTarget( target, name, value, isDefine = false ) {
  127. // In case of an object, iterate through it and call `_setToTarget` again for each property.
  128. if ( isPlainObject( name ) ) {
  129. this._setObjectToTarget( target, name, isDefine );
  130. return;
  131. }
  132. // The configuration name should be split into parts if it has dots. E.g. `resize.width` -> [`resize`, `width`].
  133. const parts = name.split( '.' );
  134. // Take the name of the configuration out of the parts. E.g. `resize.width` -> `width`.
  135. name = parts.pop();
  136. // Iterate over parts to check if currently stored configuration has proper structure.
  137. for ( const part of parts ) {
  138. // If there is no object for specified part then create one.
  139. if ( !isPlainObject( target[ part ] ) ) {
  140. target[ part ] = {};
  141. }
  142. // Nested object becomes a target.
  143. target = target[ part ];
  144. }
  145. // In case of value is an object.
  146. if ( isPlainObject( value ) ) {
  147. // We take care of proper config structure.
  148. if ( !isPlainObject( target[ name ] ) ) {
  149. target[ name ] = {};
  150. }
  151. target = target[ name ];
  152. // And iterate through this object calling `_setToTarget` again for each property.
  153. this._setObjectToTarget( target, value, isDefine );
  154. return;
  155. }
  156. // Do nothing if we are defining configuration for non empty name.
  157. if ( isDefine && typeof target[ name ] != 'undefined' ) {
  158. return;
  159. }
  160. target[ name ] = value;
  161. }
  162. /**
  163. * Get specified configuration from specified source (nested object).
  164. *
  165. * @private
  166. * @param {Object} source level of nested object.
  167. * @param {String} name The configuration name. Configuration names are case-sensitive.
  168. * @returns {*} The configuration value or `undefined` if the configuration entry was not found.
  169. */
  170. _getFromSource( source, name ) {
  171. // The configuration name should be split into parts if it has dots. E.g. `resize.width` -> [`resize`, `width`].
  172. const parts = name.split( '.' );
  173. // Take the name of the configuration out of the parts. E.g. `resize.width` -> `width`.
  174. name = parts.pop();
  175. // Iterate over parts to check if currently stored configuration has proper structure.
  176. for ( const part of parts ) {
  177. if ( !isPlainObject( source[ part ] ) ) {
  178. source = null;
  179. break;
  180. }
  181. // Nested object becomes a source.
  182. source = source[ part ];
  183. }
  184. // Always returns undefined for non existing configuration.
  185. return source ? cloneConfig( source[ name ] ) : undefined;
  186. }
  187. /**
  188. * Iterates through passed object and calls {@link #_setToTarget} method with object key and value for each property.
  189. *
  190. * @private
  191. * @param {Object} target Nested config object.
  192. * @param {Object} configuration Configuration data set
  193. * @param {Boolean} [isDefine] Defines if passed configuration is default configuration or not.
  194. */
  195. _setObjectToTarget( target, configuration, isDefine ) {
  196. Object.keys( configuration ).forEach( key => {
  197. this._setToTarget( target, key, configuration[ key ], isDefine );
  198. } );
  199. }
  200. }
  201. // Clones configuration object or value.
  202. // @param {*} source Source configuration
  203. // @returns {*} Cloned configuration value.
  204. function cloneConfig( source ) {
  205. return cloneDeepWith( source, leaveDOMReferences );
  206. }
  207. // A customizer function for cloneDeepWith.
  208. // It will leave references to DOM Elements instead of cloning them.
  209. //
  210. // @param {*} value
  211. // @returns {Element|undefined}
  212. function leaveDOMReferences( value ) {
  213. return isElement( value ) ? value : undefined;
  214. }