model-to-view-converters.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. 'use strict';
  6. import ModelTreeWalker from '../model/treewalker.js';
  7. import ModelRange from '../model/range.js';
  8. import ViewElement from '../view/element.js';
  9. import ViewText from '../view/text.js';
  10. import viewWriter from '../view/writer.js';
  11. /**
  12. * Contains {@link engine.model model} to {@link engine.view view} converters for
  13. * {@link engine.conversion.ModelConversionDispatcher}.
  14. *
  15. * @namespace engine.conversion.modelToView
  16. */
  17. /**
  18. * Function factory, creates a converter that converts node insertion changes from the model to the view.
  19. * The view element that will be added to the view depends on passed parameter. If {@link engine.view.Element} was passed,
  20. * it will be cloned and the copy will be inserted. If `Function` is provided, it is passed all the parameters of the
  21. * dispatcher's {@link engine.conversion.ModelConversionDispatcher#event:insert insert event}. It's expected that the
  22. * function returns a {@link engine.view.Element}. The result of the function will be inserted to the view.
  23. *
  24. * The converter automatically consumes corresponding value from consumables list, stops the event (see
  25. * {@link engine.conversion.ModelConversionDispatcher}) and bind model and view elements.
  26. *
  27. * modelDispatcher.on( 'insert:paragraph', insertElement( new ViewElement( 'p' ) ) );
  28. *
  29. * modelDispatcher.on(
  30. * 'insert:myElem',
  31. * insertElement( ( data, consumable, conversionApi ) => {
  32. * let myElem = new ViewElement( 'myElem', { myAttr: true }, new ViewText( 'myText' ) );
  33. *
  34. * // Do something fancy with myElem using data/consumable/conversionApi ...
  35. *
  36. * return myElem;
  37. * }
  38. * ) );
  39. *
  40. * @external engine.conversion.modelToView
  41. * @function engine.conversion.modelToView.insertElement
  42. * @param {engine.view.Element|Function} elementCreator View element, or function returning a view element, which
  43. * will be inserted.
  44. * @returns {Function} Insert element event converter.
  45. */
  46. export function insertElement( elementCreator ) {
  47. return ( evt, data, consumable, conversionApi ) => {
  48. consumable.consume( data.item, 'insert' );
  49. const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
  50. const viewElement = ( elementCreator instanceof ViewElement ) ?
  51. elementCreator.clone( true ) :
  52. elementCreator( data, consumable, conversionApi );
  53. conversionApi.mapper.bindElements( data.item, viewElement );
  54. viewWriter.insert( viewPosition, viewElement );
  55. evt.stop();
  56. };
  57. }
  58. /**
  59. * Function factory, creates a default model-to-view converter for text insertion changes.
  60. *
  61. * The converter automatically consumes corresponding value from consumables list and stops the event (see
  62. * {@link engine.conversion.ModelConversionDispatcher}).
  63. *
  64. * modelDispatcher.on( 'insert:$text', insertText() );
  65. *
  66. * @external engine.conversion.modelToView
  67. * @function engine.conversion.modelToView.insertText
  68. * @returns {Function} Insert text event converter.
  69. */
  70. export function insertText() {
  71. return ( evt, data, consumable, conversionApi ) => {
  72. consumable.consume( data.item, 'insert' );
  73. const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
  74. const viewText = new ViewText( data.item.text );
  75. viewWriter.insert( viewPosition, viewText );
  76. evt.stop();
  77. };
  78. }
  79. /**
  80. * Function factory, creates a converter that converts set/change attribute changes from the model to the view. Attributes
  81. * from model are converted to the view element attributes in the view. You may provide a custom function to generate a
  82. * key-value attribute pair to add/change. If not provided, model attributes will be converted to view elements attributes
  83. * on 1-to-1 basis.
  84. *
  85. * **Note:** Provided attribute creator should always return the same `key` for given attribute from the model.
  86. *
  87. * The converter automatically consumes corresponding value from consumables list and stops the event (see
  88. * {@link engine.conversion.ModelConversionDispatcher}).
  89. *
  90. * modelDispatcher.on( 'addAttribute:customAttr:myElem', setAttribute( ( data ) => {
  91. * // Change attribute key from `customAttr` to `class` in view.
  92. * const key = 'class';
  93. * let value = data.attributeNewValue;
  94. *
  95. * // Force attribute value to 'empty' if the model element is empty.
  96. * if ( data.item.getChildCount() === 0 ) {
  97. * value = 'empty';
  98. * }
  99. *
  100. * // Return key-value pair.
  101. * return { key, value };
  102. * } ) );
  103. *
  104. * @external engine.conversion.modelToView
  105. * @function engine.conversion.modelToView.setAttribute
  106. * @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which
  107. * represents attribute key and attribute value to be set on a {@link engine.view.Element view element}. The function
  108. * is passed all the parameters of the {@link engine.conversion.ModelConversionDispatcher.addAttribute}
  109. * or {@link engine.conversion.ModelConversionDispatcher.changeAttribute} event.
  110. * @returns {Function} Set/change attribute converter.
  111. */
  112. export function setAttribute( attributeCreator ) {
  113. attributeCreator = attributeCreator || ( ( value, key ) => ( { value, key } ) );
  114. return ( evt, data, consumable, conversionApi ) => {
  115. const { key, value } = attributeCreator( data.attributeNewValue, data.attributeKey, data, consumable, conversionApi );
  116. consumable.consume( data.item, eventNameToConsumableType( evt.name ) );
  117. conversionApi.mapper.toViewElement( data.item ).setAttribute( key, value );
  118. evt.stop();
  119. };
  120. }
  121. /**
  122. * Function factory, creates a converter that converts remove attribute changes from the model to the view. Removes attributes
  123. * that were converted to the view element attributes in the view. You may provide a custom function to generate a
  124. * key-value attribute pair to remove. If not provided, model attributes will be removed from view elements on 1-to-1 basis.
  125. *
  126. * **Note:** Provided attribute creator should always return the same `key` for given attribute from the model.
  127. *
  128. * **Note:** You can use the same attribute creator as in {@link engine.conversion.modelToView.setAttribute}.
  129. *
  130. * The converter automatically consumes corresponding value from consumables list and stops the event (see
  131. * {@link engine.conversion.ModelConversionDispatcher}).
  132. *
  133. * modelDispatcher.on( 'removeAttribute:customAttr:myElem', removeAttribute( ( data ) => {
  134. * // Change attribute key from `customAttr` to `class` in view.
  135. * const key = 'class';
  136. * let value = data.attributeNewValue;
  137. *
  138. * // Force attribute value to 'empty' if the model element is empty.
  139. * if ( data.item.getChildCount() === 0 ) {
  140. * value = 'empty';
  141. * }
  142. *
  143. * // Return key-value pair.
  144. * return { key, value };
  145. * } ) );
  146. *
  147. * @external engine.conversion.modelToView
  148. * @function engine.conversion.modelToView.removeAttribute
  149. * @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which
  150. * represents attribute key and attribute value to be removed from {@link engine.view.Element view element}. The function
  151. * is passed all the parameters of the {@link engine.conversion.ModelConversionDispatcher#event:addAttribute addAttribute event}
  152. * or {@link engine.conversion.ModelConversionDispatcher#event:changeAttribute changeAttribute event}.
  153. * @returns {Function} Remove attribute converter.
  154. */
  155. export function removeAttribute( attributeCreator ) {
  156. attributeCreator = attributeCreator || ( ( value, key ) => ( { key } ) );
  157. return ( evt, data, consumable, conversionApi ) => {
  158. const { key } = attributeCreator( data.attributeOldValue, data.attributeKey, data, consumable, conversionApi );
  159. consumable.consume( data.item, eventNameToConsumableType( evt.name ) );
  160. conversionApi.mapper.toViewElement( data.item ).removeAttribute( key );
  161. evt.stop();
  162. };
  163. }
  164. /**
  165. * Function factory, creates a converter that converts set/change attribute changes from the model to the view. In this case,
  166. * model attributes are converted to a view element that will be wrapping view nodes which corresponding model nodes had
  167. * the attribute set. This is useful for attributes like `bold`, which may be set on a text nodes in model but are
  168. * represented as an element in the view:
  169. *
  170. * [paragraph] MODEL ====> VIEW <p>
  171. * |- a {bold: true} |- <b>
  172. * |- b {bold: true} | |- ab
  173. * |- c |- c
  174. *
  175. * The wrapping node depends on passed parameter. If {@link engine.view.Element} was passed, it will be cloned and
  176. * the copy will become the wrapping element. If `Function` is provided, it is passed all the parameters of the
  177. * {@link engine.conversion.ModelConversionDispatcher#event:setAttribute setAttribute event}. It's expected that the
  178. * function returns a {@link engine.view.Element}. The result of the function will be the wrapping element.
  179. *
  180. * The converter automatically consumes corresponding value from consumables list, stops the event (see
  181. * {@link engine.conversion.ModelConversionDispatcher}).
  182. *
  183. * modelDispatcher.on( 'addAttribute:bold', wrap( new ViewElement( 'strong' ) ) );
  184. *
  185. * @external engine.conversion.modelToView
  186. * @function engine.conversion.modelToView.wrap
  187. * @param {engine.view.Element|Function} elementCreator View element, or function returning a view element, which will
  188. * be used for wrapping.
  189. * @returns {Function} Set/change attribute converter.
  190. */
  191. export function wrap( elementCreator ) {
  192. return ( evt, data, consumable, conversionApi ) => {
  193. consumable.consume( data.item, eventNameToConsumableType( evt.name ) );
  194. let viewRange = conversionApi.mapper.toViewRange( data.range );
  195. const viewElement = ( elementCreator instanceof ViewElement ) ?
  196. elementCreator.clone( true ) :
  197. elementCreator( data.attributeNewValue, data, consumable, conversionApi );
  198. // If this is a change event (because old value is not empty) and the creator is a function (so
  199. // it may create different view elements basing on attribute value) we have to create
  200. // view element basing on old value and unwrap it before wrapping with a newly created view element.
  201. if ( data.attributeOldValue !== null && !( elementCreator instanceof ViewElement ) ) {
  202. const oldViewElement = elementCreator( data.attributeOldValue, data, consumable, conversionApi );
  203. viewRange = viewWriter.unwrap( viewRange, oldViewElement, evt.priority );
  204. }
  205. viewWriter.wrap( viewRange, viewElement, evt.priority );
  206. evt.stop();
  207. };
  208. }
  209. /**
  210. * Function factory, creates a converter that converts remove attribute changes from the model to the view. It assumes, that
  211. * attributes from model were converted to elements in the view. This converter will unwrap view nodes from corresponding
  212. * view element if given attribute was removed.
  213. *
  214. * The view element type that will be unwrapped depends on passed parameter.
  215. * If {@link engine.view.Element} was passed, it will be used to look for similar element in the view for unwrapping. If `Function`
  216. * is provided, it is passed all the parameters of the
  217. * {@link engine.conversion.ModelConversionDispatcher#event:setAttribute setAttribute event}. It's expected that the
  218. * function returns a {@link engine.view.Element}. The result of the function will be used to look for similar element
  219. * in the view for unwrapping.
  220. *
  221. * The converter automatically consumes corresponding value from consumables list, stops the event (see
  222. * {@link engine.conversion.ModelConversionDispatcher}) and bind model and view elements.
  223. *
  224. * modelDispatcher.on( 'removeAttribute:bold', unwrap( new ViewElement( 'strong' ) ) );
  225. *
  226. * @see engine.conversion.modelToView.wrap
  227. * @external engine.conversion.modelToView
  228. * @function engine.conversion.modelToView.unwrap
  229. * @param {engine.view.Element|Function} elementCreator View element, or function returning a view element, which will
  230. * be used for unwrapping.
  231. * @returns {Function} Remove attribute converter.
  232. */
  233. export function unwrap( elementCreator ) {
  234. return ( evt, data, consumable, conversionApi ) => {
  235. consumable.consume( data.item, eventNameToConsumableType( evt.name ) );
  236. const viewRange = conversionApi.mapper.toViewRange( data.range );
  237. const viewNode = ( elementCreator instanceof ViewElement ) ?
  238. elementCreator.clone( true ) :
  239. elementCreator( data.attributeOldValue, data, consumable, conversionApi );
  240. viewWriter.unwrap( viewRange, viewNode );
  241. evt.stop();
  242. };
  243. }
  244. /**
  245. * Function factory, creates a default model-to-view converter for nodes move changes.
  246. *
  247. * modelDispatcher.on( 'move', move() );
  248. *
  249. * @external engine.conversion.modelToView
  250. * @function engine.conversion.modelToView.move
  251. * @returns {Function} Move event converter.
  252. */
  253. export function move() {
  254. return ( evt, data, conversionApi ) => {
  255. const walker = new ModelTreeWalker( { boundaries: data.range, shallow: true } );
  256. let length = 0;
  257. for ( let value of walker ) {
  258. length += value.length;
  259. }
  260. const sourceModelRange = ModelRange.createFromPositionAndShift( data.sourcePosition, length );
  261. const sourceViewRange = conversionApi.mapper.toViewRange( sourceModelRange );
  262. const targetViewPosition = conversionApi.mapper.toViewPosition( data.range.start );
  263. viewWriter.move( sourceViewRange, targetViewPosition );
  264. };
  265. }
  266. /**
  267. * Function factory, creates a default model-to-view converter for nodes remove changes.
  268. *
  269. * modelDispatcher.on( 'remove', remove() );
  270. *
  271. * @external engine.conversion.modelToView
  272. * @function engine.conversion.modelToView.remove
  273. * @returns {Function} Remove event converter.
  274. */
  275. export function remove() {
  276. return ( evt, data, conversionApi ) => {
  277. const walker = new ModelTreeWalker( { boundaries: data.range, shallow: true } );
  278. let length = 0;
  279. for ( let value of walker ) {
  280. length += value.length;
  281. }
  282. const sourceModelRange = ModelRange.createFromPositionAndShift( data.sourcePosition, length );
  283. const sourceViewRange = conversionApi.mapper.toViewRange( sourceModelRange );
  284. viewWriter.remove( sourceViewRange );
  285. };
  286. }
  287. /**
  288. * Returns the consumable type that is to be consumed in an event, basing on that event name.
  289. *
  290. * @param {String} evtName Event name.
  291. * @returns {String} Consumable type.
  292. */
  293. export function eventNameToConsumableType( evtName ) {
  294. const parts = evtName.split( ':' );
  295. return parts[ 0 ] + ':' + parts[ 1 ];
  296. }