| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307 |
- /**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
- import Collection from '../utils/collection.js';
- import CKEditorError from '../utils/ckeditorerror.js';
- /**
- * Manages UI Controllers.
- *
- * @memberOf ui
- * @extends utils.Collection
- */
- export default class ControllerCollection extends Collection {
- /**
- * Creates an instance of the controller collection, initializing it with a name:
- *
- * const collection = new ControllerCollection( 'foo' );
- * collection.add( someController );
- *
- * **Note**: If needed, controller collection can stay in sync with a collection of models to
- * manage list–like components. See {@link ui.ControllerCollection#bind} to learn more.
- *
- * @param {String} name Name of the collection.
- * @param {utils.Locale} [locale] The {@link core.editor.Editor#locale editor's locale} instance.
- */
- constructor( name, locale ) {
- 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.
- *
- * @member {String} ui.ControllerCollection#name
- */
- this.name = name;
- /**
- * See {@link ui.View#locale}
- *
- * @readonly
- * @member {utils.Locale} ui.ControllerCollection#locale
- */
- this.locale = locale;
- /**
- * Parent controller of this collection.
- *
- * @member {ui.Controller} ui.ControllerCollection#parent
- */
- this.parent = null;
- }
- /**
- * Adds a child controller to the collection. If {@link ui.ControllerCollection#parent} {@link ui.Controller}
- * instance is ready, the child view is initialized when added.
- *
- * @param {ui.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 ui.Controller#init} is done.
- */
- add( controller, index ) {
- super.add( controller, index );
- // ChildController.init() returns Promise.
- let promise = Promise.resolve();
- if ( this.parent && this.parent.ready && !controller.ready ) {
- promise = promise.then( () => {
- return controller.init();
- } );
- }
- return promise;
- }
- /**
- * Synchronizes controller collection with a {@link utils.Collection} of {@link ui.Model} instances.
- * The entire collection of controllers reflects changes to the collection of the models, working as a factory.
- *
- * This method helps bringing complex list–like UI components to life out of the data (like models). The process
- * can be automatic:
- *
- * // This collection stores models.
- * const models = new Collection( { idProperty: 'label' } );
- *
- * // This controller collection will become a factory for the collection of models.
- * const controllers = new ControllerCollection( 'foo', locale );
- *
- * // Activate the binding – since now, this controller collection works like a **factory**.
- * controllers.bind( models ).as( FooController, FooView );
- *
- * // As new models arrive to the collection, each becomes an instance of FooController (FooView)
- * // in the controller collection.
- * models.add( new Model( { label: 'foo' } ) );
- * models.add( new Model( { label: 'bar' } ) );
- *
- * console.log( controllers.length == 2 );
- *
- * // Controller collection is updated as the model is removed.
- * models.remove( 0 );
- * console.log( controllers.length == 1 );
- *
- * or the factory can be driven by a custom callback:
- *
- * // This collection stores any kind of data.
- * const data = new Collection();
- *
- * // This controller collection will become a custom factory for the data.
- * const controllers = new ControllerCollection( 'foo', locale );
- *
- * // Activate the binding – the **factory** is driven by a custom callback.
- * controllers.bind( data ).as( ( item, locale ) => {
- * if ( !item.foo ) {
- * return null;
- * } else if ( item.foo == 'bar' ) {
- * return new BarController( ..., BarView( locale ) );
- * } else {
- * return new DifferentController( ..., DifferentView( locale ) );
- * }
- * } );
- *
- * // As new data arrive to the collection, each is handled individually by the callback.
- * // This will produce BarController.
- * data.add( { foo: 'bar' } );
- *
- * // And this one will become DifferentController.
- * data.add( { foo: 'baz' } );
- *
- * // Also there will be no controller for data without property `foo`.
- * data.add( {} );
- *
- * console.log( controllers.length == 2 );
- *
- * // Controller collection is updated as the data is removed.
- * data.remove( 0 );
- * console.log( controllers.length == 1 );
- *
- * @param {utils.Collection.<ui.Model>} collection Models to be synchronized with this controller collection.
- * @returns {Function} The `as` function in the `bind( collection ).as( ... )` chain.
- * It activates factory using controller and view classes or uses a custom callback to produce
- * controller (view) instances.
- * @returns {Function} return.ControllerClassOrFunction Specifies the constructor of the controller to be used or
- * a custom callback function which produces controllers.
- * @returns {Function} [return.ViewClass] Specifies constructor of the view to be used. If not specified,
- * `ControllerClassOrFunction` works as as custom callback function.
- */
- bind( collection ) {
- const controllerMap = new Map();
- const that = this;
- return {
- as: ( ControllerClassOrFunction, ViewClass ) => {
- const handler = ViewClass ?
- defaultControllerHandler( ControllerClassOrFunction, ViewClass )
- :
- genericControllerHandler( ControllerClassOrFunction );
- for ( let item of collection ) {
- handler.add( item );
- }
- // Updated controller collection when a new item is added.
- collection.on( 'add', ( evt, item, index ) => {
- handler.add( item, index );
- } );
- // Update controller collection when a item is removed.
- collection.on( 'remove', ( evt, item ) => {
- handler.remove( item );
- } );
- }
- };
- function genericControllerHandler( createController ) {
- return {
- add( data, index ) {
- const controller = createController( data, that.locale );
- controllerMap.set( data, controller );
- if ( controller ) {
- that.add( controller, typeof index == 'number' ? recalculateIndex( index ) : undefined );
- }
- },
- remove( data ) {
- const controller = controllerMap.get( data );
- controllerMap.delete( controller );
- if ( controller ) {
- that.remove( controller );
- }
- }
- };
- }
- // Decrement index for each item which has no corresponding controller.
- function recalculateIndex( index ) {
- let outputIndex = index;
- for ( let i = 0; i < index; i++ ) {
- // index -> data -> controller
- if ( !controllerMap.get( collection.get( i ) ) ) {
- outputIndex--;
- }
- }
- return outputIndex;
- }
- function defaultControllerHandler( ControllerClass, ViewClass ) {
- return genericControllerHandler( ( model ) => {
- return new ControllerClass( model, new ViewClass( that.locale ) );
- }, controllerMap );
- }
- }
- /**
- * Delegates selected events coming from within controller models in the collection to desired
- * {@link utils.EmitterMixin}. For instance:
- *
- * const modelA = new Model();
- * const modelB = new Model();
- * const modelC = new Model();
- *
- * const controllers = new ControllerCollection( 'name' );
- *
- * controllers.delegate( 'eventX' ).to( modelB );
- * controllers.delegate( 'eventX', 'eventY' ).to( modelC );
- *
- * controllers.add( new Controller( modelA, ... ) );
- *
- * then `eventX` is delegated (fired by) `modelB` and `modelC` along with `customData`:
- *
- * modelA.fire( 'eventX', customData );
- *
- * and `eventY` is delegated (fired by) `modelC` along with `customData`:
- *
- * modelA.fire( 'eventY', customData );
- *
- * See {@link utils.EmitterMixin#delegate}.
- *
- * @param {...String} events {@link ui.Controller#model} event names to be delegated to another {@link utils.EmitterMixin}.
- * @returns {ui.ControllerCollection#delegate#to}
- */
- delegate( ...events ) {
- if ( !events.length || !isStringArray( events ) ) {
- /**
- * All event names must be strings.
- *
- * @error ui-controllercollection-delegate-wrong-events
- */
- throw new CKEditorError( 'ui-controllercollection-delegate-wrong-events: All event names must be strings.' );
- }
- return {
- /**
- * Selects destination for {@link utils.EmitterMixin#delegate} events.
- *
- * @method ui.ControllerCollection.delegate#to
- * @param {utils.EmitterMixin} dest An `EmitterMixin` instance which is the destination for delegated events.
- */
- to: ( dest ) => {
- // Activate delegating on existing controllers in this collection.
- for ( let controller of this ) {
- for ( let evtName of events ) {
- controller.model.delegate( evtName ).to( dest );
- }
- }
- // Activate delegating on future controllers in this collection.
- this.on( 'add', ( evt, controller ) => {
- for ( let evtName of events ) {
- controller.model.delegate( evtName ).to( dest );
- }
- } );
- // Deactivate delegating when controller is removed from this collection.
- this.on( 'remove', ( evt, controller ) => {
- for ( let evtName of events ) {
- controller.model.stopDelegating( evtName, dest );
- }
- } );
- }
- };
- }
- }
- // Check if all entries of the array are of `String` type.
- //
- // @private
- // @param {Array} arr An array to be checked.
- // @returns {Boolean}
- function isStringArray( arr ) {
- return arr.every( a => typeof a == 'string' );
- }
|