model-to-view-converters.js 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import ModelWriter from '../../src/model/writer';
  6. import Batch from '../../src/model/batch';
  7. import Model from '../../src/model/model';
  8. import ModelElement from '../../src/model/element';
  9. import ModelText from '../../src/model/text';
  10. import ModelRange from '../../src/model/range';
  11. import ModelPosition from '../../src/model/position';
  12. import ViewElement from '../../src/view/element';
  13. import ViewContainerElement from '../../src/view/containerelement';
  14. import ViewAttributeElement from '../../src/view/attributeelement';
  15. import ViewUIElement from '../../src/view/uielement';
  16. import ViewText from '../../src/view/text';
  17. import Mapper from '../../src/conversion/mapper';
  18. import ModelConversionDispatcher from '../../src/conversion/modelconversiondispatcher';
  19. import {
  20. insertElement,
  21. insertText,
  22. insertUIElement,
  23. setAttribute,
  24. removeAttribute,
  25. wrapItem,
  26. unwrapItem,
  27. remove,
  28. removeUIElement,
  29. highlightText,
  30. highlightElement,
  31. createViewElementFromHighlightDescriptor
  32. } from '../../src/conversion/model-to-view-converters';
  33. import { createRangeOnElementOnly } from '../../tests/model/_utils/utils';
  34. describe( 'model-to-view-converters', () => {
  35. let dispatcher, model, modelDoc, modelRoot, modelWriter, mapper, viewRoot;
  36. beforeEach( () => {
  37. model = new Model();
  38. modelDoc = model.document;
  39. modelRoot = modelDoc.createRoot();
  40. viewRoot = new ViewContainerElement( 'div' );
  41. mapper = new Mapper();
  42. mapper.bindElements( modelRoot, viewRoot );
  43. dispatcher = new ModelConversionDispatcher( model, { mapper } );
  44. // As an util for modifying model tree.
  45. modelWriter = new ModelWriter( model, new Batch() );
  46. } );
  47. function viewAttributesToString( item ) {
  48. let result = '';
  49. for ( const key of item.getAttributeKeys() ) {
  50. const value = item.getAttribute( key );
  51. if ( value ) {
  52. result += ' ' + key + '="' + value + '"';
  53. }
  54. }
  55. return result;
  56. }
  57. function viewToString( item ) {
  58. let result = '';
  59. if ( item instanceof ViewText ) {
  60. result = item.data;
  61. } else {
  62. // ViewElement or ViewDocumentFragment.
  63. for ( const child of item.getChildren() ) {
  64. result += viewToString( child );
  65. }
  66. if ( item instanceof ViewElement ) {
  67. result = '<' + item.name + viewAttributesToString( item ) + '>' + result + '</' + item.name + '>';
  68. }
  69. }
  70. return result;
  71. }
  72. describe( 'highlightText()', () => {
  73. let modelElement1, modelElement2, markerRange;
  74. const highlightDescriptor = {
  75. class: 'highlight-class',
  76. priority: 7,
  77. attributes: { title: 'title' }
  78. };
  79. beforeEach( () => {
  80. const modelText1 = new ModelText( 'foo' );
  81. modelElement1 = new ModelElement( 'paragraph', null, modelText1 );
  82. const modelText2 = new ModelText( 'bar' );
  83. modelElement2 = new ModelElement( 'paragraph', null, modelText2 );
  84. modelRoot.appendChildren( modelElement1 );
  85. modelRoot.appendChildren( modelElement2 );
  86. dispatcher.on( 'insert:paragraph', insertElement( () => new ViewContainerElement( 'p' ) ) );
  87. dispatcher.on( 'insert:$text', insertText() );
  88. markerRange = ModelRange.createIn( modelRoot );
  89. } );
  90. it( 'should wrap and unwrap text nodes', () => {
  91. dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) );
  92. dispatcher.on( 'removeMarker:marker', highlightText( highlightDescriptor ) );
  93. dispatcher.convertInsertion( markerRange );
  94. model.markers.set( 'marker', markerRange );
  95. dispatcher.convertMarker( 'addMarker', 'marker', markerRange );
  96. expect( viewToString( viewRoot ) ).to.equal(
  97. '<div>' +
  98. '<p>' +
  99. '<span class="highlight-class" title="title">foo</span>' +
  100. '</p>' +
  101. '<p>' +
  102. '<span class="highlight-class" title="title">bar</span>' +
  103. '</p>' +
  104. '</div>'
  105. );
  106. dispatcher.convertMarker( 'removeMarker', 'marker', markerRange );
  107. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  108. } );
  109. it( 'should not convert marker on elements already consumed', () => {
  110. const newDescriptor = { class: 'override-class' };
  111. dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) );
  112. dispatcher.on( 'addMarker:marker', highlightText( newDescriptor ), { priority: 'high' } );
  113. dispatcher.on( 'removeMarker:marker', highlightText( highlightDescriptor ) );
  114. dispatcher.on( 'removeMarker:marker', highlightText( newDescriptor ), { priority: 'high' } );
  115. dispatcher.convertInsertion( markerRange );
  116. model.markers.set( 'marker', markerRange );
  117. dispatcher.convertMarker( 'addMarker', 'marker', markerRange );
  118. expect( viewToString( viewRoot ) ).to.equal(
  119. '<div>' +
  120. '<p>' +
  121. '<span class="override-class">foo</span>' +
  122. '</p>' +
  123. '<p>' +
  124. '<span class="override-class">bar</span>' +
  125. '</p>' +
  126. '</div>'
  127. );
  128. dispatcher.convertMarker( 'removeMarker', 'marker', markerRange );
  129. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  130. } );
  131. it( 'should do nothing if descriptor is not provided', () => {
  132. dispatcher.on( 'addMarker:marker', highlightText( () => null ) );
  133. dispatcher.on( 'removeMarker:marker', highlightText( () => null ) );
  134. dispatcher.convertInsertion( markerRange );
  135. model.markers.set( 'marker', markerRange );
  136. dispatcher.convertMarker( 'addMarker', 'marker', markerRange );
  137. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  138. dispatcher.convertMarker( 'removeMarker', 'marker', markerRange );
  139. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  140. } );
  141. } );
  142. describe( 'highlightElement()', () => {
  143. let modelElement1, modelElement2, markerRange;
  144. const highlightDescriptor = {
  145. class: 'highlight-class',
  146. priority: 7,
  147. attributes: { title: 'title' },
  148. id: 'customId'
  149. };
  150. beforeEach( () => {
  151. const modelText1 = new ModelText( 'foo' );
  152. modelElement1 = new ModelElement( 'paragraph', null, modelText1 );
  153. const modelText2 = new ModelText( 'bar' );
  154. modelElement2 = new ModelElement( 'paragraph', null, modelText2 );
  155. modelRoot.appendChildren( modelElement1 );
  156. modelRoot.appendChildren( modelElement2 );
  157. dispatcher.on( 'insert:paragraph', insertElement( () => new ViewContainerElement( 'p' ) ) );
  158. dispatcher.on( 'insert:$text', insertText() );
  159. markerRange = ModelRange.createIn( modelRoot );
  160. } );
  161. it( 'should use addHighlight and removeHighlight on elements and not convert children nodes', () => {
  162. dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) );
  163. dispatcher.on( 'removeMarker:marker', highlightElement( highlightDescriptor ) );
  164. dispatcher.on( 'insert:paragraph', insertElement( data => {
  165. // Use special converter only for first paragraph.
  166. if ( data.item == modelElement2 ) {
  167. return;
  168. }
  169. const viewContainer = new ViewContainerElement( 'p' );
  170. viewContainer.setCustomProperty( 'addHighlight', ( element, descriptor ) => {
  171. element.addClass( 'highlight-own-class' );
  172. expect( descriptor ).to.equal( highlightDescriptor );
  173. } );
  174. viewContainer.setCustomProperty( 'removeHighlight', ( element, id ) => {
  175. element.removeClass( 'highlight-own-class' );
  176. expect( id ).to.equal( highlightDescriptor.id );
  177. } );
  178. return viewContainer;
  179. } ), { priority: 'high' } );
  180. dispatcher.convertInsertion( markerRange );
  181. model.markers.set( 'marker', markerRange );
  182. dispatcher.convertMarker( 'addMarker', 'marker', markerRange );
  183. expect( viewToString( viewRoot ) ).to.equal(
  184. '<div>' +
  185. '<p class="highlight-own-class">' +
  186. 'foo' +
  187. '</p>' +
  188. '<p>' +
  189. 'bar' +
  190. '</p>' +
  191. '</div>'
  192. );
  193. dispatcher.convertMarker( 'removeMarker', 'marker', markerRange );
  194. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  195. } );
  196. it( 'should not convert marker on elements already consumed', () => {
  197. const newDescriptor = { class: 'override-class' };
  198. dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) );
  199. dispatcher.on( 'removeMarker:marker', highlightElement( highlightDescriptor ) );
  200. dispatcher.on( 'addMarker:marker', highlightElement( newDescriptor ), { priority: 'high' } );
  201. dispatcher.on( 'removeMarker:marker', highlightElement( newDescriptor ), { priority: 'high' } );
  202. dispatcher.on( 'insert:paragraph', insertElement( () => {
  203. const element = new ViewContainerElement( 'p' );
  204. const descriptors = new Map();
  205. element.setCustomProperty( 'addHighlight', ( element, data ) => {
  206. descriptors.set( data.id, data );
  207. element.addClass( data.class );
  208. } );
  209. element.setCustomProperty( 'removeHighlight', ( element, id ) => element.removeClass( descriptors.get( id ).class ) );
  210. return element;
  211. } ), { priority: 'high' } );
  212. dispatcher.convertInsertion( markerRange );
  213. model.markers.set( 'marker', markerRange );
  214. dispatcher.convertMarker( 'addMarker', 'marker', markerRange );
  215. expect( viewToString( viewRoot ) ).to.equal(
  216. '<div>' +
  217. '<p class="override-class">' +
  218. 'foo' +
  219. '</p>' +
  220. '<p class="override-class">' +
  221. 'bar' +
  222. '</p>' +
  223. '</div>'
  224. );
  225. dispatcher.convertMarker( 'removeMarker', 'marker', markerRange );
  226. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  227. } );
  228. it( 'should use provide default priority and id if not provided', () => {
  229. const highlightDescriptor = { class: 'highlight-class' };
  230. dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) );
  231. dispatcher.on( 'removeMarker:marker', highlightElement( highlightDescriptor ) );
  232. dispatcher.on( 'insert:paragraph', insertElement( data => {
  233. // Use special converter only for first paragraph.
  234. if ( data.item == modelElement2 ) {
  235. return;
  236. }
  237. const viewContainer = new ViewContainerElement( 'p' );
  238. viewContainer.setCustomProperty( 'addHighlight', ( element, descriptor ) => {
  239. expect( descriptor.priority ).to.equal( 10 );
  240. expect( descriptor.id ).to.equal( 'marker:foo-bar-baz' );
  241. } );
  242. viewContainer.setCustomProperty( 'removeHighlight', ( element, descriptor ) => {
  243. expect( descriptor.priority ).to.equal( 10 );
  244. expect( descriptor.id ).to.equal( 'marker:foo-bar-baz' );
  245. } );
  246. return viewContainer;
  247. } ), { priority: 'high' } );
  248. dispatcher.convertInsertion( markerRange );
  249. model.markers.set( 'marker', markerRange );
  250. dispatcher.convertMarker( 'addMarker', 'marker:foo-bar-baz', markerRange );
  251. } );
  252. it( 'should do nothing if descriptor is not provided', () => {
  253. dispatcher.on( 'addMarker:marker', highlightElement( () => null ) );
  254. dispatcher.on( 'removeMarker:marker', highlightElement( () => null ) );
  255. dispatcher.convertInsertion( markerRange );
  256. model.markers.set( 'marker', markerRange );
  257. dispatcher.convertMarker( 'addMarker', 'marker', markerRange );
  258. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  259. dispatcher.convertMarker( 'removeMarker', 'marker', markerRange );
  260. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  261. } );
  262. } );
  263. describe( 'insertText', () => {
  264. it( 'should convert text insertion in model to view text', () => {
  265. modelRoot.appendChildren( new ModelText( 'foobar' ) );
  266. dispatcher.on( 'insert:$text', insertText() );
  267. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  268. expect( viewToString( viewRoot ) ).to.equal( '<div>foobar</div>' );
  269. } );
  270. it( 'should support unicode', () => {
  271. modelRoot.appendChildren( new ModelText( 'நிலைக்கு' ) );
  272. dispatcher.on( 'insert:$text', insertText() );
  273. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  274. expect( viewToString( viewRoot ) ).to.equal( '<div>நிலைக்கு</div>' );
  275. } );
  276. it( 'should be possible to override it', () => {
  277. modelRoot.appendChildren( new ModelText( 'foobar' ) );
  278. dispatcher.on( 'insert:$text', insertText() );
  279. dispatcher.on( 'insert:$text', ( evt, data, consumable ) => {
  280. consumable.consume( data.item, 'insert' );
  281. }, { priority: 'high' } );
  282. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  283. expect( viewToString( viewRoot ) ).to.equal( '<div></div>' );
  284. } );
  285. } );
  286. describe( 'insertElement', () => {
  287. it( 'should convert element insertion in model to and map positions for future converting', () => {
  288. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar' ) );
  289. const viewElement = new ViewContainerElement( 'p' );
  290. modelRoot.appendChildren( modelElement );
  291. dispatcher.on( 'insert:paragraph', insertElement( viewElement ) );
  292. dispatcher.on( 'insert:$text', insertText() );
  293. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  294. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  295. } );
  296. it( 'should take view element function generator as a parameter', () => {
  297. const elementGenerator = ( data, consumable ) => {
  298. if ( consumable.consume( data.item, 'addAttribute:nice' ) ) {
  299. return new ViewContainerElement( 'div' );
  300. } else {
  301. return new ViewContainerElement( 'p' );
  302. }
  303. };
  304. const niceP = new ModelElement( 'myParagraph', { nice: true }, new ModelText( 'foo' ) );
  305. const badP = new ModelElement( 'myParagraph', null, new ModelText( 'bar' ) );
  306. modelRoot.appendChildren( [ niceP, badP ] );
  307. dispatcher.on( 'insert:myParagraph', insertElement( elementGenerator ) );
  308. dispatcher.on( 'insert:$text', insertText() );
  309. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  310. expect( viewToString( viewRoot ) ).to.equal( '<div><div>foo</div><p>bar</p></div>' );
  311. } );
  312. it( 'should not convert and not consume if creator function returned null', () => {
  313. const elementGenerator = () => null;
  314. const modelP = new ModelElement( 'paragraph', null, new ModelText( 'foo' ) );
  315. modelRoot.appendChildren( [ modelP ] );
  316. sinon.spy( dispatcher, 'fire' );
  317. dispatcher.on( 'insert:paragraph', insertElement( elementGenerator ) );
  318. dispatcher.on( 'insert:paragraph', ( evt, data, consumable ) => {
  319. expect( consumable.test( data.item, 'insert' ) ).to.be.true;
  320. } );
  321. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  322. expect( viewToString( viewRoot ) ).to.equal( '<div></div>' );
  323. expect( dispatcher.fire.calledWith( 'insert:paragraph' ) ).to.be.true;
  324. } );
  325. } );
  326. describe( 'setAttribute/removeAttribute', () => {
  327. it( 'should convert attribute insert/change/remove on a model node', () => {
  328. const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) );
  329. const viewElement = new ViewContainerElement( 'p' );
  330. modelRoot.appendChildren( modelElement );
  331. dispatcher.on( 'insert:paragraph', insertElement( viewElement ) );
  332. dispatcher.on( 'insert:$text', insertText() );
  333. dispatcher.on( 'addAttribute:class', setAttribute() );
  334. dispatcher.on( 'changeAttribute:class', setAttribute() );
  335. dispatcher.on( 'removeAttribute:class', removeAttribute() );
  336. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  337. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="foo">foobar</p></div>' );
  338. modelElement.setAttribute( 'class', 'bar' );
  339. dispatcher.convertAttribute( 'changeAttribute', createRangeOnElementOnly( modelElement ), 'class', 'foo', 'bar' );
  340. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="bar">foobar</p></div>' );
  341. modelElement.removeAttribute( 'class' );
  342. dispatcher.convertAttribute( 'removeAttribute', createRangeOnElementOnly( modelElement ), 'class', 'bar', null );
  343. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  344. } );
  345. it( 'should convert insert/change/remove with attribute generating function as a parameter', () => {
  346. const modelParagraph = new ModelElement( 'paragraph', { theme: 'nice' }, new ModelText( 'foobar' ) );
  347. const modelDiv = new ModelElement( 'div', { theme: 'nice' } );
  348. const themeConverter = ( value, key, data ) => {
  349. if ( data.item instanceof ModelElement && data.item.childCount > 0 ) {
  350. value += ' fix-content';
  351. }
  352. return { key: 'class', value };
  353. };
  354. modelRoot.appendChildren( [ modelParagraph, modelDiv ] );
  355. dispatcher.on( 'insert:paragraph', insertElement( new ViewContainerElement( 'p' ) ) );
  356. dispatcher.on( 'insert:div', insertElement( new ViewContainerElement( 'div' ) ) );
  357. dispatcher.on( 'insert:$text', insertText() );
  358. dispatcher.on( 'addAttribute:theme', setAttribute( themeConverter ) );
  359. dispatcher.on( 'changeAttribute:theme', setAttribute( themeConverter ) );
  360. dispatcher.on( 'removeAttribute:theme', removeAttribute( themeConverter ) );
  361. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  362. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="nice fix-content">foobar</p><div class="nice"></div></div>' );
  363. modelParagraph.setAttribute( 'theme', 'awesome' );
  364. dispatcher.convertAttribute( 'changeAttribute', createRangeOnElementOnly( modelParagraph ), 'theme', 'nice', 'awesome' );
  365. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="awesome fix-content">foobar</p><div class="nice"></div></div>' );
  366. modelParagraph.removeAttribute( 'theme' );
  367. dispatcher.convertAttribute( 'removeAttribute', createRangeOnElementOnly( modelParagraph ), 'theme', 'awesome', null );
  368. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p><div class="nice"></div></div>' );
  369. } );
  370. it( 'should be possible to override setAttribute', () => {
  371. const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) );
  372. const viewElement = new ViewContainerElement( 'p' );
  373. modelRoot.appendChildren( modelElement );
  374. dispatcher.on( 'insert:paragraph', insertElement( viewElement ) );
  375. dispatcher.on( 'insert:$text', insertText() );
  376. dispatcher.on( 'addAttribute:class', setAttribute() );
  377. dispatcher.on( 'addAttribute:class', ( evt, data, consumable ) => {
  378. consumable.consume( data.item, 'addAttribute:class' );
  379. }, { priority: 'high' } );
  380. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  381. // No attribute set.
  382. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  383. } );
  384. it( 'should be possible to override removeAttribute', () => {
  385. const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) );
  386. const viewElement = new ViewContainerElement( 'p' );
  387. modelRoot.appendChildren( modelElement );
  388. dispatcher.on( 'insert:paragraph', insertElement( viewElement ) );
  389. dispatcher.on( 'insert:$text', insertText() );
  390. dispatcher.on( 'addAttribute:class', setAttribute() );
  391. dispatcher.on( 'removeAttribute:class', removeAttribute() );
  392. dispatcher.on( 'removeAttribute:class', ( evt, data, consumable ) => {
  393. consumable.consume( data.item, 'removeAttribute:class' );
  394. }, { priority: 'high' } );
  395. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  396. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="foo">foobar</p></div>' );
  397. modelElement.removeAttribute( 'class' );
  398. dispatcher.convertAttribute( 'removeAttribute', createRangeOnElementOnly( modelElement ), 'class', 'bar', null );
  399. // Nothing changed.
  400. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="foo">foobar</p></div>' );
  401. } );
  402. } );
  403. describe( 'wrap/unwrap', () => {
  404. it( 'should convert insert/change/remove of attribute in model into wrapping element in a view', () => {
  405. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) );
  406. const viewP = new ViewContainerElement( 'p' );
  407. const viewB = new ViewAttributeElement( 'b' );
  408. modelRoot.appendChildren( modelElement );
  409. dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
  410. dispatcher.on( 'insert:$text', insertText() );
  411. dispatcher.on( 'addAttribute:bold', wrapItem( viewB ) );
  412. dispatcher.on( 'removeAttribute:bold', unwrapItem( viewB ) );
  413. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  414. expect( viewToString( viewRoot ) ).to.equal( '<div><p><b>foobar</b></p></div>' );
  415. modelWriter.removeAttribute( 'bold', modelElement );
  416. dispatcher.convertAttribute( 'removeAttribute', ModelRange.createIn( modelElement ), 'bold', true, null );
  417. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  418. } );
  419. it( 'should convert insert/remove of attribute in model with wrapping element generating function as a parameter', () => {
  420. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { style: 'bold' } ) );
  421. const viewP = new ViewContainerElement( 'p' );
  422. const elementGenerator = value => {
  423. if ( value == 'bold' ) {
  424. return new ViewAttributeElement( 'b' );
  425. }
  426. };
  427. modelRoot.appendChildren( modelElement );
  428. dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
  429. dispatcher.on( 'insert:$text', insertText() );
  430. dispatcher.on( 'addAttribute:style', wrapItem( elementGenerator ) );
  431. dispatcher.on( 'removeAttribute:style', unwrapItem( elementGenerator ) );
  432. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  433. expect( viewToString( viewRoot ) ).to.equal( '<div><p><b>foobar</b></p></div>' );
  434. modelWriter.removeAttribute( 'style', modelElement );
  435. dispatcher.convertAttribute( 'removeAttribute', ModelRange.createIn( modelElement ), 'style', 'bold', null );
  436. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  437. } );
  438. it( 'should update range on re-wrapping attribute (#475)', () => {
  439. const modelElement = new ModelElement( 'paragraph', null, [
  440. new ModelText( 'x' ),
  441. new ModelText( 'foo', { link: 'http://foo.com' } ),
  442. new ModelText( 'x' )
  443. ] );
  444. const viewP = new ViewContainerElement( 'p' );
  445. const elementGenerator = href => new ViewAttributeElement( 'a', { href } );
  446. modelRoot.appendChildren( modelElement );
  447. dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
  448. dispatcher.on( 'insert:$text', insertText() );
  449. dispatcher.on( 'addAttribute:link', wrapItem( elementGenerator ) );
  450. dispatcher.on( 'changeAttribute:link', wrapItem( elementGenerator ) );
  451. dispatcher.convertInsertion(
  452. ModelRange.createIn( modelRoot )
  453. );
  454. expect( viewToString( viewRoot ) ).to.equal( '<div><p>x<a href="http://foo.com">foo</a>x</p></div>' );
  455. modelWriter.setAttribute( 'link', 'http://foobar.com', modelElement );
  456. dispatcher.convertAttribute(
  457. 'changeAttribute',
  458. ModelRange.createIn( modelElement ),
  459. 'link',
  460. 'http://foo.com',
  461. 'http://foobar.com'
  462. );
  463. expect( viewToString( viewRoot ) ).to.equal( '<div><p><a href="http://foobar.com">xfoox</a></p></div>' );
  464. } );
  465. it( 'should support unicode', () => {
  466. const modelElement = new ModelElement( 'paragraph', null, [ 'நி', new ModelText( 'லைக்', { bold: true } ), 'கு' ] );
  467. const viewP = new ViewContainerElement( 'p' );
  468. const viewB = new ViewAttributeElement( 'b' );
  469. modelRoot.appendChildren( modelElement );
  470. dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
  471. dispatcher.on( 'insert:$text', insertText() );
  472. dispatcher.on( 'addAttribute:bold', wrapItem( viewB ) );
  473. dispatcher.on( 'removeAttribute:bold', unwrapItem( viewB ) );
  474. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  475. expect( viewToString( viewRoot ) ).to.equal( '<div><p>நி<b>லைக்</b>கு</p></div>' );
  476. modelWriter.removeAttribute( 'bold', modelElement );
  477. dispatcher.convertAttribute( 'removeAttribute', ModelRange.createIn( modelElement ), 'bold', true, null );
  478. expect( viewToString( viewRoot ) ).to.equal( '<div><p>நிலைக்கு</p></div>' );
  479. } );
  480. it( 'should be possible to override wrap', () => {
  481. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) );
  482. const viewP = new ViewContainerElement( 'p' );
  483. const viewB = new ViewAttributeElement( 'b' );
  484. modelRoot.appendChildren( modelElement );
  485. dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
  486. dispatcher.on( 'insert:$text', insertText() );
  487. dispatcher.on( 'addAttribute:bold', wrapItem( viewB ) );
  488. dispatcher.on( 'addAttribute:bold', ( evt, data, consumable ) => {
  489. consumable.consume( data.item, 'addAttribute:bold' );
  490. }, { priority: 'high' } );
  491. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  492. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  493. } );
  494. it( 'should be possible to override unwrap', () => {
  495. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) );
  496. const viewP = new ViewContainerElement( 'p' );
  497. const viewB = new ViewAttributeElement( 'b' );
  498. modelRoot.appendChildren( modelElement );
  499. dispatcher.on( 'insert:paragraph', insertElement( viewP ) );
  500. dispatcher.on( 'insert:$text', insertText() );
  501. dispatcher.on( 'addAttribute:bold', wrapItem( viewB ) );
  502. dispatcher.on( 'removeAttribute:bold', unwrapItem( viewB ) );
  503. dispatcher.on( 'removeAttribute:bold', ( evt, data, consumable ) => {
  504. consumable.consume( data.item, 'removeAttribute:bold' );
  505. }, { priority: 'high' } );
  506. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  507. expect( viewToString( viewRoot ) ).to.equal( '<div><p><b>foobar</b></p></div>' );
  508. modelWriter.removeAttribute( 'bold', modelElement );
  509. dispatcher.convertAttribute( 'removeAttribute', ModelRange.createIn( modelElement ), 'bold', true, null );
  510. // Nothing changed.
  511. expect( viewToString( viewRoot ) ).to.equal( '<div><p><b>foobar</b></p></div>' );
  512. } );
  513. it( 'should not convert and not consume if creator function returned null', () => {
  514. const elementGenerator = () => null;
  515. const modelText = new ModelText( 'foo', { bold: true } );
  516. modelRoot.appendChildren( [ modelText ] );
  517. sinon.spy( dispatcher, 'fire' );
  518. dispatcher.on( 'insert:$text', insertText() );
  519. dispatcher.on( 'addAttribute:bold', wrapItem( elementGenerator ) );
  520. dispatcher.on( 'removeAttribute:bold', unwrapItem( elementGenerator ) );
  521. dispatcher.on( 'addAttribute:bold', ( evt, data, consumable ) => {
  522. expect( consumable.test( data.item, 'addAttribute:bold' ) ).to.be.true;
  523. } );
  524. dispatcher.on( 'removeAttribute:bold', ( evt, data, consumable ) => {
  525. expect( consumable.test( data.item, 'removeAttribute:bold' ) ).to.be.true;
  526. } );
  527. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  528. expect( viewToString( viewRoot ) ).to.equal( '<div>foo</div>' );
  529. expect( dispatcher.fire.calledWith( 'addAttribute:bold:$text' ) ).to.be.true;
  530. modelText.removeAttribute( 'bold' );
  531. dispatcher.convertAttribute( 'removeAttribute', ModelRange.createIn( modelRoot ), 'bold', true, null );
  532. expect( dispatcher.fire.calledWith( 'removeAttribute:bold:$text' ) ).to.be.true;
  533. } );
  534. } );
  535. describe( 'insertUIElement/removeUIElement', () => {
  536. let modelText, range, modelElement;
  537. beforeEach( () => {
  538. modelText = new ModelText( 'foobar' );
  539. modelElement = new ModelElement( 'paragraph', null, modelText );
  540. modelRoot.appendChildren( modelElement );
  541. const viewText = new ViewText( 'foobar' );
  542. const viewElement = new ViewContainerElement( 'p', null, viewText );
  543. viewRoot.appendChildren( viewElement );
  544. mapper.bindElements( modelElement, viewElement );
  545. range = ModelRange.createFromParentsAndOffsets( modelElement, 3, modelElement, 3 );
  546. } );
  547. it( 'should insert and remove ui element - element as a creator', () => {
  548. const viewUi = new ViewUIElement( 'span', { 'class': 'marker' } );
  549. dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
  550. dispatcher.on( 'removeMarker:marker', removeUIElement( viewUi ) );
  551. dispatcher.convertMarker( 'addMarker', 'marker', range );
  552. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo<span class="marker"></span>bar</p></div>' );
  553. dispatcher.convertMarker( 'removeMarker', 'marker', range );
  554. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  555. } );
  556. it( 'should insert and remove ui element - function as a creator', () => {
  557. const viewUi = data => new ViewUIElement( 'span', { 'class': data.markerName } );
  558. dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
  559. dispatcher.on( 'removeMarker:marker', removeUIElement( viewUi ) );
  560. dispatcher.convertMarker( 'addMarker', 'marker', range );
  561. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo<span class="marker"></span>bar</p></div>' );
  562. dispatcher.convertMarker( 'removeMarker', 'marker', range );
  563. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  564. } );
  565. it( 'should not convert or consume if generator function returned null', () => {
  566. const viewUi = () => null;
  567. sinon.spy( dispatcher, 'fire' );
  568. dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
  569. dispatcher.on( 'removeMarker:marker', removeUIElement( viewUi ) );
  570. dispatcher.on( 'addMarker:marker', ( evt, data, consumable ) => {
  571. expect( consumable.test( data.markerRange, 'addMarker:marker' ) ).to.be.true;
  572. } );
  573. dispatcher.on( 'removeMarker:marker', ( evt, data, consumable ) => {
  574. expect( consumable.test( data.markerRange, 'removeMarker:marker' ) ).to.be.true;
  575. } );
  576. dispatcher.convertMarker( 'addMarker', 'marker', range );
  577. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  578. expect( dispatcher.fire.calledWith( 'addMarker:marker' ) );
  579. dispatcher.convertMarker( 'removeMarker', 'marker', range );
  580. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  581. expect( dispatcher.fire.calledWith( 'removeMarker:marker' ) );
  582. } );
  583. it( 'should be possible to overwrite', () => {
  584. const viewUi = new ViewUIElement( 'span', { 'class': 'marker' } );
  585. sinon.spy( dispatcher, 'fire' );
  586. dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
  587. dispatcher.on( 'removeMarker:marker', removeUIElement( viewUi ) );
  588. dispatcher.on( 'addMarker:marker', ( evt, data, consumable ) => {
  589. consumable.consume( data.markerRange, 'addMarker:marker' );
  590. }, { priority: 'high' } );
  591. dispatcher.on( 'removeMarker:marker', ( evt, data, consumable ) => {
  592. consumable.consume( data.markerRange, 'removeMarker:marker' );
  593. }, { priority: 'high' } );
  594. dispatcher.convertMarker( 'addMarker', 'marker', range );
  595. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  596. expect( dispatcher.fire.calledWith( 'addMarker:marker' ) );
  597. dispatcher.convertMarker( 'removeMarker', 'marker', range );
  598. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  599. expect( dispatcher.fire.calledWith( 'removeMarker:marker' ) );
  600. } );
  601. it( 'should not convert or consume if generator function returned null', () => {
  602. const viewUi = () => null;
  603. sinon.spy( dispatcher, 'fire' );
  604. dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
  605. dispatcher.on( 'removeMarker:marker', removeUIElement( viewUi ) );
  606. dispatcher.on( 'addMarker:marker', ( evt, data, consumable ) => {
  607. expect( consumable.test( data.markerRange, 'addMarker:marker' ) ).to.be.true;
  608. } );
  609. dispatcher.on( 'removeMarker:marker', ( evt, data, consumable ) => {
  610. expect( consumable.test( data.markerRange, 'removeMarker:marker' ) ).to.be.true;
  611. } );
  612. dispatcher.convertMarker( 'addMarker', 'marker', range );
  613. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  614. expect( dispatcher.fire.calledWith( 'addMarker:marker' ) );
  615. dispatcher.convertMarker( 'removeMarker', 'marker', range );
  616. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  617. expect( dispatcher.fire.calledWith( 'removeMarker:marker' ) );
  618. } );
  619. it( 'should be possible to overwrite', () => {
  620. const viewUi = new ViewUIElement( 'span', { 'class': 'marker' } );
  621. sinon.spy( dispatcher, 'fire' );
  622. dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
  623. dispatcher.on( 'removeMarker:marker', removeUIElement( viewUi ) );
  624. dispatcher.on( 'addMarker:marker', ( evt, data, consumable ) => {
  625. consumable.consume( data.markerRange, 'addMarker:marker' );
  626. }, { priority: 'high' } );
  627. dispatcher.on( 'removeMarker:marker', ( evt, data, consumable ) => {
  628. consumable.consume( data.markerRange, 'removeMarker:marker' );
  629. }, { priority: 'high' } );
  630. dispatcher.convertMarker( 'addMarker', 'marker', range );
  631. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  632. expect( dispatcher.fire.calledWith( 'addMarker:marker' ) );
  633. dispatcher.convertMarker( 'removeMarker', 'marker', range );
  634. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  635. expect( dispatcher.fire.calledWith( 'removeMarker:marker' ) );
  636. } );
  637. describe( 'non-collapsed range', () => {
  638. beforeEach( () => {
  639. range = ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 5 );
  640. } );
  641. it( 'should insert and remove ui element - element as a creator', () => {
  642. const viewUi = new ViewUIElement( 'span', { 'class': 'marker' } );
  643. dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
  644. dispatcher.on( 'removeMarker:marker', removeUIElement( viewUi ) );
  645. dispatcher.convertMarker( 'addMarker', 'marker', range );
  646. expect( viewToString( viewRoot ) )
  647. .to.equal( '<div><p>fo<span class="marker"></span>oba<span class="marker"></span>r</p></div>' );
  648. dispatcher.convertMarker( 'removeMarker', 'marker', range );
  649. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  650. } );
  651. it( 'should insert and remove ui element - function as a creator', () => {
  652. const viewUi = data => new ViewUIElement( 'span', { 'class': data.markerName } );
  653. dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
  654. dispatcher.on( 'removeMarker:marker', removeUIElement( viewUi ) );
  655. dispatcher.convertMarker( 'addMarker', 'marker', range );
  656. expect( viewToString( viewRoot ) )
  657. .to.equal( '<div><p>fo<span class="marker"></span>oba<span class="marker"></span>r</p></div>' );
  658. dispatcher.convertMarker( 'removeMarker', 'marker', range );
  659. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  660. } );
  661. it( 'should insert and remove different opening and ending element', () => {
  662. function creator( data ) {
  663. if ( data.isOpening ) {
  664. return new ViewUIElement( 'span', { 'class': data.markerName, 'data-start': true } );
  665. }
  666. return new ViewUIElement( 'span', { 'class': data.markerName, 'data-end': true } );
  667. }
  668. dispatcher.on( 'addMarker:marker', insertUIElement( creator ) );
  669. dispatcher.on( 'removeMarker:marker', removeUIElement( creator ) );
  670. dispatcher.convertMarker( 'addMarker', 'marker', range );
  671. expect( viewToString( viewRoot ) ).to.equal(
  672. '<div><p>fo<span class="marker" data-start="true"></span>oba<span class="marker" data-end="true"></span>r</p></div>'
  673. );
  674. dispatcher.convertMarker( 'removeMarker', 'marker', range );
  675. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  676. } );
  677. it( 'should be possible to overwrite', () => {
  678. const viewUi = new ViewUIElement( 'span', { 'class': 'marker' } );
  679. sinon.spy( dispatcher, 'fire' );
  680. dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
  681. dispatcher.on( 'removeMarker:marker', removeUIElement( viewUi ) );
  682. dispatcher.on( 'addMarker:marker', ( evt, data, consumable ) => {
  683. consumable.consume( data.item, 'addMarker:marker' );
  684. }, { priority: 'high' } );
  685. dispatcher.on( 'removeMarker:marker', ( evt, data, consumable ) => {
  686. consumable.consume( data.item, 'removeMarker:marker' );
  687. }, { priority: 'high' } );
  688. dispatcher.convertMarker( 'addMarker', 'marker', range );
  689. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  690. expect( dispatcher.fire.calledWith( 'addMarker:marker' ) );
  691. dispatcher.convertMarker( 'removeMarker', 'marker', range );
  692. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  693. expect( dispatcher.fire.calledWith( 'removeMarker:marker' ) );
  694. } );
  695. } );
  696. } );
  697. describe( 'remove', () => {
  698. it( 'should remove items from view accordingly to changes in model #1', () => {
  699. const modelDiv = new ModelElement( 'div', null, [
  700. new ModelText( 'foo' ),
  701. new ModelElement( 'image' ),
  702. new ModelText( 'bar' )
  703. ] );
  704. modelRoot.appendChildren( modelDiv );
  705. dispatcher.on( 'insert:div', insertElement( new ViewContainerElement( 'div' ) ) );
  706. dispatcher.on( 'insert:image', insertElement( new ViewContainerElement( 'img' ) ) );
  707. dispatcher.on( 'insert:$text', insertText() );
  708. dispatcher.on( 'remove', remove() );
  709. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  710. const removedNodes = modelDiv.removeChildren( 0, 2 );
  711. modelDoc.graveyard.insertChildren( 0, removedNodes );
  712. dispatcher.convertRemove(
  713. ModelPosition.createFromParentAndOffset( modelDiv, 0 ),
  714. ModelRange.createFromParentsAndOffsets( modelDoc.graveyard, 0, modelDoc.graveyard, 4 )
  715. );
  716. expect( viewToString( viewRoot ) ).to.equal( '<div><div>bar</div></div>' );
  717. } );
  718. it( 'should not execute if value was already consumed', () => {
  719. const modelDiv = new ModelElement( 'div', null, new ModelText( 'foo' ) );
  720. modelRoot.appendChildren( modelDiv );
  721. dispatcher.on( 'insert:div', insertElement( new ViewContainerElement( 'div' ) ) );
  722. dispatcher.on( 'insert:$text', insertText() );
  723. dispatcher.on( 'remove', remove() );
  724. dispatcher.on( 'remove', ( evt, data, consumable ) => {
  725. consumable.consume( data.item, 'remove' );
  726. }, { priority: 'high' } );
  727. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  728. expect( viewToString( viewRoot ) ).to.equal( '<div><div>foo</div></div>' );
  729. const removedNodes = modelDiv.removeChildren( 0, 1 );
  730. modelDoc.graveyard.insertChildren( 0, removedNodes );
  731. dispatcher.convertRemove(
  732. ModelPosition.createFromParentAndOffset( modelDiv, 0 ),
  733. ModelRange.createFromParentsAndOffsets( modelDoc.graveyard, 0, modelDoc.graveyard, 3 )
  734. );
  735. expect( viewToString( viewRoot ) ).to.equal( '<div><div>foo</div></div>' );
  736. } );
  737. it( 'should support unicode', () => {
  738. const modelDiv = new ModelElement( 'div', null, 'நிலைக்கு' );
  739. modelRoot.appendChildren( modelDiv );
  740. dispatcher.on( 'insert:div', insertElement( new ViewContainerElement( 'div' ) ) );
  741. dispatcher.on( 'insert:$text', insertText() );
  742. dispatcher.on( 'remove', remove() );
  743. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  744. modelWriter.remove( ModelRange.createFromParentsAndOffsets( modelDiv, 0, modelDiv, 6 ) );
  745. dispatcher.convertRemove(
  746. ModelPosition.createFromParentAndOffset( modelDiv, 0 ),
  747. ModelRange.createFromParentsAndOffsets( modelDoc.graveyard, 0, modelDoc.graveyard, 6 )
  748. );
  749. expect( viewToString( viewRoot ) ).to.equal( '<div><div>கு</div></div>' );
  750. } );
  751. it( 'should not remove view ui elements that are placed next to removed content', () => {
  752. modelRoot.appendChildren( new ModelText( 'foobar' ) );
  753. viewRoot.appendChildren( [
  754. new ViewText( 'foo' ),
  755. new ViewUIElement( 'span' ),
  756. new ViewText( 'bar' )
  757. ] );
  758. dispatcher.on( 'remove', remove() );
  759. // Remove 'b'.
  760. modelWriter.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 3, modelRoot, 4 ) );
  761. dispatcher.convertRemove(
  762. ModelPosition.createFromParentAndOffset( modelRoot, 3 ),
  763. ModelRange.createFromParentsAndOffsets( modelDoc.graveyard, 0, modelDoc.graveyard, 1 )
  764. );
  765. expect( viewToString( viewRoot ) ).to.equal( '<div>foo<span></span>ar</div>' );
  766. } );
  767. it( 'should remove correct amount of text when it is split by view ui element', () => {
  768. modelRoot.appendChildren( new ModelText( 'foobar' ) );
  769. viewRoot.appendChildren( [
  770. new ViewText( 'foo' ),
  771. new ViewUIElement( 'span' ),
  772. new ViewText( 'bar' )
  773. ] );
  774. dispatcher.on( 'remove', remove() );
  775. // Remove 'o<span></span>b'.
  776. modelWriter.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 2, modelRoot, 4 ) );
  777. dispatcher.convertRemove(
  778. ModelPosition.createFromParentAndOffset( modelRoot, 2 ),
  779. ModelRange.createFromParentsAndOffsets( modelDoc.graveyard, 0, modelDoc.graveyard, 2 )
  780. );
  781. expect( viewToString( viewRoot ) ).to.equal( '<div>foar</div>' );
  782. } );
  783. it( 'should not unbind element that has not been moved to graveyard', () => {
  784. const modelElement = new ModelElement( 'paragraph' );
  785. const viewElement = new ViewContainerElement( 'p' );
  786. modelRoot.appendChildren( [ modelElement, new ModelText( 'b' ) ] );
  787. viewRoot.appendChildren( [ viewElement, new ViewText( 'b' ) ] );
  788. mapper.bindElements( modelElement, viewElement );
  789. dispatcher.on( 'remove', remove() );
  790. // Move <a></a> after "b". Can be e.g. a part of an unwrap delta (move + remove).
  791. modelWriter.move(
  792. ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 1 ),
  793. ModelPosition.createAt( modelRoot, 'end' )
  794. );
  795. dispatcher.convertRemove(
  796. ModelPosition.createFromParentAndOffset( modelRoot, 0 ),
  797. ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 )
  798. );
  799. expect( viewToString( viewRoot ) ).to.equal( '<div>b</div>' );
  800. expect( mapper.toModelElement( viewElement ) ).to.equal( modelElement );
  801. expect( mapper.toViewElement( modelElement ) ).to.equal( viewElement );
  802. } );
  803. it( 'should unbind elements if model element was moved to graveyard', () => {
  804. const modelElement = new ModelElement( 'paragraph' );
  805. const viewElement = new ViewContainerElement( 'p' );
  806. modelRoot.appendChildren( [ modelElement, new ModelText( 'b' ) ] );
  807. viewRoot.appendChildren( [ viewElement, new ViewText( 'b' ) ] );
  808. mapper.bindElements( modelElement, viewElement );
  809. dispatcher.on( 'remove', remove() );
  810. // Remove <a></a>.
  811. modelWriter.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 1 ) );
  812. dispatcher.convertRemove(
  813. ModelPosition.createFromParentAndOffset( modelRoot, 0 ),
  814. ModelRange.createFromParentsAndOffsets( modelDoc.graveyard, 0, modelDoc.graveyard, 1 )
  815. );
  816. expect( viewToString( viewRoot ) ).to.equal( '<div>b</div>' );
  817. expect( mapper.toModelElement( viewElement ) ).to.be.undefined;
  818. expect( mapper.toViewElement( modelElement ) ).to.be.undefined;
  819. } );
  820. // TODO move to conversion/integration.js one day.
  821. it( 'should not break when remove() is used as part of unwrapping', () => {
  822. // The whole process looks like this:
  823. // <w><a></a></w> => <a></a><w><a></a></w> => <a></a><w></w> => <a></a>
  824. // The <a> is duplicated for a while in the view.
  825. const modelAElement = new ModelElement( 'a' );
  826. const modelWElement = new ModelElement( 'w' );
  827. const viewAElement = new ViewContainerElement( 'a' );
  828. const viewA2Element = new ViewContainerElement( 'a2' );
  829. const viewWElement = new ViewContainerElement( 'w' );
  830. modelRoot.appendChildren( modelWElement );
  831. viewRoot.appendChildren( viewWElement );
  832. modelWElement.appendChildren( modelAElement );
  833. viewWElement.appendChildren( viewAElement );
  834. mapper.bindElements( modelWElement, viewWElement );
  835. mapper.bindElements( modelAElement, viewAElement );
  836. dispatcher.on( 'remove', remove() );
  837. dispatcher.on( 'insert', insertElement( () => viewA2Element ) );
  838. modelDoc.on( 'change', ( evt, type, changes ) => {
  839. dispatcher.convertChange( type, changes );
  840. } );
  841. modelWriter.unwrap( modelWElement );
  842. expect( viewToString( viewRoot ) ).to.equal( '<div><a2></a2></div>' );
  843. expect( mapper.toModelElement( viewA2Element ) ).to.equal( modelAElement );
  844. expect( mapper.toViewElement( modelAElement ) ).to.equal( viewA2Element );
  845. // This is a bit unfortunate, but we think we can live with this.
  846. // The viewAElement is not in the tree and there's a high chance that all reference to it are gone.
  847. expect( mapper.toModelElement( viewAElement ) ).to.equal( modelAElement );
  848. expect( mapper.toModelElement( viewWElement ) ).to.be.undefined;
  849. expect( mapper.toViewElement( modelWElement ) ).to.be.undefined;
  850. } );
  851. it( 'should work correctly if container element after ui element is removed', () => {
  852. const modelP1 = new ModelElement( 'paragraph' );
  853. const modelP2 = new ModelElement( 'paragraph' );
  854. const viewP1 = new ViewContainerElement( 'p' );
  855. const viewUi1 = new ViewUIElement( 'span' );
  856. const viewUi2 = new ViewUIElement( 'span' );
  857. const viewP2 = new ViewContainerElement( 'p' );
  858. modelRoot.appendChildren( [ modelP1, modelP2 ] );
  859. viewRoot.appendChildren( [ viewP1, viewUi1, viewUi2, viewP2 ] );
  860. mapper.bindElements( modelP1, viewP1 );
  861. mapper.bindElements( modelP2, viewP2 );
  862. dispatcher.on( 'remove', remove() );
  863. modelWriter.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
  864. dispatcher.convertRemove(
  865. ModelPosition.createFromParentAndOffset( modelRoot, 1 ),
  866. ModelRange.createFromParentsAndOffsets( modelDoc.graveyard, 0, modelDoc.graveyard, 1 )
  867. );
  868. expect( viewToString( viewRoot ) ).to.equal( '<div><p></p><span></span><span></span></div>' );
  869. } );
  870. it( 'should work correctly if container element after text node is removed', () => {
  871. const modelText = new ModelText( 'foo' );
  872. const modelP = new ModelElement( 'paragraph' );
  873. const viewText = new ViewText( 'foo' );
  874. const viewP = new ViewContainerElement( 'p' );
  875. modelRoot.appendChildren( [ modelText, modelP ] );
  876. viewRoot.appendChildren( [ viewText, viewP ] );
  877. mapper.bindElements( modelP, viewP );
  878. dispatcher.on( 'remove', remove() );
  879. modelWriter.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 3, modelRoot, 4 ) );
  880. dispatcher.convertRemove(
  881. ModelPosition.createFromParentAndOffset( modelRoot, 3 ),
  882. ModelRange.createFromParentsAndOffsets( modelDoc.graveyard, 0, modelDoc.graveyard, 1 )
  883. );
  884. expect( viewToString( viewRoot ) ).to.equal( '<div>foo</div>' );
  885. } );
  886. } );
  887. describe( 'createViewElementFromHighlightDescriptor()', () => {
  888. it( 'should return attribute element from descriptor object', () => {
  889. const descriptor = {
  890. class: 'foo-class',
  891. attributes: { one: 1, two: 2 },
  892. priority: 7,
  893. };
  894. const element = createViewElementFromHighlightDescriptor( descriptor );
  895. expect( element.is( 'attributeElement' ) ).to.be.true;
  896. expect( element.name ).to.equal( 'span' );
  897. expect( element.priority ).to.equal( 7 );
  898. expect( element.hasClass( 'foo-class' ) ).to.be.true;
  899. for ( const key of Object.keys( descriptor.attributes ) ) {
  900. expect( element.getAttribute( key ) ).to.equal( descriptor.attributes[ key ] );
  901. }
  902. } );
  903. it( 'should return attribute element from descriptor object - array with classes', () => {
  904. const descriptor = {
  905. class: [ 'foo-class', 'bar-class' ],
  906. attributes: { one: 1, two: 2 },
  907. priority: 7,
  908. };
  909. const element = createViewElementFromHighlightDescriptor( descriptor );
  910. expect( element.is( 'attributeElement' ) ).to.be.true;
  911. expect( element.name ).to.equal( 'span' );
  912. expect( element.priority ).to.equal( 7 );
  913. expect( element.hasClass( 'foo-class' ) ).to.be.true;
  914. expect( element.hasClass( 'bar-class' ) ).to.be.true;
  915. for ( const key of Object.keys( descriptor.attributes ) ) {
  916. expect( element.getAttribute( key ) ).to.equal( descriptor.attributes[ key ] );
  917. }
  918. } );
  919. it( 'should create element without class', () => {
  920. const descriptor = {
  921. attributes: { one: 1, two: 2 },
  922. priority: 7,
  923. };
  924. const element = createViewElementFromHighlightDescriptor( descriptor );
  925. expect( element.is( 'attributeElement' ) ).to.be.true;
  926. expect( element.name ).to.equal( 'span' );
  927. expect( element.priority ).to.equal( 7 );
  928. for ( const key of Object.keys( descriptor.attributes ) ) {
  929. expect( element.getAttribute( key ) ).to.equal( descriptor.attributes[ key ] );
  930. }
  931. } );
  932. it( 'should create element without priority', () => {
  933. const descriptor = {
  934. class: 'foo-class',
  935. attributes: { one: 1, two: 2 },
  936. };
  937. const element = createViewElementFromHighlightDescriptor( descriptor );
  938. expect( element.is( 'attributeElement' ) ).to.be.true;
  939. expect( element.name ).to.equal( 'span' );
  940. expect( element.priority ).to.equal( 10 );
  941. expect( element.hasClass( 'foo-class' ) ).to.be.true;
  942. for ( const key of Object.keys( descriptor.attributes ) ) {
  943. expect( element.getAttribute( key ) ).to.equal( descriptor.attributes[ key ] );
  944. }
  945. } );
  946. it( 'should create element without attributes', () => {
  947. const descriptor = {
  948. class: 'foo-class',
  949. priority: 7
  950. };
  951. const element = createViewElementFromHighlightDescriptor( descriptor );
  952. expect( element.is( 'attributeElement' ) ).to.be.true;
  953. expect( element.name ).to.equal( 'span' );
  954. expect( element.priority ).to.equal( 7 );
  955. expect( element.hasClass( 'foo-class' ) ).to.be.true;
  956. } );
  957. it( 'should create similar elements if they are created using same descriptor id', () => {
  958. const a = createViewElementFromHighlightDescriptor( {
  959. id: 'id',
  960. class: 'classA',
  961. priority: 1
  962. } );
  963. const b = createViewElementFromHighlightDescriptor( {
  964. id: 'id',
  965. class: 'classB',
  966. priority: 2
  967. } );
  968. expect( a.isSimilar( b ) ).to.be.true;
  969. } );
  970. it( 'should create non-similar elements if they have different descriptor id', () => {
  971. const a = createViewElementFromHighlightDescriptor( {
  972. id: 'a',
  973. class: 'foo',
  974. priority: 1
  975. } );
  976. const b = createViewElementFromHighlightDescriptor( {
  977. id: 'b',
  978. class: 'foo',
  979. priority: 1
  980. } );
  981. expect( a.isSimilar( b ) ).to.be.false;
  982. } );
  983. } );
  984. } );