8
0

model-to-view-converters.js 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* bender-tags: conversion */
  6. import ModelDocument from 'ckeditor5-engine/src/model/document';
  7. import ModelElement from 'ckeditor5-engine/src/model/element';
  8. import ModelText from 'ckeditor5-engine/src/model/text';
  9. import ModelRange from 'ckeditor5-engine/src/model/range';
  10. import ModelPosition from 'ckeditor5-engine/src/model/position';
  11. import modelWriter from 'ckeditor5-engine/src/model/writer';
  12. import ViewElement from 'ckeditor5-engine/src/view/element';
  13. import ViewContainerElement from 'ckeditor5-engine/src/view/containerelement';
  14. import ViewAttributeElement from 'ckeditor5-engine/src/view/attributeelement';
  15. import ViewText from 'ckeditor5-engine/src/view/text';
  16. import Mapper from 'ckeditor5-engine/src/conversion/mapper';
  17. import ModelConversionDispatcher from 'ckeditor5-engine/src/conversion/modelconversiondispatcher';
  18. import {
  19. insertElement,
  20. insertText,
  21. setAttribute,
  22. removeAttribute,
  23. wrapItem,
  24. unwrapItem,
  25. wrapRange,
  26. unwrapRange,
  27. move,
  28. remove,
  29. rename
  30. } from 'ckeditor5-engine/src/conversion/model-to-view-converters';
  31. import { createRangeOnElementOnly } from 'ckeditor5-engine/tests/model/_utils/utils';
  32. describe( 'model-to-view-converters', () => {
  33. let dispatcher, modelDoc, modelRoot, mapper, viewRoot;
  34. beforeEach( () => {
  35. modelDoc = new ModelDocument();
  36. modelRoot = modelDoc.createRoot();
  37. viewRoot = new ViewContainerElement( 'div' );
  38. mapper = new Mapper();
  39. mapper.bindElements( modelRoot, viewRoot );
  40. dispatcher = new ModelConversionDispatcher( { mapper } );
  41. } );
  42. function viewAttributesToString( item ) {
  43. let result = '';
  44. for ( let key of item.getAttributeKeys() ) {
  45. let value = item.getAttribute( key );
  46. if ( value ) {
  47. result += ' ' + key + '="' + value + '"';
  48. }
  49. }
  50. return result;
  51. }
  52. function viewToString( item ) {
  53. let result = '';
  54. if ( item instanceof ViewText ) {
  55. result = item.data;
  56. } else {
  57. // ViewElement or ViewDocumentFragment.
  58. for ( let child of item.getChildren() ) {
  59. result += viewToString( child );
  60. }
  61. if ( item instanceof ViewElement ) {
  62. result = '<' + item.name + viewAttributesToString( item ) + '>' + result + '</' + item.name + '>';
  63. }
  64. }
  65. return result;
  66. }
  67. describe( 'insertText', () => {
  68. it( 'should convert text insertion in model to view text', () => {
  69. modelRoot.appendChildren( new ModelText( 'foobar' ) );
  70. dispatcher.on( 'insert:$text', insertText() );
  71. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  72. expect( viewToString( viewRoot ) ).to.equal( '<div>foobar</div>' );
  73. } );
  74. it( 'should support unicode', () => {
  75. modelRoot.appendChildren( new ModelText( 'நிலைக்கு' ) );
  76. dispatcher.on( 'insert:$text', insertText() );
  77. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  78. expect( viewToString( viewRoot ) ).to.equal( '<div>நிலைக்கு</div>' );
  79. } );
  80. it( 'should be possible to override it', () => {
  81. modelRoot.appendChildren( new ModelText( 'foobar' ) );
  82. dispatcher.on( 'insert:$text', insertText() );
  83. dispatcher.on( 'insert:$text', ( evt, data, consumable ) => {
  84. consumable.consume( data.item, 'insert' );
  85. }, { priority: 'high' } );
  86. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  87. expect( viewToString( viewRoot ) ).to.equal( '<div></div>' );
  88. } );
  89. } );
  90. describe( 'insertElement', () => {
  91. it( 'should convert element insertion in model to and map positions for future converting', () => {
  92. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar' ) );
  93. const viewElement = new ViewContainerElement( 'p' );
  94. modelRoot.appendChildren( modelElement );
  95. dispatcher.on( 'insert:paragraph', insertElement( viewElement ) );
  96. dispatcher.on( 'insert:$text', insertText() );
  97. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  98. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  99. } );
  100. it( 'should take view element function generator as a parameter', () => {
  101. const elementGenerator = ( data, consumable ) => {
  102. if ( consumable.consume( data.item, 'addAttribute:nice' ) ) {
  103. return new ViewContainerElement( 'div' );
  104. } else {
  105. return new ViewContainerElement( 'p' );
  106. }
  107. };
  108. const niceP = new ModelElement( 'myParagraph', { nice: true }, new ModelText( 'foo' ) );
  109. const badP = new ModelElement( 'myParagraph', null, new ModelText( 'bar' ) );
  110. modelRoot.appendChildren( [ niceP, badP ] );
  111. dispatcher.on( 'insert:myParagraph', insertElement( elementGenerator ) );
  112. dispatcher.on( 'insert:$text', insertText() );
  113. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  114. expect( viewToString( viewRoot ) ).to.equal( '<div><div>foo</div><p>bar</p></div>' );
  115. } );
  116. } );
  117. describe( 'setAttribute/removeAttribute', () => {
  118. it( 'should convert attribute insert/change/remove on a model node', () => {
  119. const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) );
  120. const viewElement = new ViewContainerElement( 'p' );
  121. modelRoot.appendChildren( modelElement );
  122. dispatcher.on( 'insert:paragraph', insertElement( viewElement ) );
  123. dispatcher.on( 'insert:$text', insertText() );
  124. dispatcher.on( 'addAttribute:class', setAttribute() );
  125. dispatcher.on( 'changeAttribute:class', setAttribute() );
  126. dispatcher.on( 'removeAttribute:class', removeAttribute() );
  127. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  128. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="foo">foobar</p></div>' );
  129. modelElement.setAttribute( 'class', 'bar' );
  130. dispatcher.convertAttribute( 'changeAttribute', createRangeOnElementOnly( modelElement ), 'class', 'foo', 'bar' );
  131. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="bar">foobar</p></div>' );
  132. modelElement.removeAttribute( 'class' );
  133. dispatcher.convertAttribute( 'removeAttribute', createRangeOnElementOnly( modelElement ), 'class', 'bar', null );
  134. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  135. } );
  136. it( 'should convert insert/change/remove with attribute generating function as a parameter', () => {
  137. const modelParagraph = new ModelElement( 'paragraph', { theme: 'nice' }, new ModelText( 'foobar' ) );
  138. const modelDiv = new ModelElement( 'div', { theme: 'nice' } );
  139. const themeConverter = ( value, key, data ) => {
  140. if ( data.item instanceof ModelElement && data.item.childCount > 0 ) {
  141. value += ' ' + 'fix-content';
  142. }
  143. return { key: 'class', value };
  144. };
  145. modelRoot.appendChildren( [ modelParagraph, modelDiv ] );
  146. dispatcher.on( 'insert:paragraph', insertElement( new ViewContainerElement( 'p' ) ) );
  147. dispatcher.on( 'insert:div', insertElement( new ViewContainerElement( 'div' ) ) );
  148. dispatcher.on( 'insert:$text', insertText() );
  149. dispatcher.on( 'addAttribute:theme', setAttribute( themeConverter ) );
  150. dispatcher.on( 'changeAttribute:theme', setAttribute( themeConverter ) );
  151. dispatcher.on( 'removeAttribute:theme', removeAttribute( themeConverter ) );
  152. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  153. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="nice fix-content">foobar</p><div class="nice"></div></div>' );
  154. modelParagraph.setAttribute( 'theme', 'awesome' );
  155. dispatcher.convertAttribute( 'changeAttribute', createRangeOnElementOnly( modelParagraph ), 'theme', 'nice', 'awesome' );
  156. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="awesome fix-content">foobar</p><div class="nice"></div></div>' );
  157. modelParagraph.removeAttribute( 'theme' );
  158. dispatcher.convertAttribute( 'removeAttribute', createRangeOnElementOnly( modelParagraph ), 'theme', 'awesome', null );
  159. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p><div class="nice"></div></div>' );
  160. } );
  161. it( 'should be possible to override setAttribute', () => {
  162. const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) );
  163. const viewElement = new ViewContainerElement( 'p' );
  164. modelRoot.appendChildren( modelElement );
  165. dispatcher.on( 'insert:paragraph', insertElement( viewElement ) );
  166. dispatcher.on( 'insert:$text', insertText() );
  167. dispatcher.on( 'addAttribute:class', setAttribute() );
  168. dispatcher.on( 'addAttribute:class', ( evt, data, consumable ) => {
  169. consumable.consume( data.item, 'addAttribute:class' );
  170. }, { priority: 'high' } );
  171. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  172. // No attribute set.
  173. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  174. } );
  175. it( 'should be possible to override removeAttribute', () => {
  176. const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) );
  177. const viewElement = new ViewContainerElement( 'p' );
  178. modelRoot.appendChildren( modelElement );
  179. dispatcher.on( 'insert:paragraph', insertElement( viewElement ) );
  180. dispatcher.on( 'insert:$text', insertText() );
  181. dispatcher.on( 'addAttribute:class', setAttribute() );
  182. dispatcher.on( 'removeAttribute:class', removeAttribute() );
  183. dispatcher.on( 'removeAttribute:class', ( evt, data, consumable ) => {
  184. consumable.consume( data.item, 'removeAttribute:class' );
  185. }, { priority: 'high' } );
  186. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  187. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="foo">foobar</p></div>' );
  188. modelElement.removeAttribute( 'class' );
  189. dispatcher.convertAttribute( 'removeAttribute', createRangeOnElementOnly( modelElement ), 'class', 'bar', null );
  190. // Nothing changed.
  191. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="foo">foobar</p></div>' );
  192. } );
  193. } );
  194. describe( 'wrap/unwrap', () => {
  195. it( 'should convert insert/change/remove of attribute in model into wrapping element in a view', () => {
  196. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) );
  197. const viewP = new ViewContainerElement( 'p' );
  198. const viewB = new ViewAttributeElement( 'b' );
  199. modelRoot.appendChildren( modelElement );
  200. dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
  201. dispatcher.on( 'insert:$text', insertText() );
  202. dispatcher.on( 'addAttribute:bold', wrapItem( viewB ) );
  203. dispatcher.on( 'removeAttribute:bold', unwrapItem( viewB ) );
  204. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  205. expect( viewToString( viewRoot ) ).to.equal( '<div><p><b>foobar</b></p></div>' );
  206. modelWriter.removeAttribute( ModelRange.createIn( modelElement ), 'bold' );
  207. dispatcher.convertAttribute( 'removeAttribute', ModelRange.createIn( modelElement ), 'bold', true, null );
  208. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  209. } );
  210. it( 'should convert insert/remove of attribute in model with wrapping element generating function as a parameter', () => {
  211. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { style: 'bold' } ) );
  212. const viewP = new ViewContainerElement( 'p' );
  213. const elementGenerator = ( value ) => {
  214. if ( value == 'bold' ) {
  215. return new ViewAttributeElement( 'b' );
  216. }
  217. };
  218. modelRoot.appendChildren( modelElement );
  219. dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
  220. dispatcher.on( 'insert:$text', insertText() );
  221. dispatcher.on( 'addAttribute:style', wrapItem( elementGenerator ) );
  222. dispatcher.on( 'removeAttribute:style', unwrapItem( elementGenerator ) );
  223. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  224. expect( viewToString( viewRoot ) ).to.equal( '<div><p><b>foobar</b></p></div>' );
  225. modelWriter.removeAttribute( ModelRange.createIn( modelElement ), 'style' );
  226. dispatcher.convertAttribute( 'removeAttribute', ModelRange.createIn( modelElement ), 'style', 'bold', null );
  227. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  228. } );
  229. it( 'should update range on re-wrapping attribute (#475)', () => {
  230. const modelElement = new ModelElement( 'paragraph', null, [
  231. new ModelText( 'x' ),
  232. new ModelText( 'foo', { link: 'http://foo.com' } ),
  233. new ModelText( 'x' )
  234. ] );
  235. const viewP = new ViewContainerElement( 'p' );
  236. const elementGenerator = ( href ) => new ViewAttributeElement( 'a', { href } );
  237. modelRoot.appendChildren( modelElement );
  238. dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
  239. dispatcher.on( 'insert:$text', insertText() );
  240. dispatcher.on( 'addAttribute:link', wrapItem( elementGenerator ) );
  241. dispatcher.on( 'changeAttribute:link', wrapItem( elementGenerator ) );
  242. dispatcher.convertInsertion(
  243. ModelRange.createIn( modelRoot )
  244. );
  245. expect( viewToString( viewRoot ) ).to.equal( '<div><p>x<a href="http://foo.com">foo</a>x</p></div>' );
  246. modelWriter.setAttribute( ModelRange.createIn( modelElement ), 'link', 'http://foobar.com' );
  247. dispatcher.convertAttribute(
  248. 'changeAttribute',
  249. ModelRange.createIn( modelElement ),
  250. 'link',
  251. 'http://foo.com',
  252. 'http://foobar.com'
  253. );
  254. expect( viewToString( viewRoot ) ).to.equal( '<div><p><a href="http://foobar.com">xfoox</a></p></div>' );
  255. } );
  256. it( 'should support unicode', () => {
  257. const modelElement = new ModelElement( 'paragraph', null, [ 'நி', new ModelText( 'லைக்', { bold: true } ), 'கு' ] );
  258. const viewP = new ViewContainerElement( 'p' );
  259. const viewB = new ViewAttributeElement( 'b' );
  260. modelRoot.appendChildren( modelElement );
  261. dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
  262. dispatcher.on( 'insert:$text', insertText() );
  263. dispatcher.on( 'addAttribute:bold', wrapItem( viewB ) );
  264. dispatcher.on( 'removeAttribute:bold', unwrapItem( viewB ) );
  265. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  266. expect( viewToString( viewRoot ) ).to.equal( '<div><p>நி<b>லைக்</b>கு</p></div>' );
  267. modelWriter.removeAttribute( ModelRange.createIn( modelElement ), 'bold' );
  268. dispatcher.convertAttribute( 'removeAttribute', ModelRange.createIn( modelElement ), 'bold', true, null );
  269. expect( viewToString( viewRoot ) ).to.equal( '<div><p>நிலைக்கு</p></div>' );
  270. } );
  271. it( 'should be possible to override wrap', () => {
  272. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) );
  273. const viewP = new ViewContainerElement( 'p' );
  274. const viewB = new ViewAttributeElement( 'b' );
  275. modelRoot.appendChildren( modelElement );
  276. dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
  277. dispatcher.on( 'insert:$text', insertText() );
  278. dispatcher.on( 'addAttribute:bold', wrapItem( viewB ) );
  279. dispatcher.on( 'addAttribute:bold', ( evt, data, consumable ) => {
  280. consumable.consume( data.item, 'addAttribute:bold' );
  281. }, { priority: 'high' } );
  282. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  283. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  284. } );
  285. it( 'should be possible to override unwrap', () => {
  286. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) );
  287. const viewP = new ViewContainerElement( 'p' );
  288. const viewB = new ViewAttributeElement( 'b' );
  289. modelRoot.appendChildren( modelElement );
  290. dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
  291. dispatcher.on( 'insert:$text', insertText() );
  292. dispatcher.on( 'addAttribute:bold', wrapItem( viewB ) );
  293. dispatcher.on( 'removeAttribute:bold', unwrapItem( viewB ) );
  294. dispatcher.on( 'removeAttribute:bold', ( evt, data, consumable ) => {
  295. consumable.consume( data.item, 'removeAttribute:bold' );
  296. }, { priority: 'high' } );
  297. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  298. expect( viewToString( viewRoot ) ).to.equal( '<div><p><b>foobar</b></p></div>' );
  299. modelWriter.removeAttribute( ModelRange.createIn( modelElement ), 'bold' );
  300. dispatcher.convertAttribute( 'removeAttribute', ModelRange.createIn( modelElement ), 'bold', true, null );
  301. // Nothing changed.
  302. expect( viewToString( viewRoot ) ).to.equal( '<div><p><b>foobar</b></p></div>' );
  303. } );
  304. } );
  305. describe( 'wrapRange/unwrapRange', () => {
  306. let modelText, rangeJohn, rangeAlice, modelElement;
  307. beforeEach( () => {
  308. modelText = new ModelText( 'foobar' );
  309. modelElement = new ModelElement( 'paragraph', null, modelText );
  310. modelRoot.appendChildren( modelElement );
  311. const viewText = new ViewText( 'foobar' );
  312. const viewElement = new ViewContainerElement( 'p', null, viewText );
  313. viewRoot.appendChildren( viewElement );
  314. mapper.bindElements( modelElement, viewElement );
  315. rangeJohn = ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 4 );
  316. rangeAlice = ModelRange.createFromParentsAndOffsets( modelElement, 1, modelElement, 1 );
  317. } );
  318. it( 'should convert adding/removing of marker into wrapping element in a view', () => {
  319. const viewSpan = new ViewAttributeElement( 'span', { class: 'name' } );
  320. dispatcher.on( 'addMarker:name', wrapRange( viewSpan ) );
  321. dispatcher.on( 'removeMarker:name', unwrapRange( viewSpan ) );
  322. dispatcher.convertMarker( 'addMarker', 'name', rangeJohn );
  323. expect( viewToString( viewRoot ) ).to.equal( '<div><p>fo<span class="name">ob</span>ar</p></div>' );
  324. dispatcher.convertMarker( 'removeMarker', 'name', rangeJohn );
  325. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  326. } );
  327. it( 'should support collapsed markers', () => {
  328. const viewSpan = new ViewAttributeElement( 'span' );
  329. dispatcher.on( 'addMarker:name', wrapRange( viewSpan ) );
  330. dispatcher.on( 'removeMarker:name', unwrapRange( viewSpan ) );
  331. const rangeP = ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 0 );
  332. dispatcher.convertMarker( 'addMarker', 'name', rangeP );
  333. dispatcher.convertMarker( 'addMarker', 'name', rangeAlice );
  334. expect( viewToString( viewRoot ) ).to.equal( '<div><span></span><p>f<span></span>oobar</p></div>' );
  335. dispatcher.convertMarker( 'removeMarker', 'name', rangeP );
  336. expect( viewToString( viewRoot ) ).to.equal( '<div><p>f<span></span>oobar</p></div>' );
  337. dispatcher.convertMarker( 'removeMarker', 'name', rangeAlice );
  338. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  339. } );
  340. it( 'should convert insert/remove of attribute in model with wrapping element generating function as a parameter', () => {
  341. const converterCallback = ( data ) => {
  342. const name = data.name.split( ':' )[ 1 ];
  343. return new ViewAttributeElement( 'span', { class: name } );
  344. };
  345. dispatcher.on( 'addMarker:name', wrapRange( converterCallback ) );
  346. dispatcher.on( 'removeMarker:name', unwrapRange( converterCallback ) );
  347. dispatcher.convertMarker( 'addMarker', 'name:john', rangeJohn );
  348. expect( viewToString( viewRoot ) ).to.equal( '<div><p>fo<span class="john">ob</span>ar</p></div>' );
  349. dispatcher.convertMarker( 'addMarker', 'name:alice', rangeAlice );
  350. expect( viewToString( viewRoot ) ).to.equal( '<div><p>f<span class="alice"></span>o<span class="john">ob</span>ar</p></div>' );
  351. dispatcher.convertMarker( 'removeMarker', 'name:john', rangeJohn );
  352. expect( viewToString( viewRoot ) ).to.equal( '<div><p>f<span class="alice"></span>oobar</p></div>' );
  353. dispatcher.convertMarker( 'removeMarker', 'name:alice', rangeAlice );
  354. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  355. } );
  356. it( 'should support non-flat markers', () => {
  357. const modelElement2 = new ModelElement( 'paragraph', null, new ModelText( '22' ) );
  358. modelRoot.appendChildren( modelElement2 );
  359. const modelElement3 = new ModelElement( 'paragraph', null, new ModelText( '333' ) );
  360. modelRoot.appendChildren( modelElement3 );
  361. const viewElement2 = new ViewContainerElement( 'p', null, new ViewText( '22' ) );
  362. viewRoot.appendChildren( viewElement2 );
  363. const viewElement3 = new ViewContainerElement( 'p', null, new ViewText( '333' ) );
  364. viewRoot.appendChildren( viewElement3 );
  365. mapper.bindElements( modelElement2, viewElement2 );
  366. mapper.bindElements( modelElement3, viewElement3 );
  367. const range = ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement3, 1 );
  368. const viewSpan = new ViewAttributeElement( 'span', { class: 'name' } );
  369. dispatcher.on( 'addMarker:name', wrapRange( viewSpan ) );
  370. dispatcher.on( 'removeMarker:name', unwrapRange( viewSpan ) );
  371. dispatcher.convertMarker( 'addMarker', 'name', range );
  372. expect( viewToString( viewRoot ) ).to.equal(
  373. '<div><p>fo<span class="name">obar</span></p><p><span class="name">22</span></p><p><span class="name">3</span>33</p></div>'
  374. );
  375. dispatcher.convertMarker( 'removeMarker', 'name', range );
  376. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p><p>22</p><p>333</p></div>' );
  377. } );
  378. it( 'should be possible to override wrapRange', () => {
  379. const converterCallback = ( data ) => {
  380. const name = data.name.split( ':' )[ 1 ];
  381. return new ViewAttributeElement( 'span', { class: name } );
  382. };
  383. dispatcher.on( 'addMarker:name', wrapRange( converterCallback ) );
  384. dispatcher.on( 'addMarker:name:alice', ( evt, data, consumable ) => {
  385. consumable.consume( data.range, 'addMarker' );
  386. }, { priority: 'high' } );
  387. dispatcher.convertMarker( 'addMarker', 'name:john', rangeJohn );
  388. dispatcher.convertMarker( 'addMarker', 'name:alice', rangeAlice );
  389. expect( viewToString( viewRoot ) ).to.equal( '<div><p>fo<span class="john">ob</span>ar</p></div>' );
  390. } );
  391. it( 'should be possible to override unwrapRange', () => {
  392. const converterCallback = ( data ) => {
  393. const name = data.name.split( ':' )[ 1 ];
  394. return new ViewAttributeElement( 'span', { class: name } );
  395. };
  396. dispatcher.on( 'addMarker:name', wrapRange( converterCallback ) );
  397. dispatcher.on( 'removeMarker:name', unwrapRange( converterCallback ) );
  398. dispatcher.on( 'removeMarker:name:john', ( evt, data, consumable ) => {
  399. consumable.consume( data.range, 'removeMarker' );
  400. }, { priority: 'high' } );
  401. dispatcher.convertMarker( 'addMarker', 'name:john', rangeJohn );
  402. expect( viewToString( viewRoot ) ).to.equal( '<div><p>fo<span class="john">ob</span>ar</p></div>' );
  403. dispatcher.convertMarker( 'removeMarker', 'name:john', rangeJohn );
  404. expect( viewToString( viewRoot ) ).to.equal( '<div><p>fo<span class="john">ob</span>ar</p></div>' );
  405. } );
  406. it( 'should not convert if view element is not returned by element generating function', () => {
  407. const converterCallback = () => null;
  408. dispatcher.on( 'addMarker:name', wrapRange( converterCallback ) );
  409. dispatcher.on( 'addMarker:name', ( evt, data, consumable ) => {
  410. // Check whether value was not consumed from `consumable`.
  411. expect( consumable.test( data.range, 'addMarker' ) ).to.be.true;
  412. } );
  413. dispatcher.convertMarker( 'addMarker', 'name', rangeJohn );
  414. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  415. } );
  416. describe( 'multiple overlapping markers', () => {
  417. it( 'collapsed markers', () => {
  418. const converterCallbackName = ( data ) => new ViewAttributeElement( 'span', { class: data.name.split( ':' )[ 1 ] } );
  419. const converterCallbackSearch = () => new ViewAttributeElement( 'em', { class: 'search' } );
  420. dispatcher.on( 'addMarker:name', wrapRange( converterCallbackName ) );
  421. dispatcher.on( 'removeMarker:name', unwrapRange( converterCallbackName ) );
  422. dispatcher.on( 'addMarker:search', wrapRange( converterCallbackSearch ) );
  423. dispatcher.on( 'removeMarker:search', unwrapRange( converterCallbackSearch ) );
  424. const range = rangeAlice;
  425. dispatcher.convertMarker( 'addMarker', 'name:a', range );
  426. dispatcher.convertMarker( 'addMarker', 'name:b', range );
  427. dispatcher.convertMarker( 'addMarker', 'search', range );
  428. expect( viewToString( viewRoot ) ).to.equal(
  429. '<div><p>f<em class="search"></em><span class="b"></span><span class="a"></span>oobar</p></div>'
  430. );
  431. dispatcher.convertMarker( 'removeMarker', 'name:b', range );
  432. expect( viewToString( viewRoot ) ).to.equal( '<div><p>f<em class="search"></em><span class="a"></span>oobar</p></div>' );
  433. dispatcher.convertMarker( 'removeMarker', 'search', range );
  434. expect( viewToString( viewRoot ) ).to.equal( '<div><p>f<span class="a"></span>oobar</p></div>' );
  435. dispatcher.convertMarker( 'removeMarker', 'name:a', range );
  436. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  437. } );
  438. it( 'non-collapsed markers', () => {
  439. const converterCallbackName = ( data ) => {
  440. const name = data.name.split( ':' )[ 1 ];
  441. const element = new ViewAttributeElement( 'span', { class: name } );
  442. element.priority = name.charCodeAt( 0 );
  443. return element;
  444. };
  445. const converterCallbackSearch = () => new ViewAttributeElement( 'em', { class: 'search' } );
  446. dispatcher.on( 'addMarker:name', wrapRange( converterCallbackName ) );
  447. dispatcher.on( 'removeMarker:name', unwrapRange( converterCallbackName ) );
  448. dispatcher.on( 'addMarker:search', wrapRange( converterCallbackSearch ) );
  449. dispatcher.on( 'removeMarker:search', unwrapRange( converterCallbackSearch ) );
  450. const range = rangeJohn;
  451. dispatcher.convertMarker( 'addMarker', 'name:a', range );
  452. dispatcher.convertMarker( 'addMarker', 'name:b', range );
  453. dispatcher.convertMarker( 'addMarker', 'search', range );
  454. expect( viewToString( viewRoot ) ).to.equal(
  455. '<div><p>fo<em class="search"><span class="a"><span class="b">ob</span></span></em>ar</p></div>'
  456. );
  457. dispatcher.convertMarker( 'removeMarker', 'name:b', range );
  458. expect( viewToString( viewRoot ) ).to.equal( '<div><p>fo<em class="search"><span class="a">ob</span></em>ar</p></div>' );
  459. dispatcher.convertMarker( 'removeMarker', 'search', range );
  460. expect( viewToString( viewRoot ) ).to.equal( '<div><p>fo<span class="a">ob</span>ar</p></div>' );
  461. dispatcher.convertMarker( 'removeMarker', 'name:a', range );
  462. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  463. } );
  464. it( 'non-collapsed markers intersecting with same priority', () => {
  465. const converterCallbackSearch = () => new ViewAttributeElement( 'em', { class: 'search' } );
  466. dispatcher.on( 'addMarker:search', wrapRange( converterCallbackSearch ) );
  467. dispatcher.on( 'removeMarker:search', unwrapRange( converterCallbackSearch ) );
  468. const range1 = ModelRange.createFromParentsAndOffsets( modelElement, 1, modelElement, 3 );
  469. const range2 = ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 4 );
  470. const range3 = ModelRange.createFromParentsAndOffsets( modelElement, 4, modelElement, 6 );
  471. dispatcher.convertMarker( 'addMarker', 'search', range1 );
  472. dispatcher.convertMarker( 'addMarker', 'search', range2 );
  473. expect( viewToString( viewRoot ) ).to.equal( '<div><p>f<em class="search">oob</em>ar</p></div>' );
  474. dispatcher.convertMarker( 'addMarker', 'search', range3 );
  475. expect( viewToString( viewRoot ) ).to.equal( '<div><p>f<em class="search">oobar</em></p></div>' );
  476. dispatcher.convertMarker( 'removeMarker', 'search', range2 );
  477. expect( viewToString( viewRoot ) ).to.equal( '<div><p>f<em class="search">o</em>ob<em class="search">ar</em></p></div>' );
  478. dispatcher.convertMarker( 'removeMarker', 'search', range3 );
  479. expect( viewToString( viewRoot ) ).to.equal( '<div><p>f<em class="search">o</em>obar</p></div>' );
  480. dispatcher.convertMarker( 'removeMarker', 'search', range1 );
  481. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  482. } );
  483. it( 'collapsed and non-collapsed markers', () => {
  484. const converterCallbackName = ( data ) => {
  485. const name = data.name.split( ':' )[ 1 ];
  486. const element = new ViewAttributeElement( 'span', { class: name } );
  487. element.priority = name.charCodeAt( 0 );
  488. return element;
  489. };
  490. dispatcher.on( 'addMarker:name', wrapRange( converterCallbackName ) );
  491. dispatcher.on( 'removeMarker:name', unwrapRange( converterCallbackName ) );
  492. const range1 = ModelRange.createFromParentsAndOffsets( modelElement, 1, modelElement, 5 );
  493. const range2 = ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 2 );
  494. const range3 = ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 4 );
  495. dispatcher.convertMarker( 'addMarker', 'name:a', range1 );
  496. dispatcher.convertMarker( 'addMarker', 'name:b', range2 );
  497. dispatcher.convertMarker( 'addMarker', 'name:c', range3 );
  498. expect( viewToString( viewRoot ) ).to.equal(
  499. '<div><p>f<span class="a">o<span class="b"></span><span class="c">ob</span>a</span>r</p></div>'
  500. );
  501. dispatcher.convertMarker( 'removeMarker', 'name:c', range3 );
  502. expect( viewToString( viewRoot ) ).to.equal( '<div><p>f<span class="a">o<span class="b"></span>oba</span>r</p></div>' );
  503. dispatcher.convertMarker( 'removeMarker', 'name:b', range2 );
  504. expect( viewToString( viewRoot ) ).to.equal( '<div><p>f<span class="a">ooba</span>r</p></div>' );
  505. dispatcher.convertMarker( 'removeMarker', 'name:a', range1 );
  506. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  507. } );
  508. } );
  509. } );
  510. describe( 'move', () => {
  511. it( 'should move items in view accordingly to changes in model', () => {
  512. const modelDivA = new ModelElement( 'div', null, [
  513. new ModelText( 'foo' ),
  514. new ModelElement( 'image' ),
  515. new ModelText( 'bar' )
  516. ] );
  517. const modelDivB = new ModelElement( 'div', null, new ModelText( 'xxyy' ) );
  518. modelRoot.appendChildren( [ modelDivA, modelDivB ] );
  519. dispatcher.on( 'insert:div', insertElement( new ViewContainerElement( 'div' ) ) );
  520. dispatcher.on( 'insert:image', insertElement( new ViewContainerElement( 'img' ) ) );
  521. dispatcher.on( 'insert:$text', insertText() );
  522. dispatcher.on( 'move', move() );
  523. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  524. const removedNodes = modelDivA.removeChildren( 0, 2 );
  525. modelDivB.insertChildren( 0, removedNodes );
  526. dispatcher.convertMove(
  527. ModelPosition.createFromParentAndOffset( modelDivA, 0 ),
  528. ModelRange.createFromParentsAndOffsets( modelDivB, 0, modelDivB, 4 )
  529. );
  530. expect( viewToString( viewRoot ) ).to.equal( '<div><div>bar</div><div>foo<img></img>xxyy</div></div>' );
  531. } );
  532. it( 'should not execute if value was already consumed', () => {
  533. const modelDivA = new ModelElement( 'div', null, new ModelText( 'foo' ) );
  534. const modelDivB = new ModelElement( 'div', null, new ModelText( 'xxyy' ) );
  535. modelRoot.appendChildren( [ modelDivA, modelDivB ] );
  536. dispatcher.on( 'insert:div', insertElement( new ViewContainerElement( 'div' ) ) );
  537. dispatcher.on( 'insert:$text', insertText() );
  538. dispatcher.on( 'move', move() );
  539. dispatcher.on( 'move', ( evt, data, consumable ) => {
  540. consumable.consume( data.item, 'move' );
  541. }, { priority: 'high' } );
  542. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  543. expect( viewToString( viewRoot ) ).to.equal( '<div><div>foo</div><div>xxyy</div></div>' );
  544. const removedNodes = modelDivA.removeChildren( 0, 1 );
  545. modelDivB.insertChildren( 0, removedNodes );
  546. dispatcher.convertMove(
  547. ModelPosition.createFromParentAndOffset( modelDivA, 0 ),
  548. ModelRange.createFromParentsAndOffsets( modelDivB, 0, modelDivB, 3 )
  549. );
  550. expect( viewToString( viewRoot ) ).to.equal( '<div><div>foo</div><div>xxyy</div></div>' );
  551. } );
  552. it( 'should support unicode', () => {
  553. const modelDivA = new ModelElement( 'div', null, 'நிலைக்கு' );
  554. const modelDivB = new ModelElement( 'div' );
  555. modelRoot.appendChildren( [ modelDivA, modelDivB ] );
  556. dispatcher.on( 'insert:div', insertElement( new ViewContainerElement( 'div' ) ) );
  557. dispatcher.on( 'insert:$text', insertText() );
  558. dispatcher.on( 'move', move() );
  559. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  560. modelWriter.move(
  561. ModelRange.createFromParentsAndOffsets( modelDivA, 2, modelDivA, 6 ),
  562. ModelPosition.createAt( modelDivB, 'end' )
  563. );
  564. dispatcher.convertMove(
  565. ModelPosition.createFromParentAndOffset( modelDivA, 2 ),
  566. ModelRange.createFromParentsAndOffsets( modelDivB, 0, modelDivB, 4 )
  567. );
  568. expect( viewToString( viewRoot ) ).to.equal( '<div><div>நிகு</div><div>லைக்</div></div>' );
  569. } );
  570. } );
  571. describe( 'remove', () => {
  572. it( 'should remove items from view accordingly to changes in model', () => {
  573. const modelDiv = new ModelElement( 'div', null, [
  574. new ModelText( 'foo' ),
  575. new ModelElement( 'image' ),
  576. new ModelText( 'bar' )
  577. ] );
  578. modelRoot.appendChildren( modelDiv );
  579. dispatcher.on( 'insert:div', insertElement( new ViewContainerElement( 'div' ) ) );
  580. dispatcher.on( 'insert:image', insertElement( new ViewContainerElement( 'img' ) ) );
  581. dispatcher.on( 'insert:$text', insertText() );
  582. dispatcher.on( 'remove', remove() );
  583. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  584. const removedNodes = modelDiv.removeChildren( 0, 2 );
  585. modelDoc.graveyard.insertChildren( 0, removedNodes );
  586. dispatcher.convertRemove(
  587. ModelPosition.createFromParentAndOffset( modelDiv, 0 ),
  588. ModelRange.createFromParentsAndOffsets( modelDoc.graveyard, 0, modelDoc.graveyard, 4 )
  589. );
  590. expect( viewToString( viewRoot ) ).to.equal( '<div><div>bar</div></div>' );
  591. } );
  592. it( 'should not execute if value was already consumed', () => {
  593. const modelDiv = new ModelElement( 'div', null, new ModelText( 'foo' ) );
  594. modelRoot.appendChildren( modelDiv );
  595. dispatcher.on( 'insert:div', insertElement( new ViewContainerElement( 'div' ) ) );
  596. dispatcher.on( 'insert:$text', insertText() );
  597. dispatcher.on( 'remove', remove() );
  598. dispatcher.on( 'remove', ( evt, data, consumable ) => {
  599. consumable.consume( data.item, 'remove' );
  600. }, { priority: 'high' } );
  601. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  602. expect( viewToString( viewRoot ) ).to.equal( '<div><div>foo</div></div>' );
  603. const removedNodes = modelDiv.removeChildren( 0, 1 );
  604. modelDoc.graveyard.insertChildren( 0, removedNodes );
  605. dispatcher.convertRemove(
  606. ModelPosition.createFromParentAndOffset( modelDiv, 0 ),
  607. ModelRange.createFromParentsAndOffsets( modelDoc.graveyard, 0, modelDoc.graveyard, 3 )
  608. );
  609. expect( viewToString( viewRoot ) ).to.equal( '<div><div>foo</div></div>' );
  610. } );
  611. it( 'should support unicode', () => {
  612. const modelDiv = new ModelElement( 'div', null, 'நிலைக்கு' );
  613. modelRoot.appendChildren( modelDiv );
  614. dispatcher.on( 'insert:div', insertElement( new ViewContainerElement( 'div' ) ) );
  615. dispatcher.on( 'insert:$text', insertText() );
  616. dispatcher.on( 'remove', remove() );
  617. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  618. modelWriter.move(
  619. ModelRange.createFromParentsAndOffsets( modelDiv, 0, modelDiv, 6 ),
  620. ModelPosition.createAt( modelDoc.graveyard, 'end' )
  621. );
  622. dispatcher.convertRemove(
  623. ModelPosition.createFromParentAndOffset( modelDiv, 0 ),
  624. ModelRange.createFromParentsAndOffsets( modelDoc.graveyard, 0, modelDoc.graveyard, 6 )
  625. );
  626. expect( viewToString( viewRoot ) ).to.equal( '<div><div>கு</div></div>' );
  627. } );
  628. } );
  629. describe( 'rename', () => {
  630. const oldName = 'oldName';
  631. const newName = 'newName';
  632. let element, converters;
  633. beforeEach( () => {
  634. converters = {
  635. insertText: insertText(),
  636. insert: insertElement( ( data ) => new ViewContainerElement( data.item.name ) ),
  637. move: move(),
  638. remove: remove(),
  639. rename: rename()
  640. };
  641. sinon.spy( converters, 'insert' );
  642. sinon.spy( converters, 'move' );
  643. sinon.spy( converters, 'remove' );
  644. element = new ModelElement( oldName, null, new ModelText( 'foo' ) );
  645. modelRoot.appendChildren( element );
  646. dispatcher.on( 'insert:$text', converters.insertText );
  647. dispatcher.on( 'insert', converters.insert );
  648. dispatcher.on( 'move', converters.move );
  649. dispatcher.on( 'remove', converters.remove );
  650. dispatcher.on( 'rename', converters.rename );
  651. dispatcher.convertInsertion( ModelRange.createOn( element ) );
  652. element.name = newName;
  653. } );
  654. afterEach( () => {
  655. converters.insert.restore();
  656. converters.move.restore();
  657. converters.remove.restore();
  658. } );
  659. it( 'should enable default rename conversion, that uses already registered callbacks', () => {
  660. const insertCallCount = converters.insert.callCount;
  661. expect( viewRoot.getChild( 0 ).name ).to.equal( 'oldName' );
  662. dispatcher.convertRename( element, oldName );
  663. expect( converters.insert.callCount - insertCallCount ).to.equal( 1 );
  664. expect( converters.move.calledOnce ).to.be.true;
  665. expect( converters.remove.calledOnce ).to.be.true;
  666. expect( viewRoot.getChild( 0 ).name ).to.equal( 'newName' );
  667. expect( viewRoot.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foo' );
  668. } );
  669. it( 'should not execute if converted value was already consumed', () => {
  670. dispatcher.on( 'rename', ( evt, data, consumable ) => {
  671. consumable.consume( data.element, 'rename' );
  672. }, { priority: 'high' } );
  673. dispatcher.on( 'rename', ( evt, data ) => {
  674. expect( data.fakeElement ).to.be.undefined;
  675. } );
  676. dispatcher.convertRename( element, oldName );
  677. } );
  678. } );
  679. } );