model-converter-builder.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. 'use strict';
  6. import {
  7. insertElement,
  8. setAttribute,
  9. removeAttribute,
  10. wrap,
  11. unwrap
  12. } from './model-to-view-converters.js';
  13. import { convertSelectionAttribute } from './model-selection-to-view-converters.js';
  14. import ViewAttributeElement from '../treeview/attributeelement.js';
  15. import ViewContainerElement from '../treeview/containerelement.js';
  16. /**
  17. * Provides chainable, high-level API to easily build basic model-to-view converters that are appended to given
  18. * dispatchers. In many cases, this is the API that should be used to specify how abstract model elements and
  19. * attributes should be represented in the view (and then later in DOM). Instances of this class are created by
  20. * {@link engine.treeController.BuildModelConverterFor}.
  21. *
  22. * If you need more complex converters, see {@link engine.treeController.ModelConversionDispatcher},
  23. * {@link engine.treeController.modelToView}, {@link engine.treeController.ModelConsumable}, {@link engine.treeController.Mapper}.
  24. *
  25. * Using this API it is possible to create three kinds of converters:
  26. *
  27. * 1. Model element to view element converter. This is a converter that takes the model element and represents it
  28. * in the view.
  29. *
  30. * BuildModelConverterFor( dispatcher ).fromElement( 'paragraph' ).toElement( 'p' );
  31. * BuildModelConverterFor( dispatcher ).fromElement( 'image' ).toElement( 'img' );
  32. *
  33. * 2. Model attribute to view attribute converter. This is a converter that operates on model element attributes
  34. * and converts them to view element attributes. It is suitable for elements like `image` (`src`, `title` attributes).
  35. *
  36. * BuildModelConverterFor( dispatcher ).fromElement( 'image' ).toElement( 'img' );
  37. * BuildModelConverterFor( dispatcher ).fromAttribute( 'src' ).toAttribute();
  38. *
  39. * 3. Model attribute to view element converter. This is a converter that takes model attributes and represents them
  40. * as view elements. Those view elements are wrapping view elements are node that correspond to model elements and
  41. * nodes which had converter attribute. It is suitable for attributes like `bold`, where `bold` attribute set on model
  42. * text nodes is converter to `strong` view element.
  43. *
  44. * BuildModelConverterFor( dispatcher ).fromAttribute( 'bold' ).toElement( 'strong' );
  45. *
  46. * It is possible to provide various different parameters for {@link engine.treeController.ModelConverterBuilder#toElement}
  47. * and {@link engine.treeController.ModelConverterBuilder#toAttribute} methods. See their descriptions to learn more.
  48. *
  49. * It is also possible to {@link engine.treeController.ModelConverterBuilder#withPriority change default priority}
  50. * of created converters to decide which converter should be fired earlier and which later. This is useful if you provide
  51. * a general converter but want to provide different converter for a specific-case (i.e. given model element is converted
  52. * always to given view element, but if it has given attribute it is converter to other view element). For this,
  53. * use {@link engine.treeController.ModelConverterBuilder#withPriority withPriority} right after `from...` method.
  54. *
  55. * Note that `to...` methods are "terminators", which means that should be the last one used in building converter.
  56. *
  57. * You can use {@link engine.treeController.ViewConverterBuilder} to create "opposite" converters - from view to model.
  58. *
  59. * @memberOf engine.treeController
  60. */
  61. class ModelConverterBuilder {
  62. /**
  63. * Creates `ModelConverterBuilder` with given `dispatchers` registered to it.
  64. *
  65. * @param {Array.<engine.treeController.ModelConversionDispatcher>} dispatchers Dispatchers to which converters will
  66. * be attached.
  67. */
  68. constructor( dispatchers ) {
  69. /**
  70. * Dispatchers to which converters will be attached.
  71. *
  72. * @type {Array.<engine.treeController.ModelConversionDispatcher>}
  73. * @private
  74. */
  75. this._dispatchers = dispatchers;
  76. /**
  77. * Contains data about registered "from" query.
  78. *
  79. * @type {Object}
  80. * @private
  81. */
  82. this._from = null;
  83. }
  84. /**
  85. * Registers what model element should be converted.
  86. *
  87. * @chainable
  88. * @param {String} elementName Name of element to convert.
  89. * @returns {engine.treeController.ModelConverterBuilder}
  90. */
  91. fromElement( elementName ) {
  92. this._from = {
  93. type: 'element',
  94. name: elementName,
  95. priority: null
  96. };
  97. return this;
  98. }
  99. /**
  100. * Registers what model attribute should be converted.
  101. *
  102. * @chainable
  103. * @param {String} key Key of attribute to convert.
  104. * @returns {engine.treeController.ModelConverterBuilder}
  105. */
  106. fromAttribute( key ) {
  107. this._from = {
  108. type: 'attribute',
  109. key: key,
  110. priority: null
  111. };
  112. return this;
  113. }
  114. /**
  115. * Changes default priority for built converter. The lower the number, the earlier converter will be fired.
  116. * Default priority is `10`.
  117. *
  118. * **Note:** Keep in mind that event priority, that is set by this modifier, is used for attribute priority
  119. * when {@link engine.treeView.Writer} is used. This changes how model attributes converter to view elements are
  120. * ordered, i.e.: `<strong><em>foo</em></strong>` vs `<em><strong>foo</strong></em>`. Using priority you can also
  121. * prevent node merging, i.e.: `<span class="bold"><span class="theme">foo</span><span>` vs `<span class="bold theme">foo</span>`.
  122. * If you want to prevent merging, just set different priority for both converters.
  123. *
  124. * BuildModelConverterFor( dispatcher ).fromAttribute( 'bold' ).withPriority( 2 ).toElement( 'strong' );
  125. * BuildModelConverterFor( dispatcher ).fromAttribute( 'italic' ).withPriority( 3 ).toElement( 'em' );
  126. *
  127. * @chainable
  128. * @param {Number} priority Converter priority.
  129. * @returns {engine.treeController.ModelConverterBuilder}
  130. */
  131. withPriority( priority ) {
  132. this._from.priority = priority;
  133. return this;
  134. }
  135. /**
  136. * Registers what view element will be created by converter.
  137. *
  138. * Method accepts various ways of providing how the view element will be created. You can pass view element name as
  139. * `string`, view element instance which will be cloned and used, or creator function which returns view element that
  140. * will be used. Keep in mind that when you view element instance or creator function, it has to be/return a
  141. * proper type of view element: {@link engine.treeView.ViewContainerElement ViewContainerElement} if you convert
  142. * from element or {@link engine.treeView.ViewAttributeElement ViewAttributeElement} if you convert from attribute.
  143. *
  144. * BuildModelConverterFor( dispatcher ).fromElement( 'paragraph' ).toElement( 'p' );
  145. *
  146. * BuildModelConverterFor( dispatcher ).fromElement( 'image' ).toElement( new ViewContainerElement( 'img' ) );
  147. *
  148. * BuildModelConverterFor( dispatcher )
  149. * .fromElement( 'header' )
  150. * .toElement( ( data ) => new ViewContainerElement( 'h' + data.item.getAttribute( 'level' ) ) );
  151. *
  152. * BuildModelConverterFor( dispatcher ).fromAttribute( 'bold' ).toElement( new ViewAttributeElement( 'strong' ) );
  153. *
  154. * Creator function will be passed different values depending whether conversion is from element or from attribute:
  155. *
  156. * * from element: dispatcher's {@link engine.treeController.ModelConversionDispatcher#event:insert insert event} parameters
  157. * will be passed,
  158. * * from attribute: there is one parameter and it is attribute value.
  159. *
  160. * This method also registers model selection to view selection converter, if conversion is from attribute.
  161. *
  162. * This method creates the converter and adds it as a callback to a proper
  163. * {@link engine.treeController.ModelConversionDispatcher conversion dispatcher} event.
  164. *
  165. * @param {String|engine.treeView.ViewElement|Function} element Element created by converter.
  166. */
  167. toElement( element ) {
  168. const priority = this._from.priority === null ? 10 : this._from.priority;
  169. for ( let dispatcher of this._dispatchers ) {
  170. if ( this._from.type == 'element' ) {
  171. // From model element to view element -> insert element.
  172. element = typeof element == 'string' ? new ViewContainerElement( element ) : element;
  173. dispatcher.on( 'insert:' + this._from.name, insertElement( element ), null, priority );
  174. } else {
  175. // From model attribute to view element -> wrap and unwrap.
  176. element = typeof element == 'string' ? new ViewAttributeElement( element ) : element;
  177. dispatcher.on( 'addAttribute:' + this._from.key, wrap( element ), null, priority );
  178. dispatcher.on( 'changeAttribute:' + this._from.key, wrap( element ), null, priority );
  179. dispatcher.on( 'removeAttribute:' + this._from.key, unwrap( element ), null, priority );
  180. dispatcher.on( 'selectionAttribute:' + this._from.key, convertSelectionAttribute( element ), null, priority );
  181. }
  182. }
  183. }
  184. /**
  185. * Registers what view attribute will be created by converter. Keep in mind, that only model attribute to
  186. * view attribute conversion is supported.
  187. *
  188. * Method accepts various ways of providing how the view attribute will be created:
  189. *
  190. * * for no passed parameter, attribute key and value will be converted 1-to-1 to view attribute,
  191. * * if you pass one `string`, it will be used as new attribute key while attribute value will be copied,
  192. * * if you pass two `string`s, first one will be used as new attribute key and second one as new attribute value,
  193. * * if you pass a function, it is expected to return an object with `key` and `value` properties representing attribute key and value.
  194. * This function will be passed model attribute value and model attribute key as first two parameters and then
  195. * all dispatcher's {engine.treeController.ModelConversionDispatcher#event:changeAttribute changeAttribute event} parameters.
  196. *
  197. * BuildModelConverterFor( dispatcher ).fromAttribute( 'class' ).toAttribute( '' );
  198. *
  199. * BuildModelConverterFor( dispatcher ).fromAttribute( 'linkTitle' ).toAttribute( 'title' );
  200. *
  201. * BuildModelConverterFor( dispatcher ).fromAttribute( 'highlighted' ).toAttribute( 'style', 'background:yellow' );
  202. *
  203. * BuildModelConverterFor( dispatcher )
  204. * .fromAttribute( 'theme' )
  205. * .toAttribute( ( value ) => ( { key: 'class', value: value + '-theme' } ) );
  206. *
  207. * This method creates the converter and adds it as a callback to a proper
  208. * {@link engine.treeController.ModelConversionDispatcher conversion dispatcher} event.
  209. *
  210. * @param {String|Function} [keyOrCreator] Attribute key or a creator function.
  211. * @param {*} [value] Attribute value.
  212. */
  213. toAttribute( keyOrCreator, value ) {
  214. if ( this._from.type == 'element' ) {
  215. // Converting from model element to view attribute is unsupported.
  216. return;
  217. }
  218. let attributeCreator;
  219. if ( !keyOrCreator ) {
  220. // If `keyOrCreator` is not set, we assume default behavior which is 1:1 attribute re-write.
  221. // This is also a default behavior for `setAttribute` converter when no attribute creator is passed.
  222. attributeCreator = undefined;
  223. } else if ( typeof keyOrCreator == 'string' ) {
  224. // `keyOrCreator` is an attribute key.
  225. if ( value ) {
  226. // If value is set, create "dumb" creator that always returns the same object.
  227. attributeCreator = function() {
  228. return { key: keyOrCreator, value: value };
  229. };
  230. } else {
  231. // If value is not set, take it from the passed parameter.
  232. attributeCreator = function( value ) {
  233. return { key: keyOrCreator, value: value };
  234. };
  235. }
  236. } else {
  237. // `keyOrCreator` is an attribute creator function.
  238. attributeCreator = keyOrCreator;
  239. }
  240. for ( let dispatcher of this._dispatchers ) {
  241. dispatcher.on( 'addAttribute:' + this._from.key, setAttribute( attributeCreator ), null, this._from.priority || 10 );
  242. dispatcher.on( 'changeAttribute:' + this._from.key, setAttribute( attributeCreator ), null, this._from.priority || 10 );
  243. dispatcher.on( 'removeAttribute:' + this._from.key, removeAttribute( attributeCreator ), null, this._from.priority || 10 );
  244. }
  245. }
  246. }
  247. /**
  248. * Entry point for model-to-view converters builder. This chainable API makes it easy to create basic, most common
  249. * model-to-view converters and attach them to provided dispatchers. The method returns an instance of
  250. * {@link engine.treeController.ModelConverterBuilder}.
  251. *
  252. * @external engine.treeController.BuildModelConverterFor
  253. * @memberOf engine.treeController
  254. * @param {...engine.treeController.ModelConversionDispatcher} dispatchers One or more dispatchers which
  255. * the built converter will be attached to.
  256. */
  257. export default function BuildModelConverterFor( ...dispatchers ) {
  258. return new ModelConverterBuilder( dispatchers );
  259. }