viewcollection.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. /**
  2. * @license Copyright (c) 2003-2019, 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. * const parentView = new ParentView( locale );
  16. * const collection = new ViewCollection( locale );
  17. *
  18. * collection.setParent( parentView.element );
  19. *
  20. * const viewA = new ChildView( locale );
  21. * const viewB = new ChildView( locale );
  22. *
  23. * View collection renders and manages view {@link module:ui/view~View#element elements}:
  24. *
  25. * collection.add( viewA );
  26. * collection.add( viewB );
  27. *
  28. * console.log( parentView.element.firsChild ); // -> viewA.element
  29. * console.log( parentView.element.lastChild ); // -> viewB.element
  30. *
  31. * It {@link module:ui/viewcollection~ViewCollection#delegate propagates} DOM events too:
  32. *
  33. * // Delegate #click and #keydown events from viewA and viewB to the parentView.
  34. * collection.delegate( 'click' ).to( parentView );
  35. *
  36. * parentView.on( 'click', ( evt ) => {
  37. * console.log( `${ evt.source } has been clicked.` );
  38. * } );
  39. *
  40. * // This event will be delegated to the parentView.
  41. * viewB.fire( 'click' );
  42. *
  43. * **Note**: A view collection can be used directly in the {@link module:ui/template~TemplateDefinition definition}
  44. * of a {@link module:ui/template~Template template}.
  45. *
  46. * @extends module:utils/collection~Collection
  47. * @mixes module:utils/observablemixin~ObservableMixin
  48. */
  49. export default class ViewCollection extends Collection {
  50. /**
  51. * Creates a new instance of the {@link module:ui/viewcollection~ViewCollection}.
  52. *
  53. * @param {module:utils/locale~Locale} [locale] The {@link module:core/editor/editor~Editor editor's locale} instance.
  54. */
  55. constructor( locale ) {
  56. super( {
  57. // An #id Number attribute should be legal and not break the `ViewCollection` instance.
  58. // https://github.com/ckeditor/ckeditor5-ui/issues/93
  59. idProperty: 'viewUid'
  60. } );
  61. // Handle {@link module:ui/view~View#element} in DOM when a new view is added to the collection.
  62. this.on( 'add', ( evt, view, index ) => {
  63. if ( !view.isRendered ) {
  64. view.render();
  65. }
  66. if ( view.element && this._parentElement ) {
  67. this._parentElement.insertBefore( view.element, this._parentElement.children[ index ] );
  68. }
  69. } );
  70. // Handle {@link module:ui/view~View#element} in DOM when a view is removed from the collection.
  71. this.on( 'remove', ( evt, view ) => {
  72. if ( view.element && this._parentElement ) {
  73. view.element.remove();
  74. }
  75. } );
  76. /**
  77. * The {@link module:core/editor/editor~Editor#locale editor's locale} instance.
  78. * See the view {@link module:ui/view~View#locale locale} property.
  79. *
  80. * @member {module:utils/locale~Locale}
  81. */
  82. this.locale = locale;
  83. /**
  84. * A parent element within which child views are rendered and managed in DOM.
  85. *
  86. * @protected
  87. * @member {HTMLElement}
  88. */
  89. this._parentElement = null;
  90. }
  91. /**
  92. * Destroys the view collection along with child views.
  93. * See the view {@link module:ui/view~View#destroy} method.
  94. */
  95. destroy() {
  96. this.map( view => view.destroy() );
  97. }
  98. /**
  99. * Sets the parent HTML element of this collection. When parent is set, {@link #add adding} and
  100. * {@link #remove removing} views in the collection synchronizes their
  101. * {@link module:ui/view~View#element elements} in the parent element.
  102. *
  103. * @param {HTMLElement} element A new parent element.
  104. */
  105. setParent( elementOrDocFragment ) {
  106. this._parentElement = elementOrDocFragment;
  107. }
  108. /**
  109. * Delegates selected events coming from within views in the collection to any
  110. * {@link module:utils/emittermixin~Emitter}.
  111. *
  112. * For the following views and collection:
  113. *
  114. * const viewA = new View();
  115. * const viewB = new View();
  116. * const viewC = new View();
  117. *
  118. * const views = parentView.createCollection();
  119. *
  120. * views.delegate( 'eventX' ).to( viewB );
  121. * views.delegate( 'eventX', 'eventY' ).to( viewC );
  122. *
  123. * views.add( viewA );
  124. *
  125. * the `eventX` is delegated (fired by) `viewB` and `viewC` along with `customData`:
  126. *
  127. * viewA.fire( 'eventX', customData );
  128. *
  129. * and `eventY` is delegated (fired by) `viewC` along with `customData`:
  130. *
  131. * viewA.fire( 'eventY', customData );
  132. *
  133. * See {@link module:utils/emittermixin~EmitterMixin#delegate}.
  134. *
  135. * @param {...String} events {@link module:ui/view~View} event names to be delegated to another
  136. * {@link module:utils/emittermixin~Emitter}.
  137. * @returns {Object}
  138. * @returns {Function} return.to A function which accepts the destination of
  139. * {@link module:utils/emittermixin~EmitterMixin#delegate delegated} events.
  140. */
  141. delegate( ...events ) {
  142. if ( !events.length || !isStringArray( events ) ) {
  143. /**
  144. * All event names must be strings.
  145. *
  146. * @error ui-viewcollection-delegate-wrong-events
  147. */
  148. throw new CKEditorError( 'ui-viewcollection-delegate-wrong-events: All event names must be strings.' );
  149. }
  150. return {
  151. /**
  152. * Selects destination for {@link module:utils/emittermixin~EmitterMixin#delegate} events.
  153. *
  154. * @memberOf module:ui/viewcollection~ViewCollection#delegate
  155. * @function module:ui/viewcollection~ViewCollection#delegate.to
  156. * @param {module:utils/emittermixin~EmitterMixin} dest An `EmitterMixin` instance which is
  157. * the destination for delegated events.
  158. */
  159. to: dest => {
  160. // Activate delegating on existing views in this collection.
  161. for ( const view of this ) {
  162. for ( const evtName of events ) {
  163. view.delegate( evtName ).to( dest );
  164. }
  165. }
  166. // Activate delegating on future views in this collection.
  167. this.on( 'add', ( evt, view ) => {
  168. for ( const evtName of events ) {
  169. view.delegate( evtName ).to( dest );
  170. }
  171. } );
  172. // Deactivate delegating when view is removed from this collection.
  173. this.on( 'remove', ( evt, view ) => {
  174. for ( const evtName of events ) {
  175. view.stopDelegating( evtName, dest );
  176. }
  177. } );
  178. }
  179. };
  180. }
  181. /**
  182. * Removes a child view from the collection. If the {@link #setParent parent element} of the
  183. * collection has been set, the {@link module:ui/view~View#element element} of the view is also removed
  184. * in DOM, reflecting the order of the collection.
  185. *
  186. * See the {@link #add} method.
  187. *
  188. * @method #remove
  189. * @param {module:ui/view~View|Number|String} subject The view to remove, its id or index in the collection.
  190. * @returns {Object} The removed view.
  191. */
  192. }
  193. mix( Collection, ObservableMixin );
  194. // Check if all entries of the array are of `String` type.
  195. //
  196. // @private
  197. // @param {Array} arr An array to be checked.
  198. // @returns {Boolean}
  199. function isStringArray( arr ) {
  200. return arr.every( a => typeof a == 'string' );
  201. }