8
0
Pārlūkot izejas kodu

Merge pull request #19 from cksource/t/16

t/16: Introduced the configuration API
Grzegorz Pabian 10 gadi atpakaļ
vecāks
revīzija
aa2d2e1419

+ 200 - 0
packages/ckeditor5-utils/src/config.js

@@ -0,0 +1,200 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+/**
+ * Handles a configuration dictionary.
+ *
+ * @class Config
+ * @extends Model
+ */
+
+CKEDITOR.define( [ 'mvc/model', 'utils' ], function( Model, utils ) {
+	var Config = Model.extend( {
+		/**
+		 * Creates an instance of the {@link Config} class.
+		 *
+		 * @param {Object} [configurations] The initial configurations to be set.
+		 * @constructor
+		 */
+		constructor: function Config( configurations ) {
+			// Call super-constructor.
+			Model.apply( this );
+
+			if ( configurations ) {
+				this.set( 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.toolbar.collapsed; // true
+		 *     config.toolbar.color; // 'red'
+		 *
+		 * @param {String|Object} nameOrConfigurations The configuration name or an object from which take properties as
+		 * configuration entries. Configuration names are case-insensitive.
+		 * @param {*} [value=null] The configuration value. Used if a name is passed to nameOrConfigurations.
+		 */
+		set: function( name, value ) {
+			// Just pass the call to the original set() in case of an object. It'll deal with recursing through the
+			// object and calling set( name, value ) again for each property.
+			if ( utils.isObject( name ) ) {
+				Model.prototype.set.apply( this, arguments );
+
+				return;
+			}
+
+			// The target for this configuration is, for now, this object.
+			//jscs:disable safeContextKeyword
+			var target = this;
+			//jscs:enable
+
+			// The configuration name should be split into parts if it has dots. E.g: `resize.width`.
+			var parts = name.toLowerCase().split( '.' );
+
+			// Take the name of the configuration out of the parts. E.g. `resize.width` -> `width`
+			name = parts.pop();
+
+			// Retrieves the final target for this configuration recursively.
+			for ( var i = 0; i < parts.length; i++ ) {
+				// The target will always be an instance of Config.
+				if ( !( target[ parts[ i ] ] instanceof Config ) ) {
+					target.set( parts[ i ], new Config() );
+				}
+
+				target = target[ parts[ i ] ];
+			}
+
+			// Values set as pure objects will be treated as Config subsets.
+			if ( utils.isPlainObject( value ) ) {
+				// If the target is an instance of Config (a deep config subset).
+				if ( target[ name ] instanceof Config ) {
+					// Amend the target with the value, instead of replacing it.
+					target[ name ].set( value );
+
+					return;
+				}
+
+				value = new Config( value );
+			}
+
+			// Values will never be undefined.
+			if ( typeof value == 'undefined' ) {
+				value = null;
+			}
+
+			// Call the original set() on the target.
+			Model.prototype.set.call( target, name, value );
+		},
+
+		/**
+		 * 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-insensitive.
+		 * @returns {*} The configuration value or `undefined` if the configuration entry was not found.
+		 */
+		get: function( name ) {
+			// The target for this configuration is, for now, this object.
+			//jscs:disable safeContextKeyword
+			var source = this;
+			//jscs:enable
+
+			// The configuration name should be split into parts if it has dots. E.g. `resize.width` -> [`resize`, `width`]
+			var parts = name.toLowerCase().split( '.' );
+
+			// Take the name of the configuration from the parts. E.g. `resize.width` -> `width`
+			name = parts.pop();
+
+			// Retrieves the source for this configuration recursively.
+			for ( var i = 0; i < parts.length; i++ ) {
+				// The target will always be an instance of Config.
+				if ( !( source[ parts[ i ] ] instanceof Config ) ) {
+					source = null;
+					break;
+				}
+
+				source = source[ parts[ i ] ];
+			}
+
+			// Try to retrieve it from the source object.
+			if ( source && ( typeof source[ name ] != 'undefined' ) ) {
+				return source[ name ];
+			}
+
+			// If not found, take it from the definition.
+			if ( this.definition ) {
+				return this.definition[ name ];
+			}
+
+			return undefined;
+		},
+
+		/**
+		 * Defines the name and default value for configurations. It accepts the same parameters as the
+		 * {@link Config#set set()} method.
+		 *
+		 * On first call, the {@link Config#definition definition} property is created to hold all defined
+		 * configurations.
+		 *
+		 * 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} nameOrConfigurations The configuration name or an object from which take properties as
+		 * configuration entries.
+		 * @param {*} [value] The configuration value. Used if a name is passed to nameOrConfigurations. If undefined,
+		 * the configuration is set to `null`.
+		 */
+		define: function( name, value ) {
+			if ( !this.definition ) {
+				/**
+				 *
+				 *
+				 * @property
+				 * @type {Config}
+				 */
+				this.definition = new Config();
+			}
+
+			this.definition.set( name, value );
+		}
+	} );
+
+	return Config;
+} );

+ 110 - 1
packages/ckeditor5-utils/src/lib/lodash/lodash-ckeditor.js

@@ -1,7 +1,7 @@
 /**
  * @license
  * Lo-Dash 2.4.1 (Custom Build) <http://lodash.com/>
- * Build: `lodash modern exports="amd" include="clone,extend,isObject" --debug --output src/lib/lodash/lodash-ckeditor.js`
+ * Build: `lodash modern exports="amd" include="clone,extend,isPlainObject,isObject" --debug --output src/lib/lodash/lodash-ckeditor.js`
  * Copyright 2012-2013 The Dojo Foundation <http://dojofoundation.org/>
  * Based on Underscore.js 1.5.2 <http://underscorejs.org/LICENSE>
  * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
@@ -148,6 +148,7 @@
 
   /** Native method shortcuts */
   var fnToString = Function.prototype.toString,
+      getPrototypeOf = isNative(getPrototypeOf = Object.getPrototypeOf) && getPrototypeOf,
       hasOwnProperty = objectProto.hasOwnProperty,
       push = arrayRef.push,
       unshift = arrayRef.unshift;
@@ -646,6 +647,34 @@
     defineProperty(func, '__bindData__', descriptor);
   };
 
+  /**
+   * A fallback implementation of `isPlainObject` which checks if a given value
+   * is an object created by the `Object` constructor, assuming objects created
+   * by the `Object` constructor have no inherited enumerable properties and that
+   * there are no `Object.prototype` extensions.
+   *
+   * @private
+   * @param {*} value The value to check.
+   * @returns {boolean} Returns `true` if `value` is a plain object, else `false`.
+   */
+  function shimIsPlainObject(value) {
+    var ctor,
+        result;
+
+    // avoid non Object objects, `arguments` objects, and DOM elements
+    if (!(value && toString.call(value) == objectClass) ||
+        (ctor = value.constructor, isFunction(ctor) && !(ctor instanceof ctor))) {
+      return false;
+    }
+    // In most environments an object's own properties are iterated before
+    // its inherited properties. If the last iterated property is an object's
+    // own property then there are no inherited enumerable properties.
+    forIn(value, function(value, key) {
+      result = key;
+    });
+    return typeof result == 'undefined' || hasOwnProperty.call(value, result);
+  }
+
   /*--------------------------------------------------------------------------*/
 
   /**
@@ -822,6 +851,48 @@
   }
 
   /**
+   * Iterates over own and inherited enumerable properties of an object,
+   * executing the callback for each property. The callback is bound to `thisArg`
+   * and invoked with three arguments; (value, key, object). Callbacks may exit
+   * iteration early by explicitly returning `false`.
+   *
+   * @static
+   * @memberOf _
+   * @type Function
+   * @category Objects
+   * @param {Object} object The object to iterate over.
+   * @param {Function} [callback=identity] The function called per iteration.
+   * @param {*} [thisArg] The `this` binding of `callback`.
+   * @returns {Object} Returns `object`.
+   * @example
+   *
+   * function Shape() {
+   *   this.x = 0;
+   *   this.y = 0;
+   * }
+   *
+   * Shape.prototype.move = function(x, y) {
+   *   this.x += x;
+   *   this.y += y;
+   * };
+   *
+   * _.forIn(new Shape, function(value, key) {
+   *   console.log(key);
+   * });
+   * // => logs 'x', 'y', and 'move' (property order is not guaranteed across environments)
+   */
+  var forIn = function(collection, callback, thisArg) {
+    var index, iterable = collection, result = iterable;
+    if (!iterable) return result;
+    if (!objectTypes[typeof iterable]) return result;
+    callback = callback && typeof thisArg == 'undefined' ? callback : baseCreateCallback(callback, thisArg, 3);
+      for (index in iterable) {
+        if (callback(iterable[index], index, collection) === false) return result;
+      }
+    return result
+  };
+
+  /**
    * Iterates over own enumerable properties of an object, executing the callback
    * for each property. The callback is bound to `thisArg` and invoked with three
    * arguments; (value, key, object). Callbacks may exit iteration early by
@@ -903,6 +974,42 @@
     return !!(value && objectTypes[typeof value]);
   }
 
+  /**
+   * Checks if `value` is an object created by the `Object` constructor.
+   *
+   * @static
+   * @memberOf _
+   * @category Objects
+   * @param {*} value The value to check.
+   * @returns {boolean} Returns `true` if `value` is a plain object, else `false`.
+   * @example
+   *
+   * function Shape() {
+   *   this.x = 0;
+   *   this.y = 0;
+   * }
+   *
+   * _.isPlainObject(new Shape);
+   * // => false
+   *
+   * _.isPlainObject([1, 2, 3]);
+   * // => false
+   *
+   * _.isPlainObject({ 'x': 0, 'y': 0 });
+   * // => true
+   */
+  var isPlainObject = !getPrototypeOf ? shimIsPlainObject : function(value) {
+    if (!(value && toString.call(value) == objectClass)) {
+      return false;
+    }
+    var valueOf = value.valueOf,
+        objProto = isNative(valueOf) && (objProto = getPrototypeOf(valueOf)) && getPrototypeOf(objProto);
+
+    return objProto
+      ? (value == objProto || getPrototypeOf(value) == objProto)
+      : shimIsPlainObject(value);
+  };
+
   /*--------------------------------------------------------------------------*/
 
   /**
@@ -1019,6 +1126,7 @@
   lodash.assign = assign;
   lodash.bind = bind;
   lodash.forEach = forEach;
+  lodash.forIn = forIn;
   lodash.forOwn = forOwn;
   lodash.keys = keys;
 
@@ -1033,6 +1141,7 @@
   lodash.isArray = isArray;
   lodash.isFunction = isFunction;
   lodash.isObject = isObject;
+  lodash.isPlainObject = isPlainObject;
   lodash.noop = noop;
 
   /*--------------------------------------------------------------------------*/

+ 8 - 0
packages/ckeditor5-utils/src/utils-lodash.js

@@ -34,6 +34,14 @@
 		'extend',
 
 		/**
+		 * See Lo-Dash: https://lodash.com/docs#isPlainObject
+		 *
+		 * @member utils
+		 * @method isPlainObject
+		 */
+		'isPlainObject',
+
+		/**
 		 * See Lo-Dash: https://lodash.com/docs#isObject
 		 *
 		 * @member utils

+ 18 - 1
packages/ckeditor5-utils/tests/ckeditor/ckeditor.js

@@ -7,7 +7,7 @@
 
 'use strict';
 
-var modules = bender.amd.require( 'ckeditor', 'editor', 'promise' );
+var modules = bender.amd.require( 'ckeditor', 'editor', 'promise', 'config' );
 
 var content = document.getElementById( 'content' );
 
@@ -48,6 +48,14 @@ describe( 'create', function() {
 		} );
 	} );
 
+	it( 'should set configurations on the new editor', function() {
+		var CKEDITOR = modules.ckeditor;
+
+		return CKEDITOR.create( content, { test: 1 } ).then( function( editor ) {
+			expect( editor.config.test ).to.equals( 1 );
+		} );
+	} );
+
 	it( 'should add the editor to the `instances` collection', function() {
 		var CKEDITOR = modules.ckeditor;
 
@@ -95,3 +103,12 @@ describe( 'create', function() {
 		} );
 	} );
 } );
+
+describe( 'config', function() {
+	it( 'should be an instance of Config', function() {
+		var CKEDITOR = modules.ckeditor;
+		var Config = modules.config;
+
+		expect( CKEDITOR.config ).to.be.an.instanceof( Config );
+	} );
+} );

+ 252 - 0
packages/ckeditor5-utils/tests/config/config.js

@@ -0,0 +1,252 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals describe, it, expect, beforeEach */
+
+'use strict';
+
+var modules = bender.amd.require( 'config' );
+
+var config;
+
+beforeEach( function() {
+	var Config = modules.config;
+
+	config = new Config( {
+		creator: 'inline',
+		language: 'pl',
+		resize: {
+			minHeight: 300,
+			maxHeight: 800,
+			icon: {
+				path: 'xyz'
+			}
+		},
+		toolbar: 'top'
+	} );
+} );
+
+describe( 'constructor', function() {
+	it( 'should set configurations', function() {
+		expect( config ).to.have.property( 'creator' ).to.equals( 'inline' );
+		expect( config ).to.have.property( 'language' ).to.equals( 'pl' );
+		expect( config ).to.have.property( 'resize' ).to.have.property( 'minheight' ).to.equals( 300 );
+		expect( config ).to.have.property( 'resize' ).to.have.property( 'maxheight' ).to.equals( 800 );
+		expect( config ).to.have.property( 'resize' ).to.have.property( 'icon' )
+			.to.have.property( 'path' ).to.equals( 'xyz' );
+		expect( config ).to.have.property( 'toolbar' ).to.equals( 'top' );
+	} );
+
+	it( 'should work with no parameters', function() {
+		var Config = modules.config;
+
+		// No error should be thrown.
+		config = new Config();
+	} );
+} );
+
+describe( 'set', function() {
+	it( 'should create Config instances for objects', function() {
+		var Config = modules.config;
+
+		expect( config.resize ).to.be.an.instanceof( Config );
+		expect( config.resize.icon ).to.be.an.instanceof( Config );
+	} );
+
+	it( 'should set configurations when passing objects', function() {
+		config.set( {
+			option1: 1,
+			option2: {
+				subOption21: 21
+			}
+		} );
+
+		expect( config )
+			.to.have.property( 'option1' ).to.equals( 1 );
+
+		expect( config )
+			.to.have.property( 'option2' )
+			.to.have.property( 'suboption21' ).to.equals( 21 );
+	} );
+
+	it( 'should set configurations when passing name and value', function() {
+		config.set( 'something', 'anything' );
+
+		expect( config ).to.have.property( 'something' ).to.equals( 'anything' );
+	} );
+
+	it( 'should set configurations when passing name.with.deep and value', function() {
+		config.set( 'color.red', 'f00' );
+		config.set( 'background.color.blue', '00f' );
+
+		expect( config )
+			.to.have.property( 'color' )
+			.to.have.property( 'red' ).to.equals( 'f00' );
+
+		expect( config )
+			.to.have.property( 'background' )
+			.to.have.property( 'color' )
+			.to.have.property( 'blue' ).to.equals( '00f' );
+	} );
+
+	it( 'should override and expand deep configurations', function() {
+		config.set( {
+			resize: {
+				minHeight: 400,		// Override
+				hidden: true,		// Expand
+				icon: {
+					path: 'abc',	// Override
+					url: true		// Expand
+				}
+			}
+		} );
+
+		expect( config ).to.have.property( 'resize' );
+		expect( config.resize ).to.have.property( 'minheight' ).to.equals( 400 );
+		expect( config.resize ).to.have.property( 'maxheight' ).to.equals( 800 );	// Not touched
+		expect( config.resize ).to.have.property( 'hidden' ).to.equals( true );
+
+		expect( config.resize ).to.have.property( 'icon' );
+		expect( config.resize.icon ).to.have.property( 'path' ).to.equals( 'abc' );
+		expect( config.resize.icon ).to.have.property( 'url' ).to.equals( true );
+	} );
+
+	it( 'should replace a simple entry with a Config instance', function() {
+		var Config = modules.config;
+
+		config.set( 'test', 1 );
+		config.set( 'test', {
+			prop: 1
+		} );
+
+		expect( config.test ).to.be.an.instanceof( Config );
+		expect( config.test.prop ).to.equals( 1 );
+	} );
+
+	it( 'should replace a simple entry with a Config instance when passing an object', function() {
+		var Config = modules.config;
+
+		config.set( 'test', 1 );
+		config.set( {
+			test: {
+				prop: 1
+			}
+		} );
+
+		expect( config.test ).to.be.an.instanceof( Config );
+		expect( config.test.prop ).to.equals( 1 );
+	} );
+
+	it( 'should replace a simple entry with a Config instance when passing a name.with.deep', function() {
+		var Config = modules.config;
+
+		config.set( 'test.prop', 1 );
+		config.set( 'test.prop.value', 1 );
+
+		expect( config.test ).to.be.an.instanceof( Config );
+		expect( config.test.prop ).to.be.an.instanceof( Config );
+		expect( config.test.prop.value ).to.equals( 1 );
+	} );
+
+	it( 'should not create Config instances for non-pure objects', function() {
+		function SomeClass() {}
+
+		config.set( 'date', new Date() );
+		config.set( {
+			instance: new SomeClass()
+		} );
+
+		expect( config.date ).to.be.an.instanceof( Date );
+		expect( config.instance ).to.be.an.instanceof( SomeClass );
+	} );
+
+	it( 'should set `null` for undefined value', function() {
+		config.set( 'test' );
+
+		expect( config.test ).to.be.null();
+		expect( config.get( 'test' ) ).to.be.null();
+	} );
+} );
+
+describe( 'get', function() {
+	it( 'should retrieve a configuration', function() {
+		expect( config.get( 'creator' ) ).to.equals( 'inline' );
+	} );
+
+	it( 'should retrieve a deep configuration', function() {
+		expect( config.get( 'resize.minheight' ) ).to.equals( 300 );
+		expect( config.get( 'resize.icon.path' ) ).to.equals( 'xyz' );
+	} );
+
+	it( 'should retrieve a subset of the configuration', function() {
+		var resizeConfig = config.get( 'resize' );
+
+		expect( resizeConfig ).to.have.property( 'minheight' ).to.equals( 300 );
+		expect( resizeConfig ).to.have.property( 'maxheight' ).to.equals( 800 );
+		expect( resizeConfig ).to.have.property( 'icon' ).to.have.property( 'path' ).to.equals( 'xyz' );
+
+		var iconConfig = resizeConfig.get( 'icon' );
+
+		expect( iconConfig ).to.have.property( 'path' ).to.equals( 'xyz' );
+	} );
+
+	it( 'should retrieve values case-insensitively', function() {
+		expect( config.get( 'Creator' ) ).to.equals( 'inline' );
+		expect( config.get( 'CREATOR' ) ).to.equals( 'inline' );
+		expect( config.get( 'resize.minHeight' ) ).to.equals( 300 );
+		expect( config.get( 'resize.MINHEIGHT' ) ).to.equals( 300 );
+	} );
+
+	it( 'should return undefined for non existing configuration', function() {
+		expect( config.get( 'invalid' ) ).to.be.undefined();
+	} );
+
+	it( 'should return undefined for non existing deep configuration', function() {
+		expect( config.get( 'resize.invalid.value' ) ).to.be.undefined();
+	} );
+} );
+
+describe( 'define', function() {
+	it( 'should create the definition property', function() {
+		expect( config ).to.not.have.property( 'definition' );
+
+		config.define( 'test', 1 );
+
+		expect( config ).to.have.property( 'definition' );
+	} );
+
+	it( 'should set configurations in the definition property', function() {
+		config.define( 'test1', 1 );
+
+		// This is for Code Coverage to ensure that it works when `definition` is already defined.
+		config.define( 'test2', 2 );
+
+		expect( config.definition ).to.have.property( 'test1' ).to.equals( 1 );
+		expect( config.definition ).to.have.property( 'test2' ).to.equals( 2 );
+	} );
+
+	it( 'should set configurations passed as object in the definition property', function() {
+		config.define( {
+			test: 1
+		} );
+
+		expect( config.definition ).to.have.property( 'test' ).to.equals( 1 );
+	} );
+
+	it( 'should not define main config properties but still be retrieved with get()', function() {
+		config.define( 'test', 1 );
+
+		expect( config ).to.not.have.property( 'test' );
+		expect( config.get( 'test' ) ).to.equals( 1 );
+	} );
+
+	it( 'should be overridden by set()', function() {
+		config.define( 'test', 1 );
+		config.set( 'test', 2 );
+
+		expect( config ).to.have.property( 'test' ).to.equals( 2 );
+		expect( config.get( 'test' ) ).to.equals( 2 );
+	} );
+} );