8
0

controller.js 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  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 ControllerCollection from './controllercollection.js';
  7. import CKEditorError from '../utils/ckeditorerror.js';
  8. import EmitterMixin from '../utils/emittermixin.js';
  9. import mix from '../utils/mix.js';
  10. const anon = '$anonymous';
  11. /**
  12. * Basic Controller class.
  13. *
  14. * @memberOf ui
  15. * @mixes utils.EmitterMixin
  16. */
  17. export default class Controller {
  18. /**
  19. * Creates an instance of the {@link ui.Controller} class.
  20. *
  21. * @param {ui.Model} [model] Model of this Controller.
  22. * @param {ui.View} [view] View instance of this Controller.
  23. */
  24. constructor( model, view ) {
  25. /**
  26. * Model of this controller.
  27. *
  28. * @member {ui.Model} ui.Controller#model
  29. */
  30. this.model = model || null;
  31. /**
  32. * Set `true` after {@link #init}.
  33. *
  34. * @member {Boolean} ui.Controller#ready
  35. */
  36. this.ready = false;
  37. /**
  38. * View of this controller.
  39. *
  40. * @member {ui.View} ui.Controller#view
  41. */
  42. this.view = view || null;
  43. /**
  44. * A collection of {@link ControllerCollection} instances containing
  45. * child controllers.
  46. *
  47. * @member {utils.Collection} ui.Controller#collections
  48. */
  49. this.collections = new Collection( {
  50. idProperty: 'name'
  51. } );
  52. /**
  53. * Anonymous collection of this controller instance. It groups child controllers
  54. * which are not to be handled by `Controller#collections`–to–`View#region`
  55. * automation. It also means their views must be handled individually
  56. * by the view, i.e. passed as members of {@link ui.TemplateDefinition#children}.
  57. *
  58. * @protected
  59. * @member {ui.ControllerCollection} ui.Controller#_anonymousCollection
  60. */
  61. this.collections.add( this._anonymousCollection = new ControllerCollection( anon ) );
  62. // Listen to {@link ControllerCollection#add} and {@link ControllerCollection#remove}
  63. // of newly added Collection to synchronize this controller's view and children
  64. // controllers' views in the future.
  65. this.collections.on( 'add', ( evt, collection ) => {
  66. // Set the {@link ControllerCollection#parent} to this controller.
  67. // It allows the collection to determine the {@link #ready} state of this controller
  68. // and accordingly initialize a child controller when added.
  69. collection.parent = this;
  70. this.listenTo( collection, 'add', ( evt, childController, index ) => {
  71. // Child view is added to corresponding region in this controller's view
  72. // when a new Controller joins the collection.
  73. if ( this.ready && childController.view ) {
  74. this.view.regions.get( collection.name ).views.add( childController.view, index );
  75. }
  76. } );
  77. this.listenTo( collection, 'remove', ( evt, childController ) => {
  78. // Child view is removed from corresponding region in this controller's view
  79. // when a new Controller is removed from the the collection.
  80. if ( this.ready && childController.view ) {
  81. this.view.regions.get( collection.name ).views.remove( childController.view );
  82. }
  83. } );
  84. } );
  85. this.collections.on( 'remove', ( evt, collection ) => {
  86. // Release the collection. Once removed from {@link #collections}, it can be
  87. // moved to another controller.
  88. collection.parent = null;
  89. this.stopListening( collection );
  90. } );
  91. }
  92. /**
  93. * Initializes the controller instance. The process includes:
  94. *
  95. * 1. Initialization of the child {@link #view}.
  96. * 2. Initialization of child controllers in {@link #collections}.
  97. * 3. Setting {@link #ready} flag `true`.
  98. *
  99. * @returns {Promise} A Promise resolved when the initialization process is finished.
  100. */
  101. init() {
  102. if ( this.ready ) {
  103. /**
  104. * This Controller already been initialized.
  105. *
  106. * @error ui-controller-init-reinit
  107. */
  108. throw new CKEditorError( 'ui-controller-init-reinit: This Controller already been initialized.' );
  109. }
  110. return Promise.resolve()
  111. .then( this._initView.bind( this ) )
  112. .then( this._initCollections.bind( this ) )
  113. .then( () => {
  114. this.ready = true;
  115. this.fire( 'ready' );
  116. } );
  117. }
  118. /**
  119. * Destroys the controller instance. The process includes:
  120. *
  121. * 1. Destruction of the child {@link #view}.
  122. * 2. Destruction of child controllers in {@link #collections}.
  123. *
  124. * @returns {Promise} A Promise resolved when the destruction process is finished.
  125. */
  126. destroy() {
  127. let promises = [];
  128. let collection, childController;
  129. for ( collection of this.collections ) {
  130. for ( childController of collection ) {
  131. promises.push( childController.destroy() );
  132. }
  133. collection.clear();
  134. }
  135. this.collections.clear();
  136. if ( this.view ) {
  137. promises.push( Promise.resolve().then( () => {
  138. return this.view.destroy();
  139. } ) );
  140. }
  141. promises.push( Promise.resolve().then( () => {
  142. this.model = this.ready = this.view = this.collections = null;
  143. } ) );
  144. return Promise.all( promises );
  145. }
  146. /**
  147. * Adds a new collection to {@link ui.Controller#collections}.
  148. *
  149. * @param {String} collectionName Name of the controller collection.
  150. * @returns {ui.ControllerCollection} The new collection instance.
  151. */
  152. addCollection( collectionName ) {
  153. const collection = new ControllerCollection( collectionName, this.view && this.view.locale );
  154. this.collections.add( collection );
  155. return collection;
  156. }
  157. /**
  158. * Adds a child {@link Controller} instance to {@link #collections} at given index.
  159. *
  160. * // Adds child to the specified collection. The collection name
  161. * // must correspond with the region name in parent.view#regions.
  162. * parent.add( 'collection-name', child );
  163. *
  164. * // Adds child to the specified collection at specific index.
  165. * // The collection name must correspond with the region name in parent.view#regions.
  166. * parent.add( 'collection-name', child, 3 );
  167. *
  168. * // Adds child to the {@link ui.Controller#_anonymousCollection} in the parent. In such case,
  169. * // parent#view must put the child#view in the correct place in parent.view#template
  170. * // because there's no association between the {@link ui.Controller#_anonymousCollection}
  171. * // and any of the regions.
  172. * parent.add( child );
  173. *
  174. * @param {String|ui.Controller} collectionNameOrController Name of the collection or the controller instance.
  175. * @param {ui.Controller} [controller] A controller instance to be added.
  176. * @param {Number} [index] An index in the collection.
  177. * @returns {Promise} A Promise resolved when the child {@link ui.Controller#init} is done.
  178. */
  179. add( ...args ) {
  180. if ( args[ 0 ] instanceof Controller ) {
  181. return this._anonymousCollection.add( ...args );
  182. } else {
  183. return this.collections.get( args[ 0 ] ).add( args[ 1 ], args[ 2 ] );
  184. }
  185. }
  186. /**
  187. * Removes a child {@link ui.Controller} instance from one of {@link ui.Controller#collections}.
  188. *
  189. * **Note**: To remove children from {@link ui.Controller#_anonymousCollection}, use the following syntax
  190. *
  191. * parent.remove( child );
  192. *
  193. * @param {String|ui.Controller} collectionNameOrController Name of the collection or the controller instance.
  194. * @param {ui.Controller|Number} [toRemove] A Controller instance or index to be removed.
  195. * @returns {Object} The removed item.
  196. */
  197. remove( collectionNameOrController, toRemove ) {
  198. if ( collectionNameOrController instanceof Controller ) {
  199. return this._anonymousCollection.remove( collectionNameOrController );
  200. } else {
  201. return this.collections.get( collectionNameOrController ).remove( toRemove );
  202. }
  203. }
  204. /**
  205. * Initializes the {@link #view} of this controller instance.
  206. *
  207. * @protected
  208. * @returns {Promise} A Promise resolved when initialization process is finished.
  209. */
  210. _initView() {
  211. let promise = Promise.resolve();
  212. if ( this.view ) {
  213. promise = promise.then( this.view.init.bind( this.view ) );
  214. }
  215. return promise;
  216. }
  217. /**
  218. * Initializes the {@link #collections} of this controller instance.
  219. *
  220. * @protected
  221. * @returns {Promise} A Promise resolved when initialization process is finished.
  222. */
  223. _initCollections() {
  224. const promises = [];
  225. let collection, childController;
  226. for ( collection of this.collections ) {
  227. for ( childController of collection ) {
  228. // Anonymous collection {@link ui.Controller#_anonymousCollection} does not allow
  229. // automated controller-to-view binding, because there's no such thing as
  230. // anonymous Region in the View instance.
  231. if ( !isAnonymous( collection ) && this.view && childController.view ) {
  232. this.view.regions.get( collection.name ).views.add( childController.view );
  233. }
  234. promises.push( childController.init() );
  235. }
  236. }
  237. return Promise.all( promises );
  238. }
  239. }
  240. mix( Controller, EmitterMixin );
  241. // Checks whether the collection is anonymous.
  242. //
  243. // @private
  244. // @param {ui.ControllerCollection} collection
  245. // @returns {Boolean}
  246. function isAnonymous( collection ) {
  247. return collection.name == anon;
  248. }
  249. /**
  250. * Fired when the controller is fully initialized.
  251. *
  252. * @event ui.Controller#ready
  253. */