8
0

viewcollection.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module ui/viewcollection
  7. */
  8. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  9. import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
  10. import Collection from '@ckeditor/ckeditor5-utils/src/collection';
  11. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  12. /**
  13. * Collects {@link module:ui/view~View} instances.
  14. *
  15. * @extends module:utils/collection~Collection
  16. * @mixes module:utils/observablemixin~ObservableMixin
  17. */
  18. export default class ViewCollection extends Collection {
  19. /**
  20. * Creates a new {@link module:ui/viewcollection~ViewCollection} instance.
  21. *
  22. * @param {module:utils/locale~Locale} [locale] The {@link module:core/editor~Editor editor's locale} instance.
  23. */
  24. constructor( locale ) {
  25. super( {
  26. // An #id Number attribute should be legal and not break the `ViewCollection` instance.
  27. // https://github.com/ckeditor/ckeditor5-ui/issues/93
  28. idProperty: 'viewUid'
  29. } );
  30. // Handle {@link module:ui/view~View#element} in DOM when a new view is added to the collection.
  31. this.on( 'add', ( evt, view, index ) => {
  32. if ( view.element && this._parentElement ) {
  33. this._parentElement.insertBefore( view.element, this._parentElement.children[ index ] );
  34. }
  35. } );
  36. // Handle {@link module:ui/view~View#element} in DOM when a view is removed from the collection.
  37. this.on( 'remove', ( evt, view ) => {
  38. if ( view.element && this._parentElement ) {
  39. view.element.remove();
  40. }
  41. } );
  42. /**
  43. * The {@link module:core/editor/editor~Editor editor's locale} instance.
  44. *
  45. * @member {module:utils/locale~Locale}
  46. */
  47. this.locale = locale;
  48. /**
  49. * Set to `true` once the parent's {@link module:ui/view~View#ready} is true, which means
  50. * that all the views in the collection are also ready (which can be asynchronous).
  51. *
  52. * @readonly
  53. * @observable
  54. * @member {Boolean} #ready
  55. */
  56. this.set( 'ready', false );
  57. /**
  58. * A parent element within which child views are rendered and managed in DOM.
  59. *
  60. * @protected
  61. * @member {HTMLElement}
  62. */
  63. this._parentElement = null;
  64. }
  65. /**
  66. * Initializes child views by injecting {@link module:ui/view~View#element} into DOM
  67. * and calling {@link module:ui/view~View#init}.
  68. */
  69. init() {
  70. if ( this.ready ) {
  71. /**
  72. * This ViewCollection has already been initialized.
  73. *
  74. * @error ui-viewcollection-init-reinit
  75. */
  76. throw new CKEditorError( 'ui-viewcollection-init-reinit: This ViewCollection has already been initialized.' );
  77. }
  78. this.map( v => v.init() );
  79. this.ready = true;
  80. }
  81. /**
  82. * Destroys the view collection along with child views.
  83. */
  84. destroy() {
  85. this.map( v => v.destroy() );
  86. }
  87. /**
  88. * Adds a child view to the collection. If {@link module:ui/viewcollection~ViewCollection#ready}, the child view
  89. * is also initialized when added.
  90. *
  91. * @param {module:ui/view~View} view A child view.
  92. * @param {Number} [index] Index at which the child will be added to the collection.
  93. */
  94. add( view, index ) {
  95. super.add( view, index );
  96. if ( this.ready && !view.ready ) {
  97. view.init();
  98. }
  99. }
  100. /**
  101. * Sets {@link #_parentElement} of this collection.
  102. *
  103. * @param {HTMLElement} element A new parent.
  104. */
  105. setParent( elementOrDocFragment ) {
  106. this._parentElement = elementOrDocFragment;
  107. }
  108. /**
  109. * Delegates selected events coming from within the collection to desired {@link module:utils/emittermixin~EmitterMixin}.
  110. *
  111. * For instance:
  112. *
  113. * const viewA = new View();
  114. * const viewB = new View();
  115. * const viewC = new View();
  116. *
  117. * const views = parentView.createCollection();
  118. *
  119. * views.delegate( 'eventX' ).to( viewB );
  120. * views.delegate( 'eventX', 'eventY' ).to( viewC );
  121. *
  122. * views.add( viewA );
  123. *
  124. * then `eventX` is delegated (fired by) `viewB` and `viewC` along with `customData`:
  125. *
  126. * viewA.fire( 'eventX', customData );
  127. *
  128. * and `eventY` is delegated (fired by) `viewC` along with `customData`:
  129. *
  130. * viewA.fire( 'eventY', customData );
  131. *
  132. * See {@link module:utils/emittermixin~EmitterMixin#delegate}.
  133. *
  134. * @param {...String} events {@link module:ui/view~View} event names to be delegated to another
  135. * {@link module:utils/emittermixin~EmitterMixin}.
  136. * @returns {module:ui/viewcollection~ViewCollection#delegate.to}
  137. */
  138. delegate( ...events ) {
  139. if ( !events.length || !isStringArray( events ) ) {
  140. /**
  141. * All event names must be strings.
  142. *
  143. * @error ui-viewcollection-delegate-wrong-events
  144. */
  145. throw new CKEditorError( 'ui-viewcollection-delegate-wrong-events: All event names must be strings.' );
  146. }
  147. return {
  148. /**
  149. * Selects destination for {@link module:utils/emittermixin~EmitterMixin#delegate} events.
  150. *
  151. * @memberOf module:ui/viewcollection~ViewCollection#delegate
  152. * @function module:ui/viewcollection~ViewCollection#delegate.to
  153. * @param {module:utils/emittermixin~EmitterMixin} dest An `EmitterMixin` instance which is
  154. * the destination for delegated events.
  155. */
  156. to: dest => {
  157. // Activate delegating on existing views in this collection.
  158. for ( const view of this ) {
  159. for ( const evtName of events ) {
  160. view.delegate( evtName ).to( dest );
  161. }
  162. }
  163. // Activate delegating on future views in this collection.
  164. this.on( 'add', ( evt, view ) => {
  165. for ( const evtName of events ) {
  166. view.delegate( evtName ).to( dest );
  167. }
  168. } );
  169. // Deactivate delegating when view is removed from this collection.
  170. this.on( 'remove', ( evt, view ) => {
  171. for ( const evtName of events ) {
  172. view.stopDelegating( evtName, dest );
  173. }
  174. } );
  175. }
  176. };
  177. }
  178. }
  179. mix( Collection, ObservableMixin );
  180. // Check if all entries of the array are of `String` type.
  181. //
  182. // @private
  183. // @param {Array} arr An array to be checked.
  184. // @returns {Boolean}
  185. function isStringArray( arr ) {
  186. return arr.every( a => typeof a == 'string' );
  187. }