downcast-converters.js 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import ModelRange from '../model/range';
  6. import ModelSelection from '../model/selection';
  7. import ModelElement from '../model/element';
  8. import ViewAttributeElement from '../view/attributeelement';
  9. import ViewRange from '../view/range';
  10. import DocumentSelection from '../model/documentselection';
  11. import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep';
  12. /**
  13. * Contains downcast (model to view) converters for {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}.
  14. *
  15. * @module engine/conversion/downcast-converters
  16. */
  17. /**
  18. * Model element to view element conversion helper.
  19. *
  20. * This conversion results in creating a view element. For example, model `<paragraph>Foo</paragraph>` becomes `<p>Foo</p>` in the view.
  21. *
  22. * downcastElementToElement( { model: 'paragraph', view: 'p' } );
  23. *
  24. * downcastElementToElement( { model: 'paragraph', view: 'div', priority: 'high' } );
  25. *
  26. * downcastElementToElement( {
  27. * model: 'fancyParagraph',
  28. * view: {
  29. * name: 'p',
  30. * classes: 'fancy'
  31. * }
  32. * } );
  33. *
  34. * downcastElementToElement( {
  35. * model: 'heading',
  36. * view: ( modelElement, viewWriter ) => viewWriter.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) )
  37. * } );
  38. *
  39. * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  40. *
  41. * @param {Object} config Conversion configuration.
  42. * @param {String} config.model Name of the model element to convert.
  43. * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view View element definition or a function
  44. * that takes model element and view writer as a parameters and returns a view container element.
  45. * @returns {Function} Conversion helper.
  46. */
  47. export function downcastElementToElement( config ) {
  48. config = cloneDeep( config );
  49. config.view = _normalizeToElementConfig( config.view, 'container' );
  50. return dispatcher => {
  51. dispatcher.on( 'insert:' + config.model, insertElement( config.view ), { priority: config.priority || 'normal' } );
  52. };
  53. }
  54. /**
  55. * Model attribute to view element conversion helper.
  56. *
  57. * This conversion results in wrapping view nodes in a view attribute element. For example, model text node with data
  58. * `"Foo"` and `bold` attribute becomes `<strong>Foo</strong>` in the view.
  59. *
  60. * downcastAttributeToElement( { model: 'bold', view: 'strong' } );
  61. *
  62. * downcastAttributeToElement( { model: 'bold', view: 'b', priority: 'high' } );
  63. *
  64. * downcastAttributeToElement( {
  65. * model: 'invert',
  66. * view: {
  67. * name: 'span',
  68. * classes: [ 'font-light', 'bg-dark' ]
  69. * }
  70. * } );
  71. *
  72. * downcastAttributeToElement( {
  73. * model: {
  74. * key: 'fontSize',
  75. * values: [ 'big', 'small' ]
  76. * },
  77. * view: {
  78. * big: {
  79. * name: 'span',
  80. * styles: {
  81. * 'font-size': '1.2em'
  82. * }
  83. * },
  84. * small: {
  85. * name: 'span',
  86. * styles: {
  87. * 'font-size': '0.8em'
  88. * }
  89. * }
  90. * }
  91. * } );
  92. *
  93. * downcastAttributeToElement( {
  94. * model: 'bold',
  95. * view: ( modelAttributeValue, viewWriter ) => {
  96. * return viewWriter.createAttributeElement( 'span', { style: 'font-weight:' + modelAttributeValue } );
  97. * }
  98. * } );
  99. *
  100. * downcastAttributeToElement( {
  101. * model: {
  102. * key: 'color',
  103. * name: '$text'
  104. * },
  105. * view: ( modelAttributeValue, viewWriter ) => {
  106. * return viewWriter.createAttributeElement( 'span', { style: 'color:' + modelAttributeValue } );
  107. * }
  108. * } );
  109. *
  110. * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  111. *
  112. * @param {Object} config Conversion configuration.
  113. * @param {String|Object} config.model Key of the attribute to convert from or a `{ key, values }` object. `values` is an array
  114. * of `String`s with possible values if the model attribute is enumerable.
  115. * @param {module:engine/view/elementdefinition~ElementDefinition|Function|Object} config.view View element definition or a function
  116. * that takes model attribute value and view writer as parameters and returns a view attribute element. If `config.model.values` is
  117. * given, `config.view` should be an object assigning values from `config.model.values` to view element definitions or functions.
  118. * @param {module:utils/priorities~PriorityString} [config.priority='normal'] Converter priority.
  119. * @returns {Function} Conversion helper.
  120. */
  121. export function downcastAttributeToElement( config ) {
  122. config = cloneDeep( config );
  123. const modelKey = config.model.key ? config.model.key : config.model;
  124. let eventName = 'attribute:' + modelKey;
  125. if ( config.model.name ) {
  126. eventName += ':' + config.model.name;
  127. }
  128. if ( config.model.values ) {
  129. for ( const modelValue of config.model.values ) {
  130. config.view[ modelValue ] = _normalizeToElementConfig( config.view[ modelValue ], 'attribute' );
  131. }
  132. } else {
  133. config.view = _normalizeToElementConfig( config.view, 'attribute' );
  134. }
  135. const elementCreator = _getFromAttributeCreator( config );
  136. return dispatcher => {
  137. dispatcher.on( eventName, wrap( elementCreator ), { priority: config.priority || 'normal' } );
  138. };
  139. }
  140. /**
  141. * Model attribute to view attribute conversion helper.
  142. *
  143. * This conversion results in adding an attribute on a view node, basing on an attribute from a model node. For example,
  144. * `<image src='foo.jpg'></image>` is converted to `<img src='foo.jpg'></img>`.
  145. *
  146. * downcastAttributeToAttribute( { model: 'source', view: 'src' } );
  147. *
  148. * downcastAttributeToAttribute( { model: 'source', view: 'href', priority: 'high' } );
  149. *
  150. * downcastAttributeToAttribute( {
  151. * model: {
  152. * name: 'image',
  153. * key: 'source'
  154. * },
  155. * view: 'src'
  156. * } );
  157. *
  158. * downcastAttributeToAttribute( {
  159. * model: {
  160. * name: 'styled',
  161. * values: [ 'dark', 'light' ]
  162. * },
  163. * view: {
  164. * dark: {
  165. * key: 'class',
  166. * value: [ 'styled', 'styled-dark' ]
  167. * },
  168. * light: {
  169. * key: 'class',
  170. * value: [ 'styled', 'styled-light' ]
  171. * }
  172. * }
  173. * } );
  174. *
  175. * downcastAttributeToAttribute( {
  176. * model: 'styled',
  177. * view: modelAttributeValue => ( { key: 'class', value: 'styled-' + modelAttributeValue } )
  178. * } );
  179. *
  180. * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  181. *
  182. * @param {Object} config Conversion configuration.
  183. * @param {String|Object} config.model Key of the attribute to convert from or a `{ key, values, [ name ] }` object describing
  184. * the attribute key, possible values and, optionally, an element name to convert from.
  185. * @param {String|Object|Function} config.view View attribute key, or a `{ key, value }` object or a function that takes
  186. * model attribute value and returns a `{ key, value }` object. If `key` is `'class'`, `value` can be a `String` or an
  187. * array of `String`s. If `key` is `'style'`, `value` is an object with key-value pairs. In other cases, `value` is a `String`.
  188. * If `config.model.values` is set, `config.view` should be an object assigning values from `config.model.values` to
  189. * `{ key, value }` objects or a functions.
  190. * @param {module:utils/priorities~PriorityString} [config.priority='normal'] Converter priority.
  191. * @returns {Function} Conversion helper.
  192. */
  193. export function downcastAttributeToAttribute( config ) {
  194. config = cloneDeep( config );
  195. const modelKey = config.model.key ? config.model.key : config.model;
  196. let eventName = 'attribute:' + modelKey;
  197. if ( config.model.name ) {
  198. eventName += ':' + config.model.name;
  199. }
  200. if ( config.model.values ) {
  201. for ( const modelValue of config.model.values ) {
  202. config.view[ modelValue ] = _normalizeToAttributeConfig( config.view[ modelValue ] );
  203. }
  204. } else {
  205. config.view = _normalizeToAttributeConfig( config.view );
  206. }
  207. const elementCreator = _getFromAttributeCreator( config );
  208. return dispatcher => {
  209. dispatcher.on( eventName, changeAttribute( elementCreator ), { priority: config.priority || 'normal' } );
  210. };
  211. }
  212. /**
  213. * Model marker to view element conversion helper.
  214. *
  215. * This conversion results in creating a view element on the boundaries of the converted marker. If converted marker
  216. * is collapsed, only one element is created. For example, model marker set like this `<paragraph>F[oo b]ar</paragraph>`
  217. * becomes `<p>F<span data-marker="search"></span>oo b<span data-marker="search"></span>ar</p>` in the view.
  218. *
  219. * downcastMarkerToElement( { model: 'search', view: 'marker-search' } );
  220. *
  221. * downcastMarkerToElement( { model: 'search', view: 'search-result', priority: 'high' } );
  222. *
  223. * downcastMarkerToElement( {
  224. * model: 'search',
  225. * view: {
  226. * name: 'span',
  227. * attributes: {
  228. * 'data-marker': 'search'
  229. * }
  230. * }
  231. * } );
  232. *
  233. * downcastMarkerToElement( {
  234. * model: 'search',
  235. * view: ( markerData, viewWriter ) => {
  236. * return viewWriter.createUIElement( 'span', { 'data-marker': 'search', 'data-start': markerData.isOpening } );
  237. * }
  238. * } );
  239. *
  240. * If function is passed as `config.view` parameter, it will be used to generate both boundary elements. The function
  241. * receives `data` object as parameter and should return an instance of {@link module:engine/view/uielement~UIElement view.UIElement}.
  242. * The `data` and `conversionApi` objects are passed from
  243. * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. Additionally,
  244. * `data.isOpening` parameter is passed, which is set to `true` for marker start boundary element, and `false` to
  245. * marker end boundary element.
  246. *
  247. * This kind of conversion is useful for saving data into data base, so it should be used in data conversion pipeline.
  248. *
  249. * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  250. *
  251. * @param {Object} config Conversion configuration.
  252. * @param {String} config.model Name of the model marker (or model marker group) to convert.
  253. * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view View element definition or a function
  254. * that takes model marker data as a parameter and returns view ui element.
  255. * @param {module:utils/priorities~PriorityString} [config.priority='normal'] Converter priority.
  256. * @returns {Function} Conversion helper.
  257. */
  258. export function downcastMarkerToElement( config ) {
  259. config = cloneDeep( config );
  260. config.view = _normalizeToElementConfig( config.view, 'ui' );
  261. return dispatcher => {
  262. dispatcher.on( 'addMarker:' + config.model, insertUIElement( config.view ), { priority: config.priority || 'normal' } );
  263. dispatcher.on( 'removeMarker:' + config.model, removeUIElement( config.view ), { priority: config.priority || 'normal' } );
  264. };
  265. }
  266. /**
  267. * Model marker to highlight conversion helper.
  268. *
  269. * This conversion results in creating a highlight on view nodes. For this kind of conversion,
  270. * {@link module:engine/conversion/downcast-converters~HighlightDescriptor} should be provided.
  271. *
  272. * For text nodes, a `span` {@link module:engine/view/attributeelement~AttributeElement} is created and it wraps all text nodes
  273. * in the converted marker range. For example, model marker set like this `<paragraph>F[oo b]ar</paragraph>` becomes
  274. * `<p>F<span class="comment">oo b</span>ar</p>` in the view.
  275. *
  276. * {@link module:engine/view/containerelement~ContainerElement} may provide custom way of handling highlight. Most often,
  277. * the element itself is given classes and attributes described in the highlight descriptor (instead of being wrapped in `span`).
  278. * For example, model marker set like this `[<image src="foo.jpg"></image>]` becomes `<img src="foo.jpg" class="comment"></img>`
  279. * in the view.
  280. *
  281. * For container elements, the conversion is two-step. While the converter processes highlight descriptor and passes it
  282. * to a container element, it is the container element instance itself which applies values from highlight descriptor.
  283. * So, in a sense, converter takes care of stating what should be applied on what, while element decides how to apply that.
  284. *
  285. * downcastMarkerToHighlight( { model: 'comment', view: { classes: 'comment' } } );
  286. *
  287. * downcastMarkerToHighlight( { model: 'comment', view: { classes: 'new-comment' }, priority: 'high' } );
  288. *
  289. * downcastMarkerToHighlight( {
  290. * model: 'comment',
  291. * view: data => {
  292. * // Assuming that marker name is in a form of comment:commentType.
  293. * const commentType = data.markerName.split( ':' )[ 1 ];
  294. *
  295. * return {
  296. * classes: [ 'comment', 'comment-' + commentType ]
  297. * };
  298. * }
  299. * } );
  300. *
  301. * If function is passed as `config.view` parameter, it will be used to generate highlight descriptor. The function
  302. * receives `data` object as parameter and should return a {@link module:engine/conversion/downcast-converters~HighlightDescriptor}.
  303. * The `data` object properties are passed from {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}.
  304. *
  305. * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  306. *
  307. * @param {Object} config Conversion configuration.
  308. * @param {String} config.model Name of the model marker (or model marker group) to convert.
  309. * @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} config.view Highlight descriptor
  310. * which will be used for highlighting or a function that takes model marker data as a parameter and returns a highlight descriptor.
  311. * @param {module:utils/priorities~PriorityString} [config.priority='normal'] Converter priority.
  312. * @returns {Function} Conversion helper.
  313. */
  314. export function downcastMarkerToHighlight( config ) {
  315. return dispatcher => {
  316. dispatcher.on( 'addMarker:' + config.model, highlightText( config.view ), { priority: config.priority || 'normal' } );
  317. dispatcher.on( 'addMarker:' + config.model, highlightElement( config.view ), { priority: config.priority || 'normal' } );
  318. dispatcher.on( 'removeMarker:' + config.model, removeHighlight( config.view ), { priority: config.priority || 'normal' } );
  319. };
  320. }
  321. // Takes `config.view`, and if it is a {@link module:engine/view/elementdefinition~ElementDefinition}, converts it
  322. // to a function (because lower level converters accepts only element creator functions).
  323. //
  324. // @param {module:engine/view/elementdefinition~ElementDefinition|Function} view View configuration.
  325. // @param {'container'|'attribute'|'ui'} viewElementType View element type to create.
  326. // @returns {Function} Element creator function to use in lower level converters.
  327. function _normalizeToElementConfig( view, viewElementType ) {
  328. if ( typeof view == 'function' ) {
  329. // If `view` is already a function, don't do anything.
  330. return view;
  331. }
  332. return ( modelData, viewWriter ) => _createViewElementFromDefinition( view, viewWriter, viewElementType );
  333. }
  334. // Creates view element instance from provided viewElementDefinition and class.
  335. //
  336. // @param {module:engine/view/elementdefinition~ElementDefinition} viewElementDefinition
  337. // @param {module:engine/view/writer~Writer} viewWriter
  338. // @param {'container'|'attribute'|'ui'} viewElementType
  339. // @returns {module:engine/view/element~Element}
  340. function _createViewElementFromDefinition( viewElementDefinition, viewWriter, viewElementType ) {
  341. if ( typeof viewElementDefinition == 'string' ) {
  342. // If `viewElementDefinition` is given as a `String`, normalize it to an object with `name` property.
  343. viewElementDefinition = { name: viewElementDefinition };
  344. }
  345. let element;
  346. if ( viewElementType == 'container' ) {
  347. element = viewWriter.createContainerElement( viewElementDefinition.name, Object.assign( {}, viewElementDefinition.attributes ) );
  348. } else if ( viewElementType == 'attribute' ) {
  349. element = viewWriter.createAttributeElement( viewElementDefinition.name, Object.assign( {}, viewElementDefinition.attributes ) );
  350. } else {
  351. // 'ui'.
  352. element = viewWriter.createUIElement( viewElementDefinition.name, Object.assign( {}, viewElementDefinition.attributes ) );
  353. }
  354. if ( viewElementDefinition.styles ) {
  355. const keys = Object.keys( viewElementDefinition.styles );
  356. for ( const key of keys ) {
  357. viewWriter.setStyle( key, viewElementDefinition.styles[ key ], element );
  358. }
  359. }
  360. if ( viewElementDefinition.classes ) {
  361. const classes = viewElementDefinition.classes;
  362. if ( typeof classes == 'string' ) {
  363. viewWriter.addClass( classes, element );
  364. } else {
  365. for ( const className of classes ) {
  366. viewWriter.addClass( className, element );
  367. }
  368. }
  369. }
  370. return element;
  371. }
  372. function _getFromAttributeCreator( config ) {
  373. if ( config.model.values ) {
  374. return ( modelAttributeValue, viewWriter ) => {
  375. const view = config.view[ modelAttributeValue ];
  376. if ( view ) {
  377. return view( modelAttributeValue, viewWriter );
  378. }
  379. return null;
  380. };
  381. } else {
  382. return config.view;
  383. }
  384. }
  385. // Takes config and adds default parameters if they don't exist and normalizes other parameters to be used in downcast converters
  386. // for generating view attribute.
  387. //
  388. // @param {Object} view View configuration.
  389. function _normalizeToAttributeConfig( view ) {
  390. if ( typeof view == 'string' ) {
  391. return modelAttributeValue => ( { key: view, value: modelAttributeValue } );
  392. } else if ( typeof view == 'object' ) {
  393. // { key, value, ... }
  394. if ( view.value ) {
  395. return () => view;
  396. }
  397. // { key, ... }
  398. else {
  399. return modelAttributeValue => ( { key: view.key, value: modelAttributeValue } );
  400. }
  401. } else {
  402. // function.
  403. return view;
  404. }
  405. }
  406. /**
  407. * Function factory, creates a converter that converts node insertion changes from the model to the view.
  408. * Passed function will be provided with all the parameters of the dispatcher's
  409. * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert insert event}.
  410. * It's expected that the function returns a {@link module:engine/view/element~Element}.
  411. * The result of the function will be inserted to the view.
  412. *
  413. * The converter automatically consumes corresponding value from consumables list, stops the event (see
  414. * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}) and bind model and view elements.
  415. *
  416. * downcastDispatcher.on(
  417. * 'insert:myElem',
  418. * insertElement( ( modelItem, viewWriter ) => {
  419. * const text = viewWriter.createText( 'myText' );
  420. * const myElem = viewWriter.createElement( 'myElem', { myAttr: 'my-' + modelItem.getAttribute( 'myAttr' ) }, text );
  421. *
  422. * // Do something fancy with myElem using `modelItem` or other parameters.
  423. *
  424. * return myElem;
  425. * }
  426. * ) );
  427. *
  428. * @param {Function} elementCreator Function returning a view element, which will be inserted.
  429. * @returns {Function} Insert element event converter.
  430. */
  431. export function insertElement( elementCreator ) {
  432. return ( evt, data, conversionApi ) => {
  433. const viewElement = elementCreator( data.item, conversionApi.writer );
  434. if ( !viewElement ) {
  435. return;
  436. }
  437. if ( !conversionApi.consumable.consume( data.item, 'insert' ) ) {
  438. return;
  439. }
  440. const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
  441. conversionApi.mapper.bindElements( data.item, viewElement );
  442. conversionApi.writer.insert( viewPosition, viewElement );
  443. };
  444. }
  445. /**
  446. * Function factory, creates a default downcast converter for text insertion changes.
  447. *
  448. * The converter automatically consumes corresponding value from consumables list and stops the event (see
  449. * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
  450. *
  451. * modelDispatcher.on( 'insert:$text', insertText() );
  452. *
  453. * @returns {Function} Insert text event converter.
  454. */
  455. export function insertText() {
  456. return ( evt, data, conversionApi ) => {
  457. if ( !conversionApi.consumable.consume( data.item, 'insert' ) ) {
  458. return;
  459. }
  460. const viewWriter = conversionApi.writer;
  461. const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
  462. const viewText = viewWriter.createText( data.item.data );
  463. viewWriter.insert( viewPosition, viewText );
  464. };
  465. }
  466. /**
  467. * Function factory, creates a default downcast converter for node remove changes.
  468. *
  469. * modelDispatcher.on( 'remove', remove() );
  470. *
  471. * @returns {Function} Remove event converter.
  472. */
  473. export function remove() {
  474. return ( evt, data, conversionApi ) => {
  475. // Find view range start position by mapping model position at which the remove happened.
  476. const viewStart = conversionApi.mapper.toViewPosition( data.position );
  477. const modelEnd = data.position.getShiftedBy( data.length );
  478. const viewEnd = conversionApi.mapper.toViewPosition( modelEnd, { isPhantom: true } );
  479. const viewRange = new ViewRange( viewStart, viewEnd );
  480. // Trim the range to remove in case some UI elements are on the view range boundaries.
  481. const removed = conversionApi.writer.remove( viewRange.getTrimmed() );
  482. // After the range is removed, unbind all view elements from the model.
  483. // Range inside view document fragment is used to unbind deeply.
  484. for ( const child of ViewRange.createIn( removed ).getItems() ) {
  485. conversionApi.mapper.unbindViewElement( child );
  486. }
  487. };
  488. }
  489. /**
  490. * Function factory, creates a converter that converts marker adding change to the view ui element.
  491. *
  492. * The view ui element, that will be added to the view, depends on passed parameter. See {@link ~insertElement}.
  493. * In a case of a non-collapsed range, the ui element will not wrap nodes but separate elements will be placed at the beginning
  494. * and at the end of the range.
  495. *
  496. * This converter binds created {@link module:engine/view/uielement~UIElement}s with marker name using
  497. * the {@link module:engine/conversion/mapper~Mapper#bindElementToMarker}.
  498. *
  499. * @param {module:engine/view/uielement~UIElement|Function} elementCreator View ui element or a function returning a view element
  500. * which will be inserted.
  501. * @returns {Function} Insert element event converter.
  502. */
  503. export function insertUIElement( elementCreator ) {
  504. return ( evt, data, conversionApi ) => {
  505. // Create two view elements. One will be inserted at the beginning of marker, one at the end.
  506. // If marker is collapsed, only "opening" element will be inserted.
  507. data.isOpening = true;
  508. const viewStartElement = elementCreator( data, conversionApi.writer );
  509. data.isOpening = false;
  510. const viewEndElement = elementCreator( data, conversionApi.writer );
  511. if ( !viewStartElement || !viewEndElement ) {
  512. return;
  513. }
  514. const markerRange = data.markerRange;
  515. // Marker that is collapsed has consumable build differently that non-collapsed one.
  516. // For more information see `addMarker` event description.
  517. // If marker's range is collapsed - check if it can be consumed.
  518. if ( markerRange.isCollapsed && !conversionApi.consumable.consume( markerRange, evt.name ) ) {
  519. return;
  520. }
  521. // If marker's range is not collapsed - consume all items inside.
  522. for ( const value of markerRange ) {
  523. if ( !conversionApi.consumable.consume( value.item, evt.name ) ) {
  524. return;
  525. }
  526. }
  527. const mapper = conversionApi.mapper;
  528. const viewWriter = conversionApi.writer;
  529. // Add "opening" element.
  530. viewWriter.insert( mapper.toViewPosition( markerRange.start ), viewStartElement );
  531. conversionApi.mapper.bindElementToMarker( viewStartElement, data.markerName );
  532. // Add "closing" element only if range is not collapsed.
  533. if ( !markerRange.isCollapsed ) {
  534. viewWriter.insert( mapper.toViewPosition( markerRange.end ), viewEndElement );
  535. conversionApi.mapper.bindElementToMarker( viewEndElement, data.markerName );
  536. }
  537. evt.stop();
  538. };
  539. }
  540. /**
  541. * Function factory, returns a default downcast converter for removing {@link module:engine/view/uielement~UIElement ui element}
  542. * basing on marker remove change.
  543. *
  544. * This converter unbinds elements from marker name.
  545. *
  546. * @returns {Function} Remove ui element converter.
  547. */
  548. export function removeUIElement() {
  549. return ( evt, data, conversionApi ) => {
  550. const elements = conversionApi.mapper.markerNameToElements( data.markerName );
  551. if ( !elements ) {
  552. return;
  553. }
  554. conversionApi.mapper.unbindElementsFromMarkerName( data.markerName );
  555. const viewWriter = conversionApi.writer;
  556. for ( const element of elements ) {
  557. viewWriter.clear( ViewRange.createOn( element ), element );
  558. }
  559. evt.stop();
  560. };
  561. }
  562. /**
  563. * Function factory, creates a converter that converts set/change/remove attribute changes from the model to the view.
  564. *
  565. * Attributes from model are converted to the view element attributes in the view. You may provide a custom function to generate
  566. * a key-value attribute pair to add/change/remove. If not provided, model attributes will be converted to view elements
  567. * attributes on 1-to-1 basis.
  568. *
  569. * **Note:** Provided attribute creator should always return the same `key` for given attribute from the model.
  570. *
  571. * The converter automatically consumes corresponding value from consumables list and stops the event (see
  572. * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
  573. *
  574. * modelDispatcher.on( 'attribute:customAttr:myElem', changeAttribute( ( value, data ) => {
  575. * // Change attribute key from `customAttr` to `class` in view.
  576. * const key = 'class';
  577. * let value = data.attributeNewValue;
  578. *
  579. * // Force attribute value to 'empty' if the model element is empty.
  580. * if ( data.item.childCount === 0 ) {
  581. * value = 'empty';
  582. * }
  583. *
  584. * // Return key-value pair.
  585. * return { key, value };
  586. * } ) );
  587. *
  588. * @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which
  589. * represents attribute key and attribute value to be set on a {@link module:engine/view/element~Element view element}.
  590. * The function is passed model attribute value as first parameter and additional data about the change as a second parameter.
  591. * @returns {Function} Set/change attribute converter.
  592. */
  593. export function changeAttribute( attributeCreator ) {
  594. attributeCreator = attributeCreator || ( ( value, data ) => ( { value, key: data.attributeKey } ) );
  595. return ( evt, data, conversionApi ) => {
  596. const oldAttribute = attributeCreator( data.attributeOldValue, data );
  597. const newAttribute = attributeCreator( data.attributeNewValue, data );
  598. if ( !oldAttribute && !newAttribute ) {
  599. return;
  600. }
  601. if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
  602. return;
  603. }
  604. const viewElement = conversionApi.mapper.toViewElement( data.item );
  605. const viewWriter = conversionApi.writer;
  606. // First remove the old attribute if there was one.
  607. if ( data.attributeOldValue !== null && oldAttribute ) {
  608. if ( oldAttribute.key == 'class' ) {
  609. const classes = Array.isArray( oldAttribute.value ) ? oldAttribute.value : [ oldAttribute.value ];
  610. for ( const className of classes ) {
  611. viewWriter.removeClass( className, viewElement );
  612. }
  613. } else if ( oldAttribute.key == 'style' ) {
  614. const keys = Object.keys( oldAttribute.value );
  615. for ( const key of keys ) {
  616. viewWriter.removeStyle( key, viewElement );
  617. }
  618. } else {
  619. viewWriter.removeAttribute( oldAttribute.key, viewElement );
  620. }
  621. }
  622. // Then set the new attribute.
  623. if ( data.attributeNewValue !== null && newAttribute ) {
  624. if ( newAttribute.key == 'class' ) {
  625. const classes = Array.isArray( newAttribute.value ) ? newAttribute.value : [ newAttribute.value ];
  626. for ( const className of classes ) {
  627. viewWriter.addClass( className, viewElement );
  628. }
  629. } else if ( newAttribute.key == 'style' ) {
  630. const keys = Object.keys( newAttribute.value );
  631. for ( const key of keys ) {
  632. viewWriter.setStyle( key, newAttribute.value[ key ], viewElement );
  633. }
  634. } else {
  635. viewWriter.setAttribute( newAttribute.key, newAttribute.value, viewElement );
  636. }
  637. }
  638. };
  639. }
  640. /**
  641. * Function factory, creates a converter that converts set/change/remove attribute changes from the model to the view.
  642. * Also can be used to convert selection attributes. In that case, an empty attribute element will be created and the
  643. * selection will be put inside it.
  644. *
  645. * Attributes from model are converted to a view element that will be wrapping those view nodes that are bound to
  646. * model elements having given attribute. This is useful for attributes like `bold`, which may be set on text nodes in model
  647. * but are represented as an element in the view:
  648. *
  649. * [paragraph] MODEL ====> VIEW <p>
  650. * |- a {bold: true} |- <b>
  651. * |- b {bold: true} | |- ab
  652. * |- c |- c
  653. *
  654. * Passed `Function` will be provided with attribute value and then all the parameters of the
  655. * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute attribute event}.
  656. * It's expected that the function returns a {@link module:engine/view/element~Element}.
  657. * The result of the function will be the wrapping element.
  658. * When provided `Function` does not return element, then will be no conversion.
  659. *
  660. * The converter automatically consumes corresponding value from consumables list, stops the event (see
  661. * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
  662. *
  663. * modelDispatcher.on( 'attribute:bold', wrapItem( ( modelAttributeValue, viewWriter ) => {
  664. * return viewWriter.createAttributeElement( 'strong' );
  665. * } );
  666. *
  667. * @param {Function} elementCreator Function returning a view element, which will be used for wrapping.
  668. * @returns {Function} Set/change attribute converter.
  669. */
  670. export function wrap( elementCreator ) {
  671. return ( evt, data, conversionApi ) => {
  672. // Recreate current wrapping node. It will be used to unwrap view range if the attribute value has changed
  673. // or the attribute was removed.
  674. const oldViewElement = elementCreator( data.attributeOldValue, conversionApi.writer );
  675. // Create node to wrap with.
  676. const newViewElement = elementCreator( data.attributeNewValue, conversionApi.writer );
  677. if ( !oldViewElement && !newViewElement ) {
  678. return;
  679. }
  680. if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
  681. return;
  682. }
  683. const viewWriter = conversionApi.writer;
  684. const viewSelection = viewWriter.document.selection;
  685. if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) {
  686. // Selection attribute conversion.
  687. viewWriter.wrap( viewSelection.getFirstRange(), newViewElement );
  688. } else {
  689. // Node attribute conversion.
  690. let viewRange = conversionApi.mapper.toViewRange( data.range );
  691. // First, unwrap the range from current wrapper.
  692. if ( data.attributeOldValue !== null && oldViewElement ) {
  693. viewRange = viewWriter.unwrap( viewRange, oldViewElement );
  694. }
  695. if ( data.attributeNewValue !== null && newViewElement ) {
  696. viewWriter.wrap( viewRange, newViewElement );
  697. }
  698. }
  699. };
  700. }
  701. /**
  702. * Function factory, creates converter that converts text inside marker's range. Converter wraps the text with
  703. * {@link module:engine/view/attributeelement~AttributeElement} created from provided descriptor.
  704. * See {link module:engine/conversion/downcast-converters~createViewElementFromHighlightDescriptor}.
  705. *
  706. * Also can be used to convert selection that is inside a marker. In that case, an empty attribute element will be
  707. * created and the selection will be put inside it.
  708. *
  709. * If the highlight descriptor will not provide `priority` property, `10` will be used.
  710. *
  711. * If the highlight descriptor will not provide `id` property, name of the marker will be used.
  712. *
  713. * This converter binds created {@link module:engine/view/attributeelement~AttributeElement}s with marker name using
  714. * the {@link module:engine/conversion/mapper~Mapper#bindElementToMarker}.
  715. *
  716. * @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} highlightDescriptor
  717. * @return {Function}
  718. */
  719. export function highlightText( highlightDescriptor ) {
  720. return ( evt, data, conversionApi ) => {
  721. if ( data.markerRange.isCollapsed ) {
  722. return;
  723. }
  724. if ( !( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) && !data.item.is( 'textProxy' ) ) {
  725. return;
  726. }
  727. const descriptor = _prepareDescriptor( highlightDescriptor, data, conversionApi );
  728. if ( !descriptor ) {
  729. return;
  730. }
  731. if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
  732. return;
  733. }
  734. const viewElement = createViewElementFromHighlightDescriptor( descriptor );
  735. const viewWriter = conversionApi.writer;
  736. const viewSelection = viewWriter.document.selection;
  737. if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) {
  738. viewWriter.wrap( viewSelection.getFirstRange(), viewElement, viewSelection );
  739. } else {
  740. const viewRange = conversionApi.mapper.toViewRange( data.range );
  741. const rangeAfterWrap = viewWriter.wrap( viewRange, viewElement );
  742. for ( const element of rangeAfterWrap.getItems() ) {
  743. if ( element.is( 'attributeElement' ) && element.isSimilar( viewElement ) ) {
  744. conversionApi.mapper.bindElementToMarker( element, data.markerName );
  745. // One attribute element is enough, because all of them are bound together by the view writer.
  746. // Mapper uses this binding to get all the elements no matter how many of them are registered in the mapper.
  747. break;
  748. }
  749. }
  750. }
  751. };
  752. }
  753. /**
  754. * Converter function factory. Creates a function which applies the marker's highlight to an element inside the marker's range.
  755. *
  756. * The converter checks if an element has `addHighlight` function stored as
  757. * {@link module:engine/view/element~Element#_setCustomProperty custom property} and, if so, uses it to apply the highlight.
  758. * In such case converter will consume all element's children, assuming that they were handled by element itself.
  759. *
  760. * When `addHighlight` custom property is not present, element is not converted in any special way.
  761. * This means that converters will proceed to convert element's child nodes.
  762. *
  763. * If the highlight descriptor will not provide `priority` property, `10` will be used.
  764. *
  765. * If the highlight descriptor will not provide `id` property, name of the marker will be used.
  766. *
  767. * This converter binds altered {@link module:engine/view/containerelement~ContainerElement}s with marker name using
  768. * the {@link module:engine/conversion/mapper~Mapper#bindElementToMarker}.
  769. *
  770. * @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} highlightDescriptor
  771. * @return {Function}
  772. */
  773. export function highlightElement( highlightDescriptor ) {
  774. return ( evt, data, conversionApi ) => {
  775. if ( data.markerRange.isCollapsed ) {
  776. return;
  777. }
  778. if ( !( data.item instanceof ModelElement ) ) {
  779. return;
  780. }
  781. const descriptor = _prepareDescriptor( highlightDescriptor, data, conversionApi );
  782. if ( !descriptor ) {
  783. return;
  784. }
  785. if ( !conversionApi.consumable.test( data.item, evt.name ) ) {
  786. return;
  787. }
  788. const viewElement = conversionApi.mapper.toViewElement( data.item );
  789. if ( viewElement && viewElement.getCustomProperty( 'addHighlight' ) ) {
  790. // Consume element itself.
  791. conversionApi.consumable.consume( data.item, evt.name );
  792. // Consume all children nodes.
  793. for ( const value of ModelRange.createIn( data.item ) ) {
  794. conversionApi.consumable.consume( value.item, evt.name );
  795. }
  796. viewElement.getCustomProperty( 'addHighlight' )( viewElement, descriptor, conversionApi.writer );
  797. conversionApi.mapper.bindElementToMarker( viewElement, data.markerName );
  798. }
  799. };
  800. }
  801. /**
  802. * Function factory, creates a converter that converts removing model marker to the view.
  803. *
  804. * Both text nodes and elements are handled by this converter but they are handled a bit differently.
  805. *
  806. * Text nodes are unwrapped using {@link module:engine/view/attributeelement~AttributeElement} created from provided
  807. * highlight descriptor. See {link module:engine/conversion/downcast-converters~highlightDescriptorToAttributeElement}.
  808. *
  809. * For elements, the converter checks if an element has `removeHighlight` function stored as
  810. * {@link module:engine/view/element~Element#_setCustomProperty custom property}. If so, it uses it to remove the highlight.
  811. * In such case, children of that element will not be converted.
  812. *
  813. * When `removeHighlight` is not present, element is not converted in any special way.
  814. * Instead converter will proceed to convert element's child nodes.
  815. *
  816. * If the highlight descriptor will not provide `priority` property, `10` will be used.
  817. *
  818. * If the highlight descriptor will not provide `id` property, name of the marker will be used.
  819. *
  820. * This converter unbinds elements from marker name.
  821. *
  822. * @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} highlightDescriptor
  823. * @return {Function}
  824. */
  825. export function removeHighlight( highlightDescriptor ) {
  826. return ( evt, data, conversionApi ) => {
  827. // This conversion makes sense only for non-collapsed range.
  828. if ( data.markerRange.isCollapsed ) {
  829. return;
  830. }
  831. const descriptor = _prepareDescriptor( highlightDescriptor, data, conversionApi );
  832. if ( !descriptor ) {
  833. return;
  834. }
  835. // View element that will be used to unwrap `AttributeElement`s.
  836. const viewHighlightElement = createViewElementFromHighlightDescriptor( descriptor );
  837. // Get all elements bound with given marker name.
  838. const elements = conversionApi.mapper.markerNameToElements( data.markerName );
  839. if ( !elements ) {
  840. return;
  841. }
  842. conversionApi.mapper.unbindElementsFromMarkerName( data.markerName );
  843. for ( const element of elements ) {
  844. if ( element.is( 'attributeElement' ) ) {
  845. conversionApi.writer.unwrap( ViewRange.createOn( element ), viewHighlightElement );
  846. } else {
  847. // if element.is( 'containerElement' ).
  848. element.getCustomProperty( 'removeHighlight' )( element, descriptor.id, conversionApi.writer );
  849. }
  850. }
  851. evt.stop();
  852. };
  853. }
  854. // Helper function for `highlight`. Prepares the actual descriptor object using value passed to the converter.
  855. function _prepareDescriptor( highlightDescriptor, data, conversionApi ) {
  856. // If passed descriptor is a creator function, call it. If not, just use passed value.
  857. const descriptor = typeof highlightDescriptor == 'function' ?
  858. highlightDescriptor( data, conversionApi ) :
  859. highlightDescriptor;
  860. if ( !descriptor ) {
  861. return null;
  862. }
  863. // Apply default descriptor priority.
  864. if ( !descriptor.priority ) {
  865. descriptor.priority = 10;
  866. }
  867. // Default descriptor id is marker name.
  868. if ( !descriptor.id ) {
  869. descriptor.id = data.markerName;
  870. }
  871. return descriptor;
  872. }
  873. /**
  874. * Creates `span` {@link module:engine/view/attributeelement~AttributeElement view attribute element} from information
  875. * provided by {@link module:engine/conversion/downcast-converters~HighlightDescriptor} object. If priority
  876. * is not provided in descriptor - default priority will be used.
  877. *
  878. * @param {module:engine/conversion/downcast-converters~HighlightDescriptor} descriptor
  879. * @returns {module:engine/view/attributeelement~AttributeElement}
  880. */
  881. export function createViewElementFromHighlightDescriptor( descriptor ) {
  882. const viewElement = new ViewAttributeElement( 'span', descriptor.attributes );
  883. if ( descriptor.classes ) {
  884. viewElement._addClass( descriptor.classes );
  885. }
  886. if ( descriptor.priority ) {
  887. viewElement._priority = descriptor.priority;
  888. }
  889. viewElement._id = descriptor.id;
  890. return viewElement;
  891. }
  892. /**
  893. * Object describing how the marker highlight should be represented in the view.
  894. *
  895. * Each text node contained in a highlighted range will be wrapped in a `span` {@link module:engine/view/attributeelement~AttributeElement}
  896. * with CSS class(es), attributes and priority described by this object.
  897. *
  898. * Additionally, each {@link module:engine/view/containerelement~ContainerElement} can handle displaying the highlight separately
  899. * by providing `addHighlight` and `removeHighlight` custom properties. In this case:
  900. *
  901. * * `HighlightDescriptor` object is passed to the `addHighlight` function upon conversion and should be used to apply the highlight to
  902. * the element,
  903. * * descriptor `id` is passed to the `removeHighlight` function upon conversion and should be used to remove the highlight of given
  904. * id from the element.
  905. *
  906. * @typedef {Object} module:engine/conversion/downcast-converters~HighlightDescriptor
  907. *
  908. * @property {String|Array.<String>} class CSS class or array of classes to set. If descriptor is used to
  909. * create {@link module:engine/view/attributeelement~AttributeElement} over text nodes, those classes will be set
  910. * on that {@link module:engine/view/attributeelement~AttributeElement}. If descriptor is applied to an element,
  911. * usually those class will be set on that element, however this depends on how the element converts the descriptor.
  912. *
  913. * @property {String} [id] Descriptor identifier. If not provided, defaults to converted marker's name.
  914. *
  915. * @property {Number} [priority] Descriptor priority. If not provided, defaults to `10`. If descriptor is used to create
  916. * {@link module:engine/view/attributeelement~AttributeElement}, it will be that element's
  917. * {@link module:engine/view/attributeelement~AttributeElement#priority}. If descriptor is applied to an element,
  918. * the priority will be used to determine which descriptor is more important.
  919. *
  920. * @property {Object} [attributes] Attributes to set. If descriptor is used to create
  921. * {@link module:engine/view/attributeelement~AttributeElement} over text nodes, those attributes will be set on that
  922. * {@link module:engine/view/attributeelement~AttributeElement}. If descriptor is applied to an element, usually those
  923. * attributes will be set on that element, however this depends on how the element converts the descriptor.
  924. */