downcast-helpers.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import ViewContainerElement from '../view/containerelement';
  6. import ViewAttributeElement from '../view/attributeelement';
  7. import ViewUIElement from '../view/uielement';
  8. import {
  9. insertElement, wrap, changeAttribute,
  10. insertUIElement, removeUIElement, highlightText, highlightElement, removeHighlight
  11. } from './downcast-converters';
  12. import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep';
  13. /**
  14. * @module engine/conversion/downcast-helpers
  15. */
  16. /**
  17. * Model element to view element conversion helper.
  18. *
  19. * This conversion results in creating a view element. For example, model `<paragraph>Foo</paragraph>` becomes `<p>Foo</p>` in the view.
  20. *
  21. * downcastElementToElement( { model: 'paragraph', view: 'p' } );
  22. *
  23. * downcastElementToElement( { model: 'paragraph', view: 'p' }, 'high' );
  24. *
  25. * downcastElementToElement( { model: 'paragraph', view: new ViewContainerElement( 'p' ) } );
  26. *
  27. * downcastElementToElement( {
  28. * model: 'fancyParagraph',
  29. * view: {
  30. * name: 'p',
  31. * class: 'fancy'
  32. * }
  33. * } );
  34. *
  35. * downcastElementToElement( {
  36. * model: 'heading',
  37. * view: modelElement => new ViewContainerElement( 'h' + modelElement.getAttribute( 'level' ) )
  38. * } );
  39. *
  40. * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  41. *
  42. * @param {Object} config Conversion configuration.
  43. * @param {String} config.model Name of the model element to convert.
  44. * @param {String|module:engine/view/elementdefinition~elementDefinition|Function|
  45. * module:engine/view/containerelement~ContainerElement} config.view View element name, or a view element definition,
  46. * or a function that takes model element as a parameter and returns a view container element,
  47. * or a view container element instance. The view element will be used then in conversion.
  48. * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
  49. * @returns {Function} Conversion helper.
  50. */
  51. export function downcastElementToElement( config, priority = 'normal' ) {
  52. config = cloneDeep( config );
  53. _normalizeToElementConfig( config, ViewContainerElement );
  54. return dispatcher => {
  55. dispatcher.on( 'insert:' + config.model, insertElement( config.view ), { priority } );
  56. };
  57. }
  58. /**
  59. * Model attribute to view element conversion helper.
  60. *
  61. * This conversion results in wrapping view nodes in a view attribute element. For example, model text node with data
  62. * `"Foo"` and `bold` attribute becomes `<strong>Foo</strong>` in the view.
  63. *
  64. * downcastAttributeToElement( 'bold', { view: 'strong' } );
  65. *
  66. * downcastAttributeToElement( 'bold', { view: 'strong' }, 'high' );
  67. *
  68. * downcastAttributeToElement( 'bold', { view: new ViewAttributeElement( 'strong' ) } );
  69. *
  70. * downcastAttributeToElement( 'bold', {
  71. * view: {
  72. * name: 'span',
  73. * class: 'bold'
  74. * }
  75. * } );
  76. *
  77. * downcastAttributeToElement( 'styled', {
  78. * model: 'dark',
  79. * view: {
  80. * name: 'span',
  81. * class: [ 'styled', 'styled-dark' ]
  82. * }
  83. * } );
  84. *
  85. * downcastAttributeToElement( 'fontSize', [
  86. * {
  87. * model: 'big',
  88. * view: {
  89. * name: 'span',
  90. * style: {
  91. * 'font-size': '1.2em'
  92. * }
  93. * }
  94. * },
  95. * {
  96. * model: 'small',
  97. * view: {
  98. * name: 'span',
  99. * style: {
  100. * 'font-size': '0.8em'
  101. * }
  102. * }
  103. * }
  104. * ] );
  105. *
  106. * downcastAttributeToElement( 'bold', {
  107. * view: modelAttributeValue => new ViewAttributeElement( 'span', { style: 'font-weight:' + modelAttributeValue } )
  108. * } );
  109. *
  110. * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  111. *
  112. * @param {String} modelAttributeKey The key of the attribute to convert.
  113. * @param {Object|Array.<Object>} config Conversion configuration. It is possible to provide multiple configurations in an array.
  114. * @param {*} [config.model] The value of the converted model attribute for which the `view` property is defined.
  115. * If omitted, the configuration item will be used as a "default" configuration when no other item matches the attribute value.
  116. * @param {String|module:engine/view/elementdefinition~elementDefinition|Function|
  117. * module:engine/view/attributeelement~AttributeElement} config.view View element name, or a view element definition,
  118. * or a function that takes model element as a parameter and returns a view attribute element, or a view attribute element instance.
  119. * The view element will be used then in conversion.
  120. * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
  121. * @returns {Function} Conversion helper.
  122. */
  123. export function downcastAttributeToElement( modelAttributeKey, config, priority = 'normal' ) {
  124. config = cloneDeep( config );
  125. _normalizeToElementConfig( config, ViewAttributeElement );
  126. const elementCreator = _getCreatorForArrayConfig( config );
  127. return dispatcher => {
  128. dispatcher.on( 'attribute:' + modelAttributeKey, wrap( elementCreator ), { priority } );
  129. };
  130. }
  131. /**
  132. * Model attribute to view attribute conversion helper.
  133. *
  134. * This conversion results in adding an attribute on a view node, basing on an attribute from a model node. For example,
  135. * `<image src='foo.jpg'></image>` is converted to `<img src='foo.jpg'></img>`.
  136. *
  137. * downcastAttributeToAttribute( 'src' );
  138. *
  139. * downcastAttributeToAttribute( 'source', { view: 'src' } );
  140. *
  141. * downcastAttributeToAttribute( 'source', { view: 'src' }, 'high' );
  142. *
  143. * downcastAttributeToAttribute( 'stylish', {
  144. * view: {
  145. * key: 'class',
  146. * value: 'styled'
  147. * }
  148. * } );
  149. *
  150. * downcastAttributeToAttribute( 'styled', {
  151. * model: 'dark',
  152. * view: {
  153. * key: 'class',
  154. * value: 'styled styled-dark'
  155. * }
  156. * } );
  157. *
  158. * downcastAttributeToAttribute( 'style', [
  159. * {
  160. * model: 'dark',
  161. * view: {
  162. * key: 'class',
  163. * value: 'styled-dark'
  164. * }
  165. * },
  166. * {
  167. * model: 'light',
  168. * view: {
  169. * key: 'class',
  170. * value: 'styled-light'
  171. * }
  172. * }
  173. * ] );
  174. *
  175. * downcastAttributeToAttribute( 'style', {
  176. * view: attributeValue => ( { key: 'class', value: 'style-' + attributeValue } )
  177. * } );
  178. *
  179. * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  180. *
  181. * @param {String} modelAttributeKey The key of the attribute to convert.
  182. * @param {Object|Array.<Object>} [config] Conversion configuration. It is possible to provide multiple configurations in an array.
  183. * If not set, the conversion helper will assume 1-to-1 conversion, that is the view attribute key and view attribute value
  184. * will be same as model attribute key and model attribute value.
  185. * @param {*} [config.model] The value of the converted model attribute for which the `view` property is defined.
  186. * If `true` is provided, the configuration item will be used as a "default" configuration when no other item matches
  187. * the attribute value.
  188. * @param {String|Object|Function} [config.view] View attribute key, or an object with `key` and `value` properties (both `String`),
  189. * or a function that takes model attribute value and returns an object with `key` and `value` properties (both `String`).
  190. * If nothing is passed, the view attribute key and value will be equal to the model attribute key and value.
  191. * If a `String` is passed, it will be used as view attribute key and view attribute value will be equal to model attribute value.
  192. * If an object or a function returning an object is passed, its properties will be used to set view attribute key and value.
  193. * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
  194. * @returns {Function} Conversion helper.
  195. */
  196. export function downcastAttributeToAttribute( modelAttributeKey, config = {}, priority = 'normal' ) {
  197. config = cloneDeep( config );
  198. _normalizeToAttributeConfig( modelAttributeKey, config );
  199. const elementCreator = _getCreatorForArrayConfig( config );
  200. return dispatcher => {
  201. dispatcher.on( 'attribute:' + modelAttributeKey, changeAttribute( elementCreator ), { priority } );
  202. };
  203. }
  204. /**
  205. * Model marker to view element conversion helper.
  206. *
  207. * This conversion results in creating a view element on the boundaries of the converted marker. If converted marker
  208. * is collapsed, only one element is created. For example, model marker set like this `<paragraph>F[oo b]ar</paragraph>`
  209. * becomes `<p>F<span data-marker="search"></span>oo b<span data-marker="search"></span>ar</p>` in the view.
  210. *
  211. * downcastMarkerToElement( { model: 'search', view: 'marker-search' } );
  212. *
  213. * downcastMarkerToElement( { model: 'search', view: 'marker-search' }, 'high' );
  214. *
  215. * downcastMarkerToElement( { model: 'search', view: new ViewUIElement( 'span', { data-marker: 'search' } ) } );
  216. *
  217. * downcastMarkerToElement( {
  218. * model: 'search',
  219. * view: {
  220. * name: 'span',
  221. * attribute: {
  222. * 'data-marker': 'search'
  223. * }
  224. * }
  225. * } );
  226. *
  227. * downcastMarkerToElement( {
  228. * model: 'search',
  229. * view: data => {
  230. * return new ViewUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } );
  231. * }
  232. * } );
  233. *
  234. * If function is passed as `config.view` parameter, it will be used to generate both boundary elements. The function
  235. * receives `data` object as parameter and should return an instance of {@link module:engine/view/uielement~UIElement view.UIElement}.
  236. * The `data` object properties are passed from
  237. * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. Additionally,
  238. * `data.isOpening` parameter is passed, which is set to `true` for marker start boundary element, and `false` to
  239. * marker end boundary element.
  240. *
  241. * This kind of conversion is useful for saving data into data base, so it should be used in data conversion pipeline.
  242. *
  243. * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  244. *
  245. * @param {Object} config Conversion configuration.
  246. * @param {String} config.model Name of the model marker (or model marker group) to convert.
  247. * @param {module:engine/view/elementdefinition~elementDefinition|Function} config.view View element definition
  248. * which will be used to build a view element for conversion or a function that takes model marker data as a parameter and
  249. * returns view element to use in conversion.
  250. * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
  251. * @returns {Function} Conversion helper.
  252. */
  253. export function downcastMarkerToElement( config, priority = 'normal' ) {
  254. config = cloneDeep( config );
  255. _normalizeToElementConfig( config, ViewUIElement );
  256. return dispatcher => {
  257. dispatcher.on( 'addMarker:' + config.model, insertUIElement( config.view ), { priority } );
  258. dispatcher.on( 'removeMarker:' + config.model, removeUIElement( config.view ), { priority } );
  259. };
  260. }
  261. /**
  262. * Model marker to highlight conversion helper.
  263. *
  264. * This conversion results in creating a highlight on view nodes. For this kind of conversion,
  265. * {@link module:engine/conversion/downcast-converters~HighlightDescriptor} should be provided.
  266. *
  267. * For text nodes, a `span` {@link module:engine/view/attributeelement~AttributeElement} is created and it wraps all text nodes
  268. * in the converted marker range. For example, model marker set like this `<paragraph>F[oo b]ar</paragraph>` becomes
  269. * `<p>F<span class="comment">oo b</span>ar</p>` in the view.
  270. *
  271. * {@link module:engine/view/containerelement~ContainerElement} may provide custom way of handling highlight. Most often,
  272. * the element itself is given classes and attributes described in the highlight descriptor (instead of being wrapped in `span`).
  273. * For example, model marker set like this `[<image src="foo.jpg"></image>]` becomes `<img src="foo.jpg" class="comment"></img>`
  274. * in the view.
  275. *
  276. * For container elements, the conversion is two-step. While the converter processes highlight descriptor and passes it
  277. * to a container element, it is the container element instance itself which applies values from highlight descriptor.
  278. * So, in a sense, converter takes care of stating what should be applied on what, while element decides how to apply that.
  279. *
  280. * downcastMarkerToHighlight( { model: 'comment', view: { class: 'comment' } } );
  281. *
  282. * downcastMarkerToHighlight( { model: 'comment', view: { class: 'new-comment' } }, 'high' );
  283. *
  284. * downcastMarkerToHighlight( {
  285. * model: 'comment',
  286. * view: data => {
  287. * // Assuming that marker name is in a form of comment:commentType.
  288. * const commentType = data.markerName.split( ':' )[ 1 ];
  289. *
  290. * return {
  291. * class: [ 'comment', 'comment-' + commentType ]
  292. * };
  293. * }
  294. * } );
  295. *
  296. * If function is passed as `config.view` parameter, it will be used to generate highlight descriptor. The function
  297. * receives `data` object as parameter and should return an instance of {@link module:engine/view/uielement~UIElement view.UIElement}.
  298. * The `data` object properties are passed from
  299. * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}.
  300. *
  301. * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  302. *
  303. * @param {Object} config Conversion configuration.
  304. * @param {String} config.model Name of the model marker (or model marker group) to convert.
  305. * @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} config.view Highlight descriptor
  306. * which will be used for highlighting or a function that takes model marker data as a parameter and returns a highlight descriptor.
  307. * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
  308. * @returns {Function} Conversion helper.
  309. */
  310. export function downcastMarkerToHighlight( config, priority = 'normal' ) {
  311. return dispatcher => {
  312. dispatcher.on( 'addMarker:' + config.model, highlightText( config.view ), { priority } );
  313. dispatcher.on( 'addMarker:' + config.model, highlightElement( config.view ), { priority } );
  314. dispatcher.on( 'removeMarker:' + config.model, removeHighlight( config.view ), { priority } );
  315. };
  316. }
  317. // Takes config and adds default parameters if they don't exist and normalizes other parameters to be used in downcast converters
  318. // for generating a view element.
  319. //
  320. // @param {Object} config Object with conversion helper configuration.
  321. // @param {Function} ViewElementClass View element class to use when generating view element from config.
  322. function _normalizeToElementConfig( config, ViewElementClass ) {
  323. // If config is given as an array, normalize each entry separately.
  324. if ( Array.isArray( config ) ) {
  325. for ( const configEntry of config ) {
  326. _normalizeToElementConfig( configEntry, ViewElementClass );
  327. }
  328. return;
  329. }
  330. // Build `.view` property.
  331. // It is expected to be either creator function or view element instance.
  332. if ( typeof config.view == 'string' ) {
  333. // If `.view` is a string, create a proper view element instance out of given `ViewElementClass` and name given in `.view`.
  334. config.view = new ViewElementClass( config.view );
  335. } else if ( typeof config.view == 'object' && !( config.view instanceof ViewElementClass ) ) {
  336. // If `.view` is an object, use it to build view element instance.
  337. config.view = _createViewElementFromDefinition( config.view, ViewElementClass );
  338. }
  339. // `.view` can be also a function or already a view element instance.
  340. // These are already valid types which don't have to be normalized.
  341. }
  342. // Creates view element instance from provided viewElementDefinition and class.
  343. //
  344. // @param {module:engine/view/elementdefinition~elementDefinition} viewElementDefinition
  345. // @param {Function} ViewElementClass
  346. // @returns {module:engine/view/element~Element}
  347. function _createViewElementFromDefinition( viewElementDefinition, ViewElementClass ) {
  348. const element = new ViewElementClass( viewElementDefinition.name, Object.assign( {}, viewElementDefinition.attribute ) );
  349. if ( viewElementDefinition.style ) {
  350. element.setStyle( viewElementDefinition.style );
  351. }
  352. if ( viewElementDefinition.class ) {
  353. const classes = viewElementDefinition.class;
  354. if ( typeof classes == 'string' ) {
  355. element.addClass( classes );
  356. } else {
  357. element.addClass( ...classes );
  358. }
  359. }
  360. return element;
  361. }
  362. // Takes config and adds default parameters if they don't exist and normalizes other parameters to be used in downcast converters
  363. // for generating view attribute.
  364. //
  365. // @param {String} modelAttributeKey Model attribute key for which config is defined.
  366. // @param {Object} [config] Config with conversion helper configuration.
  367. function _normalizeToAttributeConfig( modelAttributeKey, config ) {
  368. // If config is given as an array, normalize each entry separately.
  369. if ( Array.isArray( config ) ) {
  370. for ( const configEntry of config ) {
  371. _normalizeToAttributeConfig( modelAttributeKey, configEntry );
  372. }
  373. return;
  374. }
  375. // Build `.view` property.
  376. // It is expected to be a creator function, that takes attribute value and model item and returns an object
  377. // with `key` property and `value` property which are view attribute key and view attribute value.
  378. if ( !config.view ) {
  379. // If `.view` is not set, take both attribute name and attribute value from model.
  380. const viewAttributeKey = modelAttributeKey;
  381. config.view = modelAttributeValue => ( { key: viewAttributeKey, value: modelAttributeValue } );
  382. } else if ( typeof config.view == 'string' ) {
  383. // If `.view` is set as a string, use it as a view attribute name. Value will be taken from model attribute value.
  384. const viewAttributeKey = config.view;
  385. config.view = modelAttributeValue => ( { key: viewAttributeKey, value: modelAttributeValue } );
  386. } else if ( typeof config.view == 'object' ) {
  387. // If `.view` is set as an object, use set key and value.
  388. const viewAttributeKey = config.view.key;
  389. const viewAttributeValue = config.view.value;
  390. config.view = () => ( { key: viewAttributeKey, value: viewAttributeValue } );
  391. }
  392. // `.view` can be also already a function.
  393. }
  394. // Takes config and creates a view element creator function that chooses an appropriate entry from the config depending on
  395. // the value of model attribute.
  396. //
  397. // Supports specifying config as a single object or an array of objects.
  398. // Supports `.view` defined as an object and as a function.
  399. //
  400. // @param {Object|Array.<Object>} config Config with conversion helper configuration.
  401. function _getCreatorForArrayConfig( config ) {
  402. if ( !Array.isArray( config ) ) {
  403. config = [ config ];
  404. }
  405. // Get "default config" entry. It is the entry with `.model` property set to `true`.
  406. // "Default" entry should be used if no other entry matched model attribute value.
  407. const defaultConfig = config.find( configEntry => configEntry.model === undefined );
  408. // Return a creator function.
  409. return modelAttributeValue => {
  410. // Set default config at first. It will be used if no other entry matches model attribute value.
  411. let matchedConfigEntry = defaultConfig;
  412. // Creator should check all entries from the config...
  413. for ( const configEntry of config ) {
  414. if ( configEntry.model === modelAttributeValue ) {
  415. // If `.model` specified in entry matches converted attribute's value, choose it.
  416. matchedConfigEntry = configEntry;
  417. break;
  418. }
  419. }
  420. // If there was default config or matched config...
  421. if ( matchedConfigEntry ) {
  422. // If the entry `.view` is a function, execute it and return the value...
  423. if ( typeof matchedConfigEntry.view == 'function' ) {
  424. return matchedConfigEntry.view( modelAttributeValue );
  425. }
  426. // Else, just return `.view`, it should be a view element instance after it got normalized earlier.
  427. return matchedConfigEntry.view;
  428. }
  429. return null;
  430. };
  431. }