view.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  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 = 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. * @param
  50. * @returns
  51. */
  52. init() {
  53. // TODO: What if we used render() here?
  54. }
  55. /**
  56. * Element of this view. The element is rendered on first reference.
  57. *
  58. * @property el
  59. */
  60. get el() {
  61. return this._el || this.render();
  62. }
  63. /**
  64. * Binds a `property` of View's model so the DOM of the View is updated when the `property`
  65. * changes. It returns a function which, once called in the context of a DOM element,
  66. * attaches a listener to the model which, in turn, brings changes to DOM.
  67. *
  68. * @param {String} property Property name in the model to be observed.
  69. * @param {Function} [callback] Callback function executed on property change in model.
  70. * If not specified, a default DOM `domUpdater` supplied by the template is used.
  71. */
  72. bind( property, callback ) {
  73. /**
  74. * Attaches a listener to View's model, which updates DOM when the model's property
  75. * changes. DOM is either updated by the `domUpdater` function supplied by the template
  76. * (like attribute changer or `innerHTML` setter) or custom `callback` passed to {@link #bind}.
  77. *
  78. * This function is called by {@link Template#render}.
  79. *
  80. * @param {HTMLElement} el DOM element to be updated when `property` in model changes.
  81. * @param {Function} domUpdater A function provided by the template which updates corresponding
  82. * DOM.
  83. */
  84. return ( el, domUpdater ) => {
  85. // TODO: Use ES6 default arguments syntax.
  86. callback = callback || domUpdater;
  87. // Execute callback when the property changes.
  88. this.listenTo( this.model, 'change:' + property, onModelChange );
  89. // Set the initial state of the view.
  90. onModelChange( null, this.model[ property ] );
  91. function onModelChange( evt, value ) {
  92. let processedValue = callback( el, value );
  93. if ( typeof processedValue != 'undefined' ) {
  94. domUpdater( el, processedValue );
  95. }
  96. }
  97. };
  98. }
  99. /**
  100. * Renders View's {@link el} using {@link Template} instance.
  101. *
  102. * @returns {HTMLElement} A root element of the View ({@link el}).
  103. */
  104. render() {
  105. if ( !this.template ) {
  106. /**
  107. * This View implements no template to render.
  108. *
  109. * @error ui-view-notemplate
  110. * @param {View} view
  111. */
  112. throw new CKEditorError(
  113. 'ui-view-notemplate: This View implements no template to render.',
  114. { view: this }
  115. );
  116. }
  117. // Prepare pre–defined listeners.
  118. this.prepareListeners();
  119. this._template = new Template( this.template );
  120. return ( this._el = this._template.render() );
  121. }
  122. /**
  123. * Destroys the View.
  124. */
  125. destroy() {
  126. // Drop the reference to the model.
  127. this.model = null;
  128. // Remove View's element from DOM.
  129. if ( this.template ) {
  130. this.el.remove();
  131. }
  132. // Remove and destroy regions.
  133. for ( let region of this.regions ) {
  134. this.regions.remove( region ).destroy();
  135. }
  136. // Remove all listeners related to this view.
  137. this.stopListening();
  138. }
  139. /**
  140. * Iterates over all "on" properties in {@link template} and replaces
  141. * listener definitions with functions which, once executed in a context of
  142. * a DOM element, will attach native DOM listeners to elements.
  143. *
  144. * The execution is performed by {@link Template} class.
  145. */
  146. prepareListeners() {
  147. if ( this.template ) {
  148. this._prepareElementListeners( this.template );
  149. }
  150. }
  151. /**
  152. * For a given event name or callback, returns a function which,
  153. * once executed in a context of an element, attaches native DOM listener
  154. * to the element. The listener executes given callback or fires View's event
  155. * of given name.
  156. *
  157. * @param {String|Function} evtNameOrCallback Event name to be fired on View or callback to execute.
  158. * @returns {Function} A function to be executed in the context of an element.
  159. */
  160. _getDOMListenerAttacher( evtNameOrCallback ) {
  161. /**
  162. * Attaches a native DOM listener to given element. The listener executes the
  163. * callback or fires View's event.
  164. *
  165. * Note: If the selector is supplied, it narrows the scope to relevant targets only.
  166. * So instead of
  167. *
  168. * children: [
  169. * { tag: 'span', on: { click: 'foo' } }
  170. * { tag: 'span', on: { click: 'foo' } }
  171. * ]
  172. *
  173. * a single, more efficient listener can be attached that uses **event delegation**:
  174. *
  175. * children: [
  176. * { tag: 'span' }
  177. * { tag: 'span' }
  178. * ],
  179. * on: {
  180. * 'click@span': 'foo',
  181. * }
  182. *
  183. * @param {HTMLElement} el Element, to which the native DOM Event listener is attached.
  184. * @param {String} domEventName The name of native DOM Event.
  185. * @param {String} [selector] If provided, the selector narrows the scope to relevant targets only.
  186. */
  187. return ( el, domEvtName, selector ) => {
  188. // Use View's listenTo, so the listener is detached, when the View dies.
  189. this.listenTo( el, domEvtName, ( evt, domEvt ) => {
  190. if ( !selector || domEvt.target.matches( selector ) ) {
  191. if ( typeof evtNameOrCallback == 'function' ) {
  192. evtNameOrCallback( domEvt );
  193. } else {
  194. this.fire( evtNameOrCallback, domEvt );
  195. }
  196. }
  197. } );
  198. };
  199. }
  200. /**
  201. * Iterates over "on" property in {@link template} definition to recursively
  202. * replace each listener declaration with a function which, once executed in a context
  203. * of an element, attaches native DOM listener to the element.
  204. *
  205. * @param {TemplateDefinition} def Template definition.
  206. */
  207. _prepareElementListeners( def ) {
  208. let on = def.on;
  209. if ( on ) {
  210. let domEvtName, evtNameOrCallback;
  211. for ( domEvtName in on ) {
  212. evtNameOrCallback = on[ domEvtName ];
  213. // Listeners allow definition with an array:
  214. //
  215. // on: {
  216. // 'DOMEventName@selector': [ 'event1', callback ],
  217. // 'DOMEventName': [ callback, 'event2', 'event3' ]
  218. // ...
  219. // }
  220. if ( Array.isArray( evtNameOrCallback ) ) {
  221. on[ domEvtName ] = on[ domEvtName ].map( this._getDOMListenerAttacher, this );
  222. }
  223. // Listeners allow definition with a string containing event name:
  224. //
  225. // on: {
  226. // 'DOMEventName@selector': 'event1',
  227. // 'DOMEventName': 'event2'
  228. // ...
  229. // }
  230. else {
  231. on[ domEvtName ] = this._getDOMListenerAttacher( evtNameOrCallback );
  232. }
  233. }
  234. }
  235. // Repeat recursively for the children.
  236. if ( def.children ) {
  237. def.children.map( this._prepareElementListeners, this );
  238. }
  239. }
  240. /**
  241. * @param
  242. * @returns
  243. */
  244. append( view, regionName ) {
  245. this.regions.get( regionName ).append( view );
  246. }
  247. }
  248. utils.extend( View.prototype, DOMEmitterMixin );
  249. return View;
  250. } );