view.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module ui/view
  7. */
  8. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  9. import ViewCollection from './viewcollection';
  10. import Template from './template';
  11. import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin';
  12. import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
  13. import Collection from '@ckeditor/ckeditor5-utils/src/collection';
  14. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  15. import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
  16. /**
  17. * Basic View class.
  18. *
  19. * class SampleView extends View {
  20. * constructor( locale ) {
  21. * super( locale );
  22. *
  23. * this.template = new Template( {
  24. * tag: 'p',
  25. * children: [
  26. * 'Hello',
  27. * {
  28. * tag: 'b',
  29. * children: [
  30. * 'world!'
  31. * ]
  32. * }
  33. * ],
  34. * attributes: {
  35. * class: 'foo'
  36. * }
  37. * } );
  38. * }
  39. * }
  40. *
  41. * const view = new SampleView( locale );
  42. *
  43. * view.init();
  44. *
  45. * // Will append <p class="foo">Hello<b>world</b></p>
  46. * document.body.appendChild( view.element );
  47. *
  48. * @mixes module:utils/observablemixin~ObservableMixin
  49. */
  50. export default class View {
  51. /**
  52. * Creates an instance of the {@link module:ui/view~View} class.
  53. *
  54. * @param {module:utils/locale~Locale} [locale] The {@link module:core/editor/editor~Editor editor's locale} instance.
  55. */
  56. constructor( locale ) {
  57. /**
  58. * A set of tools to localize the user interface. See {@link module:core/editor/editor~Editor}.
  59. *
  60. * @readonly
  61. * @member {module:utils/locale~Locale}
  62. */
  63. this.locale = locale;
  64. /**
  65. * Shorthand for {@link module:utils/locale~Locale#t}.
  66. *
  67. * Note: If locale instance hasn't been passed to the view this method may not be available.
  68. *
  69. * @see module:utils/locale~Locale#t
  70. * @method
  71. */
  72. this.t = locale && locale.t;
  73. /**
  74. * Set `true` after {@link #init}, which can be asynchronous.
  75. *
  76. * @readonly
  77. * @observable
  78. * @member {Boolean} #ready
  79. */
  80. this.set( 'ready', false );
  81. /**
  82. * Collections registered with {@link #createCollection}.
  83. *
  84. * @protected
  85. * @member {Set.<module:ui/viewcollection~ViewCollection>}
  86. */
  87. this._viewCollections = new Collection();
  88. /**
  89. * A collection of view instances, which have been added directly
  90. * into the {@link module:ui/template~Template#children}.
  91. *
  92. * @protected
  93. * @member {module:ui/viewcollection~ViewCollection}
  94. */
  95. this._unboundChildren = this.createCollection();
  96. // Pass parent locale to its children.
  97. this._viewCollections.on( 'add', ( evt, collection ) => {
  98. collection.locale = locale;
  99. } );
  100. /**
  101. * Template of this view.
  102. *
  103. * @member {module:ui/template~Template} #template
  104. */
  105. /**
  106. * Element of this view.
  107. *
  108. * @private
  109. * @member {HTMLElement} #_element
  110. */
  111. /**
  112. * Cached {@link module:ui/template~Template} binder object specific for this instance.
  113. * See {@link #bindTemplate}.
  114. *
  115. * @private
  116. * @member {Object} #_bindTemplate
  117. */
  118. }
  119. /**
  120. * Element of this view. The element is rendered on first reference
  121. * using {@link #template} definition.
  122. *
  123. * @type {HTMLElement}
  124. */
  125. get element() {
  126. if ( this._element ) {
  127. return this._element;
  128. }
  129. // No template means no element (a virtual view).
  130. if ( !this.template ) {
  131. return null;
  132. }
  133. this._addTemplateChildren();
  134. return ( this._element = this.template.render() );
  135. }
  136. set element( el ) {
  137. this._element = el;
  138. }
  139. /**
  140. * Shorthand for {@link module:ui/template~Template.bind}, bound to {@link ~View} on the first access.
  141. *
  142. * Cached {@link module:ui/template~Template.bind} object is stored in {@link #_bindTemplate}.
  143. *
  144. * @method #bindTemplate
  145. */
  146. get bindTemplate() {
  147. if ( this._bindTemplate ) {
  148. return this._bindTemplate;
  149. }
  150. return ( this._bindTemplate = Template.bind( this, this ) );
  151. }
  152. /**
  153. * Creates a new collection of views, which can be used in this view instance,
  154. * e.g. as a member of {@link module:ui/template~TemplateDefinition TemplateDefinition} children.
  155. *
  156. * class SampleView extends View {
  157. * constructor( locale ) {
  158. * super( locale );
  159. *
  160. * this.items = this.createCollection();
  161. *
  162. * this.template = new Template( {
  163. * tag: 'p',
  164. *
  165. * // `items` collection will render here.
  166. * children: this.items
  167. * } );
  168. * }
  169. * }
  170. *
  171. * const view = new SampleView( locale );
  172. * const anotherView = new AnotherSampleView( locale );
  173. *
  174. * view.init();
  175. *
  176. * // Will append <p></p>
  177. * document.body.appendChild( view.element );
  178. *
  179. * // `anotherView` becomes a child of the view, which is reflected in DOM
  180. * // <p><anotherView#element></p>
  181. * view.items.add( anotherView );
  182. *
  183. * @returns {module:ui/viewcollection~ViewCollection} A new collection of view instances.
  184. */
  185. createCollection() {
  186. const collection = new ViewCollection();
  187. this._viewCollections.add( collection );
  188. return collection;
  189. }
  190. /**
  191. * Registers a new child view under this view instance. Once registered, a child
  192. * view is managed by its parent, including initialization ({@link #init})
  193. * and destruction ({@link #destroy}).
  194. *
  195. * class SampleView extends View {
  196. * constructor( locale ) {
  197. * super( locale );
  198. *
  199. * this.childA = new SomeChildView( locale );
  200. * this.childB = new SomeChildView( locale );
  201. *
  202. * this.template = new Template( { tag: 'p' } );
  203. *
  204. * // Register children.
  205. * this.addChildren( [ this.childA, this.childB ] );
  206. * }
  207. *
  208. * init() {
  209. * this.element.appendChild( this.childA.element );
  210. * this.element.appendChild( this.childB.element );
  211. *
  212. * return super.init();
  213. * }
  214. * }
  215. *
  216. * const view = new SampleView( locale );
  217. *
  218. * view.init();
  219. *
  220. * // Will append <p><childA#element><b></b><childB#element></p>
  221. * document.body.appendChild( view.element );
  222. *
  223. * **Note**: There's no need to add child views if they're used in the
  224. * {@link #template} explicitly:
  225. *
  226. * class SampleView extends View {
  227. * constructor( locale ) {
  228. * super( locale );
  229. *
  230. * this.childA = new SomeChildView( locale );
  231. * this.childB = new SomeChildView( locale );
  232. *
  233. * this.template = new Template( {
  234. * tag: 'p',
  235. *
  236. * // These children will be added automatically. There's no
  237. * // need to call {@link #addChildren} for any of them.
  238. * children: [ this.childA, this.childB ]
  239. * } );
  240. * }
  241. *
  242. * ...
  243. * }
  244. *
  245. * @param {module:ui/view~View|Iterable.<module:ui/view~View>} children Children views to be registered.
  246. */
  247. addChildren( children ) {
  248. if ( !isIterable( children ) ) {
  249. children = [ children ];
  250. }
  251. children.map( c => this._unboundChildren.add( c ) );
  252. }
  253. /**
  254. * Initializes the view and child views located in {@link #_viewCollections}.
  255. */
  256. init() {
  257. if ( this.ready ) {
  258. /**
  259. * This View has already been initialized.
  260. *
  261. * @error ui-view-init-reinit
  262. */
  263. throw new CKEditorError( 'ui-view-init-reinit: This View has already been initialized.' );
  264. }
  265. // Initialize collections in #_viewCollections.
  266. this._viewCollections.map( c => c.init() );
  267. // Spread the word that this view is ready!
  268. this.ready = true;
  269. }
  270. /**
  271. * Destroys the view instance and child views located in {@link #_viewCollections}.
  272. */
  273. destroy() {
  274. this.stopListening();
  275. this._viewCollections.map( c => c.destroy() );
  276. }
  277. /**
  278. * Recursively traverses {@link #template} in search of {@link module:ui/view~View}
  279. * instances and automatically registers them using {@link #addChildren} method.
  280. *
  281. * @protected
  282. */
  283. _addTemplateChildren() {
  284. const search = def => {
  285. if ( def.children ) {
  286. for ( const defOrView of def.children ) {
  287. if ( defOrView instanceof View ) {
  288. this.addChildren( defOrView );
  289. } else {
  290. search( defOrView );
  291. }
  292. }
  293. }
  294. };
  295. search( this.template );
  296. }
  297. }
  298. mix( View, DomEmitterMixin );
  299. mix( View, ObservableMixin );