model-to-view-converters.js 36 KB

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