8
0

buildmodelconverter.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  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. insertUIElement,
  11. setAttribute,
  12. removeAttribute,
  13. removeUIElement,
  14. wrapItem,
  15. unwrapItem,
  16. convertTextsInsideMarker,
  17. convertElementsInsideMarker
  18. } from './model-to-view-converters';
  19. import { convertSelectionAttribute, convertSelectionMarker } from './model-selection-to-view-converters';
  20. import ViewAttributeElement from '../view/attributeelement';
  21. import ViewContainerElement from '../view/containerelement';
  22. import ViewUIElement from '../view/uielement';
  23. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  24. /**
  25. * Provides chainable, high-level API to easily build basic model-to-view converters that are appended to given
  26. * dispatchers. In many cases, this is the API that should be used to specify how abstract model elements and
  27. * attributes should be represented in the view (and then later in DOM). Instances of this class are created by
  28. * {@link module:engine/conversion/buildmodelconverter~buildModelConverter}.
  29. *
  30. * If you need more complex converters, see {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher},
  31. * {@link module:engine/conversion/model-to-view-converters}, {@link module:engine/conversion/modelconsumable~ModelConsumable},
  32. * {@link module:engine/conversion/mapper~Mapper}.
  33. *
  34. * Using this API it is possible to create five kinds of converters:
  35. *
  36. * 1. Model element to view element converter. This is a converter that takes the model element and represents it
  37. * in the view.
  38. *
  39. * buildModelConverter().for( dispatcher ).fromElement( 'paragraph' ).toElement( 'p' );
  40. * buildModelConverter().for( dispatcher ).fromElement( 'image' ).toElement( 'img' );
  41. *
  42. * 2. Model attribute to view attribute converter. This is a converter that operates on model element attributes
  43. * and converts them to view element attributes. It is suitable for elements like `image` (`src`, `title` attributes).
  44. *
  45. * buildModelConverter().for( dispatcher ).fromElement( 'image' ).toElement( 'img' );
  46. * buildModelConverter().for( dispatcher ).fromAttribute( 'src' ).toAttribute();
  47. *
  48. * 3. Model attribute to view element converter. This is a converter that takes model attributes and represents them
  49. * as view elements. Elements created by this kind of converter are wrapping other view elements. Wrapped view nodes
  50. * correspond to model nodes had converter attribute. It is suitable for attributes like `bold`, where `bold` attribute
  51. * set on model text nodes is converter to `strong` view element.
  52. *
  53. * buildModelConverter().for( dispatcher ).fromAttribute( 'bold' ).toElement( 'strong' );
  54. *
  55. * 4. Model marker to virtual selection converter. This is a converter that converts model markers to virtual
  56. * selection described by {@link module:engine/conversion/buildmodelconverter~VirtualSelectionDescriptor} object passed to
  57. * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toVirtualSelection} method.
  58. *
  59. * buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toVirtualSelection( descriptor );
  60. *
  61. * 5. Model marker to element converter. This is a converter that takes model marker and creates separate elements at
  62. * the beginning and at the end of the marker's range. For more information see
  63. * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toElement} method.
  64. *
  65. * buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toElement( 'span' );
  66. *
  67. * It is possible to provide various different parameters for
  68. * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toElement},
  69. * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toAttribute} and
  70. * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toVirtualSelection} methods.
  71. * See their descriptions to learn more.
  72. *
  73. * It is also possible to {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#withPriority change default priority}
  74. * of created converters to decide which converter should be fired earlier and which later. This is useful if you have
  75. * a general converter but also want to provide different special-case converters (i.e. given model element is converted
  76. * always to given view element, but if it has given attribute it is converter to other view element). For this,
  77. * use {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#withPriority withPriority} right after `from...` method.
  78. *
  79. * Note that `to...` methods are "terminators", which means that should be the last one used in building converter.
  80. *
  81. * You can use {@link module:engine/conversion/buildviewconverter~ViewConverterBuilder}
  82. * to create "opposite" converters - from view to model.
  83. */
  84. class ModelConverterBuilder {
  85. /**
  86. * Creates `ModelConverterBuilder` with given `dispatchers` registered to it.
  87. */
  88. constructor() {
  89. /**
  90. * Dispatchers to which converters will be attached.
  91. *
  92. * @type {Array.<module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher>}
  93. * @private
  94. */
  95. this._dispatchers = [];
  96. /**
  97. * Contains data about registered "from" query.
  98. *
  99. * @type {Object}
  100. * @private
  101. */
  102. this._from = null;
  103. }
  104. /**
  105. * Set one or more dispatchers which the built converter will be attached to.
  106. *
  107. * @chainable
  108. * @param {...module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher} dispatchers One or more dispatchers.
  109. * @returns {module:engine/conversion/buildmodelconverter~ModelConverterBuilder}
  110. */
  111. for( ...dispatchers ) {
  112. this._dispatchers = dispatchers;
  113. return this;
  114. }
  115. /**
  116. * Registers what model element should be converted.
  117. *
  118. * @chainable
  119. * @param {String} elementName Name of element to convert.
  120. * @returns {module:engine/conversion/buildmodelconverter~ModelConverterBuilder}
  121. */
  122. fromElement( elementName ) {
  123. this._from = {
  124. type: 'element',
  125. name: elementName,
  126. priority: null
  127. };
  128. return this;
  129. }
  130. /**
  131. * Registers what model attribute should be converted.
  132. *
  133. * @chainable
  134. * @param {String} key Key of attribute to convert.
  135. * @returns {module:engine/conversion/buildmodelconverter~ModelConverterBuilder}
  136. */
  137. fromAttribute( key ) {
  138. this._from = {
  139. type: 'attribute',
  140. key,
  141. priority: null
  142. };
  143. return this;
  144. }
  145. /**
  146. * Registers what type of marker should be converted.
  147. *
  148. * @chainable
  149. * @param {String} markerName Name of marker to convert.
  150. * @returns {module:engine/conversion/buildmodelconverter~ModelConverterBuilder}
  151. */
  152. fromMarker( markerName ) {
  153. this._from = {
  154. type: 'marker',
  155. name: markerName,
  156. priority: null
  157. };
  158. return this;
  159. }
  160. /**
  161. * Changes default priority for built converter. The lower the number, the earlier converter will be fired.
  162. * Default priority is `10`.
  163. *
  164. * **Note:** Keep in mind that event priority, that is set by this modifier, is used for attribute priority
  165. * when {@link module:engine/view/writer~writer} is used. This changes how view elements are ordered,
  166. * i.e.: `<strong><em>foo</em></strong>` vs `<em><strong>foo</strong></em>`. Using priority you can also
  167. * prevent node merging, i.e.: `<span class="bold"><span class="theme">foo</span><span>` vs `<span class="bold theme">foo</span>`.
  168. * If you want to prevent merging, just set different priority for both converters.
  169. *
  170. * buildModelConverter().for( dispatcher ).fromAttribute( 'bold' ).withPriority( 2 ).toElement( 'strong' );
  171. * buildModelConverter().for( dispatcher ).fromAttribute( 'italic' ).withPriority( 3 ).toElement( 'em' );
  172. *
  173. * @chainable
  174. * @param {Number} priority Converter priority.
  175. * @returns {module:engine/conversion/buildmodelconverter~ModelConverterBuilder}
  176. */
  177. withPriority( priority ) {
  178. this._from.priority = priority;
  179. return this;
  180. }
  181. /**
  182. * Registers what view element will be created by converter.
  183. *
  184. * Method accepts various ways of providing how the view element will be created. You can pass view element name as
  185. * `string`, view element instance which will be cloned and used, or creator function which returns view element that
  186. * will be used. Keep in mind that when you view element instance or creator function, it has to be/return a
  187. * proper type of view element: {@link module:engine/view/containerelement~ContainerElement ViewContainerElement} if you convert
  188. * from element, {@link module:engine/view/attributeelement~AttributeElement ViewAttributeElement} if you convert
  189. * from attribute and {@link module:engine/view/uielement~UIElement ViewUIElement} if you convert from marker.
  190. *
  191. * NOTE: When converting from model's marker, separate elements will be created at the beginning and at the end of the
  192. * marker's range. If range is collapsed then only one element will be created. See how markers
  193. * {module:engine/model/buildviewconverter~ViewConverterBuilder#toMarker view -> model serialization}
  194. * works to find out what view element format is the best for you.
  195. *
  196. * buildModelConverter().for( dispatcher ).fromElement( 'paragraph' ).toElement( 'p' );
  197. *
  198. * buildModelConverter().for( dispatcher ).fromElement( 'image' ).toElement( new ViewContainerElement( 'img' ) );
  199. *
  200. * buildModelConverter().for( dispatcher )
  201. * .fromElement( 'header' )
  202. * .toElement( ( data ) => new ViewContainerElement( 'h' + data.item.getAttribute( 'level' ) ) );
  203. *
  204. * buildModelConverter().for( dispatcher ).fromAttribute( 'bold' ).toElement( new ViewAttributeElement( 'strong' ) );
  205. *
  206. * buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toElement( 'span' );
  207. *
  208. * buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toElement( new ViewUIElement( 'span' ) );
  209. *
  210. * Creator function will be passed different values depending whether conversion is from element or from attribute:
  211. *
  212. * * from element: dispatcher's
  213. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:insert insert event}
  214. * parameters will be passed,
  215. * * from attribute: there is one parameter and it is attribute value,
  216. * * from marker: {@link module:engine/conversion/buildmodelconverter~MarkerViewElementCreatorData}.
  217. *
  218. * This method also registers model selection to view selection converter, if conversion is from attribute.
  219. *
  220. * This method creates the converter and adds it as a callback to a proper
  221. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher conversion dispatcher} event.
  222. *
  223. * @param {String|module:engine/view/element~Element|Function} element Element created by converter or
  224. * a function that returns view element.
  225. */
  226. toElement( element ) {
  227. const priority = this._from.priority === null ? 'normal' : this._from.priority;
  228. for ( const dispatcher of this._dispatchers ) {
  229. if ( this._from.type == 'element' ) {
  230. // From model element to view element -> insert element.
  231. element = typeof element == 'string' ? new ViewContainerElement( element ) : element;
  232. dispatcher.on( 'insert:' + this._from.name, insertElement( element ), { priority } );
  233. } else if ( this._from.type == 'attribute' ) {
  234. // From model attribute to view element -> wrap and unwrap.
  235. element = typeof element == 'string' ? new ViewAttributeElement( element ) : element;
  236. dispatcher.on( 'addAttribute:' + this._from.key, wrapItem( element ), { priority } );
  237. dispatcher.on( 'changeAttribute:' + this._from.key, wrapItem( element ), { priority } );
  238. dispatcher.on( 'removeAttribute:' + this._from.key, unwrapItem( element ), { priority } );
  239. dispatcher.on( 'selectionAttribute:' + this._from.key, convertSelectionAttribute( element ), { priority } );
  240. } else { // From marker to element.
  241. const priority = this._from.priority === null ? 'normal' : this._from.priority;
  242. element = typeof element == 'string' ? new ViewUIElement( element ) : element;
  243. dispatcher.on( 'addMarker:' + this._from.name, insertUIElement( element ), { priority } );
  244. dispatcher.on( 'removeMarker:' + this._from.name, removeUIElement( element ), { priority } );
  245. }
  246. }
  247. }
  248. /**
  249. * Registers that marker should be converted to virtual selection. Markers, basically,
  250. * are {@link module:engine/model/liverange~LiveRange} instances, that are named. Virtual selection is
  251. * a representation of the model marker in the view:
  252. * * each {@link module:engine/view/text~Text view text node} in the marker's range will be wrapped with `span`
  253. * {@link module:engine/view/attributeelement~AttributeElement},
  254. * * each {@link module:engine/view/containerelement~ContainerElement container view element} in the marker's
  255. * range can handle the virtual selection individually by providing `setVirtualSelection` and `removeVirtualSelection`
  256. * custom properties:
  257. *
  258. * viewElement.setCustomProperty( 'setVirtualSelection', ( element, descriptor ) => {} );
  259. * viewElement.setCustomProperty( 'removeVirtualSelection', ( element, descriptor ) => {} );
  260. *
  261. * {@link module:engine/conversion/buildmodelconverter~VirtualSelectionDescriptor Descriptor} will be used to create
  262. * spans over text nodes and also will be provided to `setVirtualSelection` and `removeVirtualSelection` methods
  263. * each time virtual selection should be set or removed from view elements.
  264. * NOTE: When `setVirtualSelection` and `removeVirtualSelection` custom properties are present, converter assumes
  265. * that element itself is taking care of presenting virtual selection on its child nodes, so it won't convert virtual
  266. * selection on them.
  267. *
  268. * Virtual selection descriptor can be provided as plain object:
  269. *
  270. * buildModelConverter.for( dispatcher ).fromMarker( 'search' ).toVirtualSelection( { class: 'search-mark' } );
  271. *
  272. * Also, descriptor creator function can be provided:
  273. *
  274. * buildModelConverter.for( dispatcher ).fromMarker( 'search:blue' ).toVirtualSelection( data => {
  275. * const color = data.markerName.split( ':' )[ 1 ];
  276. *
  277. * return { class: 'search-' + color };
  278. * } );
  279. *
  280. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
  281. * `build-model-converter-non-marker-to-virtual-selection` when trying to convert not from marker.
  282. *
  283. * @param {function|module:engine/conversion/buildmodelconverter~VirtualSelectionDescriptor} selectionDescriptor
  284. */
  285. toVirtualSelection( selectionDescriptor ) {
  286. const priority = this._from.priority === null ? 'normal' : this._from.priority;
  287. if ( this._from.type != 'marker' ) {
  288. /**
  289. * To virtual selection conversion is supported only for model markers.
  290. *
  291. * @error build-model-converter-non-marker-to-virtual-selection
  292. */
  293. throw new CKEditorError(
  294. 'build-model-converter-non-marker-to-virtual-selection: Conversion to virtual selection is supported ' +
  295. 'only from model markers.'
  296. );
  297. }
  298. for ( const dispatcher of this._dispatchers ) {
  299. // Separate converters for converting texts and elements inside marker's range.
  300. dispatcher.on( 'addMarker:' + this._from.name, convertTextsInsideMarker( selectionDescriptor ), { priority } );
  301. dispatcher.on( 'addMarker:' + this._from.name, convertElementsInsideMarker( selectionDescriptor ), { priority } );
  302. dispatcher.on( 'removeMarker:' + this._from.name, convertTextsInsideMarker( selectionDescriptor ), { priority } );
  303. dispatcher.on( 'removeMarker:' + this._from.name, convertElementsInsideMarker( selectionDescriptor ), { priority } );
  304. dispatcher.on( 'selectionMarker:' + this._from.name, convertSelectionMarker( selectionDescriptor ), { priority } );
  305. }
  306. }
  307. /**
  308. * Registers what view attribute will be created by converter. Keep in mind, that only model attribute to
  309. * view attribute conversion is supported.
  310. *
  311. * Method accepts various ways of providing how the view attribute will be created:
  312. *
  313. * * for no passed parameter, attribute key and value will be converted 1-to-1 to view attribute,
  314. * * if you pass one `string`, it will be used as new attribute key while attribute value will be copied,
  315. * * if you pass two `string`s, first one will be used as new attribute key and second one as new attribute value,
  316. * * if you pass a function, it is expected to return an object with `key` and `value` properties representing attribute key and value.
  317. * This function will be passed model attribute value and model attribute key as first two parameters and then
  318. * all dispatcher's
  319. * {module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:changeAttribute changeAttribute event}
  320. * parameters.
  321. *
  322. * buildModelConverter().for( dispatcher ).fromAttribute( 'class' ).toAttribute( '' );
  323. *
  324. * buildModelConverter().for( dispatcher ).fromAttribute( 'linkTitle' ).toAttribute( 'title' );
  325. *
  326. * buildModelConverter().for( dispatcher ).fromAttribute( 'highlighted' ).toAttribute( 'style', 'background:yellow' );
  327. *
  328. * buildModelConverter().for( dispatcher )
  329. * .fromAttribute( 'theme' )
  330. * .toAttribute( ( value ) => ( { key: 'class', value: value + '-theme' } ) );
  331. *
  332. * This method creates the converter and adds it as a callback to a proper
  333. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher conversion dispatcher} event.
  334. *
  335. * @param {String|Function} [keyOrCreator] Attribute key or a creator function.
  336. * @param {*} [value] Attribute value.
  337. */
  338. toAttribute( keyOrCreator, value ) {
  339. if ( this._from.type != 'attribute' ) {
  340. /**
  341. * To-attribute conversion is supported only for model attributes.
  342. *
  343. * @error build-model-converter-element-to-attribute
  344. */
  345. throw new CKEditorError( 'build-model-converter-non-attribute-to-attribute: ' +
  346. 'To-attribute conversion is supported only from model attributes.' );
  347. }
  348. let attributeCreator;
  349. if ( !keyOrCreator ) {
  350. // If `keyOrCreator` is not set, we assume default behavior which is 1:1 attribute re-write.
  351. // This is also a default behavior for `setAttribute` converter when no attribute creator is passed.
  352. attributeCreator = undefined;
  353. } else if ( typeof keyOrCreator == 'string' ) {
  354. // `keyOrCreator` is an attribute key.
  355. if ( value ) {
  356. // If value is set, create "dumb" creator that always returns the same object.
  357. attributeCreator = function() {
  358. return { key: keyOrCreator, value };
  359. };
  360. } else {
  361. // If value is not set, take it from the passed parameter.
  362. attributeCreator = function( value ) {
  363. return { key: keyOrCreator, value };
  364. };
  365. }
  366. } else {
  367. // `keyOrCreator` is an attribute creator function.
  368. attributeCreator = keyOrCreator;
  369. }
  370. for ( const dispatcher of this._dispatchers ) {
  371. const options = { priority: this._from.priority || 'normal' };
  372. dispatcher.on( 'addAttribute:' + this._from.key, setAttribute( attributeCreator ), options );
  373. dispatcher.on( 'changeAttribute:' + this._from.key, setAttribute( attributeCreator ), options );
  374. dispatcher.on( 'removeAttribute:' + this._from.key, removeAttribute( attributeCreator ), options );
  375. }
  376. }
  377. }
  378. /**
  379. * Entry point for model-to-view converters builder. This chainable API makes it easy to create basic, most common
  380. * model-to-view converters and attach them to provided dispatchers. The method returns an instance of
  381. * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder}.
  382. */
  383. export default function buildModelConverter() {
  384. return new ModelConverterBuilder();
  385. }
  386. /**
  387. * @typedef MarkerViewElementCreatorData
  388. * @param {Object} data Additional information about the change.
  389. * @param {String} data.markerName Marker name.
  390. * @param {module:engine/model/range~Range} data.markerRange Marker range.
  391. * @param {Boolean} data.isOpening Defines if currently converted element is a beginning or end of the marker range.
  392. * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
  393. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  394. */
  395. /**
  396. * @typedef VirtualSelectionDescriptor
  397. * Object describing how virtual selection should be created in the view. Each text node in virtual selection
  398. * will be wrapped with `span` element with CSS class, attributes and priority described by this object. Each element
  399. * can handle virtual selection separately by providing `setVirtualSelection` and `removeVirtualSelection` custom
  400. * properties.
  401. *
  402. * @property {Number} priority {@link module:engine/view/attributeelement~AttributeElement#priority} of the `span`
  403. * wrapping each text node in the virtual selection.
  404. * @property {String} class CSS class that will be added to `span`
  405. * {@link module:engine/view/attributeelement~AttributeElement} wrapping each text node in the virtual selection.
  406. * @property {Object} [attributes] Attributes that will be added to `span`
  407. * {@link module:engine/view/attributeelement~AttributeElement} wrapping each text node it the virtual selection.
  408. */