model-to-view-converters.js 41 KB

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