浏览代码

Merge pull request #13 from cksource/t/10

t/10: Basic Model implementation
Piotrek Reinmar Koszuliński 10 年之前
父节点
当前提交
2b634dc5f1

+ 57 - 0
packages/ckeditor5-ui/src/basicclass.js

@@ -0,0 +1,57 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+/**
+ * A class implementing basic features useful for other classes.
+ *
+ * @class BasicClass
+ * @mixins Emitter
+ */
+
+CKEDITOR.define( [ 'emitter', 'utils' ], function( EmitterMixin, utils ) {
+	function BasicClass() {
+	}
+
+	// Injects the events API.
+	utils.extend( BasicClass.prototype, EmitterMixin );
+
+	/**
+	 * Creates a subclass constructor based on this class.
+	 *
+	 * The function to becuase a subclass constructor can be passed as `proto.constructor`.
+	 *
+	 * @static
+	 * @param {Object} [proto] Extensions to be added to the subclass prototype.
+	 * @param {Object} [statics] Extension to be added as static members of the subclass constructor.
+	 * @returns {Object} The subclass constructor.
+	 */
+	BasicClass.extend = function( proto, statics ) {
+		var that = this;
+		var child = ( proto && proto.hasOwnProperty( 'constructor' ) ) ?
+			proto.constructor :
+			function() {
+				that.apply( this, arguments );
+			};
+
+		// Copy the statics.
+		utils.extend( child, this, statics );
+
+		// Use the same prototype.
+		child.prototype = Object.create( this.prototype );
+
+		// Add the new prototype stuff.
+		if ( proto ) {
+			proto = utils.clone( proto );
+			delete proto.constructor;
+			utils.extend( child.prototype, proto );
+		}
+
+		return child;
+	};
+
+	return BasicClass;
+} );

+ 1 - 1
packages/ckeditor5-ui/src/emitter.js

@@ -220,7 +220,7 @@ CKEDITOR.define( [ 'eventinfo', 'utils' ], function( EventInfo, utils ) {
 				return;
 				return;
 			}
 			}
 
 
-			var eventInfo = new EventInfo( event );
+			var eventInfo = new EventInfo( this, event );
 
 
 			// Take the list of arguments to pass to the callbacks.
 			// Take the list of arguments to pass to the callbacks.
 			args = Array.prototype.slice.call( arguments, 1 );
 			args = Array.prototype.slice.call( arguments, 1 );

+ 6 - 1
packages/ckeditor5-ui/src/eventinfo.js

