view.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. /**
  2. * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. 'use strict';
  6. /**
  7. * Basic View class.
  8. *
  9. * @class View
  10. * @extends Model
  11. * @mixins DOMEmitterMixin
  12. */
  13. CKEDITOR.define( [
  14. 'collection',
  15. 'model',
  16. 'ui/template',
  17. 'ckeditorerror',
  18. 'ui/domemittermixin',
  19. 'utils'
  20. ], ( Collection, Model, Template, CKEditorError, DOMEmitterMixin, utils ) => {
  21. class View extends Model {
  22. /**
  23. * Creates an instance of the {@link View} class.
  24. *
  25. * @param {Model} model (View)Model of this View.
  26. * @constructor
  27. */
  28. constructor( model ) {
  29. super();
  30. /**
  31. * Model of this view.
  32. */
  33. this.model = new Model( model );
  34. /**
  35. * Regions which belong to this view.
  36. */
  37. this.regions = new Collection( null, 'name' );
  38. /**
  39. * @property {HTMLElement} _el
  40. */
  41. /**
  42. * @property {Template} _template
  43. */
  44. /**
  45. * @property {TemplateDefinition} template
  46. */
  47. }
  48. /**
  49. * Element of this view. The element is rendered on first reference.
  50. *
  51. * @property el
  52. */
  53. get el() {
  54. return this._el || this.render();
  55. }
  56. /**
  57. * Binds a `property` of View's model so the DOM of the View is updated when the `property`
  58. * changes. It returns a function which, once called in the context of a DOM element,
  59. * attaches a listener to the model which, in turn, brings changes to DOM.
  60. *
  61. * @param {String} property Property name in the model to be observed.
  62. * @param {Function} [callback] Callback function executed on property change in model.
  63. * If not specified, a default DOM `domUpdater` supplied by the template is used.
  64. */
  65. bind( property, callback ) {
  66. /**
  67. * Attaches a listener to View's model, which updates DOM when the model's property
  68. * changes. DOM is either updated by the `domUpdater` function supplied by the template
  69. * (like attribute changer or `innerHTML` setter) or custom `callback` passed to {@link #bind}.
  70. *
  71. * This function is called by {@link Template#render}.
  72. *
  73. * @param {HTMLElement} el DOM element to be updated when `property` in model changes.
  74. * @param {Function} domUpdater A function provided by the template which updates corresponding
  75. * DOM.
  76. */
  77. return ( el, domUpdater ) => {
  78. // TODO: Use ES6 default arguments syntax.
  79. callback = callback || domUpdater;
  80. // Execute callback when the property changes.
  81. this.listenTo( this.model, 'change:' + property, onModelChange );
  82. // Set the initial state of the view.
  83. onModelChange( null, this.model[ property ] );
  84. function onModelChange( evt, value ) {
  85. let processedValue = callback( el, value );
  86. if ( typeof processedValue != 'undefined' ) {
  87. domUpdater( el, processedValue );
  88. }
  89. }
  90. };
  91. }
  92. /**
  93. * Renders View's {@link el} using {@link Template} instance.
  94. *
  95. * @returns {HTMLElement} A root element of the View ({@link el}).
  96. */
  97. render() {
  98. if ( !this.template ) {
  99. /**
  100. * This View implements no template to render.
  101. *
  102. * @error ui-view-notemplate
  103. * @param {View} view
  104. */
  105. throw new CKEditorError(
  106. 'ui-view-notemplate: This View implements no template to render.',
  107. { view: this }
  108. );
  109. }
  110. // Prepare pre–defined listeners.
  111. this.prepareListeners();
  112. this._template = new Template( this.template );
  113. return ( this._el = this._template.render() );
  114. }
  115. /**
  116. * Destroys the View.
  117. */
  118. destroy() {
  119. // Drop the reference to the model.
  120. this.model = null;
  121. // Remove View's element from DOM.
  122. if ( this.template ) {
  123. this.el.remove();
  124. }
  125. // Remove and destroy regions.
  126. for ( let region of this.regions ) {
  127. this.regions.remove( region ).destroy();
  128. }
  129. // Remove all listeners related to this view.
  130. this.stopListening();
  131. }
  132. /**
  133. * Iterates over all "on" properties in {@link template} and replaces
  134. * listener definitions with functions which, once executed in a context of
  135. * a DOM element, will attach native DOM listeners to elements.
  136. *
  137. * The execution is performed by {@link Template} class.
  138. */
  139. prepareListeners() {
  140. if ( this.template ) {
  141. this._prepareElementListeners( this.template );
  142. }
  143. }
  144. /**
  145. * For a given event name or callback, returns a function which,
  146. * once executed in a context of an element, attaches native DOM listener
  147. * to the element. The listener executes given callback or fires View's event
  148. * of given name.
  149. *
  150. * @param {String|Function} evtNameOrCallback Event name to be fired on View or callback to execute.
  151. * @returns {Function} A function to be executed in the context of an element.
  152. */
  153. _getDOMListenerAttacher( evtNameOrCallback ) {
  154. /**
  155. * Attaches a native DOM listener to given element. The listener executes the
  156. * callback or fires View's event.
  157. *
  158. * Note: If the selector is supplied, it narrows the scope to relevant targets only.
  159. * So instead of
  160. *
  161. * children: [
  162. * { tag: 'span', on: { click: 'foo' } }
  163. * { tag: 'span', on: { click: 'foo' } }
  164. * ]
  165. *
  166. * a single, more efficient listener can be attached that uses **event delegation**:
  167. *
  168. * children: [
  169. * { tag: 'span' }
  170. * { tag: 'span' }
  171. * ],
  172. * on: {
  173. * 'click@span': 'foo',
  174. * }
  175. *
  176. * @param {HTMLElement} el Element, to which the native DOM Event listener is attached.
  177. * @param {String} domEventName The name of native DOM Event.
  178. * @param {String} [selector] If provided, the selector narrows the scope to relevant targets only.
  179. */
  180. return ( el, domEvtName, selector ) => {
  181. // Use View's listenTo, so the listener is detached, when the View dies.
  182. this.listenTo( el, domEvtName, ( evt, domEvt ) => {
  183. if ( !selector || domEvt.target.matches( selector ) ) {
  184. if ( typeof evtNameOrCallback == 'function' ) {
  185. evtNameOrCallback( domEvt );
  186. } else {
  187. this.fire( evtNameOrCallback, domEvt );
  188. }
  189. }
  190. } );
  191. };
  192. }
  193. /**
  194. * Iterates over "on" property in {@link template} definition to recursively
  195. * replace each listener declaration with a function which, once executed in a context
  196. * of an element, attaches native DOM listener to the element.
  197. *
  198. * @param {TemplateDefinition} def Template definition.
  199. */
  200. _prepareElementListeners( def ) {
  201. let on = def.on;
  202. if ( on ) {
  203. let domEvtName, evtNameOrCallback;
  204. for ( domEvtName in on ) {
  205. evtNameOrCallback = on[ domEvtName ];
  206. // Listeners allow definition with an array:
  207. //
  208. // on: {
  209. // 'DOMEventName@selector': [ 'event1', callback ],
  210. // 'DOMEventName': [ callback, 'event2', 'event3' ]
  211. // ...
  212. // }
  213. if ( Array.isArray( evtNameOrCallback ) ) {
  214. on[ domEvtName ] = on[ domEvtName ].map( this._getDOMListenerAttacher, this );
  215. }
  216. // Listeners allow definition with a string containing event name:
  217. //
  218. // on: {
  219. // 'DOMEventName@selector': 'event1',
  220. // 'DOMEventName': 'event2'
  221. // ...
  222. // }
  223. else {
  224. on[ domEvtName ] = this._getDOMListenerAttacher( evtNameOrCallback );
  225. }
  226. }
  227. }
  228. // Repeat recursively for the children.
  229. if ( def.children ) {
  230. def.children.map( this._prepareElementListeners, this );
  231. }
  232. }
  233. }
  234. utils.extend( View.prototype, DOMEmitterMixin );
  235. return View;
  236. } );