view.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  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/region',
  17. 'ui/template',
  18. 'ckeditorerror',
  19. 'ui/domemittermixin',
  20. 'utils'
  21. ], ( Collection, Model, Region, Template, CKEditorError, DOMEmitterMixin, utils ) => {
  22. class View extends Model {
  23. /**
  24. * Creates an instance of the {@link View} class.
  25. *
  26. * @param {Model} model (View)Model of this View.
  27. * @constructor
  28. */
  29. constructor( model ) {
  30. super();
  31. /**
  32. * Model of this view.
  33. */
  34. this.model = model;
  35. /**
  36. * Regions which belong to this view.
  37. */
  38. this.regions = new Collection( {
  39. idProperty: 'name'
  40. } );
  41. /**
  42. * @property {Object} regionsDef
  43. */
  44. /**
  45. * @property {HTMLElement} _el
  46. */
  47. /**
  48. * @property {Template} _template
  49. */
  50. /**
  51. * @property {TemplateDefinition} template
  52. */
  53. }
  54. /**
  55. * @param
  56. * @returns
  57. */
  58. init() {
  59. this._initRegions();
  60. }
  61. /**
  62. * @param
  63. * @returns
  64. */
  65. _initRegions() {
  66. let regionName, region;
  67. // Add regions that haven't been created yet (i.e. by addChild()).
  68. // Those regions have no children Views at that point.
  69. for ( regionName in this.regionsDef ) {
  70. if ( !this.regions.get( regionName ) ) {
  71. this._createRegion( regionName );
  72. }
  73. }
  74. // Initialize regions. Feed them with an element of this View.
  75. for ( region of this.regions ) {
  76. region.init( this );
  77. }
  78. }
  79. /**
  80. * @param
  81. * @returns
  82. */
  83. addChild( childView, regionName, index ) {
  84. // Create a Region instance on demand.
  85. const region = this.regions.get( regionName ) || this._createRegion( regionName );
  86. region.addChild( childView, index );
  87. }
  88. /**
  89. * @param
  90. * @returns
  91. */
  92. removeChild( childView, regionName ) {
  93. return this.regions.get( regionName ).removeChild( childView );
  94. }
  95. /**
  96. * @param
  97. * @returns
  98. */
  99. _createRegion( regionName ) {
  100. // Use region element definition from `View#regions`.
  101. const region = new Region( regionName, this.regionsDef[ regionName ] );
  102. this.regions.add( region );
  103. return region;
  104. }
  105. /**
  106. * Element of this view. The element is rendered on first reference.
  107. *
  108. * @property el
  109. */
  110. get el() {
  111. return this._el || this.render();
  112. }
  113. set el( el ) {
  114. this._el = el;
  115. }
  116. /**
  117. * Binds an `attribute` of View's model so the DOM of the View is updated when the `attribute`
  118. * changes. It returns a function which, once called in the context of a DOM element,
  119. * attaches a listener to the model which, in turn, brings changes to DOM.
  120. *
  121. * @param {String} attribute Attribute name in the model to be observed.
  122. * @param {Function} [callback] Callback function executed on attribute change in model.
  123. * If not specified, a default DOM `domUpdater` supplied by the template is used.
  124. */
  125. bindToAttribute( attribute, callback ) {
  126. /**
  127. * Attaches a listener to View's model, which updates DOM when the model's attribute
  128. * changes. DOM is either updated by the `domUpdater` function supplied by the template
  129. * (like attribute changer or `innerHTML` setter) or custom `callback` passed to {@link #bind}.
  130. *
  131. * This function is called by {@link Template#render}.
  132. *
  133. * @param {HTMLElement} el DOM element to be updated when `attribute` in model changes.
  134. * @param {Function} domUpdater A function provided by the template which updates corresponding
  135. * DOM.
  136. */
  137. return ( el, domUpdater ) => {
  138. // TODO: Use ES6 default arguments syntax.
  139. callback = callback || domUpdater;
  140. // Execute callback when the attribute changes.
  141. this.listenTo( this.model, 'change:' + attribute, onModelChange );
  142. // Set the initial state of the view.
  143. onModelChange( null, this.model[ attribute ] );
  144. function onModelChange( evt, value ) {
  145. let processedValue = callback( el, value );
  146. if ( typeof processedValue != 'undefined' ) {
  147. domUpdater( el, processedValue );
  148. }
  149. }
  150. };
  151. }
  152. /**
  153. * Renders View's {@link el} using {@link Template} instance.
  154. *
  155. * @returns {HTMLElement} A root element of the View ({@link el}).
  156. */
  157. render() {
  158. if ( !this.template ) {
  159. /**
  160. * This View implements no template to render.
  161. *
  162. * @error ui-view-notemplate
  163. * @param {View} view
  164. */
  165. throw new CKEditorError(
  166. 'ui-view-notemplate: This View implements no template to render.',
  167. { view: this }
  168. );
  169. }
  170. // Prepare pre–defined listeners.
  171. this._prepareListeners();
  172. this._template = new Template( this.template );
  173. return ( this._el = this._template.render() );
  174. }
  175. /**
  176. * Destroys the View.
  177. */
  178. destroy() {
  179. const regions = this.regions;
  180. let region;
  181. // Drop the reference to the model.
  182. this.model = null;
  183. // Remove View's element from DOM.
  184. if ( this.template ) {
  185. this.el.remove();
  186. }
  187. // Remove and destroy regions.
  188. for ( region of regions ) {
  189. regions.remove( region ).destroy();
  190. }
  191. // Remove all listeners related to this view.
  192. this.stopListening();
  193. }
  194. /**
  195. * Iterates over all "on" properties in {@link template} and replaces
  196. * listener definitions with functions which, once executed in a context of
  197. * a DOM element, will attach native DOM listeners to elements.
  198. *
  199. * The execution is performed by {@link Template} class.
  200. */
  201. _prepareListeners() {
  202. if ( this.template ) {
  203. this._prepareElementListeners( this.template );
  204. }
  205. }
  206. /**
  207. * For a given event name or callback, returns a function which,
  208. * once executed in a context of an element, attaches native DOM listener
  209. * to the element. The listener executes given callback or fires View's event
  210. * of given name.
  211. *
  212. * @param {String|Function} evtNameOrCallback Event name to be fired on View or callback to execute.
  213. * @returns {Function} A function to be executed in the context of an element.
  214. */
  215. _getDOMListenerAttacher( evtNameOrCallback ) {
  216. /**
  217. * Attaches a native DOM listener to given element. The listener executes the
  218. * callback or fires View's event.
  219. *
  220. * Note: If the selector is supplied, it narrows the scope to relevant targets only.
  221. * So instead of
  222. *
  223. * children: [
  224. * { tag: 'span', on: { click: 'foo' } }
  225. * { tag: 'span', on: { click: 'foo' } }
  226. * ]
  227. *
  228. * a single, more efficient listener can be attached that uses **event delegation**:
  229. *
  230. * children: [
  231. * { tag: 'span' }
  232. * { tag: 'span' }
  233. * ],
  234. * on: {
  235. * 'click@span': 'foo',
  236. * }
  237. *
  238. * @param {HTMLElement} el Element, to which the native DOM Event listener is attached.
  239. * @param {String} domEventName The name of native DOM Event.
  240. * @param {String} [selector] If provided, the selector narrows the scope to relevant targets only.
  241. */
  242. return ( el, domEvtName, selector ) => {
  243. // Use View's listenTo, so the listener is detached, when the View dies.
  244. this.listenTo( el, domEvtName, ( evt, domEvt ) => {
  245. if ( !selector || domEvt.target.matches( selector ) ) {
  246. if ( typeof evtNameOrCallback == 'function' ) {
  247. evtNameOrCallback( domEvt );
  248. } else {
  249. this.fire( evtNameOrCallback, domEvt );
  250. }
  251. }
  252. } );
  253. };
  254. }
  255. /**
  256. * Iterates over "on" property in {@link template} definition to recursively
  257. * replace each listener declaration with a function which, once executed in a context
  258. * of an element, attaches native DOM listener to the element.
  259. *
  260. * @param {TemplateDefinition} def Template definition.
  261. */
  262. _prepareElementListeners( def ) {
  263. let on = def.on;
  264. if ( on ) {
  265. let domEvtName, evtNameOrCallback;
  266. for ( domEvtName in on ) {
  267. evtNameOrCallback = on[ domEvtName ];
  268. // Listeners allow definition with an array:
  269. //
  270. // on: {
  271. // 'DOMEventName@selector': [ 'event1', callback ],
  272. // 'DOMEventName': [ callback, 'event2', 'event3' ]
  273. // ...
  274. // }
  275. if ( Array.isArray( evtNameOrCallback ) ) {
  276. on[ domEvtName ] = on[ domEvtName ].map( this._getDOMListenerAttacher, this );
  277. }
  278. // Listeners allow definition with a string containing event name:
  279. //
  280. // on: {
  281. // 'DOMEventName@selector': 'event1',
  282. // 'DOMEventName': 'event2'
  283. // ...
  284. // }
  285. else {
  286. on[ domEvtName ] = this._getDOMListenerAttacher( evtNameOrCallback );
  287. }
  288. }
  289. }
  290. // Repeat recursively for the children.
  291. if ( def.children ) {
  292. def.children.map( this._prepareElementListeners, this );
  293. }
  294. }
  295. }
  296. utils.extend( View.prototype, DOMEmitterMixin );
  297. return View;
  298. } );