8
0

controllercollection.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import Collection from '../utils/collection.js';
  6. import CKEditorError from '../utils/ckeditorerror.js';
  7. /**
  8. * Manages UI Controllers.
  9. *
  10. * @memberOf ui
  11. * @extends utils.Collection
  12. */
  13. export default class ControllerCollection extends Collection {
  14. /**
  15. * Creates an instance of the controller collection, initializing it with a name:
  16. *
  17. * const collection = new ControllerCollection( 'foo' );
  18. * collection.add( someController );
  19. *
  20. * **Note**: If needed, controller collection can stay in sync with a collection of models to
  21. * manage list–like components. See {@link ui.ControllerCollection#bind} to learn more.
  22. *
  23. * @param {String} name Name of the collection.
  24. * @param {utils.Locale} [locale] The {@link core.editor.Editor#locale editor's locale} instance.
  25. */
  26. constructor( name, locale ) {
  27. super();
  28. if ( !name ) {
  29. /**
  30. * ControllerCollection must be initialized with a name.
  31. *
  32. * @error ui-controllercollection-no-name
  33. */
  34. throw new CKEditorError( 'ui-controllercollection-no-name: ControllerCollection must be initialized with a name.' );
  35. }
  36. /**
  37. * Name of this collection.
  38. *
  39. * @member {String} ui.ControllerCollection#name
  40. */
  41. this.name = name;
  42. /**
  43. * See {@link ui.View#locale}
  44. *
  45. * @readonly
  46. * @member {utils.Locale} ui.ControllerCollection#locale
  47. */
  48. this.locale = locale;
  49. /**
  50. * Parent controller of this collection.
  51. *
  52. * @member {ui.Controller} ui.ControllerCollection#parent
  53. */
  54. this.parent = null;
  55. }
  56. /**
  57. * Adds a child controller to the collection. If {@link ui.ControllerCollection#parent} {@link ui.Controller}
  58. * instance is ready, the child view is initialized when added.
  59. *
  60. * @param {ui.Controller} controller A child controller.
  61. * @param {Number} [index] Index at which the child will be added to the collection.
  62. * @returns {Promise} A Promise resolved when the child {@link ui.Controller#init} is done.
  63. */
  64. add( controller, index ) {
  65. super.add( controller, index );
  66. // ChildController.init() returns Promise.
  67. let promise = Promise.resolve();
  68. if ( this.parent && this.parent.ready && !controller.ready ) {
  69. promise = promise.then( () => {
  70. return controller.init();
  71. } );
  72. }
  73. return promise;
  74. }
  75. /**
  76. * Synchronizes controller collection with a {@link utils.Collection} of {@link ui.Model} instances.
  77. * The entire collection of controllers reflects changes to the collection of the models, working as a factory.
  78. *
  79. * This method helps bringing complex list–like UI components to life out of the data (like models). The process
  80. * can be automatic:
  81. *
  82. * // This collection stores models.
  83. * const models = new Collection( { idProperty: 'label' } );
  84. *
  85. * // This controller collection will become a factory for the collection of models.
  86. * const controllers = new ControllerCollection( 'foo', locale );
  87. *
  88. * // Activate the binding – since now, this controller collection works like a **factory**.
  89. * controllers.bind( models ).as( FooController, FooView );
  90. *
  91. * // As new models arrive to the collection, each becomes an instance of FooController (FooView)
  92. * // in the controller collection.
  93. * models.add( new Model( { label: 'foo' } ) );
  94. * models.add( new Model( { label: 'bar' } ) );
  95. *
  96. * console.log( controllers.length == 2 );
  97. *
  98. * // Controller collection is updated as the model is removed.
  99. * models.remove( 0 );
  100. * console.log( controllers.length == 1 );
  101. *
  102. * or the factory can be driven by a custom callback:
  103. *
  104. * // This collection stores any kind of data.
  105. * const data = new Collection();
  106. *
  107. * // This controller collection will become a custom factory for the data.
  108. * const controllers = new ControllerCollection( 'foo', locale );
  109. *
  110. * // Activate the binding – the **factory** is driven by a custom callback.
  111. * controllers.bind( data ).as( ( item, locale ) => {
  112. * if ( !item.foo ) {
  113. * return null;
  114. * } else if ( item.foo == 'bar' ) {
  115. * return new BarController( ..., BarView( locale ) );
  116. * } else {
  117. * return new DifferentController( ..., DifferentView( locale ) );
  118. * }
  119. * } );
  120. *
  121. * // As new data arrive to the collection, each is handled individually by the callback.
  122. * // This will produce BarController.
  123. * data.add( { foo: 'bar' } );
  124. *
  125. * // And this one will become DifferentController.
  126. * data.add( { foo: 'baz' } );
  127. *
  128. * // Also there will be no controller for data without property `foo`.
  129. * data.add( {} );
  130. *
  131. * console.log( controllers.length == 2 );
  132. *
  133. * // Controller collection is updated as the data is removed.
  134. * data.remove( 0 );
  135. * console.log( controllers.length == 1 );
  136. *
  137. * @param {utils.Collection.<ui.Model>} collection Models to be synchronized with this controller collection.
  138. * @returns {Function} The `as` function in the `bind( collection ).as( ... )` chain.
  139. * It activates factory using controller and view classes or uses a custom callback to produce
  140. * controller (view) instances.
  141. * @returns {Function} return.ControllerClassOrFunction Specifies the constructor of the controller to be used or
  142. * a custom callback function which produces controllers.
  143. * @returns {Function} [return.ViewClass] Specifies constructor of the view to be used. If not specified,
  144. * `ControllerClassOrFunction` works as as custom callback function.
  145. */
  146. bind( collection ) {
  147. const controllerMap = new Map();
  148. const that = this;
  149. return {
  150. as: ( ControllerClassOrFunction, ViewClass ) => {
  151. const handler = ViewClass ?
  152. defaultControllerHandler( ControllerClassOrFunction, ViewClass )
  153. :
  154. genericControllerHandler( ControllerClassOrFunction );
  155. for ( let item of collection ) {
  156. handler.add( item );
  157. }
  158. // Updated controller collection when a new item is added.
  159. collection.on( 'add', ( evt, item, index ) => {
  160. handler.add( item, index );
  161. } );
  162. // Update controller collection when a item is removed.
  163. collection.on( 'remove', ( evt, item ) => {
  164. handler.remove( item );
  165. } );
  166. }
  167. };
  168. function genericControllerHandler( createController ) {
  169. return {
  170. add( data, index ) {
  171. const controller = createController( data, that.locale );
  172. controllerMap.set( data, controller );
  173. if ( controller ) {
  174. that.add( controller, typeof index == 'number' ? recalculateIndex( index ) : undefined );
  175. }
  176. },
  177. remove( data ) {
  178. const controller = controllerMap.get( data );
  179. controllerMap.delete( controller );
  180. if ( controller ) {
  181. that.remove( controller );
  182. }
  183. }
  184. };
  185. }
  186. // Decrement index for each item which has no corresponding controller.
  187. function recalculateIndex( index ) {
  188. let outputIndex = index;
  189. for ( let i = 0; i < index; i++ ) {
  190. // index -> data -> controller
  191. if ( !controllerMap.get( collection.get( i ) ) ) {
  192. outputIndex--;
  193. }
  194. }
  195. return outputIndex;
  196. }
  197. function defaultControllerHandler( ControllerClass, ViewClass ) {
  198. return genericControllerHandler( ( model ) => {
  199. return new ControllerClass( model, new ViewClass( that.locale ) );
  200. }, controllerMap );
  201. }
  202. }
  203. /**
  204. * Delegates selected events coming from within controller models in the collection to desired
  205. * {@link utils.EmitterMixin}. For instance:
  206. *
  207. * const modelA = new Model();
  208. * const modelB = new Model();
  209. * const modelC = new Model();
  210. *
  211. * const controllers = new ControllerCollection( 'name' );
  212. *
  213. * controllers.delegate( 'eventX' ).to( modelB );
  214. * controllers.delegate( 'eventX', 'eventY' ).to( modelC );
  215. *
  216. * controllers.add( new Controller( modelA, ... ) );
  217. *
  218. * then `eventX` is delegated (fired by) `modelB` and `modelC` along with `customData`:
  219. *
  220. * modelA.fire( 'eventX', customData );
  221. *
  222. * and `eventY` is delegated (fired by) `modelC` along with `customData`:
  223. *
  224. * modelA.fire( 'eventY', customData );
  225. *
  226. * See {@link utils.EmitterMixin#delegate}.
  227. *
  228. * @param {...String} events {@link ui.Controller#model} event names to be delegated to another {@link utils.EmitterMixin}.
  229. * @returns {ui.ControllerCollection#delegate#to}
  230. */
  231. delegate( ...events ) {
  232. if ( !events.length || !isStringArray( events ) ) {
  233. /**
  234. * All event names must be strings.
  235. *
  236. * @error ui-controllercollection-delegate-wrong-events
  237. */
  238. throw new CKEditorError( 'ui-controllercollection-delegate-wrong-events: All event names must be strings.' );
  239. }
  240. return {
  241. /**
  242. * Selects destination for {@link utils.EmitterMixin#delegate} events.
  243. *
  244. * @method ui.ControllerCollection.delegate#to
  245. * @param {utils.EmitterMixin} dest An `EmitterMixin` instance which is the destination for delegated events.
  246. */
  247. to: ( dest ) => {
  248. // Activate delegating on existing controllers in this collection.
  249. for ( let controller of this ) {
  250. for ( let evtName of events ) {
  251. controller.model.delegate( evtName ).to( dest );
  252. }
  253. }
  254. // Activate delegating on future controllers in this collection.
  255. this.on( 'add', ( evt, controller ) => {
  256. for ( let evtName of events ) {
  257. controller.model.delegate( evtName ).to( dest );
  258. }
  259. } );
  260. // Deactivate delegating when controller is removed from this collection.
  261. this.on( 'remove', ( evt, controller ) => {
  262. for ( let evtName of events ) {
  263. controller.model.stopDelegating( evtName, dest );
  264. }
  265. } );
  266. }
  267. };
  268. }
  269. }
  270. // Check if all entries of the array are of `String` type.
  271. //
  272. // @private
  273. // @param {Array} arr An array to be checked.
  274. // @returns {Boolean}
  275. function isStringArray( arr ) {
  276. return arr.every( a => typeof a == 'string' );
  277. }