8
0

config.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  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. * Saves passed configuration to the specified target (nested object).
  108. *
  109. * @private
  110. * @param {Object} target Nested config object.
  111. * @param {String|Object} name The configuration name or an object from which take properties as
  112. * configuration entries. Configuration names are case-sensitive.
  113. * @param {*} value The configuration value. Used if a name is passed.
  114. * @param {Boolean} [isDefine=false] Define if passed configuration should overwrite existing one.
  115. */
  116. _setToTarget( target, name, value, isDefine = false ) {
  117. // In case of an object, iterate through it and call `_setToTarget` again for each property.
  118. if ( isPlainObject( name ) ) {
  119. this._setObjectToTarget( target, name, isDefine );
  120. return;
  121. }
  122. // The configuration name should be split into parts if it has dots. E.g. `resize.width` -> [`resize`, `width`].
  123. const parts = name.split( '.' );
  124. // Take the name of the configuration out of the parts. E.g. `resize.width` -> `width`.
  125. name = parts.pop();
  126. // Iterate over parts to check if currently stored configuration has proper structure.
  127. for ( const part of parts ) {
  128. // If there is no object for specified part then create one.
  129. if ( !isPlainObject( target[ part ] ) ) {
  130. target[ part ] = {};
  131. }
  132. // Nested object becomes a target.
  133. target = target[ part ];
  134. }
  135. // In case of value is an object.
  136. if ( isPlainObject( value ) ) {
  137. // We take care of proper config structure.
  138. if ( !isPlainObject( target[ name ] ) ) {
  139. target[ name ] = {};
  140. }
  141. target = target[ name ];
  142. // And iterate through this object calling `_setToTarget` again for each property.
  143. this._setObjectToTarget( target, value, isDefine );
  144. return;
  145. }
  146. // Do nothing if we are defining configuration for non empty name.
  147. if ( isDefine && typeof target[ name ] != 'undefined' ) {
  148. return;
  149. }
  150. target[ name ] = value;
  151. }
  152. /**
  153. * Get specified configuration from specified source (nested object).
  154. *
  155. * @private
  156. * @param {Object} source level of nested object.
  157. * @param {String} name The configuration name. Configuration names are case-sensitive.
  158. * @returns {*} The configuration value or `undefined` if the configuration entry was not found.
  159. */
  160. _getFromSource( source, name ) {
  161. // The configuration name should be split into parts if it has dots. E.g. `resize.width` -> [`resize`, `width`].
  162. const parts = name.split( '.' );
  163. // Take the name of the configuration out of the parts. E.g. `resize.width` -> `width`.
  164. name = parts.pop();
  165. // Iterate over parts to check if currently stored configuration has proper structure.
  166. for ( const part of parts ) {
  167. if ( !isPlainObject( source[ part ] ) ) {
  168. source = null;
  169. break;
  170. }
  171. // Nested object becomes a source.
  172. source = source[ part ];
  173. }
  174. // Always returns undefined for non existing configuration.
  175. return source ? cloneConfig( source[ name ] ) : undefined;
  176. }
  177. /**
  178. * Iterates through passed object and calls {@link #_setToTarget} method with object key and value for each property.
  179. *
  180. * @private
  181. * @param {Object} target Nested config object.
  182. * @param {Object} configuration Configuration data set
  183. * @param {Boolean} [isDefine] Defines if passed configuration is default configuration or not.
  184. */
  185. _setObjectToTarget( target, configuration, isDefine ) {
  186. Object.keys( configuration ).forEach( key => {
  187. this._setToTarget( target, key, configuration[ key ], isDefine );
  188. } );
  189. }
  190. }
  191. // Clones configuration object or value.
  192. // @param {*} source Source configuration
  193. // @returns {*} Cloned configuration value.
  194. function cloneConfig( source ) {
  195. return cloneDeepWith( source, leaveDOMReferences );
  196. }
  197. // A customizer function for cloneDeepWith.
  198. // It will leave references to DOM Elements instead of cloning them.
  199. //
  200. // @param {*} value
  201. // @returns {Element|undefined}
  202. function leaveDOMReferences( value ) {
  203. return isElement( value ) ? value : undefined;
  204. }