@@ -13,7 +13,12 @@
  */
  */
 
 
 CKEDITOR.define( [ 'utils' ], function( utils ) {
 CKEDITOR.define( [ 'utils' ], function( utils ) {
-	function EventInfo( name ) {
+	function EventInfo( source, name ) {
+		/**
+		 * The object that fired the event.
+		 */
+		this.source = source;
+
 		/**
 		/**
 		 * The event name.
 		 * The event name.
 		 */
 		 */

+ 317 - 1
packages/ckeditor5-ui/src/lib/lodash/lodash-ckeditor.js

@@ -1,7 +1,7 @@
 /**
 /**
  * @license
  * @license
  * Lo-Dash 2.4.1 (Custom Build) <http://lodash.com/>
  * Lo-Dash 2.4.1 (Custom Build) <http://lodash.com/>
- * Build: `lodash modern exports="amd" include="extend" --debug --output src/lib/lodash/lodash-ckeditor.js`
+ * Build: `lodash modern exports="amd" include="clone,extend,isObject" --debug --output src/lib/lodash/lodash-ckeditor.js`
  * Copyright 2012-2013 The Dojo Foundation <http://dojofoundation.org/>
  * Copyright 2012-2013 The Dojo Foundation <http://dojofoundation.org/>
  * Based on Underscore.js 1.5.2 <http://underscorejs.org/LICENSE>
  * Based on Underscore.js 1.5.2 <http://underscorejs.org/LICENSE>
  * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
  * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
@@ -9,12 +9,40 @@
  */
  */
 ;(function() {
 ;(function() {
 
 
+  /** Used to pool arrays and objects used internally */
+  var arrayPool = [];
+
+  /** Used as the max size of the `arrayPool` and `objectPool` */
+  var maxPoolSize = 40;
+
+  /** Used to match regexp flags from their coerced string values */
+  var reFlags = /\w*$/;
+
   /** Used to detected named functions */
   /** Used to detected named functions */
   var reFuncName = /^\s*function[ \n\r\t]+\w/;
   var reFuncName = /^\s*function[ \n\r\t]+\w/;
 
 
   /** Used to detect functions containing a `this` reference */
   /** Used to detect functions containing a `this` reference */
   var reThis = /\bthis\b/;
   var reThis = /\bthis\b/;
 
 
+  /** `Object#toString` result shortcuts */
+  var argsClass = '[object Arguments]',
+      arrayClass = '[object Array]',
+      boolClass = '[object Boolean]',
+      dateClass = '[object Date]',
+      funcClass = '[object Function]',
+      numberClass = '[object Number]',
+      objectClass = '[object Object]',
+      regexpClass = '[object RegExp]',
+      stringClass = '[object String]';
+
+  /** Used to identify object classifications that `_.clone` supports */
+  var cloneableClasses = {};
+  cloneableClasses[funcClass] = false;
+  cloneableClasses[argsClass] = cloneableClasses[arrayClass] =
+  cloneableClasses[boolClass] = cloneableClasses[dateClass] =
+  cloneableClasses[numberClass] = cloneableClasses[objectClass] =
+  cloneableClasses[regexpClass] = cloneableClasses[stringClass] = true;
+
   /** Used as the property descriptor for `__bindData__` */
   /** Used as the property descriptor for `__bindData__` */
   var descriptor = {
   var descriptor = {
     'configurable': false,
     'configurable': false,
@@ -44,6 +72,29 @@
 
 
   /*--------------------------------------------------------------------------*/
   /*--------------------------------------------------------------------------*/
 
 
+  /**
+   * Gets an array from the array pool or creates a new one if the pool is empty.
+   *
+   * @private
+   * @returns {Array} The array from the pool.
+   */
+  function getArray() {
+    return arrayPool.pop() || [];
+  }
+
+  /**
+   * Releases the given array back to the array pool.
+   *
+   * @private
+   * @param {Array} [array] The array to release.
+   */
+  function releaseArray(array) {
+    array.length = 0;
+    if (arrayPool.length < maxPoolSize) {
+      arrayPool.push(array);
+    }
+  }
+
   /**
   /**
    * Slices the `collection` from the `start` index up to, but not including,
    * Slices the `collection` from the `start` index up to, but not including,
    * the `end` index.
    * the `end` index.
@@ -114,8 +165,20 @@
 
 
   /* Native method shortcuts for methods with the same name as other `lodash` methods */
   /* Native method shortcuts for methods with the same name as other `lodash` methods */
   var nativeCreate = isNative(nativeCreate = Object.create) && nativeCreate,
   var nativeCreate = isNative(nativeCreate = Object.create) && nativeCreate,
+      nativeIsArray = isNative(nativeIsArray = Array.isArray) && nativeIsArray,
       nativeKeys = isNative(nativeKeys = Object.keys) && nativeKeys;
       nativeKeys = isNative(nativeKeys = Object.keys) && nativeKeys;
 
 
+  /** Used to lookup a built-in constructor by [[Class]] */
+  var ctorByClass = {};
+  ctorByClass[arrayClass] = Array;
+  ctorByClass[boolClass] = Boolean;
+  ctorByClass[dateClass] = Date;
+  ctorByClass[funcClass] = Function;
+  ctorByClass[objectClass] = Object;
+  ctorByClass[numberClass] = Number;
+  ctorByClass[regexpClass] = RegExp;
+  ctorByClass[stringClass] = String;
+
   /*--------------------------------------------------------------------------*/
   /*--------------------------------------------------------------------------*/
 
 
   /**
   /**
@@ -252,6 +315,98 @@
     return bound;
     return bound;
   }
   }
 
 
+  /**
+   * The base implementation of `_.clone` without argument juggling or support
+   * for `thisArg` binding.
+   *
+   * @private
+   * @param {*} value The value to clone.
+   * @param {boolean} [isDeep=false] Specify a deep clone.
+   * @param {Function} [callback] The function to customize cloning values.
+   * @param {Array} [stackA=[]] Tracks traversed source objects.
+   * @param {Array} [stackB=[]] Associates clones with source counterparts.
+   * @returns {*} Returns the cloned value.
+   */
+  function baseClone(value, isDeep, callback, stackA, stackB) {
+    if (callback) {
+      var result = callback(value);
+      if (typeof result != 'undefined') {
+        return result;
+      }
+    }
+    // inspect [[Class]]
+    var isObj = isObject(value);
+    if (isObj) {
+      var className = toString.call(value);
+      if (!cloneableClasses[className]) {
+        return value;
+      }
+      var ctor = ctorByClass[className];
+      switch (className) {
+        case boolClass:
+        case dateClass:
+          return new ctor(+value);
+
+        case numberClass:
+        case stringClass:
+          return new ctor(value);
+
+        case regexpClass:
+          result = ctor(value.source, reFlags.exec(value));
+          result.lastIndex = value.lastIndex;
+          return result;
+      }
+    } else {
+      return value;
+    }
+    var isArr = isArray(value);
+    if (isDeep) {
+      // check for circular references and return corresponding clone
+      var initedStack = !stackA;
+      stackA || (stackA = getArray());
+      stackB || (stackB = getArray());
+
+      var length = stackA.length;
+      while (length--) {
+        if (stackA[length] == value) {
+          return stackB[length];
+        }
+      }
+      result = isArr ? ctor(value.length) : {};
+    }
+    else {
+      result = isArr ? slice(value) : assign({}, value);
+    }
+    // add array properties assigned by `RegExp#exec`
+    if (isArr) {
+      if (hasOwnProperty.call(value, 'index')) {
+        result.index = value.index;
+      }
+      if (hasOwnProperty.call(value, 'input')) {
+        result.input = value.input;
+      }
+    }
+    // exit for shallow clone
+    if (!isDeep) {
+      return result;
+    }
+    // add the source value to the stack of traversed objects
+    // and associate it with its clone
+    stackA.push(value);
+    stackB.push(result);
+
+    // recursively populate clone (susceptible to call stack limits)
+    (isArr ? forEach : forOwn)(value, function(objValue, key) {
+      result[key] = baseClone(objValue, isDeep, callback, stackA, stackB);
+    });
+
+    if (initedStack) {
+      releaseArray(stackA);
+      releaseArray(stackB);
+    }
+    return result;
+  }
+
   /**
   /**
    * The base implementation of `_.create` without support for assigning
    * The base implementation of `_.create` without support for assigning
    * properties to the created object.
    * properties to the created object.
@@ -493,6 +648,28 @@
 
 
   /*--------------------------------------------------------------------------*/
   /*--------------------------------------------------------------------------*/
 
 
+  /**
+   * Checks if `value` is an array.
+   *
+   * @static
+   * @memberOf _
+   * @type Function
+   * @category Objects
+   * @param {*} value The value to check.
+   * @returns {boolean} Returns `true` if the `value` is an array, else `false`.
+   * @example
+   *
+   * (function() { return _.isArray(arguments); })();
+   * // => false
+   *
+   * _.isArray([1, 2, 3]);
+   * // => true
+   */
+  var isArray = nativeIsArray || function(value) {
+    return value && typeof value == 'object' && typeof value.length == 'number' &&
+      toString.call(value) == arrayClass || false;
+  };
+
   /**
   /**
    * A fallback implementation of `Object.keys` which produces an array of the
    * A fallback implementation of `Object.keys` which produces an array of the
    * given object's own enumerable property names.
    * given object's own enumerable property names.
@@ -593,6 +770,94 @@
     return result
     return result
   };
   };
 
 
+  /**
+   * Creates a clone of `value`. If `isDeep` is `true` nested objects will also
+   * be cloned, otherwise they will be assigned by reference. If a callback
+   * is provided it will be executed to produce the cloned values. If the
+   * callback returns `undefined` cloning will be handled by the method instead.
+   * The callback is bound to `thisArg` and invoked with one argument; (value).
+   *
+   * @static
+   * @memberOf _
+   * @category Objects
+   * @param {*} value The value to clone.
+   * @param {boolean} [isDeep=false] Specify a deep clone.
+   * @param {Function} [callback] The function to customize cloning values.
+   * @param {*} [thisArg] The `this` binding of `callback`.
+   * @returns {*} Returns the cloned value.
+   * @example
+   *
+   * var characters = [
+   *   { 'name': 'barney', 'age': 36 },
+   *   { 'name': 'fred',   'age': 40 }
+   * ];
+   *
+   * var shallow = _.clone(characters);
+   * shallow[0] === characters[0];
+   * // => true
+   *
+   * var deep = _.clone(characters, true);
+   * deep[0] === characters[0];
+   * // => false
+   *
+   * _.mixin({
+   *   'clone': _.partialRight(_.clone, function(value) {
+   *     return _.isElement(value) ? value.cloneNode(false) : undefined;
+   *   })
+   * });
+   *
+   * var clone = _.clone(document.body);
+   * clone.childNodes.length;
+   * // => 0
+   */
+  function clone(value, isDeep, callback, thisArg) {
+    // allows working with "Collections" methods without using their `index`
+    // and `collection` arguments for `isDeep` and `callback`
+    if (typeof isDeep != 'boolean' && isDeep != null) {
+      thisArg = callback;
+      callback = isDeep;
+      isDeep = false;
+    }
+    return baseClone(value, isDeep, typeof callback == 'function' && baseCreateCallback(callback, thisArg, 1));
+  }
+
+  /**
+   * 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
+   * 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
+   *
+   * _.forOwn({ '0': 'zero', '1': 'one', 'length': 2 }, function(num, key) {
+   *   console.log(key);
+   * });
+   * // => logs '0', '1', and 'length' (property order is not guaranteed across environments)
+   */
+  var forOwn = 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);
+      var ownIndex = -1,
+          ownProps = objectTypes[typeof iterable] && keys(iterable),
+          length = ownProps ? ownProps.length : 0;
+
+      while (++ownIndex < length) {
+        index = ownProps[ownIndex];
+        if (callback(iterable[index], index, collection) === false) return result;
+      }
+    return result
+  };
+
   /**
   /**
    * Checks if `value` is a function.
    * Checks if `value` is a function.
    *
    *
@@ -640,6 +905,51 @@
 
 
   /*--------------------------------------------------------------------------*/
   /*--------------------------------------------------------------------------*/
 
 
+  /**
+   * Iterates over elements of a collection, executing the callback for each
+   * element. The callback is bound to `thisArg` and invoked with three arguments;
+   * (value, index|key, collection). Callbacks may exit iteration early by
+   * explicitly returning `false`.
+   *
+   * Note: As with other "Collections" methods, objects with a `length` property
+   * are iterated like arrays. To avoid this behavior `_.forIn` or `_.forOwn`
+   * may be used for object iteration.
+   *
+   * @static
+   * @memberOf _
+   * @alias each
+   * @category Collections
+   * @param {Array|Object|string} collection The collection to iterate over.
+   * @param {Function} [callback=identity] The function called per iteration.
+   * @param {*} [thisArg] The `this` binding of `callback`.
+   * @returns {Array|Object|string} Returns `collection`.
+   * @example
+   *
+   * _([1, 2, 3]).forEach(function(num) { console.log(num); }).join(',');
+   * // => logs each number and returns '1,2,3'
+   *
+   * _.forEach({ 'one': 1, 'two': 2, 'three': 3 }, function(num) { console.log(num); });
+   * // => logs each number and returns the object (property order is not guaranteed across environments)
+   */
+  function forEach(collection, callback, thisArg) {
+    var index = -1,
+        length = collection ? collection.length : 0;
+
+    callback = callback && typeof thisArg == 'undefined' ? callback : baseCreateCallback(callback, thisArg, 3);
+    if (typeof length == 'number') {
+      while (++index < length) {
+        if (callback(collection[index], index, collection) === false) {
+          break;
+        }
+      }
+    } else {
+      forOwn(collection, callback);
+    }
+    return collection;
+  }
+
+  /*--------------------------------------------------------------------------*/
+
   /**
   /**
    * Creates a function that, when called, invokes `func` with the `this`
    * Creates a function that, when called, invokes `func` with the `this`
    * binding of `thisArg` and prepends any additional `bind` arguments to those
    * binding of `thisArg` and prepends any additional `bind` arguments to those
@@ -708,13 +1018,19 @@
 
 
   lodash.assign = assign;
   lodash.assign = assign;
   lodash.bind = bind;
   lodash.bind = bind;
+  lodash.forEach = forEach;
+  lodash.forOwn = forOwn;
   lodash.keys = keys;
   lodash.keys = keys;
 
 
+  lodash.each = forEach;
   lodash.extend = assign;
   lodash.extend = assign;
 
 
   /*--------------------------------------------------------------------------*/
   /*--------------------------------------------------------------------------*/
 
 
+  // add functions that return unwrapped values when chaining
+  lodash.clone = clone;
   lodash.identity = identity;
   lodash.identity = identity;
+  lodash.isArray = isArray;
   lodash.isFunction = isFunction;
   lodash.isFunction = isFunction;
   lodash.isObject = isObject;
   lodash.isObject = isObject;
   lodash.noop = noop;
   lodash.noop = noop;

+ 118 - 0
packages/ckeditor5-ui/src/mvc/collection.js

@@ -0,0 +1,118 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+/**
+ * Collections are ordered sets of models.
+ *
+ * @class Collection
+ * @extends BasicClass
+ */
+
+CKEDITOR.define( [ 'basicclass' ], function( BasicClass ) {
+	var Collection = BasicClass.extend( {
+		/**
+		 * Creates a new Collection instance.
+		 *
+		 * @constructor
+		 */
+		constructor: function Collection() {
+			/**
+			 * The internal list of models in the collection.
+			 *
+			 * @property _models
+			 * @private
+			 */
+			Object.defineProperty( this, '_models', {
+				value: []
+			} );
+		},
+
+		/**
+		 * Adds an item into the collection.
+		 *
+		 * Note that this is an array-like collection, so the same item can be present more than once. This behavior is
+		 * for performance purposes only and is not guaranteed to be kept in the same way in the future.
+		 *
+		 * @param {Model} model The item to be added.
+		 */
+		add: function( model ) {
+			this._models.push( model );
+
+			this.fire( 'add', model );
+		},
+
+		/**
+		 * Gets one item from the collection.
+		 *
+		 * @param {Number} index The index to take the item from.
+		 * @returns {Model} The requested item.
+		 */
+		get: function( index ) {
+			var model = this._models[ index ];
+
+			if ( !model ) {
+				throw new Error( 'Index not found' );
+			}
+
+			return model;
+		},
+
+		/**
+		 * Removes an item from the collection.
+		 *
+		 * @param {Model|Number} modelOrIndex Either the item itself or its index inside the collection.
+		 * @returns {Model} The removed item.
+		 */
+		remove: function( modelOrIndex ) {
+			// If a model has been passed, convert it to its index.
+			if ( typeof modelOrIndex != 'number' ) {
+				modelOrIndex = this._models.indexOf( modelOrIndex );
+
+				if ( modelOrIndex == -1 ) {
+					throw new Error( 'Model not found' );
+				}
+			}
+
+			var removedModel = this._models.splice( modelOrIndex, 1 )[ 0 ];
+
+			if ( !removedModel ) {
+				throw new Error( 'Index not found' );
+			}
+
+			this.fire( 'remove', removedModel );
+
+			return removedModel;
+		}
+	} );
+
+	/**
+	 * The number of items available in the collection.
+	 *
+	 * @property length
+	 */
+	Object.defineProperty( Collection.prototype, 'length', {
+		get: function() {
+			return this._models.length;
+		}
+	} );
+
+	return Collection;
+} );
+
+/**
+ * Fired when an item is added to the collection.
+ a
+ * @event add
+ * @param {Model} model The added item.
+ */
+
+/**
+ * Fired when an item is removed from the collection.
+ *
+ * @event remove
+ * @param {Model} model The removed item.
+ */

+ 106 - 0
packages/ckeditor5-ui/src/mvc/model.js

@@ -0,0 +1,106 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+/**
+ * The base MVC model class.
+ *
+ * @class Model
+ * @extends BasicClass
+ */
+
+CKEDITOR.define( [ 'basicclass', 'utils' ], function( BasicClass, utils ) {
+	var Model = BasicClass.extend( {
+		/**
+		 * Creates a new Model instance.
+		 *
+		 * @param {Object} [attributes] The model state attributes to be set during the instance creation.
+		 * @param {Object} [properties] The properties to be appended to the instance during creation.
+		 * @method constructor
+		 */
+		constructor: function Model( attributes, properties ) {
+			/**
+			 * The internal hash containing the model's state.
+			 *
+			 * @property _attributes
+			 * @private
+			 */
+			Object.defineProperty( this, '_attributes', {
+				value: {}
+			} );
+
+			// Extend this instance with the additional (out of state) properties.
+			if ( properties ) {
+				utils.extend( this, properties );
+			}
+
+			// Initialize the attributes.
+			if ( attributes ) {
+				this.set( attributes );
+			}
+		},
+
+		/**
+		 * Creates and sets the value of a model attribute of this object. This attribute will be part of the model
+		 * state and will be observable.
+		 *
+		 * It accepts also a single object literal containing key/value pairs with attributes to be set.
+		 *
+		 * @param {String} name The attributes name.
+		 * @param {*} value The attributes value.
+		 */
+		set: function( name, value ) {
+			// If the first parameter is an Object, we gonna interact through its properties.
+			if ( utils.isObject( name ) ) {
+				Object.keys( name ).forEach( function( attr ) {
+					this.set( attr, name[ attr ] );
+				}, this );
+
+				return;
+			}
+
+			Object.defineProperty( this, name, {
+				enumerable: true,
+				configurable: true,
+
+				get: function() {
+					return this._attributes[ name ];
+				},
+
+				set: function( value ) {
+					var oldValue = this._attributes[ name ];
+
+					if ( oldValue !== value ) {
+						this._attributes[ name ] = value;
+						this.fire( 'change', name, value, oldValue );
+						this.fire( 'change:' + name, value, oldValue );
+					}
+				}
+			} );
+
+			this[ name ] = value;
+		}
+	} );
+
+	return Model;
+} );
+
+/**
+ * Fired when an attribute changed value.
+ *
+ * @event change
+ * @param {String} name The attribute name.
+ * @param {*} value The new attribute value.
+ * @param {*} oldValue The previous attribute value.
+ */
+
+/**
+ * Fired when an specific attribute changed value.
+ *
+ * @event change:{attribute}
+ * @param {*} value The new attribute value.
+ * @param {*} oldValue The previous attribute value.
+ */

+ 17 - 1
packages/ckeditor5-ui/src/utils-lodash.js

@@ -17,13 +17,29 @@
 	// The list of Lo-Dash methods to include in "utils".
 	// The list of Lo-Dash methods to include in "utils".
 	// It is mandatory to execute `grunt lodash` after changes to this list.
 	// It is mandatory to execute `grunt lodash` after changes to this list.
 	var lodashInclude = [
 	var lodashInclude = [
+		/**
+		 * See Lo-Dash: https://lodash.com/docs#clone
+		 *
+		 * @member utils
+		 * @method clone
+		 */
+		'clone',
+
 		/**
 		/**
 		 * See Lo-Dash: https://lodash.com/docs#assign (alias)
 		 * See Lo-Dash: https://lodash.com/docs#assign (alias)
 		 *
 		 *
 		 * @member utils
 		 * @member utils
 		 * @method extend
 		 * @method extend
 		 */
 		 */
-		'extend'
+		'extend',
+
+		/**
+		 * See Lo-Dash: https://lodash.com/docs#isObject
+		 *
+		 * @member utils
+		 * @method isObject
+		 */
+		'isObject'
 	];
 	];
 
 
 	// Make this compatible with CommonJS as well so it can be used in Node (e.g. "grunt lodash").
 	// Make this compatible with CommonJS as well so it can be used in Node (e.g. "grunt lodash").

+ 83 - 0
packages/ckeditor5-ui/tests/basicclass/basicclass.js

@@ -0,0 +1,83 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals describe, it, expect, bender */
+
+'use strict';
+
+var modules = bender.amd.require( 'basicclass' );
+
+describe( 'extend', function() {
+	it( 'should extend classes', function() {
+		var BasicClass = modules.basicclass;
+
+		var Truck = BasicClass.extend( {
+			loadContainers: function() {}
+		} );
+
+		var volvoTruck = new Truck();
+
+		expect( volvoTruck ).to.be.an.instanceof( Truck );
+		expect( volvoTruck ).to.be.an.instanceof( BasicClass );
+		expect( volvoTruck ).to.have.property( 'loadContainers' ).to.be.a( 'function' );
+
+		var Spacecraft = Truck.extend( {
+			jumpToHyperspace: function() {}
+		} );
+
+		var falcon = new Spacecraft();
+		expect( falcon ).to.be.an.instanceof( Spacecraft );
+		expect( falcon ).to.be.an.instanceof( Truck );
+		expect( falcon ).to.be.an.instanceof( BasicClass );
+		expect( falcon ).to.have.property( 'loadContainers' ).to.be.a( 'function' );
+		expect( falcon ).to.have.property( 'jumpToHyperspace' ).to.be.a( 'function' );
+	} );
+
+	it( 'should extend the prototype and add statics', function() {
+		var BasicClass = modules.basicclass;
+
+		var Truck = BasicClass.extend( {
+			property1: 1,
+			property2: function() {}
+		}, {
+			static1: 1,
+			static2: function() {}
+		} );
+
+		expect( Truck ).to.have.property( 'static1' ).to.equals( 1 );
+		expect( Truck ).to.have.property( 'static2' ).to.be.a( 'function' );
+
+		var truck = new Truck();
+
+		expect( truck ).to.have.property( 'property1' ).to.equals( 1 );
+		expect( truck ).to.have.property( 'property2' ).to.be.a( 'function' );
+	} );
+
+	it( 'should use a custom constructor', function() {
+		var BasicClass = modules.basicclass;
+
+		function customConstructor() {}
+
+		var Truck = BasicClass.extend( {
+			constructor: customConstructor
+		} );
+
+		expect( Truck ).to.equals( customConstructor );
+		expect( Truck.prototype ).to.not.have.ownProperty( 'constructor' );
+
+		expect( new Truck() ).to.be.an.instanceof( Truck );
+		expect( new Truck() ).to.be.an.instanceof( BasicClass );
+	} );
+} );
+
+describe( 'BasicClass', function() {
+	it( 'should be an event emitter', function() {
+		var BasicClass = modules.basicclass;
+
+		var basic = new BasicClass();
+
+		expect( basic ).to.have.property( 'fire' ).to.be.a( 'function' );
+	} );
+} );

+ 4 - 3
packages/ckeditor5-ui/tests/eventinfo/eventinfo.js

@@ -13,8 +13,9 @@ describe( 'EventInfo', function() {
 	it( 'should be created properly', function() {
 	it( 'should be created properly', function() {
 		var EventInfo = modules.eventinfo;
 		var EventInfo = modules.eventinfo;
 
 
-		var event = new EventInfo( 'test' );
+		var event = new EventInfo( this, 'test' );
 
 
+		expect( event.source ).to.equals( this );
 		expect( event.name ).to.equals( 'test' );
 		expect( event.name ).to.equals( 'test' );
 		expect( event.stop.called ).to.not.be.true();
 		expect( event.stop.called ).to.not.be.true();
 		expect( event.off.called ).to.not.be.true();
 		expect( event.off.called ).to.not.be.true();
@@ -23,7 +24,7 @@ describe( 'EventInfo', function() {
 	it( 'should have stop() and off() marked', function() {
 	it( 'should have stop() and off() marked', function() {
 		var EventInfo = modules.eventinfo;
 		var EventInfo = modules.eventinfo;
 
 
-		var event = new EventInfo( 'test' );
+		var event = new EventInfo( this, 'test' );
 
 
 		event.stop();
 		event.stop();
 		event.off();
 		event.off();
@@ -35,7 +36,7 @@ describe( 'EventInfo', function() {
 	it( 'should not mark "called" in future instances', function() {
 	it( 'should not mark "called" in future instances', function() {
 		var EventInfo = modules.eventinfo;
 		var EventInfo = modules.eventinfo;
 
 
-		var event = new EventInfo( 'test' );
+		var event = new EventInfo( this, 'test' );
 
 
 		event.stop();
 		event.stop();
 		event.off();
 		event.off();

+ 127 - 0
packages/ckeditor5-ui/tests/mvc/collection/collection.js

@@ -0,0 +1,127 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals describe, it, expect, bender, sinon */
+
+'use strict';
+
+var modules = bender.amd.require( 'mvc/collection', 'mvc/model' );
+
+describe( 'add', function() {
+	it( 'should change the length and enable get', function() {
+		var Model = modules[ 'mvc/model' ];
+
+		var box = getCollection();
+
+		expect( box ).to.have.length( 0 );
+
+		box.add( getItem() );
+
+		expect( box ).to.have.length( 1 );
+
+		expect( box.get( 0 ) ).to.be.an.instanceof( Model );
+	} );
+
+	it( 'should fire the "add" event', function() {
+		var spy = sinon.spy();
+
+		var box = getCollection();
+		box.on( 'add', spy );
+
+		var item = getItem();
+		box.add( item );
+
+		sinon.assert.calledWithExactly( spy, sinon.match.has( 'source', box ), item );
+	} );
+} );
+
+describe( 'get', function() {
+	it( 'should throw an error on invalid index', function() {
+		var box = getCollection();
+		box.add( getItem() );
+
+		expect( function() {
+			box.get( 1 );
+		} ).to.throw( Error, 'Index not found' );
+	} );
+} );
+
+describe( 'remove', function() {
+	it( 'should remove the model by index', function() {
+		var box = getCollection();
+		var item = getItem();
+
+		box.add( item );
+
+		expect( box ).to.have.length( 1 );
+
+		box.remove( 0 );
+
+		expect( box ).to.have.length( 0 );
+	} );
+
+	it( 'should remove the model by model', function() {
+		var box = getCollection();
+		var item = getItem();
+
+		box.add( item );
+
+		expect( box ).to.have.length( 1 );
+
+		box.remove( item );
+
+		expect( box ).to.have.length( 0 );
+	} );
+
+	it( 'should fire the "remove" event', function() {
+		var box = getCollection();
+		var item1 = getItem();
+		var item2 = getItem();
+
+		box.add( item1 );
+		box.add( item2 );
+
+		var spy = sinon.spy();
+
+		box.on( 'remove', spy );
+
+		box.remove( 1 );		// by index
+		box.remove( item1 );	// by model
+
+		sinon.assert.calledTwice( spy );
+		sinon.assert.calledWithExactly( spy, sinon.match.has( 'source', box ), item1 );
+		sinon.assert.calledWithExactly( spy, sinon.match.has( 'source', box ), item2 );
+	} );
+
+	it( 'should throw an error on invalid index', function() {
+		var box = getCollection();
+		box.add( getItem() );
+
+		expect( function() {
+			box.remove( 1 );
+		} ).to.throw( Error, 'Index not found' );
+	} );
+
+	it( 'should throw an error on invalid model', function() {
+		var box = getCollection();
+		box.add( getItem() );
+
+		expect( function() {
+			box.remove( getItem() );
+		} ).to.throw( Error, 'Model not found' );
+	} );
+} );
+
+function getCollection() {
+	var Collection = modules[ 'mvc/collection' ];
+
+	return new Collection();
+}
+
+function getItem() {
+	var Model = modules[ 'mvc/model' ];
+
+	return new Model();
+}

+ 153 - 0
packages/ckeditor5-ui/tests/mvc/model/model.js

@@ -0,0 +1,153 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals describe, it, expect, beforeEach, bender, sinon */
+
+'use strict';
+
+var modules = bender.amd.require( 'mvc/model', 'eventinfo' );
+
+var Car, car;
+
+describe( 'Model', function() {
+	beforeEach( 'Create a test model instance', function() {
+		var Model = modules[ 'mvc/model' ];
+
+		Car = Model.extend();
+
+		car = new Car( {
+			color: 'red',
+			year: 2015
+		} );
+	} );
+
+	//////////
+
+	it( 'should set _attributes on creation', function() {
+		expect( car._attributes ).to.deep.equal( {
+			color: 'red',
+			year: 2015
+		} );
+	} );
+
+	it( 'should get correctly after set', function() {
+		car.color = 'blue';
+
+		expect( car.color ).to.equal( 'blue' );
+		expect( car._attributes.color ).to.equal( 'blue' );
+	} );
+
+	it( 'should get correctly after setting _attributes', function() {
+		car._attributes.color = 'blue';
+
+		expect( car.color ).to.equal( 'blue' );
+	} );
+
+	it( 'should add properties on creation', function() {
+		var car = new Car( null, {
+			prop: 1
+		} );
+
+		expect( car ).to.have.property( 'prop' ).to.equals( 1 );
+	} );
+
+	//////////
+
+	describe( 'set', function() {
+		it( 'should work when passing an object', function() {
+			car.set( {
+				color: 'blue',	// Override
+				wheels: 4,
+				seats: 5
+			} );
+
+			expect( car._attributes ).to.deep.equal( {
+				color: 'blue',
+				year: 2015,
+				wheels: 4,
+				seats: 5
+			} );
+		} );
+
+		it( 'should work when passing a key/value pair', function() {
+			car.set( 'color', 'blue' );
+			car.set( 'wheels', 4 );
+
+			expect( car._attributes ).to.deep.equal( {
+				color: 'blue',
+				year: 2015,
+				wheels: 4
+			} );
+		} );
+
+		it( 'should fire the "change" event', function() {
+			var EventInfo = modules.eventinfo;
+
+			var spy = sinon.spy();
+			var spyColor = sinon.spy();
+			var spyYear = sinon.spy();
+			var spyWheels = sinon.spy();
+
+			car.on( 'change', spy );
+			car.on( 'change:color', spyColor );
+			car.on( 'change:year', spyYear );
+			car.on( 'change:wheels', spyWheels );
+
+			// Set property in all possible ways.
+			car.color = 'blue';
+			car.set( { year: 2003 } );
+			car.set( 'wheels', 4 );
+
+			// Check number of calls.
+			sinon.assert.calledThrice( spy );
+			sinon.assert.calledOnce( spyColor );
+			sinon.assert.calledOnce( spyYear );
+			sinon.assert.calledOnce( spyWheels );
+
+			// Check context.
+			sinon.assert.alwaysCalledOn( spy, car );
+			sinon.assert.calledOn( spyColor, car );
+			sinon.assert.calledOn( spyYear, car );
+			sinon.assert.calledOn( spyWheels, car );
+
+			// Check params.
+			sinon.assert.calledWithExactly( spy, sinon.match.instanceOf( EventInfo ), 'color', 'blue', 'red' );
+			sinon.assert.calledWithExactly( spy, sinon.match.instanceOf( EventInfo ), 'year', 2003, 2015 );
+			sinon.assert.calledWithExactly( spy, sinon.match.instanceOf( EventInfo ), 'wheels', 4, sinon.match.typeOf( 'undefined' ) );
+			sinon.assert.calledWithExactly( spyColor, sinon.match.instanceOf( EventInfo ), 'blue', 'red' );
+			sinon.assert.calledWithExactly( spyYear, sinon.match.instanceOf( EventInfo ), 2003, 2015 );
+			sinon.assert.calledWithExactly( spyWheels, sinon.match.instanceOf( EventInfo ), 4, sinon.match.typeOf( 'undefined' ) );
+		} );
+
+		it( 'should not fire the "change" event for the same attribute value', function() {
+			var spy = sinon.spy();
+			var spyColor = sinon.spy();
+
+			car.on( 'change', spy );
+			car.on( 'change:color', spyColor );
+
+			// Set the "color" property in all possible ways.
+			car.color = 'red';
+			car.set( 'color', 'red' );
+			car.set( { color: 'red' } );
+
+			sinon.assert.notCalled( spy );
+			sinon.assert.notCalled( spyColor );
+		} );
+	} );
+
+	describe( 'extend', function() {
+		it( 'should create new Model based classes', function() {
+			var Model = modules[ 'mvc/model' ];
+
+			var Truck = Car.extend();
+
+			var truck = new Truck();
+
+			expect( truck ).to.be.an.instanceof( Car );
+			expect( truck ).to.be.an.instanceof( Model );
+		} );
+	} );
+} );