buildmodelconverter.js 15 KB

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