downcast-converters.js 49 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import EditingController from '../../src/controller/editingcontroller';
  6. import Conversion from '../../src/conversion/conversion';
  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 {
  18. downcastElementToElement, downcastAttributeToElement, downcastAttributeToAttribute, downcastMarkerToElement, downcastMarkerToHighlight,
  19. insertElement, insertUIElement, changeAttribute, wrap, removeUIElement,
  20. highlightElement, highlightText, removeHighlight, createViewElementFromHighlightDescriptor
  21. } from '../../src/conversion/downcast-converters';
  22. import { stringify } from '../../src/dev-utils/view';
  23. describe( 'downcast-helpers', () => {
  24. let conversion, model, modelRoot, viewRoot;
  25. beforeEach( () => {
  26. model = new Model();
  27. const modelDoc = model.document;
  28. modelRoot = modelDoc.createRoot();
  29. const controller = new EditingController( model );
  30. // Set name of view root the same as dom root.
  31. // This is a mock of attaching view root to dom root.
  32. controller.view.getRoot()._name = 'div';
  33. viewRoot = controller.view.getRoot();
  34. conversion = new Conversion();
  35. conversion.register( 'downcast', [ controller.downcastDispatcher ] );
  36. } );
  37. describe( 'downcastElementToElement', () => {
  38. it( 'config.view is a string', () => {
  39. const helper = downcastElementToElement( { model: 'paragraph', view: 'p' } );
  40. conversion.for( 'downcast' ).add( helper );
  41. model.change( writer => {
  42. writer.insertElement( 'paragraph', modelRoot, 0 );
  43. } );
  44. expectResult( '<p></p>' );
  45. } );
  46. it( 'can be overwritten using priority', () => {
  47. const helperA = downcastElementToElement( { model: 'paragraph', view: 'p' } );
  48. const helperB = downcastElementToElement( { model: 'paragraph', view: 'foo' }, 'high' );
  49. conversion.for( 'downcast' ).add( helperA ).add( helperB );
  50. model.change( writer => {
  51. writer.insertElement( 'paragraph', modelRoot, 0 );
  52. } );
  53. expectResult( '<foo></foo>' );
  54. } );
  55. it( 'config.view is an element instance', () => {
  56. const helper = downcastElementToElement( {
  57. model: 'paragraph',
  58. view: new ViewContainerElement( 'p' )
  59. } );
  60. conversion.for( 'downcast' ).add( helper );
  61. model.change( writer => {
  62. writer.insertElement( 'paragraph', modelRoot, 0 );
  63. } );
  64. expectResult( '<p></p>' );
  65. } );
  66. it( 'config.view is a view element definition', () => {
  67. const helper = downcastElementToElement( {
  68. model: 'fancyParagraph',
  69. view: {
  70. name: 'p',
  71. class: 'fancy'
  72. }
  73. } );
  74. conversion.for( 'downcast' ).add( helper );
  75. model.change( writer => {
  76. writer.insertElement( 'fancyParagraph', modelRoot, 0 );
  77. } );
  78. expectResult( '<p class="fancy"></p>' );
  79. } );
  80. it( 'config.view is a function', () => {
  81. const helper = downcastElementToElement( {
  82. model: 'heading',
  83. view: modelElement => new ViewContainerElement( 'h' + modelElement.getAttribute( 'level' ) )
  84. } );
  85. conversion.for( 'downcast' ).add( helper );
  86. model.change( writer => {
  87. writer.insertElement( 'heading', { level: 2 }, modelRoot, 0 );
  88. } );
  89. expectResult( '<h2></h2>' );
  90. } );
  91. } );
  92. describe( 'downcastAttributeToElement', () => {
  93. it( 'config.view is a string', () => {
  94. const helper = downcastAttributeToElement( 'bold', { view: 'strong' } );
  95. conversion.for( 'downcast' ).add( helper );
  96. model.change( writer => {
  97. writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
  98. } );
  99. expectResult( '<strong>foo</strong>' );
  100. } );
  101. it( 'can be overwritten using priority', () => {
  102. const helperA = downcastAttributeToElement( 'bold', { view: 'strong' } );
  103. const helperB = downcastAttributeToElement( 'bold', { view: 'b' }, 'high' );
  104. conversion.for( 'downcast' ).add( helperA ).add( helperB );
  105. model.change( writer => {
  106. writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
  107. } );
  108. expectResult( '<b>foo</b>' );
  109. } );
  110. it( 'config.view is an element instance', () => {
  111. const helper = downcastAttributeToElement( 'bold', {
  112. view: new ViewAttributeElement( 'strong' )
  113. } );
  114. conversion.for( 'downcast' ).add( helper );
  115. model.change( writer => {
  116. writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
  117. } );
  118. expectResult( '<strong>foo</strong>' );
  119. } );
  120. it( 'config.view is a view element definition', () => {
  121. const helper = downcastAttributeToElement( 'bold', {
  122. view: {
  123. name: 'span',
  124. class: 'bold'
  125. }
  126. } );
  127. conversion.for( 'downcast' ).add( helper );
  128. model.change( writer => {
  129. writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
  130. } );
  131. expectResult( '<span class="bold">foo</span>' );
  132. } );
  133. it( 'config.view is a view element definition, model attribute value specified', () => {
  134. const helper = downcastAttributeToElement( 'styled', {
  135. model: 'dark',
  136. view: {
  137. name: 'span',
  138. class: [ 'styled', 'styled-dark' ]
  139. }
  140. } );
  141. conversion.for( 'downcast' ).add( helper );
  142. model.change( writer => {
  143. writer.insertText( 'foo', { styled: 'dark' }, modelRoot, 0 );
  144. } );
  145. expectResult( '<span class="styled styled-dark">foo</span>' );
  146. model.change( writer => {
  147. writer.setAttribute( 'styled', 'xyz', modelRoot.getChild( 0 ) );
  148. } );
  149. expectResult( 'foo' );
  150. } );
  151. it( 'multiple config items', () => {
  152. const helper = downcastAttributeToElement( 'fontSize', [
  153. {
  154. model: 'big',
  155. view: {
  156. name: 'span',
  157. style: {
  158. 'font-size': '1.2em'
  159. }
  160. }
  161. },
  162. {
  163. model: 'small',
  164. view: {
  165. name: 'span',
  166. style: {
  167. 'font-size': '0.8em'
  168. }
  169. }
  170. }
  171. ] );
  172. conversion.for( 'downcast' ).add( helper );
  173. model.change( writer => {
  174. writer.insertText( 'foo', { fontSize: 'big' }, modelRoot, 0 );
  175. } );
  176. expectResult( '<span style="font-size:1.2em">foo</span>' );
  177. model.change( writer => {
  178. writer.setAttribute( 'fontSize', 'small', modelRoot.getChild( 0 ) );
  179. } );
  180. expectResult( '<span style="font-size:0.8em">foo</span>' );
  181. model.change( writer => {
  182. writer.removeAttribute( 'fontSize', modelRoot.getChild( 0 ) );
  183. } );
  184. expectResult( 'foo' );
  185. } );
  186. it( 'config.view is a function', () => {
  187. const helper = downcastAttributeToElement( 'bold', {
  188. view: attributeValue => new ViewAttributeElement( 'span', { style: 'font-weight:' + attributeValue } )
  189. } );
  190. conversion.for( 'downcast' ).add( helper );
  191. model.change( writer => {
  192. writer.insertText( 'foo', { bold: '500' }, modelRoot, 0 );
  193. } );
  194. expectResult( '<span style="font-weight:500">foo</span>' );
  195. } );
  196. } );
  197. describe( 'downcastAttributeToAttribute', () => {
  198. beforeEach( () => {
  199. conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'image', view: 'img' } ) );
  200. } );
  201. it( 'config not set', () => {
  202. const helper = downcastAttributeToAttribute( 'src' );
  203. conversion.for( 'downcast' ).add( helper );
  204. model.change( writer => {
  205. writer.insertElement( 'image', { src: 'foo.jpg' }, modelRoot, 0 );
  206. } );
  207. expectResult( '<img src="foo.jpg"></img>' );
  208. } );
  209. it( 'config.view is a string', () => {
  210. const helper = downcastAttributeToAttribute( 'source', { view: 'src' } );
  211. conversion.for( 'downcast' ).add( helper );
  212. model.change( writer => {
  213. writer.insertElement( 'image', { source: 'foo.jpg' }, modelRoot, 0 );
  214. } );
  215. expectResult( '<img src="foo.jpg"></img>' );
  216. } );
  217. it( 'can be overwritten using priority', () => {
  218. const helperA = downcastAttributeToAttribute( 'source', { view: 'href' } );
  219. const helperB = downcastAttributeToAttribute( 'source', { view: 'src' }, 'high' );
  220. conversion.for( 'downcast' ).add( helperA ).add( helperB );
  221. model.change( writer => {
  222. writer.insertElement( 'image', { source: 'foo.jpg' }, modelRoot, 0 );
  223. } );
  224. expectResult( '<img src="foo.jpg"></img>' );
  225. } );
  226. it( 'config.view is an object', () => {
  227. const helper = downcastAttributeToAttribute( 'stylish', { view: { key: 'class', value: 'styled' } } );
  228. conversion.for( 'downcast' ).add( helper );
  229. model.change( writer => {
  230. writer.insertElement( 'image', { stylish: true }, modelRoot, 0 );
  231. } );
  232. expectResult( '<img class="styled"></img>' );
  233. } );
  234. it( 'config.view is an object, model attribute value specified', () => {
  235. const helper = downcastAttributeToAttribute( 'styled', {
  236. model: 'dark',
  237. view: {
  238. key: 'class',
  239. value: 'styled-dark styled'
  240. }
  241. } );
  242. conversion.for( 'downcast' ).add( helper );
  243. model.change( writer => {
  244. writer.insertElement( 'image', { styled: 'dark' }, modelRoot, 0 );
  245. } );
  246. expectResult( '<img class="styled styled-dark"></img>' );
  247. model.change( writer => {
  248. writer.setAttribute( 'styled', 'xyz', modelRoot.getChild( 0 ) );
  249. } );
  250. expectResult( '<img></img>' );
  251. } );
  252. it( 'multiple config items', () => {
  253. const helper = downcastAttributeToAttribute( 'styled', [
  254. {
  255. model: 'dark',
  256. view: {
  257. key: 'class',
  258. value: 'styled-dark'
  259. }
  260. },
  261. {
  262. model: 'light',
  263. view: {
  264. key: 'class',
  265. value: 'styled-light'
  266. }
  267. }
  268. ] );
  269. conversion.for( 'downcast' ).add( helper );
  270. model.change( writer => {
  271. writer.insertElement( 'image', { styled: 'dark' }, modelRoot, 0 );
  272. } );
  273. expectResult( '<img class="styled-dark"></img>' );
  274. model.change( writer => {
  275. writer.setAttribute( 'styled', 'light', modelRoot.getChild( 0 ) );
  276. } );
  277. expectResult( '<img class="styled-light"></img>' );
  278. model.change( writer => {
  279. writer.setAttribute( 'styled', 'xyz', modelRoot.getChild( 0 ) );
  280. } );
  281. expectResult( '<img></img>' );
  282. } );
  283. it( 'config.view is a function', () => {
  284. const helper = downcastAttributeToAttribute( 'styled', {
  285. view: attributeValue => ( { key: 'class', value: 'styled-' + attributeValue } )
  286. } );
  287. conversion.for( 'downcast' ).add( helper );
  288. model.change( writer => {
  289. writer.insertElement( 'image', { styled: 'pull-out' }, modelRoot, 0 );
  290. } );
  291. expectResult( '<img class="styled-pull-out"></img>' );
  292. } );
  293. } );
  294. describe( 'downcastMarkerToElement', () => {
  295. it( 'config.view is a string', () => {
  296. const helper = downcastMarkerToElement( { model: 'search', view: 'marker-search' } );
  297. conversion.for( 'downcast' ).add( helper );
  298. model.change( writer => {
  299. writer.insertText( 'foo', modelRoot, 0 );
  300. writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
  301. } );
  302. expectResult( 'f<marker-search></marker-search>o<marker-search></marker-search>o' );
  303. } );
  304. it( 'can be overwritten using priority', () => {
  305. const helperA = downcastMarkerToElement( { model: 'search', view: 'marker-search' } );
  306. const helperB = downcastMarkerToElement( { model: 'search', view: 'search' }, 'high' );
  307. conversion.for( 'downcast' ).add( helperA ).add( helperB );
  308. model.change( writer => {
  309. writer.insertText( 'foo', modelRoot, 0 );
  310. writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
  311. } );
  312. expectResult( 'f<search></search>o<search></search>o' );
  313. } );
  314. it( 'config.view is an element instance', () => {
  315. const helper = downcastMarkerToElement( {
  316. model: 'search',
  317. view: new ViewUIElement( 'span', { 'data-marker': 'search' } )
  318. } );
  319. conversion.for( 'downcast' ).add( helper );
  320. model.change( writer => {
  321. writer.insertText( 'foo', modelRoot, 0 );
  322. writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
  323. } );
  324. expectResult( 'f<span data-marker="search"></span>o<span data-marker="search"></span>o' );
  325. } );
  326. it( 'config.view is a view element definition', () => {
  327. const helper = downcastMarkerToElement( {
  328. model: 'search',
  329. view: {
  330. name: 'span',
  331. attribute: {
  332. 'data-marker': 'search'
  333. }
  334. }
  335. } );
  336. conversion.for( 'downcast' ).add( helper );
  337. model.change( writer => {
  338. writer.insertText( 'foo', modelRoot, 0 );
  339. writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
  340. } );
  341. expectResult( 'f<span data-marker="search"></span>o<span data-marker="search"></span>o' );
  342. } );
  343. it( 'config.view is a function', () => {
  344. const helper = downcastMarkerToElement( {
  345. model: 'search',
  346. view: data => {
  347. return new ViewUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } );
  348. }
  349. } );
  350. conversion.for( 'downcast' ).add( helper );
  351. model.change( writer => {
  352. writer.insertText( 'foo', modelRoot, 0 );
  353. writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
  354. } );
  355. expectResult( 'f<span data-marker="search" data-start="true"></span>o<span data-marker="search" data-start="false"></span>o' );
  356. } );
  357. } );
  358. describe( 'downcastMarkerToHighlight', () => {
  359. it( 'config.view is a highlight descriptor', () => {
  360. const helper = downcastMarkerToHighlight( { model: 'comment', view: { class: 'comment' } } );
  361. conversion.for( 'downcast' ).add( helper );
  362. model.change( writer => {
  363. writer.insertText( 'foo', modelRoot, 0 );
  364. writer.setMarker( 'comment', ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 3 ) );
  365. } );
  366. expectResult( '<span class="comment">foo</span>' );
  367. } );
  368. it( 'can be overwritten using priority', () => {
  369. const helperA = downcastMarkerToHighlight( { model: 'comment', view: { class: 'comment' } } );
  370. const helperB = downcastMarkerToHighlight( { model: 'comment', view: { class: 'new-comment' } }, 'high' );
  371. conversion.for( 'downcast' ).add( helperA ).add( helperB );
  372. model.change( writer => {
  373. writer.insertText( 'foo', modelRoot, 0 );
  374. writer.setMarker( 'comment', ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 3 ) );
  375. } );
  376. expectResult( '<span class="new-comment">foo</span>' );
  377. } );
  378. it( 'config.view is a function', () => {
  379. const helper = downcastMarkerToHighlight( {
  380. model: 'comment',
  381. view: data => {
  382. const commentType = data.markerName.split( ':' )[ 1 ];
  383. return {
  384. class: [ 'comment', 'comment-' + commentType ]
  385. };
  386. }
  387. } );
  388. conversion.for( 'downcast' ).add( helper );
  389. model.change( writer => {
  390. writer.insertText( 'foo', modelRoot, 0 );
  391. writer.setMarker( 'comment:abc', ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 3 ) );
  392. } );
  393. expectResult( '<span class="comment comment-abc">foo</span>' );
  394. } );
  395. } );
  396. function expectResult( string ) {
  397. expect( stringify( viewRoot, null, { ignoreRoot: true } ) ).to.equal( string );
  398. }
  399. } );
  400. describe( 'downcast-converters', () => {
  401. let dispatcher, modelDoc, modelRoot, viewRoot, controller, modelRootStart, model;
  402. beforeEach( () => {
  403. model = new Model();
  404. modelDoc = model.document;
  405. modelRoot = modelDoc.createRoot();
  406. controller = new EditingController( model );
  407. viewRoot = controller.view.getRoot();
  408. // Set name of view root the same as dom root.
  409. // This is a mock of attaching view root to dom root.
  410. controller.view.getRoot()._name = 'div';
  411. dispatcher = controller.downcastDispatcher;
  412. dispatcher.on( 'insert:paragraph', insertElement( () => new ViewContainerElement( 'p' ) ) );
  413. dispatcher.on( 'attribute:class', changeAttribute() );
  414. modelRootStart = ModelPosition.createAt( modelRoot, 0 );
  415. } );
  416. function viewAttributesToString( item ) {
  417. let result = '';
  418. for ( const key of item.getAttributeKeys() ) {
  419. const value = item.getAttribute( key );
  420. if ( value ) {
  421. result += ' ' + key + '="' + value + '"';
  422. }
  423. }
  424. return result;
  425. }
  426. function viewToString( item ) {
  427. let result = '';
  428. if ( item instanceof ViewText ) {
  429. result = item.data;
  430. } else {
  431. // ViewElement or ViewDocumentFragment.
  432. for ( const child of item.getChildren() ) {
  433. result += viewToString( child );
  434. }
  435. if ( item instanceof ViewElement ) {
  436. result = '<' + item.name + viewAttributesToString( item ) + '>' + result + '</' + item.name + '>';
  437. }
  438. }
  439. return result;
  440. }
  441. describe( 'insertText', () => {
  442. it( 'should downcast text', () => {
  443. model.change( writer => {
  444. writer.insert( new ModelText( 'foobar' ), modelRootStart );
  445. } );
  446. expect( viewToString( viewRoot ) ).to.equal( '<div>foobar</div>' );
  447. } );
  448. it( 'should support unicode', () => {
  449. model.change( writer => {
  450. writer.insert( new ModelText( 'நிலைக்கு' ), modelRootStart );
  451. } );
  452. expect( viewToString( viewRoot ) ).to.equal( '<div>நிலைக்கு</div>' );
  453. } );
  454. it( 'should be possible to override it', () => {
  455. dispatcher.on( 'insert:$text', ( evt, data, consumable ) => {
  456. consumable.consume( data.item, 'insert' );
  457. }, { priority: 'high' } );
  458. model.change( writer => {
  459. writer.insert( new ModelText( 'foobar' ), modelRootStart );
  460. } );
  461. expect( viewToString( viewRoot ) ).to.equal( '<div></div>' );
  462. } );
  463. } );
  464. describe( 'insertElement', () => {
  465. it( 'should convert element insertion in model to and map positions for future converting', () => {
  466. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar' ) );
  467. model.change( writer => {
  468. writer.insert( modelElement, modelRootStart );
  469. } );
  470. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  471. } );
  472. it( 'should take view element function generator as a parameter', () => {
  473. const elementGenerator = ( modelItem, consumable ) => {
  474. if ( consumable.consume( modelItem, 'attribute:nice' ) ) {
  475. return new ViewContainerElement( 'div' );
  476. }
  477. // Test if default converter will be fired for paragraph, if `null` is returned and consumable was not consumed.
  478. return null;
  479. };
  480. dispatcher.on( 'insert:paragraph', insertElement( elementGenerator ), { priority: 'high' } );
  481. const niceP = new ModelElement( 'paragraph', { nice: true }, new ModelText( 'foo' ) );
  482. const badP = new ModelElement( 'paragraph', null, new ModelText( 'bar' ) );
  483. model.change( writer => {
  484. writer.insert( [ niceP, badP ], modelRootStart );
  485. } );
  486. expect( viewToString( viewRoot ) ).to.equal( '<div><div>foo</div><p>bar</p></div>' );
  487. } );
  488. } );
  489. describe( 'setAttribute', () => {
  490. it( 'should convert attribute insert/change/remove on a model node', () => {
  491. const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) );
  492. model.change( writer => {
  493. writer.insert( modelElement, modelRootStart );
  494. } );
  495. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="foo">foobar</p></div>' );
  496. model.change( writer => {
  497. writer.setAttribute( 'class', 'bar', modelElement );
  498. } );
  499. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="bar">foobar</p></div>' );
  500. model.change( writer => {
  501. writer.removeAttribute( 'class', modelElement );
  502. } );
  503. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  504. } );
  505. it( 'should convert insert/change/remove with attribute generating function as a parameter', () => {
  506. const themeConverter = ( value, data ) => {
  507. if ( data.item instanceof ModelElement && data.item.childCount > 0 ) {
  508. value += ' fix-content';
  509. }
  510. return { key: 'class', value };
  511. };
  512. dispatcher.on( 'insert:div', insertElement( new ViewContainerElement( 'div' ) ) );
  513. dispatcher.on( 'attribute:theme', changeAttribute( themeConverter ) );
  514. const modelParagraph = new ModelElement( 'paragraph', { theme: 'nice' }, new ModelText( 'foobar' ) );
  515. const modelDiv = new ModelElement( 'div', { theme: 'nice' } );
  516. model.change( writer => {
  517. writer.insert( [ modelParagraph, modelDiv ], modelRootStart );
  518. } );
  519. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="nice fix-content">foobar</p><div class="nice"></div></div>' );
  520. model.change( writer => {
  521. writer.setAttribute( 'theme', 'awesome', modelParagraph );
  522. } );
  523. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="awesome fix-content">foobar</p><div class="nice"></div></div>' );
  524. model.change( writer => {
  525. writer.removeAttribute( 'theme', modelParagraph );
  526. } );
  527. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p><div class="nice"></div></div>' );
  528. } );
  529. it( 'should be possible to override setAttribute', () => {
  530. const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) );
  531. dispatcher.on( 'attribute:class', ( evt, data, consumable ) => {
  532. consumable.consume( data.item, 'attribute:class' );
  533. }, { priority: 'high' } );
  534. model.change( writer => {
  535. writer.insert( modelElement, modelRootStart );
  536. } );
  537. // No attribute set.
  538. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  539. } );
  540. } );
  541. describe( 'wrap', () => {
  542. it( 'should convert insert/change/remove of attribute in model into wrapping element in a view', () => {
  543. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) );
  544. const viewB = new ViewAttributeElement( 'b' );
  545. dispatcher.on( 'attribute:bold', wrap( viewB ) );
  546. model.change( writer => {
  547. writer.insert( modelElement, modelRootStart );
  548. } );
  549. expect( viewToString( viewRoot ) ).to.equal( '<div><p><b>foobar</b></p></div>' );
  550. model.change( writer => {
  551. writer.removeAttribute( 'bold', ModelRange.createIn( modelElement ) );
  552. } );
  553. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  554. } );
  555. it( 'should convert insert/remove of attribute in model with wrapping element generating function as a parameter', () => {
  556. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { style: 'bold' } ) );
  557. const elementGenerator = value => {
  558. if ( value == 'bold' ) {
  559. return new ViewAttributeElement( 'b' );
  560. }
  561. };
  562. dispatcher.on( 'attribute:style', wrap( elementGenerator ) );
  563. model.change( writer => {
  564. writer.insert( modelElement, modelRootStart );
  565. } );
  566. expect( viewToString( viewRoot ) ).to.equal( '<div><p><b>foobar</b></p></div>' );
  567. model.change( writer => {
  568. writer.removeAttribute( 'style', ModelRange.createIn( modelElement ) );
  569. } );
  570. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  571. } );
  572. it( 'should update range on re-wrapping attribute (#475)', () => {
  573. const modelElement = new ModelElement( 'paragraph', null, [
  574. new ModelText( 'x' ),
  575. new ModelText( 'foo', { link: 'http://foo.com' } ),
  576. new ModelText( 'x' )
  577. ] );
  578. const elementGenerator = href => new ViewAttributeElement( 'a', { href } );
  579. dispatcher.on( 'attribute:link', wrap( elementGenerator ) );
  580. model.change( writer => {
  581. writer.insert( modelElement, modelRootStart );
  582. } );
  583. expect( viewToString( viewRoot ) ).to.equal( '<div><p>x<a href="http://foo.com">foo</a>x</p></div>' );
  584. // Set new attribute on old link but also on non-linked characters.
  585. model.change( writer => {
  586. writer.setAttribute( 'link', 'http://foobar.com', ModelRange.createIn( modelElement ) );
  587. } );
  588. expect( viewToString( viewRoot ) ).to.equal( '<div><p><a href="http://foobar.com">xfoox</a></p></div>' );
  589. } );
  590. it( 'should support unicode', () => {
  591. const modelElement = new ModelElement( 'paragraph', null, [ 'நி', new ModelText( 'லைக்', { bold: true } ), 'கு' ] );
  592. const viewB = new ViewAttributeElement( 'b' );
  593. dispatcher.on( 'attribute:bold', wrap( viewB ) );
  594. model.change( writer => {
  595. writer.insert( modelElement, modelRootStart );
  596. } );
  597. expect( viewToString( viewRoot ) ).to.equal( '<div><p>நி<b>லைக்</b>கு</p></div>' );
  598. model.change( writer => {
  599. writer.removeAttribute( 'bold', ModelRange.createIn( modelElement ) );
  600. } );
  601. expect( viewToString( viewRoot ) ).to.equal( '<div><p>நிலைக்கு</p></div>' );
  602. } );
  603. it( 'should be possible to override wrap', () => {
  604. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) );
  605. const viewB = new ViewAttributeElement( 'b' );
  606. dispatcher.on( 'attribute:bold', wrap( viewB ) );
  607. dispatcher.on( 'attribute:bold', ( evt, data, consumable ) => {
  608. consumable.consume( data.item, 'attribute:bold' );
  609. }, { priority: 'high' } );
  610. model.change( writer => {
  611. writer.insert( modelElement, modelRootStart );
  612. } );
  613. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  614. } );
  615. it( 'should not convert and not consume if creator function returned null', () => {
  616. const elementGenerator = () => null;
  617. sinon.spy( dispatcher, 'fire' );
  618. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { italic: true } ) );
  619. dispatcher.on( 'attribute:italic', wrap( elementGenerator ) );
  620. dispatcher.on( 'attribute:italic', ( evt, data, consumable ) => {
  621. expect( consumable.test( data.item, 'attribute:italic' ) ).to.be.true;
  622. } );
  623. model.change( writer => {
  624. writer.insert( modelElement, modelRootStart );
  625. } );
  626. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  627. expect( dispatcher.fire.calledWith( 'attribute:italic:$text' ) ).to.be.true;
  628. } );
  629. } );
  630. describe( 'insertUIElement/removeUIElement', () => {
  631. let modelText, modelElement, range;
  632. beforeEach( () => {
  633. modelText = new ModelText( 'foobar' );
  634. modelElement = new ModelElement( 'paragraph', null, modelText );
  635. model.change( writer => {
  636. writer.insert( modelElement, modelRootStart );
  637. } );
  638. } );
  639. describe( 'collapsed range', () => {
  640. beforeEach( () => {
  641. range = ModelRange.createFromParentsAndOffsets( modelElement, 3, modelElement, 3 );
  642. } );
  643. it( 'should insert and remove ui element - element as a creator', () => {
  644. const viewUi = new ViewUIElement( 'span', { 'class': 'marker' } );
  645. dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
  646. dispatcher.on( 'removeMarker:marker', removeUIElement( viewUi ) );
  647. model.change( writer => {
  648. writer.setMarker( 'marker', range );
  649. } );
  650. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo<span class="marker"></span>bar</p></div>' );
  651. model.change( writer => {
  652. writer.removeMarker( 'marker' );
  653. } );
  654. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  655. } );
  656. it( 'should insert and remove ui element - function as a creator', () => {
  657. const viewUi = new ViewUIElement( 'span', { 'class': 'marker' } );
  658. dispatcher.on( 'addMarker:marker', insertUIElement( () => viewUi ) );
  659. dispatcher.on( 'removeMarker:marker', removeUIElement( () => viewUi ) );
  660. model.change( writer => {
  661. writer.setMarker( 'marker', range );
  662. } );
  663. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo<span class="marker"></span>bar</p></div>' );
  664. model.change( writer => {
  665. writer.removeMarker( 'marker' );
  666. } );
  667. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  668. } );
  669. it( 'should not convert if consumable was consumed', () => {
  670. const viewUi = new ViewUIElement( 'span', { 'class': 'marker' } );
  671. sinon.spy( dispatcher, 'fire' );
  672. dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
  673. dispatcher.on( 'addMarker:marker', ( evt, data, consumable ) => {
  674. consumable.consume( data.markerRange, 'addMarker:marker' );
  675. }, { priority: 'high' } );
  676. dispatcher.convertMarkerAdd( 'marker', range );
  677. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  678. expect( dispatcher.fire.calledWith( 'addMarker:marker' ) );
  679. } );
  680. it( 'should not convert if creator returned null', () => {
  681. dispatcher.on( 'addMarker:marker', insertUIElement( () => null ) );
  682. dispatcher.on( 'removeMarker:marker', removeUIElement( () => null ) );
  683. model.change( writer => {
  684. writer.setMarker( 'marker', range );
  685. } );
  686. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  687. model.change( writer => {
  688. writer.removeMarker( 'marker' );
  689. } );
  690. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  691. } );
  692. } );
  693. describe( 'non-collapsed range', () => {
  694. beforeEach( () => {
  695. range = ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 5 );
  696. } );
  697. it( 'should insert and remove ui element - element as a creator', () => {
  698. const viewUi = new ViewUIElement( 'span', { 'class': 'marker' } );
  699. dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
  700. dispatcher.on( 'removeMarker:marker', removeUIElement( viewUi ) );
  701. model.change( writer => {
  702. writer.setMarker( 'marker', range );
  703. } );
  704. expect( viewToString( viewRoot ) )
  705. .to.equal( '<div><p>fo<span class="marker"></span>oba<span class="marker"></span>r</p></div>' );
  706. model.change( writer => {
  707. writer.removeMarker( 'marker' );
  708. } );
  709. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  710. } );
  711. it( 'should insert and remove ui element - function as a creator', () => {
  712. const viewUi = data => new ViewUIElement( 'span', { 'class': data.markerName } );
  713. dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
  714. dispatcher.on( 'removeMarker:marker', removeUIElement( viewUi ) );
  715. model.change( writer => {
  716. writer.setMarker( 'marker', range );
  717. } );
  718. expect( viewToString( viewRoot ) )
  719. .to.equal( '<div><p>fo<span class="marker"></span>oba<span class="marker"></span>r</p></div>' );
  720. model.change( writer => {
  721. writer.removeMarker( 'marker' );
  722. } );
  723. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  724. } );
  725. it( 'should insert and remove different opening and ending element', () => {
  726. function creator( data ) {
  727. if ( data.isOpening ) {
  728. return new ViewUIElement( 'span', { 'class': data.markerName, 'data-start': true } );
  729. }
  730. return new ViewUIElement( 'span', { 'class': data.markerName, 'data-end': true } );
  731. }
  732. dispatcher.on( 'addMarker:marker', insertUIElement( creator ) );
  733. dispatcher.on( 'removeMarker:marker', removeUIElement( creator ) );
  734. model.change( writer => {
  735. writer.setMarker( 'marker', range );
  736. } );
  737. expect( viewToString( viewRoot ) ).to.equal(
  738. '<div><p>fo<span class="marker" data-start="true"></span>oba<span class="marker" data-end="true"></span>r</p></div>'
  739. );
  740. model.change( writer => {
  741. writer.removeMarker( 'marker' );
  742. } );
  743. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  744. } );
  745. it( 'should not convert if consumable was consumed', () => {
  746. const viewUi = new ViewUIElement( 'span', { 'class': 'marker' } );
  747. sinon.spy( dispatcher, 'fire' );
  748. dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
  749. dispatcher.on( 'addMarker:marker', ( evt, data, consumable ) => {
  750. consumable.consume( data.item, 'addMarker:marker' );
  751. }, { priority: 'high' } );
  752. dispatcher.convertMarkerAdd( 'marker', range );
  753. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  754. expect( dispatcher.fire.calledWith( 'addMarker:marker' ) );
  755. } );
  756. } );
  757. } );
  758. // Remove converter is by default already added in `EditingController` instance.
  759. describe( 'remove', () => {
  760. it( 'should remove items from view accordingly to changes in model #1', () => {
  761. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar' ) );
  762. model.change( writer => {
  763. writer.insert( modelElement, modelRootStart );
  764. } );
  765. model.change( writer => {
  766. writer.remove( ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 4 ) );
  767. } );
  768. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foar</p></div>' );
  769. } );
  770. it( 'should be possible to overwrite', () => {
  771. dispatcher.on( 'remove', evt => evt.stop(), { priority: 'high' } );
  772. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar' ) );
  773. model.change( writer => {
  774. writer.insert( modelElement, modelRootStart );
  775. } );
  776. model.change( writer => {
  777. writer.remove( ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 4 ) );
  778. } );
  779. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  780. } );
  781. it( 'should support unicode', () => {
  782. const modelElement = new ModelElement( 'paragraph', null, 'நிலைக்கு' );
  783. model.change( writer => {
  784. writer.insert( modelElement, modelRootStart );
  785. } );
  786. model.change( writer => {
  787. writer.remove( ModelRange.createFromParentsAndOffsets( modelElement, 0, modelElement, 6 ) );
  788. } );
  789. expect( viewToString( viewRoot ) ).to.equal( '<div><p>கு</p></div>' );
  790. } );
  791. it( 'should not remove view ui elements that are placed next to removed content', () => {
  792. modelRoot.appendChildren( new ModelText( 'fozbar' ) );
  793. viewRoot.appendChildren( [
  794. new ViewText( 'foz' ),
  795. new ViewUIElement( 'span' ),
  796. new ViewText( 'bar' )
  797. ] );
  798. // Remove 'b'.
  799. model.change( writer => {
  800. writer.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 3, modelRoot, 4 ) );
  801. } );
  802. expect( viewToString( viewRoot ) ).to.equal( '<div>foz<span></span>ar</div>' );
  803. // Remove 'z'.
  804. model.change( writer => {
  805. writer.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 2, modelRoot, 3 ) );
  806. } );
  807. expect( viewToString( viewRoot ) ).to.equal( '<div>fo<span></span>ar</div>' );
  808. } );
  809. it( 'should remove correct amount of text when it is split by view ui element', () => {
  810. modelRoot.appendChildren( new ModelText( 'fozbar' ) );
  811. viewRoot.appendChildren( [
  812. new ViewText( 'foz' ),
  813. new ViewUIElement( 'span' ),
  814. new ViewText( 'bar' )
  815. ] );
  816. // Remove 'z<span></span>b'.
  817. model.change( writer => {
  818. writer.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 2, modelRoot, 4 ) );
  819. } );
  820. expect( viewToString( viewRoot ) ).to.equal( '<div>foar</div>' );
  821. } );
  822. it( 'should unbind elements', () => {
  823. const modelElement = new ModelElement( 'paragraph' );
  824. model.change( writer => {
  825. writer.insert( modelElement, modelRootStart );
  826. } );
  827. const viewElement = controller.mapper.toViewElement( modelElement );
  828. expect( viewElement ).not.to.be.undefined;
  829. expect( controller.mapper.toModelElement( viewElement ) ).to.equal( modelElement );
  830. model.change( writer => {
  831. writer.remove( modelElement );
  832. } );
  833. expect( controller.mapper.toViewElement( modelElement ) ).to.be.undefined;
  834. expect( controller.mapper.toModelElement( viewElement ) ).to.be.undefined;
  835. } );
  836. it( 'should not break when remove() is used as part of unwrapping', () => {
  837. const modelP = new ModelElement( 'paragraph', null, new ModelText( 'foo' ) );
  838. const modelWidget = new ModelElement( 'widget', null, modelP );
  839. dispatcher.on( 'insert:widget', insertElement( () => new ViewContainerElement( 'widget' ) ) );
  840. model.change( writer => {
  841. writer.insert( modelWidget, modelRootStart );
  842. } );
  843. expect( viewToString( viewRoot ) ).to.equal( '<div><widget><p>foo</p></widget></div>' );
  844. const viewP = controller.mapper.toViewElement( modelP );
  845. expect( viewP ).not.to.be.undefined;
  846. model.change( writer => {
  847. writer.unwrap( modelWidget );
  848. } );
  849. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p></div>' );
  850. // `modelP` is now bound with newly created view element.
  851. expect( controller.mapper.toViewElement( modelP ) ).not.to.equal( viewP );
  852. // `viewP` is no longer bound with model element.
  853. expect( controller.mapper.toModelElement( viewP ) ).to.be.undefined;
  854. // View element from view root is bound to `modelP`.
  855. expect( controller.mapper.toModelElement( viewRoot.getChild( 0 ) ) ).to.equal( modelP );
  856. } );
  857. it( 'should work correctly if container element after ui element is removed', () => {
  858. // Prepare a model and view structure.
  859. // This is done outside of conversion to put view ui elements inside easily.
  860. const modelP1 = new ModelElement( 'paragraph' );
  861. const modelP2 = new ModelElement( 'paragraph' );
  862. const viewP1 = new ViewContainerElement( 'p' );
  863. const viewUi1 = new ViewUIElement( 'span' );
  864. const viewUi2 = new ViewUIElement( 'span' );
  865. const viewP2 = new ViewContainerElement( 'p' );
  866. modelRoot.appendChildren( [ modelP1, modelP2 ] );
  867. viewRoot.appendChildren( [ viewP1, viewUi1, viewUi2, viewP2 ] );
  868. controller.mapper.bindElements( modelP1, viewP1 );
  869. controller.mapper.bindElements( modelP2, viewP2 );
  870. // Remove second paragraph element.
  871. model.change( writer => {
  872. writer.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
  873. } );
  874. expect( viewToString( viewRoot ) ).to.equal( '<div><p></p><span></span><span></span></div>' );
  875. } );
  876. it( 'should work correctly if container element after text node is removed', () => {
  877. const modelText = new ModelText( 'foo' );
  878. const modelP = new ModelElement( 'paragraph' );
  879. model.change( writer => {
  880. writer.insert( [ modelText, modelP ], modelRootStart );
  881. } );
  882. model.change( writer => {
  883. writer.remove( modelP );
  884. } );
  885. expect( viewToString( viewRoot ) ).to.equal( '<div>foo</div>' );
  886. } );
  887. } );
  888. describe( 'highlight', () => {
  889. describe( 'on text', () => {
  890. const highlightDescriptor = {
  891. class: 'highlight-class',
  892. priority: 7,
  893. attributes: { title: 'title' }
  894. };
  895. let markerRange;
  896. beforeEach( () => {
  897. const modelElement1 = new ModelElement( 'paragraph', null, new ModelText( 'foo' ) );
  898. const modelElement2 = new ModelElement( 'paragraph', null, new ModelText( 'bar' ) );
  899. model.change( writer => {
  900. writer.insert( [ modelElement1, modelElement2 ], modelRootStart );
  901. } );
  902. markerRange = ModelRange.createIn( modelRoot );
  903. } );
  904. it( 'should wrap and unwrap text nodes', () => {
  905. dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) );
  906. dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) );
  907. dispatcher.on( 'removeMarker:marker', removeHighlight( highlightDescriptor ) );
  908. model.change( writer => {
  909. writer.setMarker( 'marker', markerRange );
  910. } );
  911. expect( viewToString( viewRoot ) ).to.equal(
  912. '<div>' +
  913. '<p>' +
  914. '<span class="highlight-class" title="title">foo</span>' +
  915. '</p>' +
  916. '<p>' +
  917. '<span class="highlight-class" title="title">bar</span>' +
  918. '</p>' +
  919. '</div>'
  920. );
  921. model.change( writer => {
  922. writer.removeMarker( 'marker' );
  923. } );
  924. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  925. } );
  926. it( 'should be possible to overwrite', () => {
  927. dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) );
  928. dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) );
  929. dispatcher.on( 'removeMarker:marker', removeHighlight( highlightDescriptor ) );
  930. const newDescriptor = { class: 'override-class' };
  931. dispatcher.on( 'addMarker:marker', highlightText( newDescriptor ), { priority: 'high' } );
  932. dispatcher.on( 'addMarker:marker', highlightElement( newDescriptor ), { priority: 'high' } );
  933. dispatcher.on( 'removeMarker:marker', removeHighlight( newDescriptor ), { priority: 'high' } );
  934. model.change( writer => {
  935. writer.setMarker( 'marker', markerRange );
  936. } );
  937. expect( viewToString( viewRoot ) ).to.equal(
  938. '<div>' +
  939. '<p>' +
  940. '<span class="override-class">foo</span>' +
  941. '</p>' +
  942. '<p>' +
  943. '<span class="override-class">bar</span>' +
  944. '</p>' +
  945. '</div>'
  946. );
  947. model.change( writer => {
  948. writer.removeMarker( 'marker' );
  949. } );
  950. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  951. } );
  952. it( 'should do nothing if descriptor is not provided or generating function returns null', () => {
  953. dispatcher.on( 'addMarker:marker', highlightText( () => null ), { priority: 'high' } );
  954. dispatcher.on( 'addMarker:marker', highlightElement( () => null ), { priority: 'high' } );
  955. dispatcher.on( 'removeMarker:marker', removeHighlight( () => null ), { priority: 'high' } );
  956. model.change( writer => {
  957. writer.setMarker( 'marker', markerRange );
  958. } );
  959. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  960. model.change( writer => {
  961. writer.removeMarker( 'marker' );
  962. } );
  963. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  964. } );
  965. it( 'should do nothing if collapsed marker is converted', () => {
  966. const descriptor = { class: 'foo' };
  967. dispatcher.on( 'addMarker:marker', highlightText( descriptor ), { priority: 'high' } );
  968. dispatcher.on( 'addMarker:marker', highlightElement( descriptor ), { priority: 'high' } );
  969. dispatcher.on( 'removeMarker:marker', removeHighlight( descriptor ), { priority: 'high' } );
  970. markerRange = ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 0 );
  971. model.change( () => {
  972. model.markers.set( 'marker', markerRange );
  973. } );
  974. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  975. model.change( () => {
  976. model.markers.remove( 'marker' );
  977. } );
  978. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  979. } );
  980. } );
  981. describe( 'on element', () => {
  982. const highlightDescriptor = {
  983. class: 'highlight-class',
  984. priority: 7,
  985. attributes: { title: 'title' },
  986. id: 'customId'
  987. };
  988. let markerRange;
  989. beforeEach( () => {
  990. // Provide converter for div element. View div element will have custom highlight handling.
  991. dispatcher.on( 'insert:div', insertElement( () => {
  992. const viewContainer = new ViewContainerElement( 'div' );
  993. viewContainer.setCustomProperty( 'addHighlight', ( element, descriptor ) => {
  994. element.addClass( descriptor.class );
  995. } );
  996. viewContainer.setCustomProperty( 'removeHighlight', element => {
  997. element.setAttribute( 'class', '' );
  998. } );
  999. return viewContainer;
  1000. } ) );
  1001. const modelElement = new ModelElement( 'div', null, new ModelText( 'foo' ) );
  1002. model.change( writer => {
  1003. writer.insert( modelElement, modelRootStart );
  1004. } );
  1005. markerRange = ModelRange.createOn( modelElement );
  1006. dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) );
  1007. dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) );
  1008. dispatcher.on( 'removeMarker:marker', removeHighlight( highlightDescriptor ) );
  1009. } );
  1010. it( 'should use addHighlight and removeHighlight on elements and not convert children nodes', () => {
  1011. model.change( writer => {
  1012. writer.setMarker( 'marker', markerRange );
  1013. } );
  1014. expect( viewToString( viewRoot ) ).to.equal(
  1015. '<div>' +
  1016. '<div class="highlight-class">' +
  1017. 'foo' +
  1018. '</div>' +
  1019. '</div>'
  1020. );
  1021. model.change( writer => {
  1022. writer.removeMarker( 'marker' );
  1023. } );
  1024. expect( viewToString( viewRoot ) ).to.equal( '<div><div>foo</div></div>' );
  1025. } );
  1026. it( 'should be possible to override', () => {
  1027. const newDescriptor = { class: 'override-class' };
  1028. dispatcher.on( 'addMarker:marker', highlightText( newDescriptor ), { priority: 'high' } );
  1029. dispatcher.on( 'addMarker:marker', highlightElement( newDescriptor ), { priority: 'high' } );
  1030. dispatcher.on( 'removeMarker:marker', removeHighlight( newDescriptor ), { priority: 'high' } );
  1031. model.change( writer => {
  1032. writer.setMarker( 'marker', markerRange );
  1033. } );
  1034. expect( viewToString( viewRoot ) ).to.equal(
  1035. '<div>' +
  1036. '<div class="override-class">' +
  1037. 'foo' +
  1038. '</div>' +
  1039. '</div>'
  1040. );
  1041. model.change( writer => {
  1042. writer.removeMarker( 'marker' );
  1043. } );
  1044. expect( viewToString( viewRoot ) ).to.equal( '<div><div>foo</div></div>' );
  1045. } );
  1046. it( 'should use default priority and id if not provided', () => {
  1047. const viewDiv = viewRoot.getChild( 0 );
  1048. dispatcher.on( 'addMarker:marker2', highlightText( () => null ) );
  1049. dispatcher.on( 'addMarker:marker2', highlightElement( () => null ) );
  1050. dispatcher.on( 'removeMarker:marker2', removeHighlight( () => null ) );
  1051. viewDiv.setCustomProperty( 'addHighlight', ( element, descriptor ) => {
  1052. expect( descriptor.priority ).to.equal( 10 );
  1053. expect( descriptor.id ).to.equal( 'marker:foo-bar-baz' );
  1054. } );
  1055. viewDiv.setCustomProperty( 'removeHighlight', ( element, id ) => {
  1056. expect( id ).to.equal( 'marker:foo-bar-baz' );
  1057. } );
  1058. model.change( writer => {
  1059. writer.setMarker( 'marker2', markerRange );
  1060. } );
  1061. } );
  1062. it( 'should do nothing if descriptor is not provided', () => {
  1063. dispatcher.on( 'addMarker:marker2', highlightText( () => null ) );
  1064. dispatcher.on( 'addMarker:marker2', highlightElement( () => null ) );
  1065. dispatcher.on( 'removeMarker:marker2', removeHighlight( () => null ) );
  1066. model.change( writer => {
  1067. writer.setMarker( 'marker2', markerRange );
  1068. } );
  1069. expect( viewToString( viewRoot ) ).to.equal( '<div><div>foo</div></div>' );
  1070. model.change( writer => {
  1071. writer.removeMarker( 'marker2' );
  1072. } );
  1073. expect( viewToString( viewRoot ) ).to.equal( '<div><div>foo</div></div>' );
  1074. } );
  1075. } );
  1076. } );
  1077. describe( 'createViewElementFromHighlightDescriptor()', () => {
  1078. it( 'should return attribute element from descriptor object', () => {
  1079. const descriptor = {
  1080. class: 'foo-class',
  1081. attributes: { one: '1', two: '2' },
  1082. priority: 7,
  1083. };
  1084. const element = createViewElementFromHighlightDescriptor( descriptor );
  1085. expect( element.is( 'attributeElement' ) ).to.be.true;
  1086. expect( element.name ).to.equal( 'span' );
  1087. expect( element.priority ).to.equal( 7 );
  1088. expect( element.hasClass( 'foo-class' ) ).to.be.true;
  1089. for ( const key of Object.keys( descriptor.attributes ) ) {
  1090. expect( element.getAttribute( key ) ).to.equal( descriptor.attributes[ key ] );
  1091. }
  1092. } );
  1093. it( 'should return attribute element from descriptor object - array with classes', () => {
  1094. const descriptor = {
  1095. class: [ 'foo-class', 'bar-class' ],
  1096. attributes: { one: '1', two: '2' },
  1097. priority: 7,
  1098. };
  1099. const element = createViewElementFromHighlightDescriptor( descriptor );
  1100. expect( element.is( 'attributeElement' ) ).to.be.true;
  1101. expect( element.name ).to.equal( 'span' );
  1102. expect( element.priority ).to.equal( 7 );
  1103. expect( element.hasClass( 'foo-class' ) ).to.be.true;
  1104. expect( element.hasClass( 'bar-class' ) ).to.be.true;
  1105. for ( const key of Object.keys( descriptor.attributes ) ) {
  1106. expect( element.getAttribute( key ) ).to.equal( descriptor.attributes[ key ] );
  1107. }
  1108. } );
  1109. it( 'should create element without class', () => {
  1110. const descriptor = {
  1111. attributes: { one: '1', two: '2' },
  1112. priority: 7,
  1113. };
  1114. const element = createViewElementFromHighlightDescriptor( descriptor );
  1115. expect( element.is( 'attributeElement' ) ).to.be.true;
  1116. expect( element.name ).to.equal( 'span' );
  1117. expect( element.priority ).to.equal( 7 );
  1118. for ( const key of Object.keys( descriptor.attributes ) ) {
  1119. expect( element.getAttribute( key ) ).to.equal( descriptor.attributes[ key ] );
  1120. }
  1121. } );
  1122. it( 'should create element without priority', () => {
  1123. const descriptor = {
  1124. class: 'foo-class',
  1125. attributes: { one: '1', two: '2' },
  1126. };
  1127. const element = createViewElementFromHighlightDescriptor( descriptor );
  1128. expect( element.is( 'attributeElement' ) ).to.be.true;
  1129. expect( element.name ).to.equal( 'span' );
  1130. expect( element.priority ).to.equal( 10 );
  1131. expect( element.hasClass( 'foo-class' ) ).to.be.true;
  1132. for ( const key of Object.keys( descriptor.attributes ) ) {
  1133. expect( element.getAttribute( key ) ).to.equal( descriptor.attributes[ key ] );
  1134. }
  1135. } );
  1136. it( 'should create element without attributes', () => {
  1137. const descriptor = {
  1138. class: 'foo-class',
  1139. priority: 7
  1140. };
  1141. const element = createViewElementFromHighlightDescriptor( descriptor );
  1142. expect( element.is( 'attributeElement' ) ).to.be.true;
  1143. expect( element.name ).to.equal( 'span' );
  1144. expect( element.priority ).to.equal( 7 );
  1145. expect( element.hasClass( 'foo-class' ) ).to.be.true;
  1146. } );
  1147. it( 'should create similar elements if they are created using same descriptor id', () => {
  1148. const a = createViewElementFromHighlightDescriptor( {
  1149. id: 'id',
  1150. class: 'classA',
  1151. priority: 1
  1152. } );
  1153. const b = createViewElementFromHighlightDescriptor( {
  1154. id: 'id',
  1155. class: 'classB',
  1156. priority: 2
  1157. } );
  1158. expect( a.isSimilar( b ) ).to.be.true;
  1159. } );
  1160. it( 'should create non-similar elements if they have different descriptor id', () => {
  1161. const a = createViewElementFromHighlightDescriptor( {
  1162. id: 'a',
  1163. class: 'foo',
  1164. priority: 1
  1165. } );
  1166. const b = createViewElementFromHighlightDescriptor( {
  1167. id: 'b',
  1168. class: 'foo',
  1169. priority: 1
  1170. } );
  1171. expect( a.isSimilar( b ) ).to.be.false;
  1172. } );
  1173. } );
  1174. } );