view.js 8.6 KB

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