浏览代码

Porting... porting... porting.

Piotrek Koszuliński 10 年之前
父节点
当前提交
83a3621082

+ 2 - 2
packages/ckeditor5-ui/src/emittermixin.js

@@ -13,7 +13,7 @@
  */
 
 import EventInfo from './eventinfo.js';
-import { uid } from './utils.js';
+import utils from './utils.js';
 
 const EmitterMixin = {
 	/**
@@ -134,7 +134,7 @@ const EmitterMixin = {
 		}
 
 		if ( !( emitterId = emitter._emitterId ) ) {
-			emitterId = emitter._emitterId = uid();
+			emitterId = emitter._emitterId = utils.uid();
 		}
 
 		if ( !( emitterInfo = emitters[ emitterId ] ) ) {

+ 3 - 3
packages/ckeditor5-ui/src/eventinfo.js

@@ -12,7 +12,7 @@
  * @class EventInfo
  */
 
-import { spy } from './utils.js';
+import utils from './utils.js';
 
 export default class EventInfo {
 	constructor( source, name ) {
@@ -33,13 +33,13 @@ export default class EventInfo {
 		 *
 		 * @method
 		 */
-		this.stop = spy();
+		this.stop = utils.spy();
 
 		/**
 		 * Removes the current callback from future interactions of this event.
 		 *
 		 * @method
 		 */
-		this.off = spy();
+		this.off = utils.spy();
 	}
 }

+ 150 - 154
packages/ckeditor5-ui/src/ui/controller.js

@@ -5,188 +5,184 @@
 
 'use strict';
 
-CKEDITOR.define( [
-	'collection',
-	'model',
-	'ckeditorerror',
-], function( Collection, Model, CKEditorError ) {
-	class Controller extends Model {
+import Collection from '../collection.js';
+import Model from '../model.js';
+import CKEditorError from '../ckeditorerror.js';
+
+export default class Controller extends Model {
+	/**
+	 * Creates an instance of the {@link Controller} class.
+	 *
+	 * @param {Model} [model] Model of this Controller.
+	 * @param {View} [view] View instance of this Controller.
+	 * @constructor
+	 */
+	constructor( model, view ) {
+		super();
+
 		/**
-		 * Creates an instance of the {@link Controller} class.
+		 * Model of this controller.
 		 *
-		 * @param {Model} [model] Model of this Controller.
-		 * @param {View} [view] View instance of this Controller.
-		 * @constructor
+		 * @property {Model}
 		 */
-		constructor( model, view ) {
-			super();
-
-			/**
-			 * Model of this controller.
-			 *
-			 * @property {Model}
-			 */
-			this.model = model || null;
+		this.model = model || null;
 
-			/**
-			 * Set `true` after {@link #init}.
-			 *
-			 * @property {Boolean}
-			 */
-			this.ready = false;
+		/**
+		 * Set `true` after {@link #init}.
+		 *
+		 * @property {Boolean}
+		 */
+		this.ready = false;
 
-			/**
-			 * View of this controller.
-			 *
-			 * @property {View}
-			 */
-			this.view = view || null;
+		/**
+		 * View of this controller.
+		 *
+		 * @property {View}
+		 */
+		this.view = view || null;
 
-			/**
-			 * A collection of {@link ControllerCollection} instances containing
-			 * child controllers.
-			 *
-			 * @property {Collection}
-			 */
-			this.collections = new Collection( {
-				idProperty: 'name'
+		/**
+		 * A collection of {@link ControllerCollection} instances containing
+		 * child controllers.
+		 *
+		 * @property {Collection}
+		 */
+		this.collections = new Collection( {
+			idProperty: 'name'
+		} );
+
+		// Listen to {@link ControllerCollection#add} and {@link ControllerCollection#remove}
+		// of newly added Collection to synchronize this controller's view and children
+		// controllers' views in the future.
+		this.collections.on( 'add', ( evt, collection ) => {
+			// Set the {@link ControllerCollection#parent} to this controller.
+			// It allows the collection to determine the {@link #ready} state of this controller
+			// and accordingly initialize a child controller when added.
+			collection.parent = this;
+
+			this.listenTo( collection, 'add', ( evt, childController, index ) => {
+				// Child view is added to corresponding region in this controller's view
+				// when a new Controller joins the collection.
+				if ( this.ready && childController.view ) {
+					this.view.addChild( collection.name, childController.view, index );
+				}
 			} );
 
-			// Listen to {@link ControllerCollection#add} and {@link ControllerCollection#remove}
-			// of newly added Collection to synchronize this controller's view and children
-			// controllers' views in the future.
-			this.collections.on( 'add', ( evt, collection ) => {
-				// Set the {@link ControllerCollection#parent} to this controller.
-				// It allows the collection to determine the {@link #ready} state of this controller
-				// and accordingly initialize a child controller when added.
-				collection.parent = this;
-
-				this.listenTo( collection, 'add', ( evt, childController, index ) => {
-					// Child view is added to corresponding region in this controller's view
-					// when a new Controller joins the collection.
-					if ( this.ready && childController.view ) {
-						this.view.addChild( collection.name, childController.view, index );
-					}
-				} );
-
-				this.listenTo( collection, 'remove', ( evt, childController ) => {
-					// Child view is removed from corresponding region in this controller's view
-					// when a new Controller is removed from the the collection.
-					if ( this.ready && childController.view ) {
-						this.view.removeChild( collection.name, childController.view );
-					}
-				} );
+			this.listenTo( collection, 'remove', ( evt, childController ) => {
+				// Child view is removed from corresponding region in this controller's view
+				// when a new Controller is removed from the the collection.
+				if ( this.ready && childController.view ) {
+					this.view.removeChild( collection.name, childController.view );
+				}
 			} );
+		} );
 
-			this.collections.on( 'remove', ( evt, collection ) => {
-				// Release the collection. Once removed from {@link #collections}, it can be
-				// moved to another controller.
-				collection.parent = null;
-
-				this.stopListening( collection );
-			} );
-		}
+		this.collections.on( 'remove', ( evt, collection ) => {
+			// Release the collection. Once removed from {@link #collections}, it can be
+			// moved to another controller.
+			collection.parent = null;
 
-		/**
-		 * Initializes the controller instance. The process includes:
-		 *
-		 * 1. Initialization of the child {@link #view}.
-		 * 2. Initialization of child controllers in {@link #collections}.
-		 * 3. Setting {@link #ready} flag `true`.
-		 *
-		 * @returns {Promise} A Promise resolved when the initialization process is finished.
-		 */
-		init() {
-			if ( this.ready ) {
-				/**
-				 * This Controller already been initialized.
-				 *
-				 * @error ui-controller-init-reinit
-				 */
-				throw new CKEditorError( 'ui-controller-init-reinit: This Controller already been initialized.' );
-			}
+			this.stopListening( collection );
+		} );
+	}
 
-			return Promise.resolve()
-				.then( this._initView.bind( this ) )
-				.then( this._initCollections.bind( this ) )
-				.then( () => {
-					this.ready = true;
-				} );
+	/**
+	 * Initializes the controller instance. The process includes:
+	 *
+	 * 1. Initialization of the child {@link #view}.
+	 * 2. Initialization of child controllers in {@link #collections}.
+	 * 3. Setting {@link #ready} flag `true`.
+	 *
+	 * @returns {Promise} A Promise resolved when the initialization process is finished.
+	 */
+	init() {
+		if ( this.ready ) {
+			/**
+			 * This Controller already been initialized.
+			 *
+			 * @error ui-controller-init-reinit
+			 */
+			throw new CKEditorError( 'ui-controller-init-reinit: This Controller already been initialized.' );
 		}
 
-		/**
-		 * Destroys the controller instance. The process includes:
-		 *
-		 * 1. Destruction of the child {@link #view}.
-		 * 2. Destruction of child controllers in {@link #collections}.
-		 *
-		 * @returns {Promise} A Promise resolved when the destruction process is finished.
-		 */
-		destroy() {
-			let promises = [];
-			let collection, childController;
-
-			for ( collection of this.collections ) {
-				for ( childController of collection ) {
-					promises.push( childController.destroy() );
-					collection.remove( childController );
-				}
+		return Promise.resolve()
+			.then( this._initView.bind( this ) )
+			.then( this._initCollections.bind( this ) )
+			.then( () => {
+				this.ready = true;
+			} );
+	}
 
-				this.collections.remove( collection );
+	/**
+	 * Destroys the controller instance. The process includes:
+	 *
+	 * 1. Destruction of the child {@link #view}.
+	 * 2. Destruction of child controllers in {@link #collections}.
+	 *
+	 * @returns {Promise} A Promise resolved when the destruction process is finished.
+	 */
+	destroy() {
+		let promises = [];
+		let collection, childController;
+
+		for ( collection of this.collections ) {
+			for ( childController of collection ) {
+				promises.push( childController.destroy() );
+				collection.remove( childController );
 			}
 
-			if ( this.view ) {
-				promises.push( Promise.resolve().then( () => {
-					return this.view.destroy();
-				} ) );
-			}
+			this.collections.remove( collection );
+		}
 
+		if ( this.view ) {
 			promises.push( Promise.resolve().then( () => {
-				this.model = this.ready = this.view = this.collections = null;
+				return this.view.destroy();
 			} ) );
-
-			return Promise.all( promises );
 		}
 
-		/**
-		 * Initializes the {@link #view} of this controller instance.
-		 *
-		 * @protected
-		 * @returns {Promise} A Promise resolved when initialization process is finished.
-		 */
-		_initView() {
-			let promise = Promise.resolve();
+		promises.push( Promise.resolve().then( () => {
+			this.model = this.ready = this.view = this.collections = null;
+		} ) );
 
-			if ( this.view ) {
-				promise = promise.then( this.view.init.bind( this.view ) );
-			}
+		return Promise.all( promises );
+	}
 
-			return promise;
+	/**
+	 * Initializes the {@link #view} of this controller instance.
+	 *
+	 * @protected
+	 * @returns {Promise} A Promise resolved when initialization process is finished.
+	 */
+	_initView() {
+		let promise = Promise.resolve();
+
+		if ( this.view ) {
+			promise = promise.then( this.view.init.bind( this.view ) );
 		}
 
-		/**
-		 * Initializes the {@link #collections} of this controller instance.
-		 *
-		 * @protected
-		 * @returns {Promise} A Promise resolved when initialization process is finished.
-		 */
-		_initCollections() {
-			const promises = [];
-			let collection, childController;
-
-			for ( collection of this.collections ) {
-				for ( childController of collection ) {
-					if ( this.view && childController.view ) {
-						this.view.addChild( collection.name, childController.view );
-					}
+		return promise;
+	}
 
-					promises.push( childController.init() );
+	/**
+	 * Initializes the {@link #collections} of this controller instance.
+	 *
+	 * @protected
+	 * @returns {Promise} A Promise resolved when initialization process is finished.
+	 */
+	_initCollections() {
+		const promises = [];
+		let collection, childController;
+
+		for ( collection of this.collections ) {
+			for ( childController of collection ) {
+				if ( this.view && childController.view ) {
+					this.view.addChild( collection.name, childController.view );
 				}
-			}
 
-			return Promise.all( promises );
+				promises.push( childController.init() );
+			}
 		}
-	}
 
-	return Controller;
-} );
+		return Promise.all( promises );
+	}
+}

+ 45 - 49
packages/ckeditor5-ui/src/ui/controllercollection.js

@@ -5,72 +5,68 @@
 
 'use strict';
 
+import Collection from '../collection.js';
+import CKEditorError from '../ckeditorerror.js';
+
 /**
  * Manages UI Controllers.
  *
  * @class ControllerCollection
  * @extends Collection
  */
-CKEDITOR.define( [
-	'collection',
-	'ckeditorerror'
-], ( Collection, CKEditorError ) => {
-	class ControllerCollection extends Collection {
-		/**
-		 * Creates an instance of the ControllerCollection class, initializing it with a name.
-		 *
-		 * @constructor
-		 */
-		constructor( name ) {
-			super();
-
-			if ( !name ) {
-				/**
-				 * ControllerCollection must be initialized with a name.
-				 *
-				 * @error ui-controllercollection-no-name
-				 */
-				throw new CKEditorError( 'ui-controllercollection-no-name: ControllerCollection must be initialized with a name.' );
-			}
-
-			/**
-			 * Name of this collection.
-			 *
-			 * @property {String}
-			 */
-			this.name = name;
+export default class ControllerCollection extends Collection {
+	/**
+	 * Creates an instance of the ControllerCollection class, initializing it with a name.
+	 *
+	 * @constructor
+	 */
+	constructor( name ) {
+		super();
 
+		if ( !name ) {
 			/**
-			 * Parent controller of this collection.
+			 * ControllerCollection must be initialized with a name.
 			 *
-			 * @property {Controller}
+			 * @error ui-controllercollection-no-name
 			 */
-			this.parent = null;
+			throw new CKEditorError( 'ui-controllercollection-no-name: ControllerCollection must be initialized with a name.' );
 		}
 
 		/**
-		 * Adds a child controller to the collection. If {@link #parent} {@link Controller}
-		 * instance is ready, the child view is initialized when added.
+		 * Name of this collection.
+		 *
+		 * @property {String}
+		 */
+		this.name = name;
+
+		/**
+		 * Parent controller of this collection.
 		 *
-		 * @param {Controller} controller A child controller.
-		 * @param {Number} [index] Index at which the child will be added to the collection.
-		 * @returns {Promise} A Promise resolved when the child {@link Controller#init} is done.
+		 * @property {Controller}
 		 */
-		add( controller, index ) {
-			super.add( controller, index );
+		this.parent = null;
+	}
 
-			// ChildController.init() returns Promise.
-			let promise = Promise.resolve();
+	/**
+	 * Adds a child controller to the collection. If {@link #parent} {@link Controller}
+	 * instance is ready, the child view is initialized when added.
+	 *
+	 * @param {Controller} controller A child controller.
+	 * @param {Number} [index] Index at which the child will be added to the collection.
+	 * @returns {Promise} A Promise resolved when the child {@link Controller#init} is done.
+	 */
+	add( controller, index ) {
+		super.add( controller, index );
 
-			if ( this.parent && this.parent.ready && !controller.ready ) {
-				promise = promise.then( () => {
-					return controller.init();
-				} );
-			}
+		// ChildController.init() returns Promise.
+		let promise = Promise.resolve();
 
-			return promise;
+		if ( this.parent && this.parent.ready && !controller.ready ) {
+			promise = promise.then( () => {
+				return controller.init();
+			} );
 		}
-	}
 
-	return ControllerCollection;
-} );
+		return promise;
+	}
+}

+ 198 - 195
packages/ckeditor5-ui/src/ui/domemittermixin.js

@@ -5,6 +5,121 @@
 
 'use strict';
 
+import EmitterMixin from '../emittermixin.js';
+import utils from '../utils.js';
+import objectUtils from '../lib/lodash/object.js';
+import log from '../log.js';
+
+/**
+ * Creates a ProxyEmitter instance. Such an instance is a bridge between a DOM Node firing events
+ * and any Host listening to them. It is backwards compatible with {@link EmitterMixin#on}.
+ *
+ * @class DOMEmitterMixin
+ * @mixins EmitterMixin
+ * @param {Node} node DOM Node that fires events.
+ * @returns {Object} ProxyEmitter instance bound to the DOM Node.
+ */
+class ProxyEmitter {
+	constructor( node ) {
+		// Set emitter ID to match DOM Node "expando" property.
+		this._emitterId = getNodeUID( node );
+
+		// Remember the DOM Node this ProxyEmitter is bound to.
+		this._domNode = node;
+	}
+}
+
+objectUtils.extend( ProxyEmitter.prototype, EmitterMixin, {
+	/**
+	 * Collection of native DOM listeners.
+	 *
+	 * @property {Object} _domListeners
+	 */
+
+	/**
+	 * Registers a callback function to be executed when an event is fired.
+	 *
+	 * It attaches a native DOM listener to the DOM Node. When fired,
+	 * a corresponding Emitter event will also fire with DOM Event object as an argument.
+	 *
+	 * @param {String} event The name of the event.
+	 * @param {Function} callback The function to be called on event.
+	 * @param {Object} [ctx] The object that represents `this` in the callback. Defaults to the object firing the
+	 * event.
+	 * @param {Number} [priority=10] The priority of this callback in relation to other callbacks to that same event.
+	 * Lower values are called first.
+	 */
+	on( event ) {
+		// Execute parent class method first.
+		EmitterMixin.on.apply( this, arguments );
+
+		// If the DOM Listener for given event already exist it is pointless
+		// to attach another one.
+		if ( this._domListeners && this._domListeners[ event ] ) {
+			return;
+		}
+
+		const domListener = this._createDomListener( event );
+
+		// Attach the native DOM listener to DOM Node.
+		this._domNode.addEventListener( event, domListener );
+
+		if ( !this._domListeners ) {
+			this._domListeners = {};
+		}
+
+		// Store the native DOM listener in this ProxyEmitter. It will be helpful
+		// when stopping listening to the event.
+		this._domListeners[ event ] = domListener;
+	},
+
+	/**
+	 * Stops executing the callback on the given event.
+	 *
+	 * @param {String} event The name of the event.
+	 * @param {Function} callback The function to stop being called.
+	 * @param {Object} [ctx] The context object to be removed, pared with the given callback. To handle cases where
+	 * the same callback is used several times with different contexts.
+	 */
+	off( event ) {
+		// Execute parent class method first.
+		EmitterMixin.off.apply( this, arguments );
+
+		let callbacks;
+
+		// Remove native DOM listeners which are orphans. If no callbacks
+		// are awaiting given event, detach native DOM listener from DOM Node.
+		// See: {@link on}.
+		if ( !( callbacks = this._events[ event ] ) || !callbacks.length ) {
+			this._domListeners[ event ].removeListener();
+		}
+	},
+
+	/**
+	 * Create a native DOM listener callback. When the native DOM event
+	 * is fired it will fire corresponding event on this ProxyEmitter.
+	 * Note: A native DOM Event is passed as an argument.
+	 *
+	 * @param {String} event
+	 * @returns {Function} The DOM listener callback.
+	 */
+	_createDomListener( event ) {
+		const domListener = domEvt => {
+			this.fire( event, domEvt );
+		};
+
+		// Supply the DOM listener callback with a function that will help
+		// detach it from the DOM Node, when it is no longer necessary.
+		// See: {@link off}.
+		domListener.removeListener = () => {
+			this._domNode.removeEventListener( event, domListener );
+			delete this._domListeners[ event ];
+		};
+
+		return domListener;
+	}
+} );
+
 /**
  * Mixin that injects the DOM events API into its host. It provides the API
  * compatible with {@link EmitterMixin}.
@@ -36,212 +151,100 @@
  * @singleton
  */
 
-CKEDITOR.define( [ 'emittermixin', 'utils', 'log' ], ( EmitterMixin, utils, log ) => {
-	const DOMEmitterMixin = {
-		/**
-		 * Registers a callback function to be executed when an event is fired in a specific Emitter or DOM Node.
-		 * It is backwards compatible with {@link EmitterMixin#listenTo}.
-		 *
-		 * @param {Emitter|Node} emitter The object that fires the event.
-		 * @param {String} event The name of the event.
-		 * @param {Function} callback The function to be called on event.
-		 * @param {Object} [ctx] The object that represents `this` in the callback. Defaults to `emitter`.
-		 * @param {Number} [priority=10] The priority of this callback in relation to other callbacks to that same event.
-		 * Lower values are called first.
-		 */
-		listenTo() {
-			const args = Array.prototype.slice.call( arguments );
-			const emitter = args[ 0 ];
-
-			// Check if emitter is an instance of DOM Node. If so, replace the argument with
-			// corresponding ProxyEmitter (or create one if not existing).
-			if ( emitter instanceof Node ) {
-				args[ 0 ] = this._getProxyEmitter( emitter ) || new ProxyEmitter( emitter );
-			}
-
-			// Execute parent class method with Emitter (or ProxyEmitter) instance.
-			EmitterMixin.listenTo.apply( this, args );
-		},
-
-		/**
-		 * Stops listening for events. It can be used at different levels:
-		 * It is backwards compatible with {@link EmitterMixin#listenTo}.
-		 *
-		 * * To stop listening to a specific callback.
-		 * * To stop listening to a specific event.
-		 * * To stop listening to all events fired by a specific object.
-		 * * To stop listening to all events fired by all object.
-		 *
-		 * @param {Emitter|Node} [emitter] The object to stop listening to. If omitted, stops it for all objects.
-		 * @param {String} [event] (Requires the `emitter`) The name of the event to stop listening to. If omitted, stops it
-		 * for all events from `emitter`.
-		 * @param {Function} [callback] (Requires the `event`) The function to be removed from the call list for the given
-		 * `event`.
-		 */
-		stopListening() {
-			const args = Array.prototype.slice.call( arguments );
-			const emitter = args[ 0 ];
-
-			// Check if emitter is an instance of DOM Node. If so, replace the argument with corresponding ProxyEmitter.
-			if ( emitter instanceof Node ) {
-				let proxy = this._getProxyEmitter( emitter );
-
-				if ( proxy ) {
-					args[ 0 ] = proxy;
-				} else {
-					log.error(
-						'domemittermixin-stoplistening: Stopped listening on a DOM Node that has no emitter or emitter is gone.',
-						emitter
-					);
-				}
-			}
-
-			// Execute parent class method with Emitter (or ProxyEmitter) instance.
-			EmitterMixin.stopListening.apply( this, args );
-		},
-
-		/**
-		 * Retrieves ProxyEmitter instance for given DOM Node residing in this Host.
-		 *
-		 * @param {Node} node DOM Node of the ProxyEmitter.
-		 * @return {ProxyEmitter} ProxyEmitter instance or null.
-		 */
-		_getProxyEmitter( node ) {
-			let proxy, emitters, emitterInfo;
-
-			// Get node UID. It allows finding Proxy Emitter for this DOM Node.
-			const uid = getNodeUID( node );
-
-			// Find existing Proxy Emitter for this DOM Node among emitters.
-			if ( ( emitters = this._listeningTo ) ) {
-				if ( ( emitterInfo = emitters[ uid ] ) ) {
-					proxy = emitterInfo.emitter;
-				}
-			}
-
-			return proxy || null;
-		}
-	};
-
+const DOMEmitterMixin = {
 	/**
-	 * Creates a ProxyEmitter instance. Such an instance is a bridge between a DOM Node firing events
-	 * and any Host listening to them. It is backwards compatible with {@link EmitterMixin#on}.
+	 * Registers a callback function to be executed when an event is fired in a specific Emitter or DOM Node.
+	 * It is backwards compatible with {@link EmitterMixin#listenTo}.
 	 *
-	 * @class DOMEmitterMixin
-	 * @mixins EmitterMixin
-	 * @param {Node} node DOM Node that fires events.
-	 * @returns {Object} ProxyEmitter instance bound to the DOM Node.
+	 * @param {Emitter|Node} emitter The object that fires the event.
+	 * @param {String} event The name of the event.
+	 * @param {Function} callback The function to be called on event.
+	 * @param {Object} [ctx] The object that represents `this` in the callback. Defaults to `emitter`.
+	 * @param {Number} [priority=10] The priority of this callback in relation to other callbacks to that same event.
+	 * Lower values are called first.
 	 */
-	class ProxyEmitter {
-		constructor( node ) {
-			// Set emitter ID to match DOM Node "expando" property.
-			this._emitterId = getNodeUID( node );
-
-			// Remember the DOM Node this ProxyEmitter is bound to.
-			this._domNode = node;
+	listenTo() {
+		const args = Array.prototype.slice.call( arguments );
+		const emitter = args[ 0 ];
+
+		// Check if emitter is an instance of DOM Node. If so, replace the argument with
+		// corresponding ProxyEmitter (or create one if not existing).
+		if ( emitter instanceof Node ) {
+			args[ 0 ] = this._getProxyEmitter( emitter ) || new ProxyEmitter( emitter );
 		}
-	}
 
-	utils.extend( ProxyEmitter.prototype, EmitterMixin, {
-		/**
-		 * Collection of native DOM listeners.
-		 *
-		 * @property {Object} _domListeners
-		 */
-
-		/**
-		 * Registers a callback function to be executed when an event is fired.
-		 *
-		 * It attaches a native DOM listener to the DOM Node. When fired,
-		 * a corresponding Emitter event will also fire with DOM Event object as an argument.
-		 *
-		 * @param {String} event The name of the event.
-		 * @param {Function} callback The function to be called on event.
-		 * @param {Object} [ctx] The object that represents `this` in the callback. Defaults to the object firing the
-		 * event.
-		 * @param {Number} [priority=10] The priority of this callback in relation to other callbacks to that same event.
-		 * Lower values are called first.
-		 */
-		on( event ) {
-			// Execute parent class method first.
-			EmitterMixin.on.apply( this, arguments );
-
-			// If the DOM Listener for given event already exist it is pointless
-			// to attach another one.
-			if ( this._domListeners && this._domListeners[ event ] ) {
-				return;
-			}
-
-			const domListener = this._createDomListener( event );
+		// Execute parent class method with Emitter (or ProxyEmitter) instance.
+		EmitterMixin.listenTo.apply( this, args );
+	},
 
-			// Attach the native DOM listener to DOM Node.
-			this._domNode.addEventListener( event, domListener );
-
-			if ( !this._domListeners ) {
-				this._domListeners = {};
-			}
-
-			// Store the native DOM listener in this ProxyEmitter. It will be helpful
-			// when stopping listening to the event.
-			this._domListeners[ event ] = domListener;
-		},
-
-		/**
-		 * Stops executing the callback on the given event.
-		 *
-		 * @param {String} event The name of the event.
-		 * @param {Function} callback The function to stop being called.
-		 * @param {Object} [ctx] The context object to be removed, pared with the given callback. To handle cases where
-		 * the same callback is used several times with different contexts.
-		 */
-		off( event ) {
-			// Execute parent class method first.
-			EmitterMixin.off.apply( this, arguments );
-
-			let callbacks;
-
-			// Remove native DOM listeners which are orphans. If no callbacks
-			// are awaiting given event, detach native DOM listener from DOM Node.
-			// See: {@link on}.
-			if ( !( callbacks = this._events[ event ] ) || !callbacks.length ) {
-				this._domListeners[ event ].removeListener();
+	/**
+	 * Stops listening for events. It can be used at different levels:
+	 * It is backwards compatible with {@link EmitterMixin#listenTo}.
+	 *
+	 * * To stop listening to a specific callback.
+	 * * To stop listening to a specific event.
+	 * * To stop listening to all events fired by a specific object.
+	 * * To stop listening to all events fired by all object.
+	 *
+	 * @param {Emitter|Node} [emitter] The object to stop listening to. If omitted, stops it for all objects.
+	 * @param {String} [event] (Requires the `emitter`) The name of the event to stop listening to. If omitted, stops it
+	 * for all events from `emitter`.
+	 * @param {Function} [callback] (Requires the `event`) The function to be removed from the call list for the given
+	 * `event`.
+	 */
+	stopListening() {
+		const args = Array.prototype.slice.call( arguments );
+		const emitter = args[ 0 ];
+
+		// Check if emitter is an instance of DOM Node. If so, replace the argument with corresponding ProxyEmitter.
+		if ( emitter instanceof Node ) {
+			let proxy = this._getProxyEmitter( emitter );
+
+			if ( proxy ) {
+				args[ 0 ] = proxy;
+			} else {
+				log.error(
+					'domemittermixin-stoplistening: Stopped listening on a DOM Node that has no emitter or emitter is gone.',
+					emitter
+				);
 			}
-		},
-
-		/**
-		 * Create a native DOM listener callback. When the native DOM event
-		 * is fired it will fire corresponding event on this ProxyEmitter.
-		 * Note: A native DOM Event is passed as an argument.
-		 *
-		 * @param {String} event
-		 * @returns {Function} The DOM listener callback.
-		 */
-		_createDomListener( event ) {
-			const domListener = domEvt => {
-				this.fire( event, domEvt );
-			};
-
-			// Supply the DOM listener callback with a function that will help
-			// detach it from the DOM Node, when it is no longer necessary.
-			// See: {@link off}.
-			domListener.removeListener = () => {
-				this._domNode.removeEventListener( event, domListener );
-				delete this._domListeners[ event ];
-			};
-
-			return domListener;
 		}
-	} );
 
-	return DOMEmitterMixin;
+		// Execute parent class method with Emitter (or ProxyEmitter) instance.
+		EmitterMixin.stopListening.apply( this, args );
+	},
 
 	/**
-	 * Gets an unique DOM Node identifier. The identifier will be set if not defined.
+	 * Retrieves ProxyEmitter instance for given DOM Node residing in this Host.
 	 *
-	 * @param {Node} node
-	 * @return {Number} UID for given DOM Node.
+	 * @param {Node} node DOM Node of the ProxyEmitter.
+	 * @return {ProxyEmitter} ProxyEmitter instance or null.
 	 */
-	function getNodeUID( node ) {
-		return node[ 'data-ck-expando' ] || ( node[ 'data-ck-expando' ] = utils.uid() );
+	_getProxyEmitter( node ) {
+		let proxy, emitters, emitterInfo;
+
+		// Get node UID. It allows finding Proxy Emitter for this DOM Node.
+		const uid = getNodeUID( node );
+
+		// Find existing Proxy Emitter for this DOM Node among emitters.
+		if ( ( emitters = this._listeningTo ) ) {
+			if ( ( emitterInfo = emitters[ uid ] ) ) {
+				proxy = emitterInfo.emitter;
+			}
+		}
+
+		return proxy || null;
 	}
-} );
+};
+
+export default DOMEmitterMixin;
+
+/**
+ * Gets an unique DOM Node identifier. The identifier will be set if not defined.
+ *
+ * @param {Node} node
+ * @return {Number} UID for given DOM Node.
+ */
+function getNodeUID( node ) {
+	return node[ 'data-ck-expando' ] || ( node[ 'data-ck-expando' ] = utils.uid() );
+}

+ 57 - 61
packages/ckeditor5-ui/src/ui/region.js

@@ -5,6 +5,9 @@
 
 'use strict';
 
+import Collection from '../collection.js';
+import Model from '../model.js';
+
 /**
  * Basic Region class.
  *
@@ -12,79 +15,72 @@
  * @extends Model
  */
 
-CKEDITOR.define( [
-	'collection',
-	'model'
-], ( Collection, Model ) => {
-	class Region extends Model {
+export default class Region extends Model {
+	/**
+	 * Creates an instance of the {@link Region} class.
+	 *
+	 * @param {String} name The name of the Region.
+	 * @param {HTMLElement} [el] The element used for this region.
+	 * @constructor
+	 */
+	constructor( name ) {
+		super();
+
 		/**
-		 * Creates an instance of the {@link Region} class.
+		 * The name of the region.
 		 *
-		 * @param {String} name The name of the Region.
-		 * @param {HTMLElement} [el] The element used for this region.
-		 * @constructor
+		 * @property {String}
 		 */
-		constructor( name ) {
-			super();
-
-			/**
-			 * The name of the region.
-			 *
-			 * @property {String}
-			 */
-			this.name = name;
+		this.name = name;
 
-			/**
-			 * Views which belong to the region.
-			 *
-			 * @property {Collection}
-			 */
-			this.views = new Collection();
-
-			/**
-			 * Element of this region (see {@link #init}).
-			 *
-			 * @property {HTMLElement}
-			 */
-			this.el = null;
-		}
+		/**
+		 * Views which belong to the region.
+		 *
+		 * @property {Collection}
+		 */
+		this.views = new Collection();
 
 		/**
-		 * Initializes region instance with an element. Usually it comes from {@link View#init}.
+		 * Element of this region (see {@link #init}).
 		 *
-		 * @param {HTMLElement} regiobEl Element of this region.
+		 * @property {HTMLElement}
 		 */
-		init( regionEl ) {
-			this.el = regionEl;
+		this.el = null;
+	}
 
-			if ( regionEl ) {
-				this.views.on( 'add', ( evt, childView, index ) => {
-					regionEl.insertBefore( childView.el, regionEl.childNodes[ index + 1 ] );
-				} );
+	/**
+	 * Initializes region instance with an element. Usually it comes from {@link View#init}.
+	 *
+	 * @param {HTMLElement} regiobEl Element of this region.
+	 */
+	init( regionEl ) {
+		this.el = regionEl;
 
-				this.views.on( 'remove', ( evt, childView ) => {
-					childView.el.remove();
-				} );
-			}
+		if ( regionEl ) {
+			this.views.on( 'add', ( evt, childView, index ) => {
+				regionEl.insertBefore( childView.el, regionEl.childNodes[ index + 1 ] );
+			} );
+
+			this.views.on( 'remove', ( evt, childView ) => {
+				childView.el.remove();
+			} );
 		}
+	}
 
-		/**
-		 * Destroys region instance.
-		 */
-		destroy() {
-			if ( this.el ) {
-				for ( let view of this.views ) {
-					view.el.remove();
-					this.views.remove( view );
-				}
+	/**
+	 * Destroys region instance.
+	 */
+	destroy() {
+		if ( this.el ) {
+			for ( let view of this.views ) {
+				view.el.remove();
+				this.views.remove( view );
 			}
-
-			// Drop the reference to HTMLElement but don't remove it from DOM.
-			// Element comes as a parameter and it could be a part of the View.
-			// Then it's up to the View what to do with it when the View is destroyed.
-			this.el = this.views = null;
 		}
-	}
 
-	return Region;
-} );
+		// Drop the reference to HTMLElement but don't remove it from DOM.
+		// Element comes as a parameter and it could be a part of the View.
+		// Then it's up to the View what to do with it when the View is destroyed.
+		this.el = this.views = null;
+	}
+}

+ 143 - 147
packages/ckeditor5-ui/src/ui/template.js

@@ -13,192 +13,188 @@
  * @class Template
  */
 
-CKEDITOR.define( () => {
-	class Template {
+export default class Template {
+	/**
+	 * Creates an instance of the {@link Template} class.
+	 *
+	 * @param {TemplateDefinition} def The definition of the template.
+	 * @constructor
+	 */
+	constructor( def ) {
 		/**
-		 * Creates an instance of the {@link Template} class.
+		 * Definition of this template.
 		 *
-		 * @param {TemplateDefinition} def The definition of the template.
-		 * @constructor
+		 * @property {TemplateDefinition}
 		 */
-		constructor( def ) {
-			/**
-			 * Definition of this template.
-			 *
-			 * @property {TemplateDefinition}
-			 */
-			this.def = def;
-		}
+		this.def = def;
+	}
 
-		/**
-		 * Renders HTMLElement using {@link #def}.
-		 *
-		 * @returns {HTMLElement}
-		 */
-		render() {
-			return this._renderElement( this.def, true );
-		}
+	/**
+	 * Renders HTMLElement using {@link #def}.
+	 *
+	 * @returns {HTMLElement}
+	 */
+	render() {
+		return this._renderElement( this.def, true );
+	}
 
-		/**
-		 * Renders an element from definition.
-		 *
-		 * @protected
-		 * @param {TemplateDefinition} def Definition of an element.
-		 * @param {Boolean} intoFragment If set, children are rendered into DocumentFragment.
-		 * @returns {HTMLElement} A rendered element.
-		 */
-		_renderElement( def, intoFragment ) {
-			if ( !def ) {
-				return null;
-			}
+	/**
+	 * Renders an element from definition.
+	 *
+	 * @protected
+	 * @param {TemplateDefinition} def Definition of an element.
+	 * @param {Boolean} intoFragment If set, children are rendered into DocumentFragment.
+	 * @returns {HTMLElement} A rendered element.
+	 */
+	_renderElement( def, intoFragment ) {
+		if ( !def ) {
+			return null;
+		}
 
-			const el = document.createElement( def.tag );
+		const el = document.createElement( def.tag );
 
-			// Set the text first.
-			this._renderElementText( def, el );
+		// Set the text first.
+		this._renderElementText( def, el );
 
-			// Set attributes.
-			this._renderElementAttributes( def, el );
+		// Set attributes.
+		this._renderElementAttributes( def, el );
 
-			// Invoke children recursively.
-			if ( intoFragment ) {
-				const docFragment = document.createDocumentFragment();
+		// Invoke children recursively.
+		if ( intoFragment ) {
+			const docFragment = document.createDocumentFragment();
 
-				this._renderElementChildren( def, docFragment );
+			this._renderElementChildren( def, docFragment );
 
-				el.appendChild( docFragment );
-			} else {
-				this._renderElementChildren( def, el );
-			}
+			el.appendChild( docFragment );
+		} else {
+			this._renderElementChildren( def, el );
+		}
 
-			// Activate DOM binding for event listeners.
-			this._activateElementListeners( def, el );
+		// Activate DOM binding for event listeners.
+		this._activateElementListeners( def, el );
 
-			return el;
-		}
+		return el;
+	}
 
-		/**
-		 * Renders element text content from definition.
-		 *
-		 * @protected
-		 * @param {TemplateDefinition} def Definition of an element.
-		 * @param {HTMLElement} el Element which is rendered.
-		 */
-		_renderElementText( def, el ) {
-			if ( def.text ) {
-				if ( typeof def.text == 'function' ) {
-					def.text( el, getTextUpdater() );
-				} else {
-					el.textContent = def.text;
-				}
+	/**
+	 * Renders element text content from definition.
+	 *
+	 * @protected
+	 * @param {TemplateDefinition} def Definition of an element.
+	 * @param {HTMLElement} el Element which is rendered.
+	 */
+	_renderElementText( def, el ) {
+		if ( def.text ) {
+			if ( typeof def.text == 'function' ) {
+				def.text( el, getTextUpdater() );
+			} else {
+				el.textContent = def.text;
 			}
 		}
+	}
 
-		/**
-		 * Renders element attributes from definition.
-		 *
-		 * @protected
-		 * @param {TemplateDefinition} def Definition of an element.
-		 * @param {HTMLElement} el Element which is rendered.
-		 */
-		_renderElementAttributes( def, el ) {
-			let attr, value;
-
-			for ( attr in def.attrs ) {
-				value = def.attrs[ attr ];
-
-				// Attribute bound directly to the model.
-				if ( typeof value == 'function' ) {
-					value( el, getAttributeUpdater( attr ) );
-				}
+	/**
+	 * Renders element attributes from definition.
+	 *
+	 * @protected
+	 * @param {TemplateDefinition} def Definition of an element.
+	 * @param {HTMLElement} el Element which is rendered.
+	 */
+	_renderElementAttributes( def, el ) {
+		let attr, value;
 
-				// Explicit attribute definition (string).
-				else {
-					// Attribute can be an array, i.e. classes.
-					if ( Array.isArray( value ) ) {
-						value = value.join( ' ' );
-					}
+		for ( attr in def.attrs ) {
+			value = def.attrs[ attr ];
 
-					el.setAttribute( attr, value );
-				}
+			// Attribute bound directly to the model.
+			if ( typeof value == 'function' ) {
+				value( el, getAttributeUpdater( attr ) );
 			}
-		}
 
-		/**
-		 * Recursively renders element children from definition by
-		 * calling {@link #_renderElement}.
-		 *
-		 * @protected
-		 * @param {TemplateDefinition} def Definition of an element.
-		 * @param {HTMLElement} el Element which is rendered.
-		 */
-		_renderElementChildren( def, el ) {
-			let child;
-
-			if ( def.children ) {
-				for ( child of def.children ) {
-					el.appendChild( this._renderElement( child ) );
+			// Explicit attribute definition (string).
+			else {
+				// Attribute can be an array, i.e. classes.
+				if ( Array.isArray( value ) ) {
+					value = value.join( ' ' );
 				}
-			}
-		}
 
-		/**
-		 * Activates element `on` listeners passed in element definition.
-		 *
-		 * @protected
-		 * @param {TemplateDefinition} def Definition of an element.
-		 * @param {HTMLElement} el Element which is rendered.
-		 */
-		_activateElementListeners( def, el ) {
-			if ( def.on ) {
-				let l, domEvtDef, name, selector;
-
-				for ( l in def.on ) {
-					domEvtDef = l.split( '@' );
-
-					if ( domEvtDef.length == 2 ) {
-						name = domEvtDef[ 0 ];
-						selector = domEvtDef[ 1 ];
-					} else {
-						name = l;
-						selector = null;
-					}
-
-					if ( Array.isArray( def.on[ l ] ) ) {
-						def.on[ l ].map( i => i( el, name, selector ) );
-					} else {
-						def.on[ l ]( el, name, selector );
-					}
-				}
+				el.setAttribute( attr, value );
 			}
 		}
 	}
 
 	/**
-	 * Returns a function which, when called in the context of HTMLElement,
-	 * it replaces element children with a text node of given value.
+	 * Recursively renders element children from definition by
+	 * calling {@link #_renderElement}.
 	 *
 	 * @protected
-	 * @param {Function}
+	 * @param {TemplateDefinition} def Definition of an element.
+	 * @param {HTMLElement} el Element which is rendered.
 	 */
-	function getTextUpdater() {
-		return ( el, value ) => el.textContent = value;
+	_renderElementChildren( def, el ) {
+		let child;
+
+		if ( def.children ) {
+			for ( child of def.children ) {
+				el.appendChild( this._renderElement( child ) );
+			}
+		}
 	}
 
 	/**
-	 * Returns a function which, when called in the context of HTMLElement,
-	 * it updates element's attribute with given value.
+	 * Activates element `on` listeners passed in element definition.
 	 *
 	 * @protected
-	 * @param {String} attr A name of the attribute to be updated.
-	 * @param {Function}
+	 * @param {TemplateDefinition} def Definition of an element.
+	 * @param {HTMLElement} el Element which is rendered.
 	 */
-	function getAttributeUpdater( attr ) {
-		return ( el, value ) => el.setAttribute( attr, value );
+	_activateElementListeners( def, el ) {
+		if ( def.on ) {
+			let l, domEvtDef, name, selector;
+
+			for ( l in def.on ) {
+				domEvtDef = l.split( '@' );
+
+				if ( domEvtDef.length == 2 ) {
+					name = domEvtDef[ 0 ];
+					selector = domEvtDef[ 1 ];
+				} else {
+					name = l;
+					selector = null;
+				}
+
+				if ( Array.isArray( def.on[ l ] ) ) {
+					def.on[ l ].map( i => i( el, name, selector ) );
+				} else {
+					def.on[ l ]( el, name, selector );
+				}
+			}
+		}
 	}
+}
 
-	return Template;
-} );
+/**
+ * Returns a function which, when called in the context of HTMLElement,
+ * it replaces element children with a text node of given value.
+ *
+ * @protected
+ * @param {Function}
+ */
+function getTextUpdater() {
+	return ( el, value ) => el.textContent = value;
+}
+
+/**
+ * Returns a function which, when called in the context of HTMLElement,
+ * it updates element's attribute with given value.
+ *
+ * @protected
+ * @param {String} attr A name of the attribute to be updated.
+ * @param {Function}
+ */
+function getAttributeUpdater( attr ) {
+	return ( el, value ) => el.setAttribute( attr, value );
+}
 
 /**
  * Definition of {@link Template}.

+ 408 - 412
packages/ckeditor5-ui/src/ui/view.js

@@ -5,6 +5,14 @@
 
 'use strict';
 
+import Collection from '../collection.js';
+import Model from '../model.js';
+import Region from './region.js';
+import Template from './template.js';
+import CKEditorError from '../ckeditorerror.js';
+import DOMEmitterMixin from './domemittermixin.js';
+import objectUtils from '../lib/lodash/object.js';
+
 /**
  * Basic View class.
  *
@@ -13,500 +21,488 @@
  * @mixins DOMEmitterMixin
  */
 
-CKEDITOR.define( [
-	'collection',
-	'model',
-	'ui/region',
-	'ui/template',
-	'ckeditorerror',
-	'ui/domemittermixin',
-	'utils'
-], ( Collection, Model, Region, Template, CKEditorError, DOMEmitterMixin, utils ) => {
-	class View extends Model {
+export default class View extends Model {
+	/**
+	 * Creates an instance of the {@link View} class.
+	 *
+	 * @param {Model} model (View)Model of this View.
+	 * @constructor
+	 */
+	constructor( model ) {
+		super();
+
+		/**
+		 * Model of this view.
+		 *
+		 * @property {Model}
+		 */
+		this.model = model || null;
+
+		/**
+		 * Regions of this view. See {@link #register}.
+		 *
+		 * @property {Collection}
+		 */
+		this.regions = new Collection( {
+			idProperty: 'name'
+		} );
+
 		/**
-		 * Creates an instance of the {@link View} class.
+		 * Template of this view.
 		 *
-		 * @param {Model} model (View)Model of this View.
-		 * @constructor
+		 * @property {Object}
 		 */
-		constructor( model ) {
-			super();
+		this.template = null;
+
+		/**
+		 * Region selectors of this view. See {@link #register}.
+		 *
+		 * @private
+		 * @property {Object}
+		 */
+		this._regionsSelectors = {};
+
+		/**
+		 * Element of this view.
+		 *
+		 * @private
+		 * @property {HTMLElement}
+		 */
+		this._el = null;
+
+		/**
+		 * An instance of Template to generate {@link #_el}.
+		 *
+		 * @private
+		 * @property {Template}
+		 */
+		this._template = null;
+	}
 
+	/**
+	 * Element of this view. The element is rendered on first reference
+	 * using {@link #template} definition and {@link #_template} object.
+	 *
+	 * @property el
+	 */
+	get el() {
+		if ( this._el ) {
+			return this._el;
+		}
+
+		if ( !this.template ) {
 			/**
-			 * Model of this view.
+			 * Attempting to access an element of a view, which has no `template`
+			 * property.
 			 *
-			 * @property {Model}
+			 * @error ui-view-notemplate
 			 */
-			this.model = model || null;
+			throw new CKEditorError( 'ui-view-notemplate' );
+		}
+
+		// Prepare pre–defined listeners.
+		this._prepareElementListeners( this.template );
 
+		this._template = new Template( this.template );
+
+		return ( this._el = this._template.render() );
+	}
+
+	set el( el ) {
+		this._el = el;
+	}
+
+	/**
+	 * Initializes the view.
+	 */
+	init() {
+		this._initRegions();
+	}
+
+	/**
+	 * Adds a child view to one of the {@link #regions} (see {@link #register}) in DOM
+	 * at given, optional index position.
+	 *
+	 * @param {String} regionName One of {@link #regions} the child should be added to.
+	 * @param {View} childView A child view.
+	 * @param {Number} [index] Index at which the child will be added to the region.
+	 */
+	addChild( regionName, childView, index ) {
+		if ( !regionName ) {
 			/**
-			 * Regions of this view. See {@link #register}.
+			 * The name of the region is required.
 			 *
-			 * @property {Collection}
+			 * @error ui-view-addchild-badrname
 			 */
-			this.regions = new Collection( {
-				idProperty: 'name'
-			} );
+			throw new CKEditorError( 'ui-view-addchild-badrname' );
+		}
+
+		const region = this.regions.get( regionName );
 
+		if ( !region ) {
 			/**
-			 * Template of this view.
+			 * No such region of given name.
 			 *
-			 * @property {Object}
+			 * @error ui-view-addchild-noreg
 			 */
-			this.template = null;
+			throw new CKEditorError( 'ui-view-addchild-noreg' );
+		}
 
+		if ( !childView ) {
 			/**
-			 * Region selectors of this view. See {@link #register}.
+			 * No child view passed.
 			 *
-			 * @private
-			 * @property {Object}
+			 * @error ui-view-addchild-no-view
 			 */
-			this._regionsSelectors = {};
+			throw new CKEditorError( 'ui-view-addchild-no-view' );
+		}
 
+		region.views.add( childView, index );
+	}
+
+	/**
+	 * Removes a child view from one of the {@link #regions} (see {@link #register}) in DOM.
+	 *
+	 * @param {String} regionName One of {@link #regions} the view should be removed from.
+	 * @param {View} childVIew A child view.
+	 * @returns {View} A child view instance after removal.
+	 */
+	removeChild( regionName, childView ) {
+		if ( !regionName ) {
 			/**
-			 * Element of this view.
+			 * The name of the region is required.
 			 *
-			 * @private
-			 * @property {HTMLElement}
+			 * @error ui-view-removechild-badrname
 			 */
-			this._el = null;
+			throw new CKEditorError( 'ui-view-removechild-badrname' );
+		}
+
+		const region = this.regions.get( regionName );
 
+		if ( !region ) {
 			/**
-			 * An instance of Template to generate {@link #_el}.
+			 * No such region of given name.
 			 *
-			 * @private
-			 * @property {Template}
+			 * @error ui-view-removechild-noreg
 			 */
-			this._template = null;
+			throw new CKEditorError( 'ui-view-removechild-noreg' );
 		}
 
-		/**
-		 * Element of this view. The element is rendered on first reference
-		 * using {@link #template} definition and {@link #_template} object.
-		 *
-		 * @property el
-		 */
-		get el() {
-			if ( this._el ) {
-				return this._el;
-			}
+		if ( !childView ) {
+			/**
+			 * The view must be an instance of View.
+			 *
+			 * @error ui-view-removechild-no-view
+			 */
+			throw new CKEditorError( 'ui-view-removechild-no-view' );
+		}
 
-			if ( !this.template ) {
-				/**
-				 * Attempting to access an element of a view, which has no `template`
-				 * property.
-				 *
-				 * @error ui-view-notemplate
-				 */
-				throw new CKEditorError( 'ui-view-notemplate' );
-			}
+		region.views.remove( childView );
 
-			// Prepare pre–defined listeners.
-			this._prepareElementListeners( this.template );
+		return childView;
+	}
 
-			this._template = new Template( this.template );
+	/**
+	 * Returns a child view from one of the {@link #regions}
+	 * (see {@link #register}) at given `index`.
+	 *
+	 * @param {String} regionName One of {@link #regions} the child should be retrieved from.
+	 * @param {Number} [index] An index of desired view.
+	 * @returns {View} A view instance.
+	 */
+	getChild( regionName, index ) {
+		const region = this.regions.get( regionName );
 
-			return ( this._el = this._template.render() );
+		if ( !region ) {
+			/**
+			 * No such region of given name.
+			 *
+			 * @error ui-view-getchild-noreg
+			 */
+			throw new CKEditorError( 'ui-view-getchild-noreg' );
 		}
 
-		set el( el ) {
-			this._el = el;
-		}
+		return region.views.get( index );
+	}
 
-		/**
-		 * Initializes the view.
-		 */
-		init() {
-			this._initRegions();
+	/**
+	 * Registers a region in {@link #regions}.
+	 *
+	 *		let view = new View();
+	 *
+	 *		// region.name == "foo", region.el == view.el.firstChild
+	 *		view.register( 'foo', el => el.firstChild );
+	 *
+	 *		// region.name == "bar", region.el == view.el.querySelector( 'span' )
+	 *		view.register( new Region( 'bar' ), 'span' );
+	 *
+	 *		// region.name == "bar", region.el == view.el.querySelector( '#div#id' )
+	 *		view.register( 'bar', 'div#id', true );
+	 *
+	 *		// region.name == "baz", region.el == null
+	 *		view.register( 'baz', true );
+	 *
+	 * @param {String|Region} stringOrRegion The name or an instance of the Region
+	 * to be registered. If `String`, the region will be created on the fly.
+	 * @param {String|Function|true} regionSelector The selector to retrieve region's element
+	 * in DOM when the region instance is initialized (see {@link Region#init}, {@link #init}).
+	 * @param {Boolean} [override] When set `true` it will allow overriding of registered regions.
+	 */
+	register( ...args ) {
+		let region, regionName;
+
+		if ( typeof args[ 0 ] === 'string' ) {
+			regionName = args[ 0 ];
+			region = this.regions.get( regionName ) || new Region( regionName );
+		} else if ( args[ 0 ] instanceof Region ) {
+			regionName = args[ 0 ].name;
+			region = args[ 0 ];
+		} else {
+			/**
+			 * A name of the region or an instance of Region is required.
+			 *
+			 * @error ui-view-register-wrongtype
+			 */
+			throw new CKEditorError( 'ui-view-register-wrongtype' );
 		}
 
-		/**
-		 * Adds a child view to one of the {@link #regions} (see {@link #register}) in DOM
-		 * at given, optional index position.
-		 *
-		 * @param {String} regionName One of {@link #regions} the child should be added to.
-		 * @param {View} childView A child view.
-		 * @param {Number} [index] Index at which the child will be added to the region.
-		 */
-		addChild( regionName, childView, index ) {
-			if ( !regionName ) {
-				/**
-				 * The name of the region is required.
-				 *
-				 * @error ui-view-addchild-badrname
-				 */
-				throw new CKEditorError( 'ui-view-addchild-badrname' );
-			}
-
-			const region = this.regions.get( regionName );
-
-			if ( !region ) {
-				/**
-				 * No such region of given name.
-				 *
-				 * @error ui-view-addchild-noreg
-				 */
-				throw new CKEditorError( 'ui-view-addchild-noreg' );
-			}
+		const regionSelector = args[ 1 ];
 
-			if ( !childView ) {
-				/**
-				 * No child view passed.
-				 *
-				 * @error ui-view-addchild-no-view
-				 */
-				throw new CKEditorError( 'ui-view-addchild-no-view' );
-			}
-
-			region.views.add( childView, index );
+		if ( !regionSelector || !isValidRegionSelector( regionSelector ) ) {
+			/**
+			 * The selector must be String, Function or `true`.
+			 *
+			 * @error ui-view-register-badselector
+			 */
+			throw new CKEditorError( 'ui-view-register-badselector' );
 		}
 
-		/**
-		 * Removes a child view from one of the {@link #regions} (see {@link #register}) in DOM.
-		 *
-		 * @param {String} regionName One of {@link #regions} the view should be removed from.
-		 * @param {View} childVIew A child view.
-		 * @returns {View} A child view instance after removal.
-		 */
-		removeChild( regionName, childView ) {
-			if ( !regionName ) {
-				/**
-				 * The name of the region is required.
-				 *
-				 * @error ui-view-removechild-badrname
-				 */
-				throw new CKEditorError( 'ui-view-removechild-badrname' );
-			}
-
-			const region = this.regions.get( regionName );
-
-			if ( !region ) {
-				/**
-				 * No such region of given name.
-				 *
-				 * @error ui-view-removechild-noreg
-				 */
-				throw new CKEditorError( 'ui-view-removechild-noreg' );
-			}
+		const registered = this.regions.get( regionName );
+
+		if ( !registered ) {
+			this.regions.add( region );
+		} else {
+			if ( registered !== region ) {
+				if ( !args[ 2 ] ) {
+					/**
+					 * Overriding is possible only when `override` flag is set.
+					 *
+					 * @error ui-view-register-override
+					 */
+					throw new CKEditorError( 'ui-view-register-override' );
+				}
 
-			if ( !childView ) {
-				/**
-				 * The view must be an instance of View.
-				 *
-				 * @error ui-view-removechild-no-view
-				 */
-				throw new CKEditorError( 'ui-view-removechild-no-view' );
+				this.regions.remove( registered );
+				this.regions.add( region );
 			}
-
-			region.views.remove( childView );
-
-			return childView;
 		}
 
-		/**
-		 * Returns a child view from one of the {@link #regions}
-		 * (see {@link #register}) at given `index`.
-		 *
-		 * @param {String} regionName One of {@link #regions} the child should be retrieved from.
-		 * @param {Number} [index] An index of desired view.
-		 * @returns {View} A view instance.
-		 */
-		getChild( regionName, index ) {
-			const region = this.regions.get( regionName );
-
-			if ( !region ) {
-				/**
-				 * No such region of given name.
-				 *
-				 * @error ui-view-getchild-noreg
-				 */
-				throw new CKEditorError( 'ui-view-getchild-noreg' );
-			}
-
-			return region.views.get( index );
-		}
+		this._regionsSelectors[ regionName ] = regionSelector;
+	}
 
+	/**
+	 * Binds an `attribute` of View's model so the DOM of the View is updated when the `attribute`
+	 * changes. It returns a function which, once called in the context of a DOM element,
+	 * attaches a listener to the model which, in turn, brings changes to DOM.
+	 *
+	 * @param {String} attribute Attribute name in the model to be observed.
+	 * @param {Function} [callback] Callback function executed on attribute change in model.
+	 * If not specified, a default DOM `domUpdater` supplied by the template is used.
+	 */
+	bindToAttribute( attribute, callback ) {
 		/**
-		 * Registers a region in {@link #regions}.
+		 * Attaches a listener to View's model, which updates DOM when the model's attribute
+		 * changes. DOM is either updated by the `domUpdater` function supplied by the template
+		 * (like attribute changer or `innerHTML` setter) or custom `callback` passed to {@link #bind}.
 		 *
-		 *		let view = new View();
+		 * This function is called by {@link Template#render}.
 		 *
-		 *		// region.name == "foo", region.el == view.el.firstChild
-		 *		view.register( 'foo', el => el.firstChild );
-		 *
-		 *		// region.name == "bar", region.el == view.el.querySelector( 'span' )
-		 *		view.register( new Region( 'bar' ), 'span' );
-		 *
-		 *		// region.name == "bar", region.el == view.el.querySelector( '#div#id' )
-		 *		view.register( 'bar', 'div#id', true );
-		 *
-		 *		// region.name == "baz", region.el == null
-		 *		view.register( 'baz', true );
-		 *
-		 * @param {String|Region} stringOrRegion The name or an instance of the Region
-		 * to be registered. If `String`, the region will be created on the fly.
-		 * @param {String|Function|true} regionSelector The selector to retrieve region's element
-		 * in DOM when the region instance is initialized (see {@link Region#init}, {@link #init}).
-		 * @param {Boolean} [override] When set `true` it will allow overriding of registered regions.
+		 * @param {HTMLElement} el DOM element to be updated when `attribute` in model changes.
+		 * @param {Function} domUpdater A function provided by the template which updates corresponding
+		 * DOM.
 		 */
-		register( ...args ) {
-			let region, regionName;
-
-			if ( typeof args[ 0 ] === 'string' ) {
-				regionName = args[ 0 ];
-				region = this.regions.get( regionName ) || new Region( regionName );
-			} else if ( args[ 0 ] instanceof Region ) {
-				regionName = args[ 0 ].name;
-				region = args[ 0 ];
+		return ( el, domUpdater ) => {
+			let onModelChange;
+
+			if ( callback ) {
+				onModelChange = ( evt, value ) => {
+					let processedValue = callback( el, value );
+
+					if ( typeof processedValue != 'undefined' ) {
+						domUpdater( el, processedValue );
+					}
+				};
 			} else {
-				/**
-				 * A name of the region or an instance of Region is required.
-				 *
-				 * @error ui-view-register-wrongtype
-				 */
-				throw new CKEditorError( 'ui-view-register-wrongtype' );
+				onModelChange = ( evt, value ) => domUpdater( el, value );
 			}
 
-			const regionSelector = args[ 1 ];
+			// Execute callback when the attribute changes.
+			this.listenTo( this.model, 'change:' + attribute, onModelChange );
 
-			if ( !regionSelector || !isValidRegionSelector( regionSelector ) ) {
-				/**
-				 * The selector must be String, Function or `true`.
-				 *
-				 * @error ui-view-register-badselector
-				 */
-				throw new CKEditorError( 'ui-view-register-badselector' );
-			}
+			// Set the initial state of the view.
+			onModelChange( null, this.model[ attribute ] );
+		};
+	}
 
-			const registered = this.regions.get( regionName );
+	/**
+	 * Destroys the view instance. The process includes:
+	 *  1. Removal of child views from {@link #regions}.
+	 *  2. Destruction of the {@link #regions}.
+	 *  3. Removal of {#link #_el} from DOM.
+	 */
+	destroy() {
+		let childView;
 
-			if ( !registered ) {
-				this.regions.add( region );
-			} else {
-				if ( registered !== region ) {
-					if ( !args[ 2 ] ) {
-						/**
-						 * Overriding is possible only when `override` flag is set.
-						 *
-						 * @error ui-view-register-override
-						 */
-						throw new CKEditorError( 'ui-view-register-override' );
-					}
+		this.stopListening();
 
-					this.regions.remove( registered );
-					this.regions.add( region );
-				}
+		for ( let region of this.regions ) {
+			while ( ( childView = this.getChild( region.name, 0 ) ) ) {
+				this.removeChild( region.name, childView );
 			}
 
-			this._regionsSelectors[ regionName ] = regionSelector;
+			this.regions.remove( region ).destroy();
 		}
 
-		/**
-		 * Binds an `attribute` of View's model so the DOM of the View is updated when the `attribute`
-		 * changes. It returns a function which, once called in the context of a DOM element,
-		 * attaches a listener to the model which, in turn, brings changes to DOM.
-		 *
-		 * @param {String} attribute Attribute name in the model to be observed.
-		 * @param {Function} [callback] Callback function executed on attribute change in model.
-		 * If not specified, a default DOM `domUpdater` supplied by the template is used.
-		 */
-		bindToAttribute( attribute, callback ) {
-			/**
-			 * Attaches a listener to View's model, which updates DOM when the model's attribute
-			 * changes. DOM is either updated by the `domUpdater` function supplied by the template
-			 * (like attribute changer or `innerHTML` setter) or custom `callback` passed to {@link #bind}.
-			 *
-			 * This function is called by {@link Template#render}.
-			 *
-			 * @param {HTMLElement} el DOM element to be updated when `attribute` in model changes.
-			 * @param {Function} domUpdater A function provided by the template which updates corresponding
-			 * DOM.
-			 */
-			return ( el, domUpdater ) => {
-				let onModelChange;
-
-				if ( callback ) {
-					onModelChange = ( evt, value ) => {
-						let processedValue = callback( el, value );
-
-						if ( typeof processedValue != 'undefined' ) {
-							domUpdater( el, processedValue );
-						}
-					};
-				} else {
-					onModelChange = ( evt, value ) => domUpdater( el, value );
-				}
-
-				// Execute callback when the attribute changes.
-				this.listenTo( this.model, 'change:' + attribute, onModelChange );
-
-				// Set the initial state of the view.
-				onModelChange( null, this.model[ attribute ] );
-			};
+		if ( this.template ) {
+			this.el.remove();
 		}
 
-		/**
-		 * Destroys the view instance. The process includes:
-		 *  1. Removal of child views from {@link #regions}.
-		 *  2. Destruction of the {@link #regions}.
-		 *  3. Removal of {#link #_el} from DOM.
-		 */
-		destroy() {
-			let childView;
-
-			this.stopListening();
+		this.model = this.regions = this.template = this._regionsSelectors = this._el = this._template = null;
+	}
 
-			for ( let region of this.regions ) {
-				while ( ( childView = this.getChild( region.name, 0 ) ) ) {
-					this.removeChild( region.name, childView );
-				}
+	/**
+	 * Initializes {@link #regions} of this view by passing a DOM element
+	 * generated from {@link #_regionsSelectors} into {@link Region#init}.
+	 *
+	 * @protected
+	 */
+	_initRegions() {
+		let region, regionEl, regionSelector;
 
-				this.regions.remove( region ).destroy();
-			}
+		for ( region of this.regions ) {
+			regionSelector = this._regionsSelectors[ region.name ];
 
-			if ( this.template ) {
-				this.el.remove();
+			if ( typeof regionSelector == 'string' ) {
+				regionEl = this.el.querySelector( regionSelector );
+			} else if ( typeof regionSelector == 'function' ) {
+				regionEl = regionSelector( this.el );
+			} else {
+				regionEl = null;
 			}
 
-			this.model = this.regions = this.template = this._regionsSelectors = this._el = this._template = null;
+			region.init( regionEl );
 		}
+	}
 
+	/**
+	 * For a given event name or callback, returns a function which,
+	 * once executed in a context of an element, attaches native DOM listener
+	 * to the element. The listener executes given callback or fires View's event
+	 * of given name.
+	 *
+	 * @protected
+	 * @param {String|Function} evtNameOrCallback Event name to be fired on View or callback to execute.
+	 * @returns {Function} A function to be executed in the context of an element.
+	 */
+	_getDOMListenerAttacher( evtNameOrCallback ) {
 		/**
-		 * Initializes {@link #regions} of this view by passing a DOM element
-		 * generated from {@link #_regionsSelectors} into {@link Region#init}.
+		 * Attaches a native DOM listener to given element. The listener executes the
+		 * callback or fires View's event.
 		 *
-		 * @protected
-		 */
-		_initRegions() {
-			let region, regionEl, regionSelector;
-
-			for ( region of this.regions ) {
-				regionSelector = this._regionsSelectors[ region.name ];
-
-				if ( typeof regionSelector == 'string' ) {
-					regionEl = this.el.querySelector( regionSelector );
-				} else if ( typeof regionSelector == 'function' ) {
-					regionEl = regionSelector( this.el );
-				} else {
-					regionEl = null;
-				}
-
-				region.init( regionEl );
-			}
-		}
-
-		/**
-		 * For a given event name or callback, returns a function which,
-		 * once executed in a context of an element, attaches native DOM listener
-		 * to the element. The listener executes given callback or fires View's event
-		 * of given name.
+		 * Note: If the selector is supplied, it narrows the scope to relevant targets only.
+		 * So instead of
 		 *
-		 * @protected
-		 * @param {String|Function} evtNameOrCallback Event name to be fired on View or callback to execute.
-		 * @returns {Function} A function to be executed in the context of an element.
-		 */
-		_getDOMListenerAttacher( evtNameOrCallback ) {
-			/**
-			 * Attaches a native DOM listener to given element. The listener executes the
-			 * callback or fires View's event.
-			 *
-			 * Note: If the selector is supplied, it narrows the scope to relevant targets only.
-			 * So instead of
-			 *
-			 *     children: [
-			 *         { tag: 'span', on: { click: 'foo' } }
-			 *         { tag: 'span', on: { click: 'foo' } }
-			 *     ]
-			 *
-			 * a single, more efficient listener can be attached that uses **event delegation**:
-			 *
-			 *     children: [
-			 *     	   { tag: 'span' }
-			 *     	   { tag: 'span' }
-			 *     ],
-			 *     on: {
-			 *     	   'click@span': 'foo',
-			 *     }
-			 *
-			 * @param {HTMLElement} el Element, to which the native DOM Event listener is attached.
-			 * @param {String} domEventName The name of native DOM Event.
-			 * @param {String} [selector] If provided, the selector narrows the scope to relevant targets only.
-			 */
-			return ( el, domEvtName, selector ) => {
-				// Use View's listenTo, so the listener is detached, when the View dies.
-				this.listenTo( el, domEvtName, ( evt, domEvt ) => {
-					if ( !selector || domEvt.target.matches( selector ) ) {
-						if ( typeof evtNameOrCallback == 'function' ) {
-							evtNameOrCallback( domEvt );
-						} else {
-							this.fire( evtNameOrCallback, domEvt );
-						}
-					}
-				} );
-			};
-		}
-
-		/**
-		 * Iterates over "on" property in {@link template} definition to recursively
-		 * replace each listener declaration with a function which, once executed in a context
-		 * of an element, attaches native DOM listener to the element.
+		 *     children: [
+		 *         { tag: 'span', on: { click: 'foo' } }
+		 *         { tag: 'span', on: { click: 'foo' } }
+		 *     ]
 		 *
-		 * @protected
-		 * @param {TemplateDefinition} def Template definition.
+		 * a single, more efficient listener can be attached that uses **event delegation**:
+		 *
+		 *     children: [
+		 *     	   { tag: 'span' }
+		 *     	   { tag: 'span' }
+		 *     ],
+		 *     on: {
+		 *     	   'click@span': 'foo',
+		 *     }
+		 *
+		 * @param {HTMLElement} el Element, to which the native DOM Event listener is attached.
+		 * @param {String} domEventName The name of native DOM Event.
+		 * @param {String} [selector] If provided, the selector narrows the scope to relevant targets only.
 		 */
-		_prepareElementListeners( def ) {
-			let on = def.on;
-
-			if ( on ) {
-				let domEvtName, evtNameOrCallback;
-
-				for ( domEvtName in on ) {
-					evtNameOrCallback = on[ domEvtName ];
-
-					// Listeners allow definition with an array:
-					//
-					//    on: {
-					//        'DOMEventName@selector': [ 'event1', callback ],
-					//        'DOMEventName': [ callback, 'event2', 'event3' ]
-					//        ...
-					//    }
-					if ( Array.isArray( evtNameOrCallback ) ) {
-						on[ domEvtName ] = on[ domEvtName ].map( this._getDOMListenerAttacher, this );
-					}
-					// Listeners allow definition with a string containing event name:
-					//
-					//    on: {
-					//       'DOMEventName@selector': 'event1',
-					//       'DOMEventName': 'event2'
-					//       ...
-					//    }
-					else {
-						on[ domEvtName ] = this._getDOMListenerAttacher( evtNameOrCallback );
+		return ( el, domEvtName, selector ) => {
+			// Use View's listenTo, so the listener is detached, when the View dies.
+			this.listenTo( el, domEvtName, ( evt, domEvt ) => {
+				if ( !selector || domEvt.target.matches( selector ) ) {
+					if ( typeof evtNameOrCallback == 'function' ) {
+						evtNameOrCallback( domEvt );
+					} else {
+						this.fire( evtNameOrCallback, domEvt );
 					}
 				}
-			}
-
-			// Repeat recursively for the children.
-			if ( def.children ) {
-				def.children.map( this._prepareElementListeners, this );
-			}
-		}
+			} );
+		};
 	}
 
-	utils.extend( View.prototype, DOMEmitterMixin );
-
-	const validSelectorTypes = new Set( [ 'string', 'boolean', 'function' ] );
-
 	/**
-	 * Check whether region selector is valid.
+	 * Iterates over "on" property in {@link template} definition to recursively
+	 * replace each listener declaration with a function which, once executed in a context
+	 * of an element, attaches native DOM listener to the element.
 	 *
 	 * @protected
-	 * @param {*} selector Selector to be checked.
-	 * @returns {Boolean}
+	 * @param {TemplateDefinition} def Template definition.
 	 */
-	function isValidRegionSelector( selector ) {
-		return validSelectorTypes.has( typeof selector ) && selector !== false;
+	_prepareElementListeners( def ) {
+		let on = def.on;
+
+		if ( on ) {
+			let domEvtName, evtNameOrCallback;
+
+			for ( domEvtName in on ) {
+				evtNameOrCallback = on[ domEvtName ];
+
+				// Listeners allow definition with an array:
+				//
+				//    on: {
+				//        'DOMEventName@selector': [ 'event1', callback ],
+				//        'DOMEventName': [ callback, 'event2', 'event3' ]
+				//        ...
+				//    }
+				if ( Array.isArray( evtNameOrCallback ) ) {
+					on[ domEvtName ] = on[ domEvtName ].map( this._getDOMListenerAttacher, this );
+				}
+				// Listeners allow definition with a string containing event name:
+				//
+				//    on: {
+				//       'DOMEventName@selector': 'event1',
+				//       'DOMEventName': 'event2'
+				//       ...
+				//    }
+				else {
+					on[ domEvtName ] = this._getDOMListenerAttacher( evtNameOrCallback );
+				}
+			}
+		}
+
+		// Repeat recursively for the children.
+		if ( def.children ) {
+			def.children.map( this._prepareElementListeners, this );
+		}
 	}
+}
+
+objectUtils.extend( View.prototype, DOMEmitterMixin );
+
+const validSelectorTypes = new Set( [ 'string', 'boolean', 'function' ] );
 
-	return View;
-} );
+/**
+ * Check whether region selector is valid.
+ *
+ * @private
+ * @param {*} selector Selector to be checked.
+ * @returns {Boolean}
+ */
+function isValidRegionSelector( selector ) {
+	return validSelectorTypes.has( typeof selector ) && selector !== false;
+}

+ 94 - 90
packages/ckeditor5-ui/src/utils.js

@@ -5,121 +5,125 @@
 
 'use strict';
 
-/**
- * Creates a spy function (ala Sinon.js) that can be used to inspect call to it.
- *
- * The following are the present features:
- *
- *  * spy.called: property set to `true` if the function has been called at least once.
- *
- * @returns {Function} The spy function.
- */
-export function spy() {
-	return function spy() {
-		spy.called = true;
-	};
-}
+const utils = {
+	/**
+	 * Creates a spy function (ala Sinon.js) that can be used to inspect call to it.
+	 *
+	 * The following are the present features:
+	 *
+	 *  * spy.called: property set to `true` if the function has been called at least once.
+	 *
+	 * @returns {Function} The spy function.
+	 */
+	spy() {
+		return function spy() {
+			spy.called = true;
+		};
+	},
 
-/**
- * Returns a unique id. This id is a number (starting from 1) which will never get repeated on successive calls
- * to this method.
- *
- * @returns {Number} A number representing the id.
- */
-export let uid = ( () => {
-	let next = 1;
+	/**
+	 * Returns a unique id. This id is a number (starting from 1) which will never get repeated on successive calls
+	 * to this method.
+	 *
+	 * @returns {Number} A number representing the id.
+	 */
+	uid: ( () => {
+		let next = 1;
 
-	return () => {
-		return next++;
-	};
-} )();
+		return () => {
+			return next++;
+		};
+	} )(),
 
-/**
- * Checks if value implements iterator interface.
- *
- * @param {Mixed} value The value to check.
- * @returns {Boolean} True if value implements iterator interface.
- */
-export function isIterable( value ) {
-	return !!( value && value[ Symbol.iterator ] );
-}
+	/**
+	 * Checks if value implements iterator interface.
+	 *
+	 * @param {Mixed} value The value to check.
+	 * @returns {Boolean} True if value implements iterator interface.
+	 */
+	isIterable( value ) {
+		return !!( value && value[ Symbol.iterator ] );
+	},
 
-/**
- * Compares how given arrays relate to each other. One array can be: same as another array, prefix of another array
- * or completely different. If arrays are different, first index at which they differ is returned. Otherwise,
- * a flag specifying the relation is returned. Flags are negative numbers, so whenever a number >= 0 is returned
- * it means that arrays differ.
- *
- *   compareArrays( [ 0, 2 ], [ 0, 2 ] ); // SAME
- *   compareArrays( [ 0, 2 ], [ 0, 2, 1 ] ); // PREFIX
- *   compareArrays( [ 0, 2 ], [ 0 ] ); // EXTENSION
- *   compareArrays( [ 0, 2 ], [ 1, 2 ] ); // 0
- *   compareArrays( [ 0, 2 ], [ 0, 1 ] ); // 1
- *
- * @param {Array} a Array that is compared.
- * @param {Array} b Array to compare with.
- * @returns {Number} An index at which arrays differ, or if they do not differ, how array `a` is related to array `b`.
- * This is represented by one of flags: `a` is {@link utils.compareArrays#SAME same}, `a` is
- * a {@link utils.compareArrays#PREFIX prefix) or `a` is an {@link utils.compareArrays#EXTENSION extension}.
- */
-export function compareArrays( a, b ) {
-	const minLen = Math.min( a.length, b.length );
+	/**
+	 * Compares how given arrays relate to each other. One array can be: same as another array, prefix of another array
+	 * or completely different. If arrays are different, first index at which they differ is returned. Otherwise,
+	 * a flag specifying the relation is returned. Flags are negative numbers, so whenever a number >= 0 is returned
+	 * it means that arrays differ.
+	 *
+	 *   compareArrays( [ 0, 2 ], [ 0, 2 ] ); // SAME
+	 *   compareArrays( [ 0, 2 ], [ 0, 2, 1 ] ); // PREFIX
+	 *   compareArrays( [ 0, 2 ], [ 0 ] ); // EXTENSION
+	 *   compareArrays( [ 0, 2 ], [ 1, 2 ] ); // 0
+	 *   compareArrays( [ 0, 2 ], [ 0, 1 ] ); // 1
+	 *
+	 * @param {Array} a Array that is compared.
+	 * @param {Array} b Array to compare with.
+	 * @returns {Number} An index at which arrays differ, or if they do not differ, how array `a` is related to array `b`.
+	 * This is represented by one of flags: `a` is {@link utils.compareArrays#SAME same}, `a` is
+	 * a {@link utils.compareArrays#PREFIX prefix) or `a` is an {@link utils.compareArrays#EXTENSION extension}.
+	 */
+	compareArrays( a, b ) {
+		const minLen = Math.min( a.length, b.length );
 
-	for ( let i = 0; i < minLen; i++ ) {
-		if ( a[ i ] != b[ i ] ) {
-			// The arrays are different.
-			return i;
+		for ( let i = 0; i < minLen; i++ ) {
+			if ( a[ i ] != b[ i ] ) {
+				// The arrays are different.
+				return i;
+			}
 		}
-	}
 
-	// Both arrays were same at all points.
-	if ( a.length == b.length ) {
-		// If their length is also same, they are the same.
-		return compareArrays.SAME;
-	} else if ( a.length < b.length ) {
-		// Compared array is shorter so it is a prefix of the other array.
-		return compareArrays.PREFIX;
-	} else {
-		// Compared array is longer so it is an extension of the other array.
-		return compareArrays.EXTENSION;
-	}
-}
+		// Both arrays were same at all points.
+		if ( a.length == b.length ) {
+			// If their length is also same, they are the same.
+			return utils.compareArrays.SAME;
+		} else if ( a.length < b.length ) {
+			// Compared array is shorter so it is a prefix of the other array.
+			return utils.compareArrays.PREFIX;
+		} else {
+			// Compared array is longer so it is an extension of the other array.
+			return utils.compareArrays.EXTENSION;
+		}
+	},
 
-/**
- * Returns `nth` (starts from `0` of course) item of an `iterable`.
- *
- * @param {Number} index
- * @param {Iterable.<*>} iterable
- * @returns {*}
- */
-export function nth( index, iterable ) {
-	for ( let item of iterable ) {
-		if ( index === 0 ) {
-			return item;
+	/**
+	 * Returns `nth` (starts from `0` of course) item of an `iterable`.
+	 *
+	 * @param {Number} index
+	 * @param {Iterable.<*>} iterable
+	 * @returns {*}
+	 */
+	nth( index, iterable ) {
+		for ( let item of iterable ) {
+			if ( index === 0 ) {
+				return item;
+			}
+			index -= 1;
 		}
-		index -= 1;
-	}
 
-	return null;
-}
+		return null;
+	}
+};
 
 /**
  * Flag for "is same as" relation between arrays.
  *
  * @type {Number}
  */
-compareArrays.SAME = -1;
+utils.compareArrays.SAME = -1;
 
 /**
  * Flag for "is a prefix of" relation between arrays.
  *
  * @type {Number}
  */
-compareArrays.PREFIX = -2;
+utils.compareArrays.PREFIX = -2;
 
 /**
  * Flag for "is a suffix of" relation between arrays.
  *
  * @type {number}
  */
-compareArrays.EXTENSION = -3;
+utils.compareArrays.EXTENSION = -3;
+
+export default utils;

+ 7 - 11
packages/ckeditor5-ui/tests/_tools/tools.js

@@ -13,22 +13,18 @@
 		 * If `proto` is not set or it does not define `create()` and `destroy()` methods,
 		 * then they will be set to Sinon spies. Therefore the shortest usage is:
 		 *
-		 *	  bender.tools.defineEditorCreatorMock( 'test1' );
+		 *		bender.tools.defineEditorCreatorMock( 'test1' );
 		 *
 		 * The mocked creator is available under:
 		 *
-		 *	  editor.plugins.get( 'creator-thename' );
+		 *		editor.plugins.get( 'creator-thename' );
 		 *
 		 * @param {String} creatorName Name of the creator.
 		 * @param {Object} [proto] Prototype of the creator. Properties from the proto param will
 		 * be copied to the prototype of the creator.
 		 */
-		defineEditorCreatorMock: ( creatorName, proto ) => {
-			CKEDITOR.define( 'plugin!creator-' + creatorName, [ 'creator' ], ( Creator ) => {
-				return mockCreator( Creator );
-			} );
-
-			function mockCreator( Creator ) {
+		defineEditorCreatorMock( creatorName, proto ) {
+			bender.amd.define( 'creator-' + creatorName, [ 'core/creator' ], ( Creator ) => {
 				class TestCreator extends Creator {}
 
 				if ( proto ) {
@@ -46,18 +42,18 @@
 				}
 
 				return TestCreator;
-			}
+			} );
 		},
 
 		/**
 		 * Returns the number of elements return by the iterator.
 		 *
-		 *	  bender.tools.core.getIteratorCount( [ 1, 2, 3, 4, 5 ] ); // 5;
+		 *		bender.tools.core.getIteratorCount( [ 1, 2, 3, 4, 5 ] ); // 5;
 		 *
 		 * @param {Iterable.<*>} iterator Any iterator.
 		 * @returns {Number} Number of elements returned by that iterator.
 		 */
-		getIteratorCount: ( iterator ) => {
+		getIteratorCount( iterator ) {
 			let count = 0;
 
 			for ( let _ of iterator ) { // jshint ignore:line

+ 13 - 9
packages/ckeditor5-ui/tests/bender/tools.js

@@ -20,16 +20,20 @@ bender.tools.core.defineEditorCreatorMock( 'test3', {
 	destroy: destroyFn3
 } );
 
-const modules = bender.amd.require( 'creator', 'plugin!creator-test1', 'plugin!creator-test2', 'plugin!creator-test3' );
+const modules = bender.amd.require( 'core/creator', 'creator-test1', 'creator-test2', 'creator-test3' );
+let Creator;
 
 ///////////////////
 
+before( () => {
+	Creator = modules[ 'core/creator' ];
+} );
+
 describe( 'bender.tools.core.defineEditorCreatorMock()', () => {
 	it( 'should register all creators', () => {
-		const Creator = modules.creator;
-		const TestCreator1 = modules[ 'plugin!creator-test1' ];
-		const TestCreator2 = modules[ 'plugin!creator-test2' ];
-		const TestCreator3 = modules[ 'plugin!creator-test3' ];
+		const TestCreator1 = modules[ 'creator-test1' ];
+		const TestCreator2 = modules[ 'creator-test2' ];
+		const TestCreator3 = modules[ 'creator-test3' ];
 
 		expect( TestCreator1.prototype ).to.be.instanceof( Creator );
 		expect( TestCreator2.prototype ).to.be.instanceof( Creator );
@@ -37,16 +41,16 @@ describe( 'bender.tools.core.defineEditorCreatorMock()', () => {
 	} );
 
 	it( 'should copy properties from the second argument', () => {
-		const TestCreator = modules[ 'plugin!creator-test2' ];
+		const TestCreator = modules[ 'creator-test2' ];
 
 		expect( TestCreator.prototype ).to.have.property( 'foo', 1 );
 		expect( TestCreator.prototype ).to.have.property( 'bar', 2 );
 	} );
 
 	it( 'should create spies for create() and destroy() if not defined', () => {
-		const TestCreator1 = modules[ 'plugin!creator-test1' ];
-		const TestCreator2 = modules[ 'plugin!creator-test2' ];
-		const TestCreator3 = modules[ 'plugin!creator-test3' ];
+		const TestCreator1 = modules[ 'creator-test1' ];
+		const TestCreator2 = modules[ 'creator-test2' ];
+		const TestCreator3 = modules[ 'creator-test3' ];
 
 		expect( TestCreator1.prototype.create ).to.have.property( 'called', false, 'test1.create' );
 		expect( TestCreator1.prototype.destroy ).to.have.property( 'called', false, 'test1.destroy' );

+ 3 - 3
packages/ckeditor5-ui/tests/collection/collection.js

@@ -5,7 +5,7 @@
 
 'use strict';
 
-const modules = bender.amd.require( 'collection', 'ckeditorerror' );
+const modules = bender.amd.require( 'core/collection', 'core/ckeditorerror' );
 
 bender.tools.createSinonSandbox();
 
@@ -21,8 +21,8 @@ describe( 'Collection', () => {
 	let Collection, CKEditorError;
 
 	before( () => {
-		Collection = modules.collection;
-		CKEditorError = modules.CKEditorError;
+		Collection = modules[ 'core/collection' ];
+		CKEditorError = modules[ 'core/ckeditorerror' ];
 	} );
 
 	let collection;

+ 6 - 14
packages/ckeditor5-ui/tests/config/config.js

@@ -5,13 +5,15 @@
 
 'use strict';
 
-const modules = bender.amd.require( 'config' );
+const modules = bender.amd.require( 'core/config' );
 
-let config;
+let Config, config;
 
-beforeEach( () => {
-	const Config = modules.config;
+before( () => {
+	Config = modules[ 'core/config' ];
+} );
 
+beforeEach( () => {
 	config = new Config( {
 		creator: 'inline',
 		language: 'pl',
@@ -38,8 +40,6 @@ describe( 'constructor', () => {
 	} );
 
 	it( 'should work with no parameters', () => {
-		const Config = modules.config;
-
 		// No error should be thrown.
 		config = new Config();
 	} );
@@ -47,8 +47,6 @@ describe( 'constructor', () => {
 
 describe( 'set', () => {
 	it( 'should create Config instances for objects', () => {
-		const Config = modules.config;
-
 		expect( config.resize ).to.be.an.instanceof( Config );
 		expect( config.resize.icon ).to.be.an.instanceof( Config );
 	} );
@@ -112,8 +110,6 @@ describe( 'set', () => {
 	} );
 
 	it( 'should replace a simple entry with a Config instance', () => {
-		const Config = modules.config;
-
 		config.set( 'test', 1 );
 		config.set( 'test', {
 			prop: 1
@@ -124,8 +120,6 @@ describe( 'set', () => {
 	} );
 
 	it( 'should replace a simple entry with a Config instance when passing an object', () => {
-		const Config = modules.config;
-
 		config.set( 'test', 1 );
 		config.set( {
 			test: {
@@ -138,8 +132,6 @@ describe( 'set', () => {
 	} );
 
 	it( 'should replace a simple entry with a Config instance when passing a name.with.deep', () => {
-		const Config = modules.config;
-
 		config.set( 'test.prop', 1 );
 		config.set( 'test.prop.value', 1 );
 

+ 0 - 44
packages/ckeditor5-ui/tests/editorconfig/editorconfig.js

@@ -1,44 +0,0 @@
-/**
- * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-'use strict';
-
-const modules = bender.amd.require( 'editorconfig', 'ckeditor' );
-
-let config;
-
-beforeEach( () => {
-	const EditorConfig = modules.editorconfig;
-
-	config = new EditorConfig( {
-		test: 1
-	} );
-} );
-
-describe( 'constructor', () => {
-	it( 'should set configurations', () => {
-		expect( config ).to.have.property( 'test' ).to.equal( 1 );
-	} );
-} );
-
-describe( 'get', () => {
-	it( 'should retrieve a configuration', () => {
-		expect( config.get( 'test' ) ).to.equal( 1 );
-	} );
-
-	it( 'should fallback to CKEDITOR.config', () => {
-		const CKEDITOR = modules.ckeditor;
-
-		CKEDITOR.config.set( {
-			globalConfig: 2
-		} );
-
-		expect( config.get( 'globalConfig' ) ).to.equal( 2 );
-	} );
-
-	it( 'should return undefined for non existing configuration', () => {
-		expect( config.get( 'invalid' ) ).to.be.undefined();
-	} );
-} );

+ 9 - 10
packages/ckeditor5-ui/tests/emittermixin/emittermixin.js

@@ -5,10 +5,16 @@
 
 'use strict';
 
-const modules = bender.amd.require( 'emittermixin', 'eventinfo', 'utils' );
-
+const modules = bender.amd.require( 'core/emittermixin', 'core/eventinfo', 'core/lib/lodash/object' );
+let EmitterMixin, EventInfo, utilsObject;
 let emitter, listener;
 
+before( () => {
+	EmitterMixin = modules[ 'core/emittermixin' ];
+	EventInfo = modules[ 'core/eventinfo' ];
+	utilsObject = modules[ 'core/lib/lodash/object' ];
+} );
+
 beforeEach( refreshEmitter );
 
 describe( 'fire', () => {
@@ -45,8 +51,6 @@ describe( 'fire', () => {
 	} );
 
 	it( 'should pass arguments to callbacks', () => {
-		const EventInfo = modules.eventinfo;
-
 		let spy1 = sinon.spy();
 		let spy2 = sinon.spy();
 
@@ -212,8 +216,6 @@ describe( 'once', () => {
 	} );
 
 	it( 'should have proper arguments', () => {
-		const EventInfo = modules.eventinfo;
-
 		let spy = sinon.spy();
 
 		emitter.once( 'test', spy );
@@ -419,8 +421,5 @@ function refreshListener() {
 }
 
 function getEmitterInstance() {
-	const EmitterMixin = modules.emittermixin;
-	let utils = modules.utils;
-
-	return utils.extend( {}, EmitterMixin );
+	return utilsObject.extend( {}, EmitterMixin );
 }

+ 7 - 7
packages/ckeditor5-ui/tests/eventinfo/eventinfo.js

@@ -5,12 +5,16 @@
 
 'use strict';
 
-const modules = bender.amd.require( 'eventinfo' );
+const modules = bender.amd.require( 'core/eventinfo' );
 
 describe( 'EventInfo', () => {
-	it( 'should be created properly', () => {
-		const EventInfo = modules.eventinfo;
+	let EventInfo;
+
+	before( () => {
+		EventInfo = modules[ 'core/eventinfo' ];
+	} );
 
+	it( 'should be created properly', () => {
 		let event = new EventInfo( this, 'test' );
 
 		expect( event.source ).to.equal( this );
@@ -20,8 +24,6 @@ describe( 'EventInfo', () => {
 	} );
 
 	it( 'should have stop() and off() marked', () => {
-		const EventInfo = modules.eventinfo;
-
 		let event = new EventInfo( this, 'test' );
 
 		event.stop();
@@ -32,8 +34,6 @@ describe( 'EventInfo', () => {
 	} );
 
 	it( 'should not mark "called" in future instances', () => {
-		const EventInfo = modules.eventinfo;
-
 		let event = new EventInfo( this, 'test' );
 
 		event.stop();

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

@@ -7,10 +7,13 @@
 
 'use strict';
 
-const modules = bender.amd.require( 'log' );
+const modules = bender.amd.require( 'core/log' );
+let log;
 let spy;
 
 beforeEach( () => {
+	log = modules[ 'core/log' ];
+
 	if ( spy ) {
 		spy.restore();
 	}
@@ -18,7 +21,6 @@ beforeEach( () => {
 
 describe( 'warn()', () => {
 	it( 'logs the message to the console using console.warn()', () => {
-		let log = modules.log;
 		let spy = sinon.stub( console, 'warn' );
 		let data = { bar: 1 };
 
@@ -35,7 +37,6 @@ describe( 'warn()', () => {
 
 describe( 'error()', () => {
 	it( 'logs the message to the console using console.error()', () => {
-		let log = modules.log;
 		let spy = sinon.stub( console, 'error' );
 		let data = { bar: 1 };
 

+ 14 - 14
packages/ckeditor5-ui/tests/ui/controller.js

@@ -8,14 +8,14 @@
 'use strict';
 
 const modules = bender.amd.require( 'ckeditor',
-	'ui/view',
-	'ui/controller',
-	'ui/controllercollection',
-	'ui/region',
-	'ckeditorerror',
-	'model',
-	'collection',
-	'eventinfo'
+	'core/ui/view',
+	'core/ui/controller',
+	'core/ui/controllercollection',
+	'core/ui/region',
+	'core/ckeditorerror',
+	'core/model',
+	'core/collection',
+	'core/eventinfo'
 );
 
 let View, Controller, Model, CKEditorError, Collection, ControllerCollection;
@@ -270,12 +270,12 @@ describe( 'Controller', () => {
 } );
 
 function updateModuleReference() {
-	View = modules[ 'ui/view' ];
-	Controller = modules[ 'ui/controller' ];
-	Model = modules.model;
-	Collection = modules.collection;
-	ControllerCollection = modules[ 'ui/controllercollection' ];
-	CKEditorError = modules.ckeditorerror;
+	View = modules[ 'core/ui/view' ];
+	Controller = modules[ 'core/ui/controller' ];
+	Model = modules[ 'core/model' ];
+	Collection = modules[ 'core/collection '];
+	ControllerCollection = modules[ 'core/ui/controllercollection' ];
+	CKEditorError = modules[ 'core/ckeditorerror' ];
 }
 
 function defineParentViewClass() {