8
0

downcasthelpers.js 55 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839
  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 ViewElement from '../../src/view/element';
  11. import ViewAttributeElement from '../../src/view/attributeelement';
  12. import ViewContainerElement from '../../src/view/containerelement';
  13. import ViewUIElement from '../../src/view/uielement';
  14. import ViewText from '../../src/view/text';
  15. import log from '@ckeditor/ckeditor5-utils/src/log';
  16. import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
  17. import DowncastHelpers, { createViewElementFromHighlightDescriptor } from '../../src/conversion/downcasthelpers';
  18. import { stringify } from '../../src/dev-utils/view';
  19. describe( 'DowncastHelpers', () => {
  20. let conversion, model, modelRoot, viewRoot, downcastHelpers, controller;
  21. let modelRootStart;
  22. beforeEach( () => {
  23. model = new Model();
  24. const modelDoc = model.document;
  25. modelRoot = modelDoc.createRoot();
  26. controller = new EditingController( model );
  27. // Set name of view root the same as dom root.
  28. // This is a mock of attaching view root to dom root.
  29. controller.view.document.getRoot()._name = 'div';
  30. viewRoot = controller.view.document.getRoot();
  31. downcastHelpers = new DowncastHelpers( controller.downcastDispatcher );
  32. conversion = new Conversion();
  33. conversion.register( 'downcast', downcastHelpers );
  34. modelRootStart = model.createPositionAt( modelRoot, 0 );
  35. } );
  36. describe( 'elementToElement()', () => {
  37. it( 'should be chainable', () => {
  38. expect( downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ) ).to.equal( downcastHelpers );
  39. } );
  40. it( 'config.view is a string', () => {
  41. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  42. model.change( writer => {
  43. writer.insertElement( 'paragraph', modelRoot, 0 );
  44. } );
  45. expectResult( '<p></p>' );
  46. } );
  47. it( 'can be overwritten using converterPriority', () => {
  48. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  49. downcastHelpers.elementToElement( { model: 'paragraph', view: 'foo', converterPriority: 'high' } );
  50. model.change( writer => {
  51. writer.insertElement( 'paragraph', modelRoot, 0 );
  52. } );
  53. expectResult( '<foo></foo>' );
  54. } );
  55. it( 'config.view is a view element definition', () => {
  56. downcastHelpers.elementToElement( {
  57. model: 'fancyParagraph',
  58. view: {
  59. name: 'p',
  60. classes: 'fancy'
  61. }
  62. } );
  63. model.change( writer => {
  64. writer.insertElement( 'fancyParagraph', modelRoot, 0 );
  65. } );
  66. expectResult( '<p class="fancy"></p>' );
  67. } );
  68. it( 'config.view is a function', () => {
  69. downcastHelpers.elementToElement( {
  70. model: 'heading',
  71. view: ( modelElement, viewWriter ) => viewWriter.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) )
  72. } );
  73. model.change( writer => {
  74. writer.insertElement( 'heading', { level: 2 }, modelRoot, 0 );
  75. } );
  76. expectResult( '<h2></h2>' );
  77. } );
  78. } );
  79. describe( 'attributeToElement()', () => {
  80. beforeEach( () => {
  81. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  82. } );
  83. it( 'should be chainable', () => {
  84. expect( downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } ) ).to.equal( downcastHelpers );
  85. } );
  86. it( 'config.view is a string', () => {
  87. downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } );
  88. model.change( writer => {
  89. writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
  90. } );
  91. expectResult( '<strong>foo</strong>' );
  92. } );
  93. it( 'can be overwritten using converterPriority', () => {
  94. downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } );
  95. downcastHelpers.attributeToElement( { model: 'bold', view: 'b', converterPriority: 'high' } );
  96. model.change( writer => {
  97. writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
  98. } );
  99. expectResult( '<b>foo</b>' );
  100. } );
  101. it( 'config.view is a view element definition', () => {
  102. downcastHelpers.attributeToElement( {
  103. model: 'invert',
  104. view: {
  105. name: 'span',
  106. classes: [ 'font-light', 'bg-dark' ]
  107. }
  108. } );
  109. model.change( writer => {
  110. writer.insertText( 'foo', { invert: true }, modelRoot, 0 );
  111. } );
  112. expectResult( '<span class="bg-dark font-light">foo</span>' );
  113. expect( viewRoot.getChild( 0 ).priority ).to.equal( ViewAttributeElement.DEFAULT_PRIORITY );
  114. } );
  115. it( 'config.view allows specifying the element\'s priority', () => {
  116. downcastHelpers.attributeToElement( {
  117. model: 'invert',
  118. view: {
  119. name: 'span',
  120. priority: 5
  121. }
  122. } );
  123. model.change( writer => {
  124. writer.insertText( 'foo', { invert: true }, modelRoot, 0 );
  125. } );
  126. expect( viewRoot.getChild( 0 ).priority ).to.equal( 5 );
  127. } );
  128. it( 'model attribute value is enum', () => {
  129. downcastHelpers.attributeToElement( {
  130. model: {
  131. key: 'fontSize',
  132. values: [ 'big', 'small' ]
  133. },
  134. view: {
  135. big: {
  136. name: 'span',
  137. styles: {
  138. 'font-size': '1.2em'
  139. }
  140. },
  141. small: {
  142. name: 'span',
  143. styles: {
  144. 'font-size': '0.8em'
  145. },
  146. priority: 5
  147. }
  148. }
  149. } );
  150. model.change( writer => {
  151. writer.insertText( 'foo', { fontSize: 'big' }, modelRoot, 0 );
  152. } );
  153. expect( viewRoot.getChild( 0 ).priority ).to.equal( ViewAttributeElement.DEFAULT_PRIORITY );
  154. expectResult( '<span style="font-size:1.2em">foo</span>' );
  155. model.change( writer => {
  156. writer.setAttribute( 'fontSize', 'small', modelRoot.getChild( 0 ) );
  157. } );
  158. expectResult( '<span style="font-size:0.8em">foo</span>' );
  159. expect( viewRoot.getChild( 0 ).priority ).to.equal( 5 );
  160. model.change( writer => {
  161. writer.removeAttribute( 'fontSize', modelRoot.getChild( 0 ) );
  162. } );
  163. expectResult( 'foo' );
  164. } );
  165. it( 'config.view is a function', () => {
  166. downcastHelpers.attributeToElement( {
  167. model: 'bold',
  168. view: ( modelAttributeValue, viewWriter ) => {
  169. return viewWriter.createAttributeElement( 'span', { style: 'font-weight:' + modelAttributeValue } );
  170. }
  171. } );
  172. model.change( writer => {
  173. writer.insertText( 'foo', { bold: '500' }, modelRoot, 0 );
  174. } );
  175. expectResult( '<span style="font-weight:500">foo</span>' );
  176. } );
  177. it( 'config.model.name is given', () => {
  178. downcastHelpers.attributeToElement( {
  179. model: {
  180. key: 'color',
  181. name: '$text'
  182. },
  183. view: ( modelAttributeValue, viewWriter ) => {
  184. return viewWriter.createAttributeElement( 'span', { style: 'color:' + modelAttributeValue } );
  185. }
  186. } );
  187. downcastHelpers.elementToElement( {
  188. model: 'smiley',
  189. view: ( modelElement, viewWriter ) => {
  190. return viewWriter.createEmptyElement( 'img', {
  191. src: 'smile.jpg',
  192. class: 'smiley'
  193. } );
  194. }
  195. } );
  196. model.change( writer => {
  197. writer.insertText( 'foo', { color: '#FF0000' }, modelRoot, 0 );
  198. writer.insertElement( 'smiley', { color: '#FF0000' }, modelRoot, 3 );
  199. } );
  200. expectResult( '<span style="color:#FF0000">foo</span><img class="smiley" src="smile.jpg"></img>' );
  201. } );
  202. it( 'should not convert if creator returned null', () => {
  203. downcastHelpers.elementToElement( { model: 'div', view: () => null } );
  204. const modelElement = new ModelElement( 'div' );
  205. model.change( writer => {
  206. writer.insert( modelElement, modelRootStart );
  207. } );
  208. expect( viewToString( viewRoot ) ).to.equal( '<div></div>' );
  209. } );
  210. it( 'should convert insert/change/remove of attribute in model into wrapping element in a view', () => {
  211. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) );
  212. downcastHelpers.attributeToElement( {
  213. model: 'bold',
  214. view: ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'b' )
  215. } );
  216. model.change( writer => {
  217. writer.insert( modelElement, modelRootStart );
  218. } );
  219. expect( viewToString( viewRoot ) ).to.equal( '<div><p><b>foobar</b></p></div>' );
  220. model.change( writer => {
  221. writer.removeAttribute( 'bold', writer.createRangeIn( modelElement ) );
  222. } );
  223. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  224. } );
  225. it( 'should convert insert/remove of attribute in model with wrapping element generating function as a parameter', () => {
  226. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { style: 'bold' } ) );
  227. downcastHelpers.attributeToElement( {
  228. model: 'style',
  229. view: ( modelAttributeValue, viewWriter ) => {
  230. if ( modelAttributeValue == 'bold' ) {
  231. return viewWriter.createAttributeElement( 'b' );
  232. }
  233. }
  234. } );
  235. model.change( writer => {
  236. writer.insert( modelElement, modelRootStart );
  237. } );
  238. expect( viewToString( viewRoot ) ).to.equal( '<div><p><b>foobar</b></p></div>' );
  239. model.change( writer => {
  240. writer.removeAttribute( 'style', writer.createRangeIn( modelElement ) );
  241. } );
  242. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  243. } );
  244. it( 'should update range on re-wrapping attribute (#475)', () => {
  245. const modelElement = new ModelElement( 'paragraph', null, [
  246. new ModelText( 'x' ),
  247. new ModelText( 'foo', { link: 'http://foo.com' } ),
  248. new ModelText( 'x' )
  249. ] );
  250. downcastHelpers.attributeToElement( {
  251. model: 'link',
  252. view: ( modelAttributeValue, viewWriter ) => {
  253. return viewWriter.createAttributeElement( 'a', { href: modelAttributeValue } );
  254. }
  255. } );
  256. model.change( writer => {
  257. writer.insert( modelElement, modelRootStart );
  258. } );
  259. expect( viewToString( viewRoot ) ).to.equal( '<div><p>x<a href="http://foo.com">foo</a>x</p></div>' );
  260. // Set new attribute on old link but also on non-linked characters.
  261. model.change( writer => {
  262. writer.setAttribute( 'link', 'http://foobar.com', writer.createRangeIn( modelElement ) );
  263. } );
  264. expect( viewToString( viewRoot ) ).to.equal( '<div><p><a href="http://foobar.com">xfoox</a></p></div>' );
  265. } );
  266. it( 'should support unicode', () => {
  267. const modelElement = new ModelElement( 'paragraph', null, [ 'நி', new ModelText( 'லைக்', { bold: true } ), 'கு' ] );
  268. downcastHelpers.attributeToElement( {
  269. model: 'bold',
  270. view: ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'b' )
  271. } );
  272. model.change( writer => {
  273. writer.insert( modelElement, modelRootStart );
  274. } );
  275. expect( viewToString( viewRoot ) ).to.equal( '<div><p>நி<b>லைக்</b>கு</p></div>' );
  276. model.change( writer => {
  277. writer.removeAttribute( 'bold', writer.createRangeIn( modelElement ) );
  278. } );
  279. expect( viewToString( viewRoot ) ).to.equal( '<div><p>நிலைக்கு</p></div>' );
  280. } );
  281. it( 'should be possible to override ', () => {
  282. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) );
  283. downcastHelpers.attributeToElement( {
  284. model: 'bold',
  285. view: ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'b' )
  286. } );
  287. downcastHelpers.attributeToElement( {
  288. model: 'bold',
  289. view: ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'strong' ),
  290. converterPriority: 'high'
  291. } );
  292. model.change( writer => {
  293. writer.insert( modelElement, modelRootStart );
  294. } );
  295. expect( viewToString( viewRoot ) ).to.equal( '<div><p><strong>foobar</strong></p></div>' );
  296. } );
  297. it( 'should not convert and not consume if creator function returned null', () => {
  298. sinon.spy( controller.downcastDispatcher, 'fire' );
  299. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { italic: true } ) );
  300. downcastHelpers.attributeToElement( {
  301. model: 'italic',
  302. view: () => null
  303. } );
  304. const spy = sinon.spy();
  305. controller.downcastDispatcher.on( 'attribute:italic', spy );
  306. model.change( writer => {
  307. writer.insert( modelElement, modelRootStart );
  308. } );
  309. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  310. expect( controller.downcastDispatcher.fire.calledWith( 'attribute:italic:$text' ) ).to.be.true;
  311. expect( spy.called ).to.be.true;
  312. } );
  313. } );
  314. describe( 'attributeToAttribute()', () => {
  315. testUtils.createSinonSandbox();
  316. beforeEach( () => {
  317. downcastHelpers.elementToElement( { model: 'image', view: 'img' } );
  318. downcastHelpers.elementToElement( {
  319. model: 'paragraph',
  320. view: ( modelItem, viewWriter ) => viewWriter.createContainerElement( 'p' )
  321. } );
  322. downcastHelpers.attributeToAttribute( {
  323. model: 'class',
  324. view: 'class'
  325. } );
  326. } );
  327. it( 'should be chainable', () => {
  328. expect( downcastHelpers.attributeToAttribute( { model: 'source', view: 'src' } ) ).to.equal( downcastHelpers );
  329. } );
  330. it( 'config.view is a string', () => {
  331. downcastHelpers.attributeToAttribute( { model: 'source', view: 'src' } );
  332. model.change( writer => {
  333. writer.insertElement( 'image', { source: 'foo.jpg' }, modelRoot, 0 );
  334. } );
  335. expectResult( '<img src="foo.jpg"></img>' );
  336. model.change( writer => {
  337. writer.removeAttribute( 'source', modelRoot.getChild( 0 ) );
  338. } );
  339. expectResult( '<img></img>' );
  340. } );
  341. it( 'can be overwritten using converterPriority', () => {
  342. downcastHelpers.attributeToAttribute( { model: 'source', view: 'href' } );
  343. downcastHelpers.attributeToAttribute( { model: 'source', view: 'src', converterPriority: 'high' } );
  344. model.change( writer => {
  345. writer.insertElement( 'image', { source: 'foo.jpg' }, modelRoot, 0 );
  346. } );
  347. expectResult( '<img src="foo.jpg"></img>' );
  348. } );
  349. it( 'model element name specified', () => {
  350. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  351. downcastHelpers.attributeToAttribute( {
  352. model: {
  353. name: 'image',
  354. key: 'source'
  355. },
  356. view: 'src'
  357. } );
  358. model.change( writer => {
  359. writer.insertElement( 'image', { source: 'foo.jpg' }, modelRoot, 0 );
  360. } );
  361. expectResult( '<img src="foo.jpg"></img>' );
  362. model.change( writer => {
  363. writer.rename( modelRoot.getChild( 0 ), 'paragraph' );
  364. } );
  365. expectResult( '<p></p>' );
  366. } );
  367. it( 'config.view is an object, model attribute value is enum', () => {
  368. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  369. downcastHelpers.attributeToAttribute( {
  370. model: {
  371. key: 'styled',
  372. values: [ 'dark', 'light' ]
  373. },
  374. view: {
  375. dark: {
  376. key: 'class',
  377. value: [ 'styled', 'styled-dark' ]
  378. },
  379. light: {
  380. key: 'class',
  381. value: [ 'styled', 'styled-light' ]
  382. }
  383. }
  384. } );
  385. model.change( writer => {
  386. writer.insertElement( 'paragraph', { styled: 'dark' }, modelRoot, 0 );
  387. } );
  388. expectResult( '<p class="styled styled-dark"></p>' );
  389. model.change( writer => {
  390. writer.setAttribute( 'styled', 'light', modelRoot.getChild( 0 ) );
  391. } );
  392. expectResult( '<p class="styled styled-light"></p>' );
  393. model.change( writer => {
  394. writer.removeAttribute( 'styled', modelRoot.getChild( 0 ) );
  395. } );
  396. expectResult( '<p></p>' );
  397. } );
  398. it( 'config.view is an object, model attribute value is enum, view has style', () => {
  399. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  400. downcastHelpers.attributeToAttribute( {
  401. model: {
  402. key: 'align',
  403. values: [ 'right', 'center' ]
  404. },
  405. view: {
  406. right: {
  407. key: 'style',
  408. value: {
  409. 'text-align': 'right'
  410. }
  411. },
  412. center: {
  413. key: 'style',
  414. value: {
  415. 'text-align': 'center'
  416. }
  417. }
  418. }
  419. } );
  420. model.change( writer => {
  421. writer.insertElement( 'paragraph', { align: 'right' }, modelRoot, 0 );
  422. } );
  423. expectResult( '<p style="text-align:right"></p>' );
  424. model.change( writer => {
  425. writer.setAttribute( 'align', 'center', modelRoot.getChild( 0 ) );
  426. } );
  427. expectResult( '<p style="text-align:center"></p>' );
  428. model.change( writer => {
  429. writer.removeAttribute( 'align', modelRoot.getChild( 0 ) );
  430. } );
  431. expectResult( '<p></p>' );
  432. } );
  433. it( 'config.view is an object, only name and key are provided', () => {
  434. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  435. downcastHelpers.attributeToAttribute( {
  436. model: {
  437. name: 'paragraph',
  438. key: 'class'
  439. },
  440. view: {
  441. name: 'paragraph',
  442. key: 'class'
  443. }
  444. } );
  445. model.change( writer => {
  446. writer.insertElement( 'paragraph', { class: 'dark' }, modelRoot, 0 );
  447. } );
  448. expectResult( '<p class="dark"></p>' );
  449. model.change( writer => {
  450. writer.setAttribute( 'class', 'light', modelRoot.getChild( 0 ) );
  451. } );
  452. expectResult( '<p class="light"></p>' );
  453. model.change( writer => {
  454. writer.removeAttribute( 'class', modelRoot.getChild( 0 ) );
  455. } );
  456. expectResult( '<p></p>' );
  457. } );
  458. it( 'config.view is a function', () => {
  459. downcastHelpers.attributeToAttribute( {
  460. model: 'styled',
  461. view: attributeValue => ( { key: 'class', value: 'styled-' + attributeValue } )
  462. } );
  463. model.change( writer => {
  464. writer.insertElement( 'image', { styled: 'pull-out' }, modelRoot, 0 );
  465. } );
  466. expectResult( '<img class="styled-pull-out"></img>' );
  467. } );
  468. // #1587
  469. it( 'config.view and config.model as strings in generic conversion (elements only)', () => {
  470. const logSpy = testUtils.sinon.spy( log, 'warn' );
  471. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  472. downcastHelpers.attributeToAttribute( { model: 'test', view: 'test' } );
  473. model.change( writer => {
  474. writer.insertElement( 'paragraph', { test: '1' }, modelRoot, 0 );
  475. writer.insertElement( 'paragraph', { test: '2' }, modelRoot, 1 );
  476. } );
  477. expectResult( '<p test="1"></p><p test="2"></p>' );
  478. expect( logSpy.callCount ).to.equal( 0 );
  479. model.change( writer => {
  480. writer.removeAttribute( 'test', modelRoot.getChild( 1 ) );
  481. } );
  482. expectResult( '<p test="1"></p><p></p>' );
  483. } );
  484. // #1587
  485. it( 'config.view and config.model as strings in generic conversion (elements + text)', () => {
  486. const logSpy = testUtils.sinon.spy( log, 'warn' );
  487. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  488. downcastHelpers.attributeToAttribute( { model: 'test', view: 'test' } );
  489. model.change( writer => {
  490. writer.insertElement( 'paragraph', modelRoot, 0 );
  491. writer.insertElement( 'paragraph', { test: '1' }, modelRoot, 1 );
  492. writer.insertText( 'Foo', { test: '2' }, modelRoot.getChild( 0 ), 0 );
  493. writer.insertText( 'Bar', { test: '3' }, modelRoot.getChild( 1 ), 0 );
  494. } );
  495. expectResult( '<p>Foo</p><p test="1">Bar</p>' );
  496. expect( logSpy.callCount ).to.equal( 2 );
  497. expect( logSpy.alwaysCalledWithMatch( 'conversion-attribute-to-attribute-on-text' ) ).to.true;
  498. model.change( writer => {
  499. writer.removeAttribute( 'test', modelRoot.getChild( 1 ) );
  500. } );
  501. expectResult( '<p>Foo</p><p>Bar</p>' );
  502. } );
  503. it( 'should convert attribute insert/change/remove on a model node', () => {
  504. const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) );
  505. model.change( writer => {
  506. writer.insert( modelElement, modelRootStart );
  507. } );
  508. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="foo">foobar</p></div>' );
  509. model.change( writer => {
  510. writer.setAttribute( 'class', 'bar', modelElement );
  511. } );
  512. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="bar">foobar</p></div>' );
  513. model.change( writer => {
  514. writer.removeAttribute( 'class', modelElement );
  515. } );
  516. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  517. } );
  518. it( 'should convert insert/change/remove with attribute generating function as a parameter', () => {
  519. downcastHelpers.elementToElement( { model: 'div', view: 'div' } );
  520. downcastHelpers.attributeToAttribute( {
  521. model: 'theme',
  522. view: ( value, data ) => {
  523. if ( data.item instanceof ModelElement && data.item.childCount > 0 ) {
  524. value += ' fix-content';
  525. }
  526. return { key: 'class', value };
  527. }
  528. } );
  529. const modelParagraph = new ModelElement( 'paragraph', { theme: 'nice' }, new ModelText( 'foobar' ) );
  530. const modelDiv = new ModelElement( 'div', { theme: 'nice' } );
  531. model.change( writer => {
  532. writer.insert( [ modelParagraph, modelDiv ], modelRootStart );
  533. } );
  534. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="nice fix-content">foobar</p><div class="nice"></div></div>' );
  535. model.change( writer => {
  536. writer.setAttribute( 'theme', 'awesome', modelParagraph );
  537. } );
  538. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="awesome fix-content">foobar</p><div class="nice"></div></div>' );
  539. model.change( writer => {
  540. writer.removeAttribute( 'theme', modelParagraph );
  541. } );
  542. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p><div class="nice"></div></div>' );
  543. } );
  544. it( 'should be possible to override setAttribute', () => {
  545. downcastHelpers.attributeToAttribute( {
  546. model: 'class',
  547. view: ( evt, data, conversionApi ) => {
  548. conversionApi.consumable.consume( data.item, 'attribute:class' );
  549. },
  550. converterPriority: 'high'
  551. } );
  552. model.change( writer => {
  553. const modelElement = new ModelElement( 'paragraph', { classes: 'foo' }, new ModelText( 'foobar' ) );
  554. writer.insert( modelElement, modelRootStart );
  555. } );
  556. // No attribute set.
  557. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  558. } );
  559. it( 'should not convert or consume if element creator returned null', () => {
  560. const callback = sinon.stub().returns( null );
  561. downcastHelpers.attributeToAttribute( {
  562. model: 'class',
  563. view: callback,
  564. converterPriority: 'high'
  565. } );
  566. const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) );
  567. model.change( writer => {
  568. writer.insert( modelElement, modelRootStart );
  569. } );
  570. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="foo">foobar</p></div>' );
  571. sinon.assert.called( callback );
  572. } );
  573. } );
  574. describe( 'markerToElement()', () => {
  575. let modelText, modelElement, range;
  576. it( 'should be chainable', () => {
  577. expect( downcastHelpers.markerToElement( { model: 'search', view: 'marker-search' } ) ).to.equal( downcastHelpers );
  578. } );
  579. it( 'config.view is a string', () => {
  580. downcastHelpers.markerToElement( { model: 'search', view: 'marker-search' } );
  581. model.change( writer => {
  582. writer.insertText( 'foo', modelRoot, 0 );
  583. const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 2 ) );
  584. writer.addMarker( 'search', { range, usingOperation: false } );
  585. } );
  586. expectResult( 'f<marker-search></marker-search>o<marker-search></marker-search>o' );
  587. } );
  588. it( 'can be overwritten using converterPriority', () => {
  589. downcastHelpers.markerToElement( { model: 'search', view: 'marker-search' } );
  590. downcastHelpers.markerToElement( { model: 'search', view: 'search', converterPriority: 'high' } );
  591. model.change( writer => {
  592. writer.insertText( 'foo', modelRoot, 0 );
  593. const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 2 ) );
  594. writer.addMarker( 'search', { range, usingOperation: false } );
  595. } );
  596. expectResult( 'f<search></search>o<search></search>o' );
  597. } );
  598. it( 'config.view is a view element definition', () => {
  599. downcastHelpers.markerToElement( {
  600. model: 'search',
  601. view: {
  602. name: 'span',
  603. attributes: {
  604. 'data-marker': 'search'
  605. }
  606. }
  607. } );
  608. model.change( writer => {
  609. writer.insertText( 'foo', modelRoot, 0 );
  610. const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 2 ) );
  611. writer.addMarker( 'search', { range, usingOperation: false } );
  612. } );
  613. expectResult( 'f<span data-marker="search"></span>o<span data-marker="search"></span>o' );
  614. } );
  615. it( 'config.view is a function', () => {
  616. downcastHelpers.markerToElement( {
  617. model: 'search',
  618. view: ( data, viewWriter ) => {
  619. return viewWriter.createUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } );
  620. }
  621. } );
  622. model.change( writer => {
  623. writer.insertText( 'foo', modelRoot, 0 );
  624. const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 2 ) );
  625. writer.addMarker( 'search', { range, usingOperation: false } );
  626. } );
  627. expectResult( 'f<span data-marker="search" data-start="true"></span>o<span data-marker="search" data-start="false"></span>o' );
  628. } );
  629. describe( 'collapsed range', () => {
  630. beforeEach( () => {
  631. modelText = new ModelText( 'foobar' );
  632. modelElement = new ModelElement( 'paragraph', null, modelText );
  633. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  634. model.change( writer => {
  635. writer.insert( modelElement, modelRootStart );
  636. } );
  637. range = model.createRange( model.createPositionAt( modelElement, 3 ), model.createPositionAt( modelElement, 3 ) );
  638. } );
  639. it( 'should insert and remove ui element', () => {
  640. downcastHelpers.markerToElement( {
  641. model: 'marker',
  642. view: ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } )
  643. } );
  644. model.change( writer => {
  645. writer.addMarker( 'marker', { range, usingOperation: false } );
  646. } );
  647. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo<span class="marker"></span>bar</p></div>' );
  648. model.change( writer => {
  649. writer.removeMarker( 'marker' );
  650. } );
  651. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  652. } );
  653. it( 'should not convert if consumable was consumed', () => {
  654. sinon.spy( controller.downcastDispatcher, 'fire' );
  655. downcastHelpers.markerToElement( {
  656. model: 'marker',
  657. view: ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } )
  658. } );
  659. controller.downcastDispatcher.on( 'addMarker:marker', ( evt, data, conversionApi ) => {
  660. conversionApi.consumable.consume( data.markerRange, 'addMarker:marker' );
  661. }, { priority: 'high' } );
  662. model.change( writer => {
  663. writer.addMarker( 'marker', { range, usingOperation: false } );
  664. } );
  665. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  666. expect( controller.downcastDispatcher.fire.calledWith( 'addMarker:marker' ) );
  667. } );
  668. it( 'should not convert if creator returned null', () => {
  669. downcastHelpers.markerToElement( {
  670. model: 'marker',
  671. view: () => null
  672. } );
  673. model.change( writer => {
  674. writer.addMarker( 'marker', { range, usingOperation: false } );
  675. } );
  676. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  677. model.change( writer => {
  678. writer.removeMarker( 'marker' );
  679. } );
  680. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  681. } );
  682. } );
  683. describe( 'non-collapsed range', () => {
  684. beforeEach( () => {
  685. modelText = new ModelText( 'foobar' );
  686. modelElement = new ModelElement( 'paragraph', null, modelText );
  687. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  688. model.change( writer => {
  689. writer.insert( modelElement, modelRootStart );
  690. } );
  691. range = model.createRange( model.createPositionAt( modelElement, 2 ), model.createPositionAt( modelElement, 5 ) );
  692. } );
  693. it( 'should insert and remove ui element - element as a creator', () => {
  694. downcastHelpers.markerToElement( {
  695. model: 'marker',
  696. view: ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } )
  697. } );
  698. model.change( writer => {
  699. writer.addMarker( 'marker', { range, usingOperation: false } );
  700. } );
  701. expect( viewToString( viewRoot ) )
  702. .to.equal( '<div><p>fo<span class="marker"></span>oba<span class="marker"></span>r</p></div>' );
  703. model.change( writer => {
  704. writer.removeMarker( 'marker' );
  705. } );
  706. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  707. } );
  708. it( 'should insert and remove ui element - function as a creator', () => {
  709. downcastHelpers.markerToElement( {
  710. model: 'marker',
  711. view: ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': data.markerName } )
  712. } );
  713. model.change( writer => {
  714. writer.addMarker( 'marker', { range, usingOperation: false } );
  715. } );
  716. expect( viewToString( viewRoot ) )
  717. .to.equal( '<div><p>fo<span class="marker"></span>oba<span class="marker"></span>r</p></div>' );
  718. model.change( writer => {
  719. writer.removeMarker( 'marker' );
  720. } );
  721. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  722. } );
  723. it( 'should insert and remove different opening and ending element', () => {
  724. downcastHelpers.markerToElement( {
  725. model: 'marker',
  726. view: ( data, viewWriter ) => {
  727. if ( data.isOpening ) {
  728. return viewWriter.createUIElement( 'span', { 'class': data.markerName, 'data-start': true } );
  729. }
  730. return viewWriter.createUIElement( 'span', { 'class': data.markerName, 'data-end': true } );
  731. }
  732. } );
  733. model.change( writer => {
  734. writer.addMarker( 'marker', { range, usingOperation: false } );
  735. } );
  736. expect( viewToString( viewRoot ) ).to.equal(
  737. '<div><p>fo<span class="marker" data-start="true"></span>oba<span class="marker" data-end="true"></span>r</p></div>'
  738. );
  739. model.change( writer => {
  740. writer.removeMarker( 'marker' );
  741. } );
  742. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  743. } );
  744. it( 'should not convert if consumable was consumed', () => {
  745. sinon.spy( controller.downcastDispatcher, 'fire' );
  746. downcastHelpers.markerToElement( {
  747. model: 'marker',
  748. view: ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } )
  749. } );
  750. controller.downcastDispatcher.on( 'addMarker:marker', ( evt, data, conversionApi ) => {
  751. conversionApi.consumable.consume( data.item, 'addMarker:marker' );
  752. }, { priority: 'high' } );
  753. model.change( writer => {
  754. writer.addMarker( 'marker', { range, usingOperation: false } );
  755. } );
  756. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  757. expect( controller.downcastDispatcher.fire.calledWith( 'addMarker:marker' ) );
  758. } );
  759. } );
  760. } );
  761. describe( 'markerToHighlight()', () => {
  762. it( 'should be chainable', () => {
  763. expect( downcastHelpers.markerToHighlight( { model: 'comment', view: { classes: 'comment' } } ) ).to.equal( downcastHelpers );
  764. } );
  765. it( 'config.view is a highlight descriptor', () => {
  766. downcastHelpers.markerToHighlight( { model: 'comment', view: { classes: 'comment' } } );
  767. model.change( writer => {
  768. writer.insertText( 'foo', modelRoot, 0 );
  769. const range = writer.createRange( writer.createPositionAt( modelRoot, 0 ), writer.createPositionAt( modelRoot, 3 ) );
  770. writer.addMarker( 'comment', { range, usingOperation: false } );
  771. } );
  772. expectResult( '<span class="comment">foo</span>' );
  773. } );
  774. it( 'can be overwritten using converterPriority', () => {
  775. downcastHelpers.markerToHighlight( { model: 'comment', view: { classes: 'comment' } } );
  776. downcastHelpers.markerToHighlight( { model: 'comment', view: { classes: 'new-comment' }, converterPriority: 'high' } );
  777. model.change( writer => {
  778. writer.insertText( 'foo', modelRoot, 0 );
  779. const range = writer.createRange( writer.createPositionAt( modelRoot, 0 ), writer.createPositionAt( modelRoot, 3 ) );
  780. writer.addMarker( 'comment', { range, usingOperation: false } );
  781. } );
  782. expectResult( '<span class="new-comment">foo</span>' );
  783. } );
  784. it( 'config.view is a function', () => {
  785. downcastHelpers.markerToHighlight( {
  786. model: 'comment',
  787. view: data => {
  788. const commentType = data.markerName.split( ':' )[ 1 ];
  789. return {
  790. classes: [ 'comment', 'comment-' + commentType ]
  791. };
  792. }
  793. } );
  794. model.change( writer => {
  795. writer.insertText( 'foo', modelRoot, 0 );
  796. const range = writer.createRange( writer.createPositionAt( modelRoot, 0 ), writer.createPositionAt( modelRoot, 3 ) );
  797. writer.addMarker( 'comment:abc', { range, usingOperation: false } );
  798. } );
  799. expectResult( '<span class="comment comment-abc">foo</span>' );
  800. } );
  801. describe( 'highlight', () => {
  802. const highlightConfig = {
  803. model: 'marker',
  804. view: {
  805. classes: 'highlight-class',
  806. attributes: { title: 'title' }
  807. },
  808. converterPriority: 7
  809. };
  810. describe( 'on text', () => {
  811. let markerRange;
  812. beforeEach( () => {
  813. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  814. const modelElement1 = new ModelElement( 'paragraph', null, new ModelText( 'foo' ) );
  815. const modelElement2 = new ModelElement( 'paragraph', null, new ModelText( 'bar' ) );
  816. model.change( writer => {
  817. writer.insert( [ modelElement1, modelElement2 ], modelRootStart );
  818. } );
  819. markerRange = model.createRangeIn( modelRoot );
  820. } );
  821. it( 'should wrap and unwrap text nodes', () => {
  822. downcastHelpers.markerToHighlight( highlightConfig );
  823. model.change( writer => {
  824. writer.addMarker( 'marker', { range: markerRange, usingOperation: false } );
  825. } );
  826. expect( viewToString( viewRoot ) ).to.equal(
  827. '<div>' +
  828. '<p>' +
  829. '<span class="highlight-class" title="title">foo</span>' +
  830. '</p>' +
  831. '<p>' +
  832. '<span class="highlight-class" title="title">bar</span>' +
  833. '</p>' +
  834. '</div>'
  835. );
  836. model.change( writer => {
  837. writer.removeMarker( 'marker' );
  838. } );
  839. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  840. } );
  841. it( 'should be possible to overwrite', () => {
  842. downcastHelpers.markerToHighlight( highlightConfig );
  843. downcastHelpers.markerToHighlight( {
  844. model: 'marker',
  845. view: { classes: 'override-class' },
  846. converterPriority: 'high'
  847. } );
  848. model.change( writer => {
  849. writer.addMarker( 'marker', { range: markerRange, usingOperation: false } );
  850. } );
  851. expect( viewToString( viewRoot ) ).to.equal(
  852. '<div>' +
  853. '<p>' +
  854. '<span class="override-class">foo</span>' +
  855. '</p>' +
  856. '<p>' +
  857. '<span class="override-class">bar</span>' +
  858. '</p>' +
  859. '</div>'
  860. );
  861. model.change( writer => {
  862. writer.removeMarker( 'marker' );
  863. } );
  864. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  865. } );
  866. it( 'should do nothing if descriptor is not provided or generating function returns null', () => {
  867. downcastHelpers.markerToHighlight( {
  868. model: 'marker',
  869. view: () => null,
  870. converterPriority: 'high'
  871. } );
  872. model.change( writer => {
  873. writer.addMarker( 'marker', { range: markerRange, usingOperation: false } );
  874. } );
  875. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  876. model.change( writer => {
  877. writer.removeMarker( 'marker' );
  878. } );
  879. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  880. } );
  881. it( 'should do nothing if collapsed marker is converted', () => {
  882. downcastHelpers.markerToHighlight( {
  883. model: 'marker',
  884. view: { classes: 'foo' },
  885. converterPriority: 'high'
  886. } );
  887. markerRange = model.createRange( model.createPositionAt( modelRoot, 0 ), model.createPositionAt( modelRoot, 0 ) );
  888. model.change( writer => {
  889. writer.addMarker( 'marker', { range: markerRange, usingOperation: false } );
  890. } );
  891. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  892. model.change( () => {
  893. model.markers._remove( 'marker' );
  894. } );
  895. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  896. } );
  897. it( 'should correctly wrap and unwrap multiple, intersecting markers', () => {
  898. downcastHelpers.markerToHighlight( {
  899. model: 'markerFoo',
  900. view: { classes: 'foo' }
  901. } );
  902. downcastHelpers.markerToHighlight( {
  903. model: 'markerBar',
  904. view: { classes: 'bar' }
  905. } );
  906. downcastHelpers.markerToHighlight( {
  907. model: 'markerXyz',
  908. view: { classes: 'xyz' }
  909. } );
  910. const p1 = modelRoot.getChild( 0 );
  911. const p2 = modelRoot.getChild( 1 );
  912. model.change( writer => {
  913. const range = writer.createRange( writer.createPositionAt( p1, 0 ), writer.createPositionAt( p1, 3 ) );
  914. writer.addMarker( 'markerFoo', { range, usingOperation: false } );
  915. } );
  916. expect( viewToString( viewRoot ) ).to.equal(
  917. '<div>' +
  918. '<p>' +
  919. '<span class="foo">foo</span>' +
  920. '</p>' +
  921. '<p>bar</p>' +
  922. '</div>'
  923. );
  924. model.change( writer => {
  925. const range = writer.createRange( writer.createPositionAt( p1, 1 ), writer.createPositionAt( p2, 2 ) );
  926. writer.addMarker( 'markerBar', { range, usingOperation: false } );
  927. } );
  928. expect( viewToString( viewRoot ) ).to.equal(
  929. '<div>' +
  930. '<p>' +
  931. '<span class="foo">f</span>' +
  932. '<span class="bar">' +
  933. '<span class="foo">oo</span>' +
  934. '</span>' +
  935. '</p>' +
  936. '<p>' +
  937. '<span class="bar">ba</span>' +
  938. 'r' +
  939. '</p>' +
  940. '</div>'
  941. );
  942. model.change( writer => {
  943. const range = writer.createRange( writer.createPositionAt( p1, 2 ), writer.createPositionAt( p2, 3 ) );
  944. writer.addMarker( 'markerXyz', { range, usingOperation: false } );
  945. } );
  946. expect( viewToString( viewRoot ) ).to.equal(
  947. '<div>' +
  948. '<p>' +
  949. '<span class="foo">f</span>' +
  950. '<span class="bar">' +
  951. '<span class="foo">' +
  952. 'o' +
  953. '<span class="xyz">o</span>' +
  954. '</span>' +
  955. '</span>' +
  956. '</p>' +
  957. '<p>' +
  958. '<span class="bar">' +
  959. '<span class="xyz">ba</span>' +
  960. '</span>' +
  961. '<span class="xyz">r</span>' +
  962. '</p>' +
  963. '</div>'
  964. );
  965. model.change( writer => {
  966. writer.removeMarker( 'markerBar' );
  967. } );
  968. expect( viewToString( viewRoot ) ).to.equal(
  969. '<div>' +
  970. '<p>' +
  971. '<span class="foo">' +
  972. 'fo' +
  973. '<span class="xyz">o</span>' +
  974. '</span>' +
  975. '</p>' +
  976. '<p>' +
  977. '<span class="xyz">bar</span>' +
  978. '</p>' +
  979. '</div>'
  980. );
  981. model.change( writer => {
  982. writer.removeMarker( 'markerFoo' );
  983. } );
  984. expect( viewToString( viewRoot ) ).to.equal(
  985. '<div>' +
  986. '<p>' +
  987. 'fo' +
  988. '<span class="xyz">o</span>' +
  989. '</p>' +
  990. '<p>' +
  991. '<span class="xyz">bar</span>' +
  992. '</p>' +
  993. '</div>'
  994. );
  995. model.change( writer => {
  996. writer.removeMarker( 'markerXyz' );
  997. } );
  998. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  999. } );
  1000. it( 'should do nothing if marker is applied and removed on empty-ish range', () => {
  1001. downcastHelpers.markerToHighlight( highlightConfig );
  1002. const p1 = modelRoot.getChild( 0 );
  1003. const p2 = modelRoot.getChild( 1 );
  1004. const markerRange = model.createRange( model.createPositionAt( p1, 3 ), model.createPositionAt( p2, 0 ) );
  1005. model.change( writer => {
  1006. writer.addMarker( 'marker', { range: markerRange, usingOperation: false } );
  1007. } );
  1008. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  1009. model.change( writer => {
  1010. writer.removeMarker( 'marker', { range: markerRange, usingOperation: false } );
  1011. } );
  1012. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  1013. } );
  1014. } );
  1015. describe( 'on element', () => {
  1016. const highlightConfig = {
  1017. model: 'marker',
  1018. view: {
  1019. classes: 'highlight-class',
  1020. attributes: { title: 'title' },
  1021. id: 'customId'
  1022. },
  1023. converterPriority: 7
  1024. };
  1025. let markerRange;
  1026. beforeEach( () => {
  1027. downcastHelpers.elementToElement( {
  1028. model: 'div',
  1029. view: () => {
  1030. const viewContainer = new ViewContainerElement( 'div' );
  1031. viewContainer._setCustomProperty( 'addHighlight', ( element, descriptor, writer ) => {
  1032. writer.addClass( descriptor.classes, element );
  1033. } );
  1034. viewContainer._setCustomProperty( 'removeHighlight', ( element, id, writer ) => {
  1035. writer.setAttribute( 'class', '', element );
  1036. } );
  1037. return viewContainer;
  1038. }
  1039. } );
  1040. const modelElement = new ModelElement( 'div', null, new ModelText( 'foo' ) );
  1041. model.change( writer => {
  1042. writer.insert( modelElement, modelRootStart );
  1043. } );
  1044. markerRange = model.createRangeOn( modelElement );
  1045. downcastHelpers.markerToHighlight( highlightConfig );
  1046. } );
  1047. it( 'should use addHighlight and removeHighlight on elements and not convert children nodes', () => {
  1048. model.change( writer => {
  1049. writer.addMarker( 'marker', { range: markerRange, usingOperation: false } );
  1050. } );
  1051. expect( viewToString( viewRoot ) ).to.equal(
  1052. '<div>' +
  1053. '<div class="highlight-class">' +
  1054. 'foo' +
  1055. '</div>' +
  1056. '</div>'
  1057. );
  1058. model.change( writer => {
  1059. writer.removeMarker( 'marker' );
  1060. } );
  1061. expect( viewToString( viewRoot ) ).to.equal( '<div><div>foo</div></div>' );
  1062. } );
  1063. it( 'should be possible to override', () => {
  1064. downcastHelpers.markerToHighlight( {
  1065. model: 'marker',
  1066. view: { classes: 'override-class' },
  1067. converterPriority: 'high'
  1068. } );
  1069. model.change( writer => {
  1070. writer.addMarker( 'marker', { range: markerRange, usingOperation: false } );
  1071. } );
  1072. expect( viewToString( viewRoot ) ).to.equal(
  1073. '<div>' +
  1074. '<div class="override-class">' +
  1075. 'foo' +
  1076. '</div>' +
  1077. '</div>'
  1078. );
  1079. model.change( writer => {
  1080. writer.removeMarker( 'marker' );
  1081. } );
  1082. expect( viewToString( viewRoot ) ).to.equal( '<div><div>foo</div></div>' );
  1083. } );
  1084. it( 'should use default priority and id if not provided', () => {
  1085. const viewDiv = viewRoot.getChild( 0 );
  1086. downcastHelpers.markerToHighlight( {
  1087. model: 'marker2',
  1088. view: () => null,
  1089. converterPriority: 'high'
  1090. } );
  1091. viewDiv._setCustomProperty( 'addHighlight', ( element, descriptor ) => {
  1092. expect( descriptor.priority ).to.equal( ViewAttributeElement.DEFAULT_PRIORITY );
  1093. expect( descriptor.id ).to.equal( 'marker:foo-bar-baz' );
  1094. } );
  1095. viewDiv._setCustomProperty( 'removeHighlight', ( element, id ) => {
  1096. expect( id ).to.equal( 'marker:foo-bar-baz' );
  1097. } );
  1098. model.change( writer => {
  1099. writer.addMarker( 'marker2', { range: markerRange, usingOperation: false } );
  1100. } );
  1101. } );
  1102. it( 'should do nothing if descriptor is not provided', () => {
  1103. downcastHelpers.markerToHighlight( {
  1104. model: 'marker2',
  1105. view: () => null
  1106. } );
  1107. model.change( writer => {
  1108. writer.addMarker( 'marker2', { range: markerRange, usingOperation: false } );
  1109. } );
  1110. expect( viewToString( viewRoot ) ).to.equal( '<div><div>foo</div></div>' );
  1111. model.change( writer => {
  1112. writer.removeMarker( 'marker2' );
  1113. } );
  1114. expect( viewToString( viewRoot ) ).to.equal( '<div><div>foo</div></div>' );
  1115. } );
  1116. } );
  1117. } );
  1118. } );
  1119. function expectResult( string ) {
  1120. expect( stringify( viewRoot, null, { ignoreRoot: true } ) ).to.equal( string );
  1121. }
  1122. function viewAttributesToString( item ) {
  1123. let result = '';
  1124. for ( const key of item.getAttributeKeys() ) {
  1125. const value = item.getAttribute( key );
  1126. if ( value ) {
  1127. result += ' ' + key + '="' + value + '"';
  1128. }
  1129. }
  1130. return result;
  1131. }
  1132. function viewToString( item ) {
  1133. let result = '';
  1134. if ( item instanceof ViewText ) {
  1135. result = item.data;
  1136. } else {
  1137. // ViewElement or ViewDocumentFragment.
  1138. for ( const child of item.getChildren() ) {
  1139. result += viewToString( child );
  1140. }
  1141. if ( item instanceof ViewElement ) {
  1142. result = '<' + item.name + viewAttributesToString( item ) + '>' + result + '</' + item.name + '>';
  1143. }
  1144. }
  1145. return result;
  1146. }
  1147. } );
  1148. describe( 'downcast-converters', () => {
  1149. let dispatcher, modelDoc, modelRoot, viewRoot, controller, modelRootStart, model;
  1150. beforeEach( () => {
  1151. model = new Model();
  1152. modelDoc = model.document;
  1153. modelRoot = modelDoc.createRoot();
  1154. controller = new EditingController( model );
  1155. viewRoot = controller.view.document.getRoot();
  1156. // Set name of view root the same as dom root.
  1157. // This is a mock of attaching view root to dom root.
  1158. controller.view.document.getRoot()._name = 'div';
  1159. dispatcher = controller.downcastDispatcher;
  1160. const downcastHelpers = new DowncastHelpers( dispatcher );
  1161. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  1162. modelRootStart = model.createPositionAt( modelRoot, 0 );
  1163. } );
  1164. function viewAttributesToString( item ) {
  1165. let result = '';
  1166. for ( const key of item.getAttributeKeys() ) {
  1167. const value = item.getAttribute( key );
  1168. if ( value ) {
  1169. result += ' ' + key + '="' + value + '"';
  1170. }
  1171. }
  1172. return result;
  1173. }
  1174. function viewToString( item ) {
  1175. let result = '';
  1176. if ( item instanceof ViewText ) {
  1177. result = item.data;
  1178. } else {
  1179. // ViewElement or ViewDocumentFragment.
  1180. for ( const child of item.getChildren() ) {
  1181. result += viewToString( child );
  1182. }
  1183. if ( item instanceof ViewElement ) {
  1184. result = '<' + item.name + viewAttributesToString( item ) + '>' + result + '</' + item.name + '>';
  1185. }
  1186. }
  1187. return result;
  1188. }
  1189. describe( 'insertText', () => {
  1190. it( 'should downcast text', () => {
  1191. model.change( writer => {
  1192. writer.insert( new ModelText( 'foobar' ), modelRootStart );
  1193. } );
  1194. expect( viewToString( viewRoot ) ).to.equal( '<div>foobar</div>' );
  1195. } );
  1196. it( 'should support unicode', () => {
  1197. model.change( writer => {
  1198. writer.insert( new ModelText( 'நிலைக்கு' ), modelRootStart );
  1199. } );
  1200. expect( viewToString( viewRoot ) ).to.equal( '<div>நிலைக்கு</div>' );
  1201. } );
  1202. it( 'should be possible to override it', () => {
  1203. dispatcher.on( 'insert:$text', ( evt, data, conversionApi ) => {
  1204. conversionApi.consumable.consume( data.item, 'insert' );
  1205. }, { converterPriority: 'high' } );
  1206. model.change( writer => {
  1207. writer.insert( new ModelText( 'foobar' ), modelRootStart );
  1208. } );
  1209. expect( viewToString( viewRoot ) ).to.equal( '<div></div>' );
  1210. } );
  1211. } );
  1212. // Remove converter is by default already added in `EditingController` instance.
  1213. describe( 'remove', () => {
  1214. it( 'should remove items from view accordingly to changes in model #1', () => {
  1215. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar' ) );
  1216. model.change( writer => {
  1217. writer.insert( modelElement, modelRootStart );
  1218. } );
  1219. model.change( writer => {
  1220. writer.remove(
  1221. writer.createRange( writer.createPositionAt( modelElement, 2 ), writer.createPositionAt( modelElement, 4 ) )
  1222. );
  1223. } );
  1224. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foar</p></div>' );
  1225. } );
  1226. it( 'should be possible to overwrite', () => {
  1227. dispatcher.on( 'remove', evt => evt.stop(), { priority: 'high' } );
  1228. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar' ) );
  1229. model.change( writer => {
  1230. writer.insert( modelElement, modelRootStart );
  1231. } );
  1232. model.change( writer => {
  1233. writer.remove(
  1234. writer.createRange( writer.createPositionAt( modelElement, 2 ), writer.createPositionAt( modelElement, 4 ) )
  1235. );
  1236. } );
  1237. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  1238. } );
  1239. it( 'should support unicode', () => {
  1240. const modelElement = new ModelElement( 'paragraph', null, 'நிலைக்கு' );
  1241. model.change( writer => {
  1242. writer.insert( modelElement, modelRootStart );
  1243. } );
  1244. model.change( writer => {
  1245. writer.remove(
  1246. writer.createRange( writer.createPositionAt( modelElement, 0 ), writer.createPositionAt( modelElement, 6 ) )
  1247. );
  1248. } );
  1249. expect( viewToString( viewRoot ) ).to.equal( '<div><p>கு</p></div>' );
  1250. } );
  1251. it( 'should not remove view ui elements that are placed next to removed content', () => {
  1252. modelRoot._appendChild( new ModelText( 'fozbar' ) );
  1253. viewRoot._appendChild( [
  1254. new ViewText( 'foz' ),
  1255. new ViewUIElement( 'span' ),
  1256. new ViewText( 'bar' )
  1257. ] );
  1258. // Remove 'b'.
  1259. model.change( writer => {
  1260. writer.remove(
  1261. writer.createRange( writer.createPositionAt( modelRoot, 3 ), writer.createPositionAt( modelRoot, 4 ) )
  1262. );
  1263. } );
  1264. expect( viewToString( viewRoot ) ).to.equal( '<div>foz<span></span>ar</div>' );
  1265. // Remove 'z'.
  1266. model.change( writer => {
  1267. writer.remove(
  1268. writer.createRange( writer.createPositionAt( modelRoot, 2 ), writer.createPositionAt( modelRoot, 3 ) )
  1269. );
  1270. } );
  1271. expect( viewToString( viewRoot ) ).to.equal( '<div>fo<span></span>ar</div>' );
  1272. } );
  1273. it( 'should remove correct amount of text when it is split by view ui element', () => {
  1274. modelRoot._appendChild( new ModelText( 'fozbar' ) );
  1275. viewRoot._appendChild( [
  1276. new ViewText( 'foz' ),
  1277. new ViewUIElement( 'span' ),
  1278. new ViewText( 'bar' )
  1279. ] );
  1280. // Remove 'z<span></span>b'.
  1281. model.change( writer => {
  1282. writer.remove(
  1283. writer.createRange( writer.createPositionAt( modelRoot, 2 ), writer.createPositionAt( modelRoot, 4 ) )
  1284. );
  1285. } );
  1286. expect( viewToString( viewRoot ) ).to.equal( '<div>foar</div>' );
  1287. } );
  1288. it( 'should unbind elements', () => {
  1289. const modelElement = new ModelElement( 'paragraph' );
  1290. model.change( writer => {
  1291. writer.insert( modelElement, modelRootStart );
  1292. } );
  1293. const viewElement = controller.mapper.toViewElement( modelElement );
  1294. expect( viewElement ).not.to.be.undefined;
  1295. expect( controller.mapper.toModelElement( viewElement ) ).to.equal( modelElement );
  1296. model.change( writer => {
  1297. writer.remove( modelElement );
  1298. } );
  1299. expect( controller.mapper.toViewElement( modelElement ) ).to.be.undefined;
  1300. expect( controller.mapper.toModelElement( viewElement ) ).to.be.undefined;
  1301. } );
  1302. it( 'should not break when remove() is used as part of unwrapping', () => {
  1303. const modelP = new ModelElement( 'paragraph', null, new ModelText( 'foo' ) );
  1304. const modelWidget = new ModelElement( 'widget', null, modelP );
  1305. const downcastHelpers = new DowncastHelpers( dispatcher );
  1306. downcastHelpers.elementToElement( { model: 'widget', view: 'widget' } );
  1307. model.change( writer => {
  1308. writer.insert( modelWidget, modelRootStart );
  1309. } );
  1310. expect( viewToString( viewRoot ) ).to.equal( '<div><widget><p>foo</p></widget></div>' );
  1311. const viewP = controller.mapper.toViewElement( modelP );
  1312. expect( viewP ).not.to.be.undefined;
  1313. model.change( writer => {
  1314. writer.unwrap( modelWidget );
  1315. } );
  1316. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p></div>' );
  1317. // `modelP` is now bound with newly created view element.
  1318. expect( controller.mapper.toViewElement( modelP ) ).not.to.equal( viewP );
  1319. // `viewP` is no longer bound with model element.
  1320. expect( controller.mapper.toModelElement( viewP ) ).to.be.undefined;
  1321. // View element from view root is bound to `modelP`.
  1322. expect( controller.mapper.toModelElement( viewRoot.getChild( 0 ) ) ).to.equal( modelP );
  1323. } );
  1324. it( 'should work correctly if container element after ui element is removed', () => {
  1325. // Prepare a model and view structure.
  1326. // This is done outside of conversion to put view ui elements inside easily.
  1327. const modelP1 = new ModelElement( 'paragraph' );
  1328. const modelP2 = new ModelElement( 'paragraph' );
  1329. const viewP1 = new ViewContainerElement( 'p' );
  1330. const viewUi1 = new ViewUIElement( 'span' );
  1331. const viewUi2 = new ViewUIElement( 'span' );
  1332. const viewP2 = new ViewContainerElement( 'p' );
  1333. modelRoot._appendChild( [ modelP1, modelP2 ] );
  1334. viewRoot._appendChild( [ viewP1, viewUi1, viewUi2, viewP2 ] );
  1335. controller.mapper.bindElements( modelP1, viewP1 );
  1336. controller.mapper.bindElements( modelP2, viewP2 );
  1337. // Remove second paragraph element.
  1338. model.change( writer => {
  1339. writer.remove( writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 2 ) ) );
  1340. } );
  1341. expect( viewToString( viewRoot ) ).to.equal( '<div><p></p><span></span><span></span></div>' );
  1342. } );
  1343. it( 'should work correctly if container element after text node is removed', () => {
  1344. const modelText = new ModelText( 'foo' );
  1345. const modelP = new ModelElement( 'paragraph' );
  1346. model.change( writer => {
  1347. writer.insert( [ modelText, modelP ], modelRootStart );
  1348. } );
  1349. model.change( writer => {
  1350. writer.remove( modelP );
  1351. } );
  1352. expect( viewToString( viewRoot ) ).to.equal( '<div>foo</div>' );
  1353. } );
  1354. } );
  1355. describe( 'createViewElementFromHighlightDescriptor()', () => {
  1356. it( 'should return attribute element from descriptor object', () => {
  1357. const descriptor = {
  1358. classes: 'foo-class',
  1359. attributes: { one: '1', two: '2' },
  1360. priority: 7
  1361. };
  1362. const element = createViewElementFromHighlightDescriptor( descriptor );
  1363. expect( element.is( 'attributeElement' ) ).to.be.true;
  1364. expect( element.name ).to.equal( 'span' );
  1365. expect( element.priority ).to.equal( 7 );
  1366. expect( element.hasClass( 'foo-class' ) ).to.be.true;
  1367. for ( const key of Object.keys( descriptor.attributes ) ) {
  1368. expect( element.getAttribute( key ) ).to.equal( descriptor.attributes[ key ] );
  1369. }
  1370. } );
  1371. it( 'should return attribute element from descriptor object - array with classes', () => {
  1372. const descriptor = {
  1373. classes: [ 'foo-class', 'bar-class' ],
  1374. attributes: { one: '1', two: '2' },
  1375. priority: 7
  1376. };
  1377. const element = createViewElementFromHighlightDescriptor( descriptor );
  1378. expect( element.is( 'attributeElement' ) ).to.be.true;
  1379. expect( element.name ).to.equal( 'span' );
  1380. expect( element.priority ).to.equal( 7 );
  1381. expect( element.hasClass( 'foo-class' ) ).to.be.true;
  1382. expect( element.hasClass( 'bar-class' ) ).to.be.true;
  1383. for ( const key of Object.keys( descriptor.attributes ) ) {
  1384. expect( element.getAttribute( key ) ).to.equal( descriptor.attributes[ key ] );
  1385. }
  1386. } );
  1387. it( 'should create element without class', () => {
  1388. const descriptor = {
  1389. attributes: { one: '1', two: '2' },
  1390. priority: 7
  1391. };
  1392. const element = createViewElementFromHighlightDescriptor( descriptor );
  1393. expect( element.is( 'attributeElement' ) ).to.be.true;
  1394. expect( element.name ).to.equal( 'span' );
  1395. expect( element.priority ).to.equal( 7 );
  1396. for ( const key of Object.keys( descriptor.attributes ) ) {
  1397. expect( element.getAttribute( key ) ).to.equal( descriptor.attributes[ key ] );
  1398. }
  1399. } );
  1400. it( 'should create element without priority', () => {
  1401. const descriptor = {
  1402. classes: 'foo-class',
  1403. attributes: { one: '1', two: '2' }
  1404. };
  1405. const element = createViewElementFromHighlightDescriptor( descriptor );
  1406. expect( element.is( 'attributeElement' ) ).to.be.true;
  1407. expect( element.name ).to.equal( 'span' );
  1408. expect( element.priority ).to.equal( ViewAttributeElement.DEFAULT_PRIORITY );
  1409. expect( element.hasClass( 'foo-class' ) ).to.be.true;
  1410. for ( const key of Object.keys( descriptor.attributes ) ) {
  1411. expect( element.getAttribute( key ) ).to.equal( descriptor.attributes[ key ] );
  1412. }
  1413. } );
  1414. it( 'should create element without attributes', () => {
  1415. const descriptor = {
  1416. classes: 'foo-class',
  1417. priority: 7
  1418. };
  1419. const element = createViewElementFromHighlightDescriptor( descriptor );
  1420. expect( element.is( 'attributeElement' ) ).to.be.true;
  1421. expect( element.name ).to.equal( 'span' );
  1422. expect( element.priority ).to.equal( 7 );
  1423. expect( element.hasClass( 'foo-class' ) ).to.be.true;
  1424. } );
  1425. } );
  1426. } );