model-to-view-converters.js 13 KB

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