buildmodelconverter.js 12 KB

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