8
0

downcasthelpers.js 73 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import EditingController from '../../src/controller/editingcontroller';
  6. import Model from '../../src/model/model';
  7. import ModelElement from '../../src/model/element';
  8. import ModelText from '../../src/model/text';
  9. import ViewElement from '../../src/view/element';
  10. import ViewAttributeElement from '../../src/view/attributeelement';
  11. import ViewContainerElement from '../../src/view/containerelement';
  12. import ViewUIElement from '../../src/view/uielement';
  13. import ViewText from '../../src/view/text';
  14. import log from '@ckeditor/ckeditor5-utils/src/log';
  15. import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
  16. import DowncastHelpers, {
  17. clearAttributes,
  18. convertCollapsedSelection,
  19. convertRangeSelection,
  20. createViewElementFromHighlightDescriptor,
  21. insertText
  22. } from '../../src/conversion/downcasthelpers';
  23. import Mapper from '../../src/conversion/mapper';
  24. import DowncastDispatcher from '../../src/conversion/downcastdispatcher';
  25. import { stringify as stringifyView } from '../../src/dev-utils/view';
  26. import View from '../../src/view/view';
  27. import createViewRoot from '../view/_utils/createroot';
  28. import { setData as setModelData } from '../../src/dev-utils/model';
  29. describe( 'DowncastHelpers', () => {
  30. let model, modelRoot, viewRoot, downcastHelpers, controller;
  31. let modelRootStart;
  32. beforeEach( () => {
  33. model = new Model();
  34. const modelDoc = model.document;
  35. modelRoot = modelDoc.createRoot();
  36. controller = new EditingController( model );
  37. // Set name of view root the same as dom root.
  38. // This is a mock of attaching view root to dom root.
  39. controller.view.document.getRoot()._name = 'div';
  40. viewRoot = controller.view.document.getRoot();
  41. downcastHelpers = new DowncastHelpers( controller.downcastDispatcher );
  42. modelRootStart = model.createPositionAt( modelRoot, 0 );
  43. } );
  44. describe( 'elementToElement()', () => {
  45. it( 'should be chainable', () => {
  46. expect( downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ) ).to.equal( downcastHelpers );
  47. } );
  48. it( 'config.view is a string', () => {
  49. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  50. model.change( writer => {
  51. writer.insertElement( 'paragraph', modelRoot, 0 );
  52. } );
  53. expectResult( '<p></p>' );
  54. } );
  55. it( 'can be overwritten using converterPriority', () => {
  56. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  57. downcastHelpers.elementToElement( { model: 'paragraph', view: 'foo', converterPriority: 'high' } );
  58. model.change( writer => {
  59. writer.insertElement( 'paragraph', modelRoot, 0 );
  60. } );
  61. expectResult( '<foo></foo>' );
  62. } );
  63. it( 'config.view is a view element definition', () => {
  64. downcastHelpers.elementToElement( {
  65. model: 'fancyParagraph',
  66. view: {
  67. name: 'p',
  68. classes: 'fancy'
  69. }
  70. } );
  71. model.change( writer => {
  72. writer.insertElement( 'fancyParagraph', modelRoot, 0 );
  73. } );
  74. expectResult( '<p class="fancy"></p>' );
  75. } );
  76. it( 'config.view is a function', () => {
  77. downcastHelpers.elementToElement( {
  78. model: 'heading',
  79. view: ( modelElement, viewWriter ) => viewWriter.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) )
  80. } );
  81. model.change( writer => {
  82. writer.insertElement( 'heading', { level: 2 }, modelRoot, 0 );
  83. } );
  84. expectResult( '<h2></h2>' );
  85. } );
  86. } );
  87. describe( 'attributeToElement()', () => {
  88. beforeEach( () => {
  89. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  90. } );
  91. it( 'should be chainable', () => {
  92. expect( downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } ) ).to.equal( downcastHelpers );
  93. } );
  94. it( 'config.view is a string', () => {
  95. downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } );
  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 converterPriority', () => {
  102. downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } );
  103. downcastHelpers.attributeToElement( { model: 'bold', view: 'b', converterPriority: 'high' } );
  104. model.change( writer => {
  105. writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
  106. } );
  107. expectResult( '<b>foo</b>' );
  108. } );
  109. it( 'config.view is a view element definition', () => {
  110. downcastHelpers.attributeToElement( {
  111. model: 'invert',
  112. view: {
  113. name: 'span',
  114. classes: [ 'font-light', 'bg-dark' ]
  115. }
  116. } );
  117. model.change( writer => {
  118. writer.insertText( 'foo', { invert: true }, modelRoot, 0 );
  119. } );
  120. expectResult( '<span class="bg-dark font-light">foo</span>' );
  121. expect( viewRoot.getChild( 0 ).priority ).to.equal( ViewAttributeElement.DEFAULT_PRIORITY );
  122. } );
  123. it( 'config.view allows specifying the element\'s priority', () => {
  124. downcastHelpers.attributeToElement( {
  125. model: 'invert',
  126. view: {
  127. name: 'span',
  128. priority: 5
  129. }
  130. } );
  131. model.change( writer => {
  132. writer.insertText( 'foo', { invert: true }, modelRoot, 0 );
  133. } );
  134. expect( viewRoot.getChild( 0 ).priority ).to.equal( 5 );
  135. } );
  136. it( 'model attribute value is enum', () => {
  137. downcastHelpers.attributeToElement( {
  138. model: {
  139. key: 'fontSize',
  140. values: [ 'big', 'small' ]
  141. },
  142. view: {
  143. big: {
  144. name: 'span',
  145. styles: {
  146. 'font-size': '1.2em'
  147. }
  148. },
  149. small: {
  150. name: 'span',
  151. styles: {
  152. 'font-size': '0.8em'
  153. },
  154. priority: 5
  155. }
  156. }
  157. } );
  158. model.change( writer => {
  159. writer.insertText( 'foo', { fontSize: 'big' }, modelRoot, 0 );
  160. } );
  161. expect( viewRoot.getChild( 0 ).priority ).to.equal( ViewAttributeElement.DEFAULT_PRIORITY );
  162. expectResult( '<span style="font-size:1.2em">foo</span>' );
  163. model.change( writer => {
  164. writer.setAttribute( 'fontSize', 'small', modelRoot.getChild( 0 ) );
  165. } );
  166. expectResult( '<span style="font-size:0.8em">foo</span>' );
  167. expect( viewRoot.getChild( 0 ).priority ).to.equal( 5 );
  168. model.change( writer => {
  169. writer.removeAttribute( 'fontSize', modelRoot.getChild( 0 ) );
  170. } );
  171. expectResult( 'foo' );
  172. } );
  173. it( 'config.view is a function', () => {
  174. downcastHelpers.attributeToElement( {
  175. model: 'bold',
  176. view: ( modelAttributeValue, viewWriter ) => {
  177. return viewWriter.createAttributeElement( 'span', { style: 'font-weight:' + modelAttributeValue } );
  178. }
  179. } );
  180. model.change( writer => {
  181. writer.insertText( 'foo', { bold: '500' }, modelRoot, 0 );
  182. } );
  183. expectResult( '<span style="font-weight:500">foo</span>' );
  184. } );
  185. it( 'config.model.name is given', () => {
  186. downcastHelpers.attributeToElement( {
  187. model: {
  188. key: 'color',
  189. name: '$text'
  190. },
  191. view: ( modelAttributeValue, viewWriter ) => {
  192. return viewWriter.createAttributeElement( 'span', { style: 'color:' + modelAttributeValue } );
  193. }
  194. } );
  195. downcastHelpers.elementToElement( {
  196. model: 'smiley',
  197. view: ( modelElement, viewWriter ) => {
  198. return viewWriter.createEmptyElement( 'img', {
  199. src: 'smile.jpg',
  200. class: 'smiley'
  201. } );
  202. }
  203. } );
  204. model.change( writer => {
  205. writer.insertText( 'foo', { color: '#FF0000' }, modelRoot, 0 );
  206. writer.insertElement( 'smiley', { color: '#FF0000' }, modelRoot, 3 );
  207. } );
  208. expectResult( '<span style="color:#FF0000">foo</span><img class="smiley" src="smile.jpg"></img>' );
  209. } );
  210. it( 'should not convert if creator returned null', () => {
  211. downcastHelpers.elementToElement( { model: 'div', view: () => null } );
  212. const modelElement = new ModelElement( 'div' );
  213. model.change( writer => {
  214. writer.insert( modelElement, modelRootStart );
  215. } );
  216. expect( viewToString( viewRoot ) ).to.equal( '<div></div>' );
  217. } );
  218. it( 'should convert insert/change/remove of attribute in model into wrapping element in a view', () => {
  219. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) );
  220. downcastHelpers.attributeToElement( {
  221. model: 'bold',
  222. view: ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'b' )
  223. } );
  224. model.change( writer => {
  225. writer.insert( modelElement, modelRootStart );
  226. } );
  227. expect( viewToString( viewRoot ) ).to.equal( '<div><p><b>foobar</b></p></div>' );
  228. model.change( writer => {
  229. writer.removeAttribute( 'bold', writer.createRangeIn( modelElement ) );
  230. } );
  231. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  232. } );
  233. it( 'should convert insert/remove of attribute in model with wrapping element generating function as a parameter', () => {
  234. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { style: 'bold' } ) );
  235. downcastHelpers.attributeToElement( {
  236. model: 'style',
  237. view: ( modelAttributeValue, viewWriter ) => {
  238. if ( modelAttributeValue == 'bold' ) {
  239. return viewWriter.createAttributeElement( 'b' );
  240. }
  241. }
  242. } );
  243. model.change( writer => {
  244. writer.insert( modelElement, modelRootStart );
  245. } );
  246. expect( viewToString( viewRoot ) ).to.equal( '<div><p><b>foobar</b></p></div>' );
  247. model.change( writer => {
  248. writer.removeAttribute( 'style', writer.createRangeIn( modelElement ) );
  249. } );
  250. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  251. } );
  252. it( 'should update range on re-wrapping attribute (#475)', () => {
  253. const modelElement = new ModelElement( 'paragraph', null, [
  254. new ModelText( 'x' ),
  255. new ModelText( 'foo', { link: 'http://foo.com' } ),
  256. new ModelText( 'x' )
  257. ] );
  258. downcastHelpers.attributeToElement( {
  259. model: 'link',
  260. view: ( modelAttributeValue, viewWriter ) => {
  261. return viewWriter.createAttributeElement( 'a', { href: modelAttributeValue } );
  262. }
  263. } );
  264. model.change( writer => {
  265. writer.insert( modelElement, modelRootStart );
  266. } );
  267. expect( viewToString( viewRoot ) ).to.equal( '<div><p>x<a href="http://foo.com">foo</a>x</p></div>' );
  268. // Set new attribute on old link but also on non-linked characters.
  269. model.change( writer => {
  270. writer.setAttribute( 'link', 'http://foobar.com', writer.createRangeIn( modelElement ) );
  271. } );
  272. expect( viewToString( viewRoot ) ).to.equal( '<div><p><a href="http://foobar.com">xfoox</a></p></div>' );
  273. } );
  274. it( 'should support unicode', () => {
  275. const modelElement = new ModelElement( 'paragraph', null, [ 'நி', new ModelText( 'லைக்', { bold: true } ), 'கு' ] );
  276. downcastHelpers.attributeToElement( {
  277. model: 'bold',
  278. view: ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'b' )
  279. } );
  280. model.change( writer => {
  281. writer.insert( modelElement, modelRootStart );
  282. } );
  283. expect( viewToString( viewRoot ) ).to.equal( '<div><p>நி<b>லைக்</b>கு</p></div>' );
  284. model.change( writer => {
  285. writer.removeAttribute( 'bold', writer.createRangeIn( modelElement ) );
  286. } );
  287. expect( viewToString( viewRoot ) ).to.equal( '<div><p>நிலைக்கு</p></div>' );
  288. } );
  289. it( 'should be possible to override ', () => {
  290. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) );
  291. downcastHelpers.attributeToElement( {
  292. model: 'bold',
  293. view: ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'b' )
  294. } );
  295. downcastHelpers.attributeToElement( {
  296. model: 'bold',
  297. view: ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'strong' ),
  298. converterPriority: 'high'
  299. } );
  300. model.change( writer => {
  301. writer.insert( modelElement, modelRootStart );
  302. } );
  303. expect( viewToString( viewRoot ) ).to.equal( '<div><p><strong>foobar</strong></p></div>' );
  304. } );
  305. it( 'should not convert and not consume if creator function returned null', () => {
  306. sinon.spy( controller.downcastDispatcher, 'fire' );
  307. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { italic: true } ) );
  308. downcastHelpers.attributeToElement( {
  309. model: 'italic',
  310. view: () => null
  311. } );
  312. const spy = sinon.spy();
  313. controller.downcastDispatcher.on( 'attribute:italic', spy );
  314. model.change( writer => {
  315. writer.insert( modelElement, modelRootStart );
  316. } );
  317. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  318. expect( controller.downcastDispatcher.fire.calledWith( 'attribute:italic:$text' ) ).to.be.true;
  319. expect( spy.called ).to.be.true;
  320. } );
  321. } );
  322. describe( 'attributeToAttribute()', () => {
  323. testUtils.createSinonSandbox();
  324. beforeEach( () => {
  325. downcastHelpers.elementToElement( { model: 'image', view: 'img' } );
  326. downcastHelpers.elementToElement( {
  327. model: 'paragraph',
  328. view: ( modelItem, viewWriter ) => viewWriter.createContainerElement( 'p' )
  329. } );
  330. downcastHelpers.attributeToAttribute( {
  331. model: 'class',
  332. view: 'class'
  333. } );
  334. } );
  335. it( 'should be chainable', () => {
  336. expect( downcastHelpers.attributeToAttribute( { model: 'source', view: 'src' } ) ).to.equal( downcastHelpers );
  337. } );
  338. it( 'config.view is a string', () => {
  339. downcastHelpers.attributeToAttribute( { model: 'source', view: 'src' } );
  340. model.change( writer => {
  341. writer.insertElement( 'image', { source: 'foo.jpg' }, modelRoot, 0 );
  342. } );
  343. expectResult( '<img src="foo.jpg"></img>' );
  344. model.change( writer => {
  345. writer.removeAttribute( 'source', modelRoot.getChild( 0 ) );
  346. } );
  347. expectResult( '<img></img>' );
  348. } );
  349. it( 'can be overwritten using converterPriority', () => {
  350. downcastHelpers.attributeToAttribute( { model: 'source', view: 'href' } );
  351. downcastHelpers.attributeToAttribute( { model: 'source', view: 'src', converterPriority: 'high' } );
  352. model.change( writer => {
  353. writer.insertElement( 'image', { source: 'foo.jpg' }, modelRoot, 0 );
  354. } );
  355. expectResult( '<img src="foo.jpg"></img>' );
  356. } );
  357. it( 'model element name specified', () => {
  358. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  359. downcastHelpers.attributeToAttribute( {
  360. model: {
  361. name: 'image',
  362. key: 'source'
  363. },
  364. view: 'src'
  365. } );
  366. model.change( writer => {
  367. writer.insertElement( 'image', { source: 'foo.jpg' }, modelRoot, 0 );
  368. } );
  369. expectResult( '<img src="foo.jpg"></img>' );
  370. model.change( writer => {
  371. writer.rename( modelRoot.getChild( 0 ), 'paragraph' );
  372. } );
  373. expectResult( '<p></p>' );
  374. } );
  375. it( 'config.view is an object, model attribute value is enum', () => {
  376. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  377. downcastHelpers.attributeToAttribute( {
  378. model: {
  379. key: 'styled',
  380. values: [ 'dark', 'light' ]
  381. },
  382. view: {
  383. dark: {
  384. key: 'class',
  385. value: [ 'styled', 'styled-dark' ]
  386. },
  387. light: {
  388. key: 'class',
  389. value: [ 'styled', 'styled-light' ]
  390. }
  391. }
  392. } );
  393. model.change( writer => {
  394. writer.insertElement( 'paragraph', { styled: 'dark' }, modelRoot, 0 );
  395. } );
  396. expectResult( '<p class="styled styled-dark"></p>' );
  397. model.change( writer => {
  398. writer.setAttribute( 'styled', 'light', modelRoot.getChild( 0 ) );
  399. } );
  400. expectResult( '<p class="styled styled-light"></p>' );
  401. model.change( writer => {
  402. writer.removeAttribute( 'styled', modelRoot.getChild( 0 ) );
  403. } );
  404. expectResult( '<p></p>' );
  405. } );
  406. it( 'config.view is an object, model attribute value is enum, view has style', () => {
  407. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  408. downcastHelpers.attributeToAttribute( {
  409. model: {
  410. key: 'align',
  411. values: [ 'right', 'center' ]
  412. },
  413. view: {
  414. right: {
  415. key: 'style',
  416. value: {
  417. 'text-align': 'right'
  418. }
  419. },
  420. center: {
  421. key: 'style',
  422. value: {
  423. 'text-align': 'center'
  424. }
  425. }
  426. }
  427. } );
  428. model.change( writer => {
  429. writer.insertElement( 'paragraph', { align: 'right' }, modelRoot, 0 );
  430. } );
  431. expectResult( '<p style="text-align:right"></p>' );
  432. model.change( writer => {
  433. writer.setAttribute( 'align', 'center', modelRoot.getChild( 0 ) );
  434. } );
  435. expectResult( '<p style="text-align:center"></p>' );
  436. model.change( writer => {
  437. writer.removeAttribute( 'align', modelRoot.getChild( 0 ) );
  438. } );
  439. expectResult( '<p></p>' );
  440. } );
  441. it( 'config.view is an object, only name and key are provided', () => {
  442. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  443. downcastHelpers.attributeToAttribute( {
  444. model: {
  445. name: 'paragraph',
  446. key: 'class'
  447. },
  448. view: {
  449. name: 'paragraph',
  450. key: 'class'
  451. }
  452. } );
  453. model.change( writer => {
  454. writer.insertElement( 'paragraph', { class: 'dark' }, modelRoot, 0 );
  455. } );
  456. expectResult( '<p class="dark"></p>' );
  457. model.change( writer => {
  458. writer.setAttribute( 'class', 'light', modelRoot.getChild( 0 ) );
  459. } );
  460. expectResult( '<p class="light"></p>' );
  461. model.change( writer => {
  462. writer.removeAttribute( 'class', modelRoot.getChild( 0 ) );
  463. } );
  464. expectResult( '<p></p>' );
  465. } );
  466. it( 'config.view is a function', () => {
  467. downcastHelpers.attributeToAttribute( {
  468. model: 'styled',
  469. view: attributeValue => ( { key: 'class', value: 'styled-' + attributeValue } )
  470. } );
  471. model.change( writer => {
  472. writer.insertElement( 'image', { styled: 'pull-out' }, modelRoot, 0 );
  473. } );
  474. expectResult( '<img class="styled-pull-out"></img>' );
  475. } );
  476. // #1587
  477. it( 'config.view and config.model as strings in generic conversion (elements only)', () => {
  478. const logStub = testUtils.sinon.stub( log, 'warn' );
  479. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  480. downcastHelpers.attributeToAttribute( { model: 'test', view: 'test' } );
  481. model.change( writer => {
  482. writer.insertElement( 'paragraph', { test: '1' }, modelRoot, 0 );
  483. writer.insertElement( 'paragraph', { test: '2' }, modelRoot, 1 );
  484. } );
  485. expectResult( '<p test="1"></p><p test="2"></p>' );
  486. expect( logStub.callCount ).to.equal( 0 );
  487. model.change( writer => {
  488. writer.removeAttribute( 'test', modelRoot.getChild( 1 ) );
  489. } );
  490. expectResult( '<p test="1"></p><p></p>' );
  491. } );
  492. // #1587
  493. it( 'config.view and config.model as strings in generic conversion (elements + text)', () => {
  494. const logStub = testUtils.sinon.stub( log, 'warn' );
  495. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  496. downcastHelpers.attributeToAttribute( { model: 'test', view: 'test' } );
  497. model.change( writer => {
  498. writer.insertElement( 'paragraph', modelRoot, 0 );
  499. writer.insertElement( 'paragraph', { test: '1' }, modelRoot, 1 );
  500. writer.insertText( 'Foo', { test: '2' }, modelRoot.getChild( 0 ), 0 );
  501. writer.insertText( 'Bar', { test: '3' }, modelRoot.getChild( 1 ), 0 );
  502. } );
  503. expectResult( '<p>Foo</p><p test="1">Bar</p>' );
  504. expect( logStub.callCount ).to.equal( 2 );
  505. expect( logStub.alwaysCalledWithMatch( 'conversion-attribute-to-attribute-on-text' ) ).to.true;
  506. model.change( writer => {
  507. writer.removeAttribute( 'test', modelRoot.getChild( 1 ) );
  508. } );
  509. expectResult( '<p>Foo</p><p>Bar</p>' );
  510. } );
  511. it( 'should convert attribute insert/change/remove on a model node', () => {
  512. const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) );
  513. model.change( writer => {
  514. writer.insert( modelElement, modelRootStart );
  515. } );
  516. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="foo">foobar</p></div>' );
  517. model.change( writer => {
  518. writer.setAttribute( 'class', 'bar', modelElement );
  519. } );
  520. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="bar">foobar</p></div>' );
  521. model.change( writer => {
  522. writer.removeAttribute( 'class', modelElement );
  523. } );
  524. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  525. } );
  526. it( 'should convert insert/change/remove with attribute generating function as a parameter', () => {
  527. downcastHelpers.elementToElement( { model: 'div', view: 'div' } );
  528. downcastHelpers.attributeToAttribute( {
  529. model: 'theme',
  530. view: ( value, data ) => {
  531. if ( data.item instanceof ModelElement && data.item.childCount > 0 ) {
  532. value += ' fix-content';
  533. }
  534. return { key: 'class', value };
  535. }
  536. } );
  537. const modelParagraph = new ModelElement( 'paragraph', { theme: 'nice' }, new ModelText( 'foobar' ) );
  538. const modelDiv = new ModelElement( 'div', { theme: 'nice' } );
  539. model.change( writer => {
  540. writer.insert( [ modelParagraph, modelDiv ], modelRootStart );
  541. } );
  542. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="nice fix-content">foobar</p><div class="nice"></div></div>' );
  543. model.change( writer => {
  544. writer.setAttribute( 'theme', 'awesome', modelParagraph );
  545. } );
  546. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="awesome fix-content">foobar</p><div class="nice"></div></div>' );
  547. model.change( writer => {
  548. writer.removeAttribute( 'theme', modelParagraph );
  549. } );
  550. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p><div class="nice"></div></div>' );
  551. } );
  552. it( 'should be possible to override setAttribute', () => {
  553. downcastHelpers.attributeToAttribute( {
  554. model: 'class',
  555. view: ( evt, data, conversionApi ) => {
  556. conversionApi.consumable.consume( data.item, 'attribute:class' );
  557. },
  558. converterPriority: 'high'
  559. } );
  560. model.change( writer => {
  561. const modelElement = new ModelElement( 'paragraph', { classes: 'foo' }, new ModelText( 'foobar' ) );
  562. writer.insert( modelElement, modelRootStart );
  563. } );
  564. // No attribute set.
  565. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  566. } );
  567. it( 'should not convert or consume if element creator returned null', () => {
  568. const callback = sinon.stub().returns( null );
  569. downcastHelpers.attributeToAttribute( {
  570. model: 'class',
  571. view: callback,
  572. converterPriority: 'high'
  573. } );
  574. const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) );
  575. model.change( writer => {
  576. writer.insert( modelElement, modelRootStart );
  577. } );
  578. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="foo">foobar</p></div>' );
  579. sinon.assert.called( callback );
  580. } );
  581. } );
  582. describe( 'markerToElement()', () => {
  583. let modelText, modelElement, range;
  584. it( 'should be chainable', () => {
  585. expect( downcastHelpers.markerToElement( { model: 'search', view: 'marker-search' } ) ).to.equal( downcastHelpers );
  586. } );
  587. it( 'config.view is a string', () => {
  588. downcastHelpers.markerToElement( { model: 'search', view: 'marker-search' } );
  589. model.change( writer => {
  590. writer.insertText( 'foo', modelRoot, 0 );
  591. const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 2 ) );
  592. writer.addMarker( 'search', { range, usingOperation: false } );
  593. } );
  594. expectResult( 'f<marker-search></marker-search>o<marker-search></marker-search>o' );
  595. } );
  596. it( 'can be overwritten using converterPriority', () => {
  597. downcastHelpers.markerToElement( { model: 'search', view: 'marker-search' } );
  598. downcastHelpers.markerToElement( { model: 'search', view: 'search', converterPriority: 'high' } );
  599. model.change( writer => {
  600. writer.insertText( 'foo', modelRoot, 0 );
  601. const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 2 ) );
  602. writer.addMarker( 'search', { range, usingOperation: false } );
  603. } );
  604. expectResult( 'f<search></search>o<search></search>o' );
  605. } );
  606. it( 'config.view is a view element definition', () => {
  607. downcastHelpers.markerToElement( {
  608. model: 'search',
  609. view: {
  610. name: 'span',
  611. attributes: {
  612. 'data-marker': 'search'
  613. }
  614. }
  615. } );
  616. model.change( writer => {
  617. writer.insertText( 'foo', modelRoot, 0 );
  618. const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 2 ) );
  619. writer.addMarker( 'search', { range, usingOperation: false } );
  620. } );
  621. expectResult( 'f<span data-marker="search"></span>o<span data-marker="search"></span>o' );
  622. } );
  623. it( 'config.view is a function', () => {
  624. downcastHelpers.markerToElement( {
  625. model: 'search',
  626. view: ( data, viewWriter ) => {
  627. return viewWriter.createUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } );
  628. }
  629. } );
  630. model.change( writer => {
  631. writer.insertText( 'foo', modelRoot, 0 );
  632. const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 2 ) );
  633. writer.addMarker( 'search', { range, usingOperation: false } );
  634. } );
  635. expectResult( 'f<span data-marker="search" data-start="true"></span>o<span data-marker="search" data-start="false"></span>o' );
  636. } );
  637. describe( 'collapsed range', () => {
  638. beforeEach( () => {
  639. modelText = new ModelText( 'foobar' );
  640. modelElement = new ModelElement( 'paragraph', null, modelText );
  641. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  642. model.change( writer => {
  643. writer.insert( modelElement, modelRootStart );
  644. } );
  645. range = model.createRange( model.createPositionAt( modelElement, 3 ), model.createPositionAt( modelElement, 3 ) );
  646. } );
  647. it( 'should insert and remove ui element', () => {
  648. downcastHelpers.markerToElement( {
  649. model: 'marker',
  650. view: ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } )
  651. } );
  652. model.change( writer => {
  653. writer.addMarker( 'marker', { range, usingOperation: false } );
  654. } );
  655. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo<span class="marker"></span>bar</p></div>' );
  656. model.change( writer => {
  657. writer.removeMarker( 'marker' );
  658. } );
  659. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  660. } );
  661. it( 'should not convert if consumable was consumed', () => {
  662. sinon.spy( controller.downcastDispatcher, 'fire' );
  663. downcastHelpers.markerToElement( {
  664. model: 'marker',
  665. view: ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } )
  666. } );
  667. controller.downcastDispatcher.on( 'addMarker:marker', ( evt, data, conversionApi ) => {
  668. conversionApi.consumable.consume( data.markerRange, 'addMarker:marker' );
  669. }, { priority: 'high' } );
  670. model.change( writer => {
  671. writer.addMarker( 'marker', { range, usingOperation: false } );
  672. } );
  673. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  674. expect( controller.downcastDispatcher.fire.calledWith( 'addMarker:marker' ) );
  675. } );
  676. it( 'should not convert if creator returned null', () => {
  677. downcastHelpers.markerToElement( {
  678. model: 'marker',
  679. view: () => null
  680. } );
  681. model.change( writer => {
  682. writer.addMarker( 'marker', { range, usingOperation: false } );
  683. } );
  684. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  685. model.change( writer => {
  686. writer.removeMarker( 'marker' );
  687. } );
  688. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  689. } );
  690. } );
  691. describe( 'non-collapsed range', () => {
  692. beforeEach( () => {
  693. modelText = new ModelText( 'foobar' );
  694. modelElement = new ModelElement( 'paragraph', null, modelText );
  695. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  696. model.change( writer => {
  697. writer.insert( modelElement, modelRootStart );
  698. } );
  699. range = model.createRange( model.createPositionAt( modelElement, 2 ), model.createPositionAt( modelElement, 5 ) );
  700. } );
  701. it( 'should insert and remove ui element - element as a creator', () => {
  702. downcastHelpers.markerToElement( {
  703. model: 'marker',
  704. view: ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } )
  705. } );
  706. model.change( writer => {
  707. writer.addMarker( 'marker', { range, usingOperation: false } );
  708. } );
  709. expect( viewToString( viewRoot ) )
  710. .to.equal( '<div><p>fo<span class="marker"></span>oba<span class="marker"></span>r</p></div>' );
  711. model.change( writer => {
  712. writer.removeMarker( 'marker' );
  713. } );
  714. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  715. } );
  716. it( 'should insert and remove ui element - function as a creator', () => {
  717. downcastHelpers.markerToElement( {
  718. model: 'marker',
  719. view: ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': data.markerName } )
  720. } );
  721. model.change( writer => {
  722. writer.addMarker( 'marker', { range, usingOperation: false } );
  723. } );
  724. expect( viewToString( viewRoot ) )
  725. .to.equal( '<div><p>fo<span class="marker"></span>oba<span class="marker"></span>r</p></div>' );
  726. model.change( writer => {
  727. writer.removeMarker( 'marker' );
  728. } );
  729. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  730. } );
  731. it( 'should insert and remove different opening and ending element', () => {
  732. downcastHelpers.markerToElement( {
  733. model: 'marker',
  734. view: ( data, viewWriter ) => {
  735. if ( data.isOpening ) {
  736. return viewWriter.createUIElement( 'span', { 'class': data.markerName, 'data-start': true } );
  737. }
  738. return viewWriter.createUIElement( 'span', { 'class': data.markerName, 'data-end': true } );
  739. }
  740. } );
  741. model.change( writer => {
  742. writer.addMarker( 'marker', { range, usingOperation: false } );
  743. } );
  744. expect( viewToString( viewRoot ) ).to.equal(
  745. '<div><p>fo<span class="marker" data-start="true"></span>oba<span class="marker" data-end="true"></span>r</p></div>'
  746. );
  747. model.change( writer => {
  748. writer.removeMarker( 'marker' );
  749. } );
  750. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  751. } );
  752. it( 'should not convert if consumable was consumed', () => {
  753. sinon.spy( controller.downcastDispatcher, 'fire' );
  754. downcastHelpers.markerToElement( {
  755. model: 'marker',
  756. view: ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } )
  757. } );
  758. controller.downcastDispatcher.on( 'addMarker:marker', ( evt, data, conversionApi ) => {
  759. conversionApi.consumable.consume( data.item, 'addMarker:marker' );
  760. }, { priority: 'high' } );
  761. model.change( writer => {
  762. writer.addMarker( 'marker', { range, usingOperation: false } );
  763. } );
  764. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  765. expect( controller.downcastDispatcher.fire.calledWith( 'addMarker:marker' ) );
  766. } );
  767. } );
  768. } );
  769. describe( 'markerToHighlight()', () => {
  770. it( 'should be chainable', () => {
  771. expect( downcastHelpers.markerToHighlight( { model: 'comment', view: { classes: 'comment' } } ) ).to.equal( downcastHelpers );
  772. } );
  773. it( 'config.view is a highlight descriptor', () => {
  774. downcastHelpers.markerToHighlight( { model: 'comment', view: { classes: 'comment' } } );
  775. model.change( writer => {
  776. writer.insertText( 'foo', modelRoot, 0 );
  777. const range = writer.createRange( writer.createPositionAt( modelRoot, 0 ), writer.createPositionAt( modelRoot, 3 ) );
  778. writer.addMarker( 'comment', { range, usingOperation: false } );
  779. } );
  780. expectResult( '<span class="comment">foo</span>' );
  781. } );
  782. it( 'can be overwritten using converterPriority', () => {
  783. downcastHelpers.markerToHighlight( { model: 'comment', view: { classes: 'comment' } } );
  784. downcastHelpers.markerToHighlight( { model: 'comment', view: { classes: 'new-comment' }, converterPriority: 'high' } );
  785. model.change( writer => {
  786. writer.insertText( 'foo', modelRoot, 0 );
  787. const range = writer.createRange( writer.createPositionAt( modelRoot, 0 ), writer.createPositionAt( modelRoot, 3 ) );
  788. writer.addMarker( 'comment', { range, usingOperation: false } );
  789. } );
  790. expectResult( '<span class="new-comment">foo</span>' );
  791. } );
  792. it( 'config.view is a function', () => {
  793. downcastHelpers.markerToHighlight( {
  794. model: 'comment',
  795. view: data => {
  796. const commentType = data.markerName.split( ':' )[ 1 ];
  797. return {
  798. classes: [ 'comment', 'comment-' + commentType ]
  799. };
  800. }
  801. } );
  802. model.change( writer => {
  803. writer.insertText( 'foo', modelRoot, 0 );
  804. const range = writer.createRange( writer.createPositionAt( modelRoot, 0 ), writer.createPositionAt( modelRoot, 3 ) );
  805. writer.addMarker( 'comment:abc', { range, usingOperation: false } );
  806. } );
  807. expectResult( '<span class="comment comment-abc">foo</span>' );
  808. } );
  809. describe( 'highlight', () => {
  810. const highlightConfig = {
  811. model: 'marker',
  812. view: {
  813. classes: 'highlight-class',
  814. attributes: { title: 'title' }
  815. },
  816. converterPriority: 7
  817. };
  818. describe( 'on text', () => {
  819. let markerRange;
  820. beforeEach( () => {
  821. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  822. const modelElement1 = new ModelElement( 'paragraph', null, new ModelText( 'foo' ) );
  823. const modelElement2 = new ModelElement( 'paragraph', null, new ModelText( 'bar' ) );
  824. model.change( writer => {
  825. writer.insert( [ modelElement1, modelElement2 ], modelRootStart );
  826. } );
  827. markerRange = model.createRangeIn( modelRoot );
  828. } );
  829. it( 'should wrap and unwrap text nodes', () => {
  830. downcastHelpers.markerToHighlight( highlightConfig );
  831. model.change( writer => {
  832. writer.addMarker( 'marker', { range: markerRange, usingOperation: false } );
  833. } );
  834. expect( viewToString( viewRoot ) ).to.equal(
  835. '<div>' +
  836. '<p>' +
  837. '<span class="highlight-class" title="title">foo</span>' +
  838. '</p>' +
  839. '<p>' +
  840. '<span class="highlight-class" title="title">bar</span>' +
  841. '</p>' +
  842. '</div>'
  843. );
  844. model.change( writer => {
  845. writer.removeMarker( 'marker' );
  846. } );
  847. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  848. } );
  849. it( 'should be possible to overwrite', () => {
  850. downcastHelpers.markerToHighlight( highlightConfig );
  851. downcastHelpers.markerToHighlight( {
  852. model: 'marker',
  853. view: { classes: 'override-class' },
  854. converterPriority: 'high'
  855. } );
  856. model.change( writer => {
  857. writer.addMarker( 'marker', { range: markerRange, usingOperation: false } );
  858. } );
  859. expect( viewToString( viewRoot ) ).to.equal(
  860. '<div>' +
  861. '<p>' +
  862. '<span class="override-class">foo</span>' +
  863. '</p>' +
  864. '<p>' +
  865. '<span class="override-class">bar</span>' +
  866. '</p>' +
  867. '</div>'
  868. );
  869. model.change( writer => {
  870. writer.removeMarker( 'marker' );
  871. } );
  872. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  873. } );
  874. it( 'should do nothing if descriptor is not provided or generating function returns null', () => {
  875. downcastHelpers.markerToHighlight( {
  876. model: 'marker',
  877. view: () => null,
  878. converterPriority: 'high'
  879. } );
  880. model.change( writer => {
  881. writer.addMarker( 'marker', { range: markerRange, usingOperation: false } );
  882. } );
  883. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  884. model.change( writer => {
  885. writer.removeMarker( 'marker' );
  886. } );
  887. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  888. } );
  889. it( 'should do nothing if collapsed marker is converted', () => {
  890. downcastHelpers.markerToHighlight( {
  891. model: 'marker',
  892. view: { classes: 'foo' },
  893. converterPriority: 'high'
  894. } );
  895. markerRange = model.createRange( model.createPositionAt( modelRoot, 0 ), model.createPositionAt( modelRoot, 0 ) );
  896. model.change( writer => {
  897. writer.addMarker( 'marker', { range: markerRange, usingOperation: false } );
  898. } );
  899. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  900. model.change( () => {
  901. model.markers._remove( 'marker' );
  902. } );
  903. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  904. } );
  905. it( 'should correctly wrap and unwrap multiple, intersecting markers', () => {
  906. downcastHelpers.markerToHighlight( {
  907. model: 'markerFoo',
  908. view: { classes: 'foo' }
  909. } );
  910. downcastHelpers.markerToHighlight( {
  911. model: 'markerBar',
  912. view: { classes: 'bar' }
  913. } );
  914. downcastHelpers.markerToHighlight( {
  915. model: 'markerXyz',
  916. view: { classes: 'xyz' }
  917. } );
  918. const p1 = modelRoot.getChild( 0 );
  919. const p2 = modelRoot.getChild( 1 );
  920. model.change( writer => {
  921. const range = writer.createRange( writer.createPositionAt( p1, 0 ), writer.createPositionAt( p1, 3 ) );
  922. writer.addMarker( 'markerFoo', { range, usingOperation: false } );
  923. } );
  924. expect( viewToString( viewRoot ) ).to.equal(
  925. '<div>' +
  926. '<p>' +
  927. '<span class="foo">foo</span>' +
  928. '</p>' +
  929. '<p>bar</p>' +
  930. '</div>'
  931. );
  932. model.change( writer => {
  933. const range = writer.createRange( writer.createPositionAt( p1, 1 ), writer.createPositionAt( p2, 2 ) );
  934. writer.addMarker( 'markerBar', { range, usingOperation: false } );
  935. } );
  936. expect( viewToString( viewRoot ) ).to.equal(
  937. '<div>' +
  938. '<p>' +
  939. '<span class="foo">f</span>' +
  940. '<span class="bar">' +
  941. '<span class="foo">oo</span>' +
  942. '</span>' +
  943. '</p>' +
  944. '<p>' +
  945. '<span class="bar">ba</span>' +
  946. 'r' +
  947. '</p>' +
  948. '</div>'
  949. );
  950. model.change( writer => {
  951. const range = writer.createRange( writer.createPositionAt( p1, 2 ), writer.createPositionAt( p2, 3 ) );
  952. writer.addMarker( 'markerXyz', { range, usingOperation: false } );
  953. } );
  954. expect( viewToString( viewRoot ) ).to.equal(
  955. '<div>' +
  956. '<p>' +
  957. '<span class="foo">f</span>' +
  958. '<span class="bar">' +
  959. '<span class="foo">' +
  960. 'o' +
  961. '<span class="xyz">o</span>' +
  962. '</span>' +
  963. '</span>' +
  964. '</p>' +
  965. '<p>' +
  966. '<span class="bar">' +
  967. '<span class="xyz">ba</span>' +
  968. '</span>' +
  969. '<span class="xyz">r</span>' +
  970. '</p>' +
  971. '</div>'
  972. );
  973. model.change( writer => {
  974. writer.removeMarker( 'markerBar' );
  975. } );
  976. expect( viewToString( viewRoot ) ).to.equal(
  977. '<div>' +
  978. '<p>' +
  979. '<span class="foo">' +
  980. 'fo' +
  981. '<span class="xyz">o</span>' +
  982. '</span>' +
  983. '</p>' +
  984. '<p>' +
  985. '<span class="xyz">bar</span>' +
  986. '</p>' +
  987. '</div>'
  988. );
  989. model.change( writer => {
  990. writer.removeMarker( 'markerFoo' );
  991. } );
  992. expect( viewToString( viewRoot ) ).to.equal(
  993. '<div>' +
  994. '<p>' +
  995. 'fo' +
  996. '<span class="xyz">o</span>' +
  997. '</p>' +
  998. '<p>' +
  999. '<span class="xyz">bar</span>' +
  1000. '</p>' +
  1001. '</div>'
  1002. );
  1003. model.change( writer => {
  1004. writer.removeMarker( 'markerXyz' );
  1005. } );
  1006. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  1007. } );
  1008. it( 'should do nothing if marker is applied and removed on empty-ish range', () => {
  1009. downcastHelpers.markerToHighlight( highlightConfig );
  1010. const p1 = modelRoot.getChild( 0 );
  1011. const p2 = modelRoot.getChild( 1 );
  1012. const markerRange = model.createRange( model.createPositionAt( p1, 3 ), model.createPositionAt( p2, 0 ) );
  1013. model.change( writer => {
  1014. writer.addMarker( 'marker', { range: markerRange, usingOperation: false } );
  1015. } );
  1016. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  1017. model.change( writer => {
  1018. writer.removeMarker( 'marker', { range: markerRange, usingOperation: false } );
  1019. } );
  1020. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  1021. } );
  1022. } );
  1023. describe( 'on element', () => {
  1024. const highlightConfig = {
  1025. model: 'marker',
  1026. view: {
  1027. classes: 'highlight-class',
  1028. attributes: { title: 'title' },
  1029. id: 'customId'
  1030. },
  1031. converterPriority: 7
  1032. };
  1033. let markerRange;
  1034. beforeEach( () => {
  1035. downcastHelpers.elementToElement( {
  1036. model: 'div',
  1037. view: () => {
  1038. const viewContainer = new ViewContainerElement( 'div' );
  1039. viewContainer._setCustomProperty( 'addHighlight', ( element, descriptor, writer ) => {
  1040. writer.addClass( descriptor.classes, element );
  1041. } );
  1042. viewContainer._setCustomProperty( 'removeHighlight', ( element, id, writer ) => {
  1043. writer.setAttribute( 'class', '', element );
  1044. } );
  1045. return viewContainer;
  1046. }
  1047. } );
  1048. const modelElement = new ModelElement( 'div', null, new ModelText( 'foo' ) );
  1049. model.change( writer => {
  1050. writer.insert( modelElement, modelRootStart );
  1051. } );
  1052. markerRange = model.createRangeOn( modelElement );
  1053. downcastHelpers.markerToHighlight( highlightConfig );
  1054. } );
  1055. it( 'should use addHighlight and removeHighlight on elements and not convert children nodes', () => {
  1056. model.change( writer => {
  1057. writer.addMarker( 'marker', { range: markerRange, usingOperation: false } );
  1058. } );
  1059. expect( viewToString( viewRoot ) ).to.equal(
  1060. '<div>' +
  1061. '<div class="highlight-class">' +
  1062. 'foo' +
  1063. '</div>' +
  1064. '</div>'
  1065. );
  1066. model.change( writer => {
  1067. writer.removeMarker( 'marker' );
  1068. } );
  1069. expect( viewToString( viewRoot ) ).to.equal( '<div><div>foo</div></div>' );
  1070. } );
  1071. it( 'should be possible to override', () => {
  1072. downcastHelpers.markerToHighlight( {
  1073. model: 'marker',
  1074. view: { classes: 'override-class' },
  1075. converterPriority: 'high'
  1076. } );
  1077. model.change( writer => {
  1078. writer.addMarker( 'marker', { range: markerRange, usingOperation: false } );
  1079. } );
  1080. expect( viewToString( viewRoot ) ).to.equal(
  1081. '<div>' +
  1082. '<div class="override-class">' +
  1083. 'foo' +
  1084. '</div>' +
  1085. '</div>'
  1086. );
  1087. model.change( writer => {
  1088. writer.removeMarker( 'marker' );
  1089. } );
  1090. expect( viewToString( viewRoot ) ).to.equal( '<div><div>foo</div></div>' );
  1091. } );
  1092. it( 'should use default priority and id if not provided', () => {
  1093. const viewDiv = viewRoot.getChild( 0 );
  1094. downcastHelpers.markerToHighlight( {
  1095. model: 'marker2',
  1096. view: () => null,
  1097. converterPriority: 'high'
  1098. } );
  1099. viewDiv._setCustomProperty( 'addHighlight', ( element, descriptor ) => {
  1100. expect( descriptor.priority ).to.equal( ViewAttributeElement.DEFAULT_PRIORITY );
  1101. expect( descriptor.id ).to.equal( 'marker:foo-bar-baz' );
  1102. } );
  1103. viewDiv._setCustomProperty( 'removeHighlight', ( element, id ) => {
  1104. expect( id ).to.equal( 'marker:foo-bar-baz' );
  1105. } );
  1106. model.change( writer => {
  1107. writer.addMarker( 'marker2', { range: markerRange, usingOperation: false } );
  1108. } );
  1109. } );
  1110. it( 'should do nothing if descriptor is not provided', () => {
  1111. downcastHelpers.markerToHighlight( {
  1112. model: 'marker2',
  1113. view: () => null
  1114. } );
  1115. model.change( writer => {
  1116. writer.addMarker( 'marker2', { range: markerRange, usingOperation: false } );
  1117. } );
  1118. expect( viewToString( viewRoot ) ).to.equal( '<div><div>foo</div></div>' );
  1119. model.change( writer => {
  1120. writer.removeMarker( 'marker2' );
  1121. } );
  1122. expect( viewToString( viewRoot ) ).to.equal( '<div><div>foo</div></div>' );
  1123. } );
  1124. } );
  1125. } );
  1126. } );
  1127. function expectResult( string ) {
  1128. expect( stringifyView( viewRoot, null, { ignoreRoot: true } ) ).to.equal( string );
  1129. }
  1130. } );
  1131. describe( 'downcast converters', () => {
  1132. let dispatcher, modelDoc, modelRoot, viewRoot, controller, modelRootStart, model;
  1133. beforeEach( () => {
  1134. model = new Model();
  1135. modelDoc = model.document;
  1136. modelRoot = modelDoc.createRoot();
  1137. controller = new EditingController( model );
  1138. viewRoot = controller.view.document.getRoot();
  1139. // Set name of view root the same as dom root.
  1140. // This is a mock of attaching view root to dom root.
  1141. controller.view.document.getRoot()._name = 'div';
  1142. dispatcher = controller.downcastDispatcher;
  1143. const downcastHelpers = new DowncastHelpers( dispatcher );
  1144. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  1145. modelRootStart = model.createPositionAt( modelRoot, 0 );
  1146. } );
  1147. describe( 'insertText()', () => {
  1148. it( 'should downcast text', () => {
  1149. model.change( writer => {
  1150. writer.insert( new ModelText( 'foobar' ), modelRootStart );
  1151. } );
  1152. expect( viewToString( viewRoot ) ).to.equal( '<div>foobar</div>' );
  1153. } );
  1154. it( 'should support unicode', () => {
  1155. model.change( writer => {
  1156. writer.insert( new ModelText( 'நிலைக்கு' ), modelRootStart );
  1157. } );
  1158. expect( viewToString( viewRoot ) ).to.equal( '<div>நிலைக்கு</div>' );
  1159. } );
  1160. it( 'should be possible to override it', () => {
  1161. dispatcher.on( 'insert:$text', ( evt, data, conversionApi ) => {
  1162. conversionApi.consumable.consume( data.item, 'insert' );
  1163. }, { converterPriority: 'high' } );
  1164. model.change( writer => {
  1165. writer.insert( new ModelText( 'foobar' ), modelRootStart );
  1166. } );
  1167. expect( viewToString( viewRoot ) ).to.equal( '<div></div>' );
  1168. } );
  1169. } );
  1170. // Remove converter is by default already added in `EditingController` instance.
  1171. describe( 'remove()', () => {
  1172. it( 'should remove items from view accordingly to changes in model #1', () => {
  1173. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar' ) );
  1174. model.change( writer => {
  1175. writer.insert( modelElement, modelRootStart );
  1176. } );
  1177. model.change( writer => {
  1178. writer.remove(
  1179. writer.createRange( writer.createPositionAt( modelElement, 2 ), writer.createPositionAt( modelElement, 4 ) )
  1180. );
  1181. } );
  1182. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foar</p></div>' );
  1183. } );
  1184. it( 'should be possible to overwrite', () => {
  1185. dispatcher.on( 'remove', evt => evt.stop(), { priority: 'high' } );
  1186. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar' ) );
  1187. model.change( writer => {
  1188. writer.insert( modelElement, modelRootStart );
  1189. } );
  1190. model.change( writer => {
  1191. writer.remove(
  1192. writer.createRange( writer.createPositionAt( modelElement, 2 ), writer.createPositionAt( modelElement, 4 ) )
  1193. );
  1194. } );
  1195. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  1196. } );
  1197. it( 'should support unicode', () => {
  1198. const modelElement = new ModelElement( 'paragraph', null, 'நிலைக்கு' );
  1199. model.change( writer => {
  1200. writer.insert( modelElement, modelRootStart );
  1201. } );
  1202. model.change( writer => {
  1203. writer.remove(
  1204. writer.createRange( writer.createPositionAt( modelElement, 0 ), writer.createPositionAt( modelElement, 6 ) )
  1205. );
  1206. } );
  1207. expect( viewToString( viewRoot ) ).to.equal( '<div><p>கு</p></div>' );
  1208. } );
  1209. it( 'should not remove view ui elements that are placed next to removed content', () => {
  1210. modelRoot._appendChild( new ModelText( 'fozbar' ) );
  1211. viewRoot._appendChild( [
  1212. new ViewText( 'foz' ),
  1213. new ViewUIElement( 'span' ),
  1214. new ViewText( 'bar' )
  1215. ] );
  1216. // Remove 'b'.
  1217. model.change( writer => {
  1218. writer.remove(
  1219. writer.createRange( writer.createPositionAt( modelRoot, 3 ), writer.createPositionAt( modelRoot, 4 ) )
  1220. );
  1221. } );
  1222. expect( viewToString( viewRoot ) ).to.equal( '<div>foz<span></span>ar</div>' );
  1223. // Remove 'z'.
  1224. model.change( writer => {
  1225. writer.remove(
  1226. writer.createRange( writer.createPositionAt( modelRoot, 2 ), writer.createPositionAt( modelRoot, 3 ) )
  1227. );
  1228. } );
  1229. expect( viewToString( viewRoot ) ).to.equal( '<div>fo<span></span>ar</div>' );
  1230. } );
  1231. it( 'should remove correct amount of text when it is split by view ui element', () => {
  1232. modelRoot._appendChild( new ModelText( 'fozbar' ) );
  1233. viewRoot._appendChild( [
  1234. new ViewText( 'foz' ),
  1235. new ViewUIElement( 'span' ),
  1236. new ViewText( 'bar' )
  1237. ] );
  1238. // Remove 'z<span></span>b'.
  1239. model.change( writer => {
  1240. writer.remove(
  1241. writer.createRange( writer.createPositionAt( modelRoot, 2 ), writer.createPositionAt( modelRoot, 4 ) )
  1242. );
  1243. } );
  1244. expect( viewToString( viewRoot ) ).to.equal( '<div>foar</div>' );
  1245. } );
  1246. it( 'should unbind elements', () => {
  1247. const modelElement = new ModelElement( 'paragraph' );
  1248. model.change( writer => {
  1249. writer.insert( modelElement, modelRootStart );
  1250. } );
  1251. const viewElement = controller.mapper.toViewElement( modelElement );
  1252. expect( viewElement ).not.to.be.undefined;
  1253. expect( controller.mapper.toModelElement( viewElement ) ).to.equal( modelElement );
  1254. model.change( writer => {
  1255. writer.remove( modelElement );
  1256. } );
  1257. expect( controller.mapper.toViewElement( modelElement ) ).to.be.undefined;
  1258. expect( controller.mapper.toModelElement( viewElement ) ).to.be.undefined;
  1259. } );
  1260. it( 'should not break when remove() is used as part of unwrapping', () => {
  1261. const modelP = new ModelElement( 'paragraph', null, new ModelText( 'foo' ) );
  1262. const modelWidget = new ModelElement( 'widget', null, modelP );
  1263. const downcastHelpers = new DowncastHelpers( dispatcher );
  1264. downcastHelpers.elementToElement( { model: 'widget', view: 'widget' } );
  1265. model.change( writer => {
  1266. writer.insert( modelWidget, modelRootStart );
  1267. } );
  1268. expect( viewToString( viewRoot ) ).to.equal( '<div><widget><p>foo</p></widget></div>' );
  1269. const viewP = controller.mapper.toViewElement( modelP );
  1270. expect( viewP ).not.to.be.undefined;
  1271. model.change( writer => {
  1272. writer.unwrap( modelWidget );
  1273. } );
  1274. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p></div>' );
  1275. // `modelP` is now bound with newly created view element.
  1276. expect( controller.mapper.toViewElement( modelP ) ).not.to.equal( viewP );
  1277. // `viewP` is no longer bound with model element.
  1278. expect( controller.mapper.toModelElement( viewP ) ).to.be.undefined;
  1279. // View element from view root is bound to `modelP`.
  1280. expect( controller.mapper.toModelElement( viewRoot.getChild( 0 ) ) ).to.equal( modelP );
  1281. } );
  1282. it( 'should work correctly if container element after ui element is removed', () => {
  1283. // Prepare a model and view structure.
  1284. // This is done outside of conversion to put view ui elements inside easily.
  1285. const modelP1 = new ModelElement( 'paragraph' );
  1286. const modelP2 = new ModelElement( 'paragraph' );
  1287. const viewP1 = new ViewContainerElement( 'p' );
  1288. const viewUi1 = new ViewUIElement( 'span' );
  1289. const viewUi2 = new ViewUIElement( 'span' );
  1290. const viewP2 = new ViewContainerElement( 'p' );
  1291. modelRoot._appendChild( [ modelP1, modelP2 ] );
  1292. viewRoot._appendChild( [ viewP1, viewUi1, viewUi2, viewP2 ] );
  1293. controller.mapper.bindElements( modelP1, viewP1 );
  1294. controller.mapper.bindElements( modelP2, viewP2 );
  1295. // Remove second paragraph element.
  1296. model.change( writer => {
  1297. writer.remove( writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 2 ) ) );
  1298. } );
  1299. expect( viewToString( viewRoot ) ).to.equal( '<div><p></p><span></span><span></span></div>' );
  1300. } );
  1301. it( 'should work correctly if container element after text node is removed', () => {
  1302. const modelText = new ModelText( 'foo' );
  1303. const modelP = new ModelElement( 'paragraph' );
  1304. model.change( writer => {
  1305. writer.insert( [ modelText, modelP ], modelRootStart );
  1306. } );
  1307. model.change( writer => {
  1308. writer.remove( modelP );
  1309. } );
  1310. expect( viewToString( viewRoot ) ).to.equal( '<div>foo</div>' );
  1311. } );
  1312. } );
  1313. describe( 'createViewElementFromHighlightDescriptor()', () => {
  1314. it( 'should return attribute element from descriptor object', () => {
  1315. const descriptor = {
  1316. classes: 'foo-class',
  1317. attributes: { one: '1', two: '2' },
  1318. priority: 7
  1319. };
  1320. const element = createViewElementFromHighlightDescriptor( descriptor );
  1321. expect( element.is( 'attributeElement' ) ).to.be.true;
  1322. expect( element.name ).to.equal( 'span' );
  1323. expect( element.priority ).to.equal( 7 );
  1324. expect( element.hasClass( 'foo-class' ) ).to.be.true;
  1325. for ( const key of Object.keys( descriptor.attributes ) ) {
  1326. expect( element.getAttribute( key ) ).to.equal( descriptor.attributes[ key ] );
  1327. }
  1328. } );
  1329. it( 'should return attribute element from descriptor object - array with classes', () => {
  1330. const descriptor = {
  1331. classes: [ 'foo-class', 'bar-class' ],
  1332. attributes: { one: '1', two: '2' },
  1333. priority: 7
  1334. };
  1335. const element = createViewElementFromHighlightDescriptor( descriptor );
  1336. expect( element.is( 'attributeElement' ) ).to.be.true;
  1337. expect( element.name ).to.equal( 'span' );
  1338. expect( element.priority ).to.equal( 7 );
  1339. expect( element.hasClass( 'foo-class' ) ).to.be.true;
  1340. expect( element.hasClass( 'bar-class' ) ).to.be.true;
  1341. for ( const key of Object.keys( descriptor.attributes ) ) {
  1342. expect( element.getAttribute( key ) ).to.equal( descriptor.attributes[ key ] );
  1343. }
  1344. } );
  1345. it( 'should create element without class', () => {
  1346. const descriptor = {
  1347. attributes: { one: '1', two: '2' },
  1348. priority: 7
  1349. };
  1350. const element = createViewElementFromHighlightDescriptor( descriptor );
  1351. expect( element.is( 'attributeElement' ) ).to.be.true;
  1352. expect( element.name ).to.equal( 'span' );
  1353. expect( element.priority ).to.equal( 7 );
  1354. for ( const key of Object.keys( descriptor.attributes ) ) {
  1355. expect( element.getAttribute( key ) ).to.equal( descriptor.attributes[ key ] );
  1356. }
  1357. } );
  1358. it( 'should create element without priority', () => {
  1359. const descriptor = {
  1360. classes: 'foo-class',
  1361. attributes: { one: '1', two: '2' }
  1362. };
  1363. const element = createViewElementFromHighlightDescriptor( descriptor );
  1364. expect( element.is( 'attributeElement' ) ).to.be.true;
  1365. expect( element.name ).to.equal( 'span' );
  1366. expect( element.priority ).to.equal( ViewAttributeElement.DEFAULT_PRIORITY );
  1367. expect( element.hasClass( 'foo-class' ) ).to.be.true;
  1368. for ( const key of Object.keys( descriptor.attributes ) ) {
  1369. expect( element.getAttribute( key ) ).to.equal( descriptor.attributes[ key ] );
  1370. }
  1371. } );
  1372. it( 'should create element without attributes', () => {
  1373. const descriptor = {
  1374. classes: 'foo-class',
  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. } );
  1383. } );
  1384. } );
  1385. describe( 'downcast selection converters', () => {
  1386. let dispatcher, mapper, model, view, modelDoc, modelRoot, docSelection, viewDoc, viewRoot, viewSelection, downcastHelpers;
  1387. beforeEach( () => {
  1388. model = new Model();
  1389. modelDoc = model.document;
  1390. modelRoot = modelDoc.createRoot();
  1391. docSelection = modelDoc.selection;
  1392. model.schema.extend( '$text', { allowIn: '$root' } );
  1393. view = new View();
  1394. viewDoc = view.document;
  1395. viewRoot = createViewRoot( viewDoc );
  1396. viewSelection = viewDoc.selection;
  1397. mapper = new Mapper();
  1398. mapper.bindElements( modelRoot, viewRoot );
  1399. dispatcher = new DowncastDispatcher( { mapper, viewSelection } );
  1400. dispatcher.on( 'insert:$text', insertText() );
  1401. downcastHelpers = new DowncastHelpers( dispatcher );
  1402. downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } );
  1403. downcastHelpers.markerToHighlight( { model: 'marker', view: { classes: 'marker' }, converterPriority: 1 } );
  1404. // Default selection converters.
  1405. dispatcher.on( 'selection', clearAttributes(), { priority: 'low' } );
  1406. dispatcher.on( 'selection', convertRangeSelection(), { priority: 'low' } );
  1407. dispatcher.on( 'selection', convertCollapsedSelection(), { priority: 'low' } );
  1408. } );
  1409. afterEach( () => {
  1410. view.destroy();
  1411. } );
  1412. describe( 'default converters', () => {
  1413. describe( 'range selection', () => {
  1414. it( 'in same container', () => {
  1415. test(
  1416. [ 1, 4 ],
  1417. 'foobar',
  1418. 'f{oob}ar'
  1419. );
  1420. } );
  1421. it( 'in same container with unicode characters', () => {
  1422. test(
  1423. [ 2, 6 ],
  1424. 'நிலைக்கு',
  1425. 'நி{லைக்}கு'
  1426. );
  1427. } );
  1428. it( 'in same container, over attribute', () => {
  1429. test(
  1430. [ 1, 5 ],
  1431. 'fo<$text bold="true">ob</$text>ar',
  1432. 'f{o<strong>ob</strong>a}r'
  1433. );
  1434. } );
  1435. it( 'in same container, next to attribute', () => {
  1436. test(
  1437. [ 1, 2 ],
  1438. 'fo<$text bold="true">ob</$text>ar',
  1439. 'f{o}<strong>ob</strong>ar'
  1440. );
  1441. } );
  1442. it( 'in same attribute', () => {
  1443. test(
  1444. [ 2, 4 ],
  1445. 'f<$text bold="true">ooba</$text>r',
  1446. 'f<strong>o{ob}a</strong>r'
  1447. );
  1448. } );
  1449. it( 'in same attribute, selection same as attribute', () => {
  1450. test(
  1451. [ 2, 4 ],
  1452. 'fo<$text bold="true">ob</$text>ar',
  1453. 'fo{<strong>ob</strong>}ar'
  1454. );
  1455. } );
  1456. it( 'starts in text node, ends in attribute #1', () => {
  1457. test(
  1458. [ 1, 3 ],
  1459. 'fo<$text bold="true">ob</$text>ar',
  1460. 'f{o<strong>o}b</strong>ar'
  1461. );
  1462. } );
  1463. it( 'starts in text node, ends in attribute #2', () => {
  1464. test(
  1465. [ 1, 4 ],
  1466. 'fo<$text bold="true">ob</$text>ar',
  1467. 'f{o<strong>ob</strong>}ar'
  1468. );
  1469. } );
  1470. it( 'starts in attribute, ends in text node', () => {
  1471. test(
  1472. [ 3, 5 ],
  1473. 'fo<$text bold="true">ob</$text>ar',
  1474. 'fo<strong>o{b</strong>a}r'
  1475. );
  1476. } );
  1477. it( 'consumes consumable values properly', () => {
  1478. // Add callback that will fire before default ones.
  1479. // This should prevent default callback doing anything.
  1480. dispatcher.on( 'selection', ( evt, data, conversionApi ) => {
  1481. expect( conversionApi.consumable.consume( data.selection, 'selection' ) ).to.be.true;
  1482. }, { priority: 'high' } );
  1483. // Similar test case as the first in this suite.
  1484. test(
  1485. [ 1, 4 ],
  1486. 'foobar',
  1487. 'foobar' // No selection in view.
  1488. );
  1489. } );
  1490. it( 'should convert backward selection', () => {
  1491. test(
  1492. [ 1, 3, 'backward' ],
  1493. 'foobar',
  1494. 'f{oo}bar'
  1495. );
  1496. expect( viewSelection.focus.offset ).to.equal( 1 );
  1497. } );
  1498. } );
  1499. describe( 'collapsed selection', () => {
  1500. let marker;
  1501. it( 'in container', () => {
  1502. test(
  1503. [ 1, 1 ],
  1504. 'foobar',
  1505. 'f{}oobar'
  1506. );
  1507. } );
  1508. it( 'in attribute', () => {
  1509. test(
  1510. [ 3, 3 ],
  1511. 'f<$text bold="true">ooba</$text>r',
  1512. 'f<strong>oo{}ba</strong>r'
  1513. );
  1514. } );
  1515. it( 'in attribute and marker', () => {
  1516. setModelData( model, 'fo<$text bold="true">ob</$text>ar' );
  1517. model.change( writer => {
  1518. const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) );
  1519. marker = writer.addMarker( 'marker', { range, usingOperation: false } );
  1520. writer.setSelection( modelRoot, 3 );
  1521. } );
  1522. // Remove view children manually (without firing additional conversion).
  1523. viewRoot._removeChildren( 0, viewRoot.childCount );
  1524. // Convert model to view.
  1525. view.change( writer => {
  1526. dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer );
  1527. dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer );
  1528. dispatcher.convertSelection( docSelection, model.markers, writer );
  1529. } );
  1530. // Stringify view and check if it is same as expected.
  1531. expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ).to.equal(
  1532. '<div>f<span class="marker">o<strong>o{}b</strong>a</span>r</div>'
  1533. );
  1534. } );
  1535. it( 'in attribute and marker - no attribute', () => {
  1536. setModelData( model, 'fo<$text bold="true">ob</$text>ar' );
  1537. model.change( writer => {
  1538. const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) );
  1539. marker = writer.addMarker( 'marker', { range, usingOperation: false } );
  1540. writer.setSelection( modelRoot, 3 );
  1541. writer.removeSelectionAttribute( 'bold' );
  1542. } );
  1543. // Remove view children manually (without firing additional conversion).
  1544. viewRoot._removeChildren( 0, viewRoot.childCount );
  1545. // Convert model to view.
  1546. view.change( writer => {
  1547. dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer );
  1548. dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer );
  1549. dispatcher.convertSelection( docSelection, model.markers, writer );
  1550. } );
  1551. // Stringify view and check if it is same as expected.
  1552. expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
  1553. .to.equal( '<div>f<span class="marker">o<strong>o</strong>[]<strong>b</strong>a</span>r</div>' );
  1554. } );
  1555. it( 'in marker - using highlight descriptor creator', () => {
  1556. downcastHelpers.markerToHighlight( {
  1557. model: 'marker2',
  1558. view: data => ( { classes: data.markerName } )
  1559. } );
  1560. setModelData( model, 'foobar' );
  1561. model.change( writer => {
  1562. const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) );
  1563. marker = writer.addMarker( 'marker2', { range, usingOperation: false } );
  1564. writer.setSelection( modelRoot, 3 );
  1565. } );
  1566. // Remove view children manually (without firing additional conversion).
  1567. viewRoot._removeChildren( 0, viewRoot.childCount );
  1568. // Convert model to view.
  1569. view.change( writer => {
  1570. dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer );
  1571. dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer );
  1572. dispatcher.convertSelection( docSelection, model.markers, writer );
  1573. } );
  1574. // Stringify view and check if it is same as expected.
  1575. expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
  1576. .to.equal( '<div>f<span class="marker2">oo{}ba</span>r</div>' );
  1577. } );
  1578. it( 'should do nothing if creator return null', () => {
  1579. downcastHelpers.markerToHighlight( {
  1580. model: 'marker3',
  1581. view: () => null
  1582. } );
  1583. setModelData( model, 'foobar' );
  1584. model.change( writer => {
  1585. const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) );
  1586. marker = writer.addMarker( 'marker3', { range, usingOperation: false } );
  1587. writer.setSelection( modelRoot, 3 );
  1588. } );
  1589. // Remove view children manually (without firing additional conversion).
  1590. viewRoot._removeChildren( 0, viewRoot.childCount );
  1591. // Convert model to view.
  1592. view.change( writer => {
  1593. dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer );
  1594. dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer );
  1595. dispatcher.convertSelection( docSelection, model.markers, writer );
  1596. } );
  1597. // Stringify view and check if it is same as expected.
  1598. expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
  1599. .to.equal( '<div>foo{}bar</div>' );
  1600. } );
  1601. // #1072 - if the container has only ui elements, collapsed selection attribute should be rendered after those ui elements.
  1602. it( 'selection with attribute before ui element - no non-ui children', () => {
  1603. setModelData( model, '' );
  1604. // Add two ui elements to view.
  1605. viewRoot._appendChild( [
  1606. new ViewUIElement( 'span' ),
  1607. new ViewUIElement( 'span' )
  1608. ] );
  1609. model.change( writer => {
  1610. writer.setSelection( writer.createRange( writer.createPositionFromPath( modelRoot, [ 0 ] ) ) );
  1611. writer.setSelectionAttribute( 'bold', true );
  1612. } );
  1613. // Convert model to view.
  1614. view.change( writer => {
  1615. dispatcher.convertSelection( docSelection, model.markers, writer );
  1616. } );
  1617. // Stringify view and check if it is same as expected.
  1618. expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
  1619. .to.equal( '<div><span></span><span></span><strong>[]</strong></div>' );
  1620. } );
  1621. // #1072.
  1622. it( 'selection with attribute before ui element - has non-ui children #1', () => {
  1623. setModelData( model, 'x' );
  1624. model.change( writer => {
  1625. writer.setSelection( writer.createRange( writer.createPositionFromPath( modelRoot, [ 1 ] ) ) );
  1626. writer.setSelectionAttribute( 'bold', true );
  1627. } );
  1628. // Convert model to view.
  1629. view.change( writer => {
  1630. dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer );
  1631. // Add ui element to view.
  1632. const uiElement = new ViewUIElement( 'span' );
  1633. viewRoot._insertChild( 1, uiElement );
  1634. dispatcher.convertSelection( docSelection, model.markers, writer );
  1635. } );
  1636. // Stringify view and check if it is same as expected.
  1637. expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
  1638. .to.equal( '<div>x<strong>[]</strong><span></span></div>' );
  1639. } );
  1640. // #1072.
  1641. it( 'selection with attribute before ui element - has non-ui children #2', () => {
  1642. setModelData( model, '<$text bold="true">x</$text>y' );
  1643. model.change( writer => {
  1644. writer.setSelection( writer.createRange( writer.createPositionFromPath( modelRoot, [ 1 ] ) ) );
  1645. writer.setSelectionAttribute( 'bold', true );
  1646. } );
  1647. // Convert model to view.
  1648. view.change( writer => {
  1649. dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer );
  1650. // Add ui element to view.
  1651. const uiElement = new ViewUIElement( 'span' );
  1652. viewRoot._insertChild( 1, uiElement, writer );
  1653. dispatcher.convertSelection( docSelection, model.markers, writer );
  1654. } );
  1655. // Stringify view and check if it is same as expected.
  1656. expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
  1657. .to.equal( '<div><strong>x{}</strong><span></span>y</div>' );
  1658. } );
  1659. it( 'consumes consumable values properly', () => {
  1660. // Add callbacks that will fire before default ones.
  1661. // This should prevent default callbacks doing anything.
  1662. dispatcher.on( 'selection', ( evt, data, conversionApi ) => {
  1663. expect( conversionApi.consumable.consume( data.selection, 'selection' ) ).to.be.true;
  1664. }, { priority: 'high' } );
  1665. dispatcher.on( 'attribute:bold', ( evt, data, conversionApi ) => {
  1666. expect( conversionApi.consumable.consume( data.item, 'attribute:bold' ) ).to.be.true;
  1667. }, { priority: 'high' } );
  1668. // Similar test case as above.
  1669. test(
  1670. [ 3, 3 ],
  1671. 'f<$text bold="true">ooba</$text>r',
  1672. 'foobar' // No selection in view and no attribute.
  1673. );
  1674. } );
  1675. } );
  1676. } );
  1677. describe( 'clean-up', () => {
  1678. describe( 'convertRangeSelection', () => {
  1679. it( 'should remove all ranges before adding new range', () => {
  1680. test(
  1681. [ 0, 2 ],
  1682. 'foobar',
  1683. '{fo}obar'
  1684. );
  1685. test(
  1686. [ 3, 5 ],
  1687. 'foobar',
  1688. 'foo{ba}r'
  1689. );
  1690. expect( viewSelection.rangeCount ).to.equal( 1 );
  1691. } );
  1692. } );
  1693. describe( 'convertCollapsedSelection', () => {
  1694. it( 'should remove all ranges before adding new range', () => {
  1695. test(
  1696. [ 2, 2 ],
  1697. 'foobar',
  1698. 'fo{}obar'
  1699. );
  1700. test(
  1701. [ 3, 3 ],
  1702. 'foobar',
  1703. 'foo{}bar'
  1704. );
  1705. expect( viewSelection.rangeCount ).to.equal( 1 );
  1706. } );
  1707. } );
  1708. describe( 'clearAttributes', () => {
  1709. it( 'should remove all ranges before adding new range', () => {
  1710. test(
  1711. [ 3, 3 ],
  1712. 'foobar',
  1713. 'foo<strong>[]</strong>bar',
  1714. { bold: 'true' }
  1715. );
  1716. view.change( writer => {
  1717. const modelRange = model.createRange( model.createPositionAt( modelRoot, 1 ), model.createPositionAt( modelRoot, 1 ) );
  1718. model.change( writer => {
  1719. writer.setSelection( modelRange );
  1720. } );
  1721. dispatcher.convertSelection( modelDoc.selection, model.markers, writer );
  1722. } );
  1723. expect( viewSelection.rangeCount ).to.equal( 1 );
  1724. const viewString = stringifyView( viewRoot, viewSelection, { showType: false } );
  1725. expect( viewString ).to.equal( '<div>f{}oobar</div>' );
  1726. } );
  1727. it( 'should do nothing if the attribute element had been already removed', () => {
  1728. test(
  1729. [ 3, 3 ],
  1730. 'foobar',
  1731. 'foo<strong>[]</strong>bar',
  1732. { bold: 'true' }
  1733. );
  1734. view.change( writer => {
  1735. // Remove <strong></strong> manually.
  1736. writer.mergeAttributes( viewSelection.getFirstPosition() );
  1737. const modelRange = model.createRange( model.createPositionAt( modelRoot, 1 ), model.createPositionAt( modelRoot, 1 ) );
  1738. model.change( writer => {
  1739. writer.setSelection( modelRange );
  1740. } );
  1741. dispatcher.convertSelection( modelDoc.selection, model.markers, writer );
  1742. } );
  1743. expect( viewSelection.rangeCount ).to.equal( 1 );
  1744. const viewString = stringifyView( viewRoot, viewSelection, { showType: false } );
  1745. expect( viewString ).to.equal( '<div>f{}oobar</div>' );
  1746. } );
  1747. it( 'should clear fake selection', () => {
  1748. const modelRange = model.createRange( model.createPositionAt( modelRoot, 1 ), model.createPositionAt( modelRoot, 1 ) );
  1749. view.change( writer => {
  1750. writer.setSelection( modelRange, { fake: true } );
  1751. dispatcher.convertSelection( docSelection, model.markers, writer );
  1752. } );
  1753. expect( viewSelection.isFake ).to.be.false;
  1754. } );
  1755. } );
  1756. } );
  1757. describe( 'table cell selection converter', () => {
  1758. beforeEach( () => {
  1759. model.schema.register( 'table', { isLimit: true } );
  1760. model.schema.register( 'tr', { isLimit: true } );
  1761. model.schema.register( 'td', { isLimit: true } );
  1762. model.schema.extend( 'table', { allowIn: '$root' } );
  1763. model.schema.extend( 'tr', { allowIn: 'table' } );
  1764. model.schema.extend( 'td', { allowIn: 'tr' } );
  1765. model.schema.extend( '$text', { allowIn: 'td' } );
  1766. const downcastHelpers = new DowncastHelpers( dispatcher );
  1767. // "Universal" converter to convert table structure.
  1768. downcastHelpers.elementToElement( { model: 'table', view: 'table' } );
  1769. downcastHelpers.elementToElement( { model: 'tr', view: 'tr' } );
  1770. downcastHelpers.elementToElement( { model: 'td', view: 'td' } );
  1771. // Special converter for table cells.
  1772. dispatcher.on( 'selection', ( evt, data, conversionApi ) => {
  1773. const selection = data.selection;
  1774. if ( !conversionApi.consumable.test( selection, 'selection' ) || selection.isCollapsed ) {
  1775. return;
  1776. }
  1777. for ( const range of selection.getRanges() ) {
  1778. const node = range.start.parent;
  1779. if ( !!node && node.is( 'td' ) ) {
  1780. conversionApi.consumable.consume( selection, 'selection' );
  1781. const viewNode = conversionApi.mapper.toViewElement( node );
  1782. conversionApi.writer.addClass( 'selected', viewNode );
  1783. }
  1784. }
  1785. }, { priority: 'high' } );
  1786. } );
  1787. it( 'should not be used to convert selection that is not on table cell', () => {
  1788. test(
  1789. [ 1, 5 ],
  1790. 'f{o<$text bold="true">ob</$text>a}r',
  1791. 'f{o<strong>ob</strong>a}r'
  1792. );
  1793. } );
  1794. it( 'should add a class to the selected table cell', () => {
  1795. test(
  1796. // table tr#0 td#0 [foo, table tr#0 td#0 bar]
  1797. [ [ 0, 0, 0, 0 ], [ 0, 0, 0, 3 ] ],
  1798. '<table><tr><td>foo</td></tr><tr><td>bar</td></tr></table>',
  1799. '<table><tr><td class="selected">foo</td></tr><tr><td>bar</td></tr></table>'
  1800. );
  1801. } );
  1802. it( 'should not be used if selection contains more than just a table cell', () => {
  1803. test(
  1804. // table tr td#1 f{oo bar, table tr#2 bar]
  1805. [ [ 0, 0, 0, 1 ], [ 0, 0, 1, 3 ] ],
  1806. '<table><tr><td>foo</td><td>bar</td></tr></table>',
  1807. '[<table><tr><td>foo</td><td>bar</td></tr></table>]'
  1808. );
  1809. } );
  1810. } );
  1811. // Tests if the selection got correctly converted.
  1812. // Because `setData` might use selection converters itself to set the selection, we can't use it
  1813. // to set the selection (because then we would test converters using converters).
  1814. // Instead, the `test` function expects to be passed `selectionPaths` which is an array containing two numbers or two arrays,
  1815. // that are offsets or paths of selection positions in root element.
  1816. function test( selectionPaths, modelInput, expectedView, selectionAttributes = {} ) {
  1817. // Parse passed `modelInput` string and set it as current model.
  1818. setModelData( model, modelInput );
  1819. // Manually set selection ranges using passed `selectionPaths`.
  1820. const startPath = typeof selectionPaths[ 0 ] == 'number' ? [ selectionPaths[ 0 ] ] : selectionPaths[ 0 ];
  1821. const endPath = typeof selectionPaths[ 1 ] == 'number' ? [ selectionPaths[ 1 ] ] : selectionPaths[ 1 ];
  1822. const startPos = model.createPositionFromPath( modelRoot, startPath );
  1823. const endPos = model.createPositionFromPath( modelRoot, endPath );
  1824. const isBackward = selectionPaths[ 2 ] === 'backward';
  1825. model.change( writer => {
  1826. writer.setSelection( writer.createRange( startPos, endPos ), { backward: isBackward } );
  1827. // And add or remove passed attributes.
  1828. for ( const key in selectionAttributes ) {
  1829. const value = selectionAttributes[ key ];
  1830. if ( value ) {
  1831. writer.setSelectionAttribute( key, value );
  1832. } else {
  1833. writer.removeSelectionAttribute( key );
  1834. }
  1835. }
  1836. } );
  1837. // Remove view children manually (without firing additional conversion).
  1838. viewRoot._removeChildren( 0, viewRoot.childCount );
  1839. // Convert model to view.
  1840. view.change( writer => {
  1841. dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer );
  1842. dispatcher.convertSelection( docSelection, model.markers, writer );
  1843. } );
  1844. // Stringify view and check if it is same as expected.
  1845. expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ).to.equal( '<div>' + expectedView + '</div>' );
  1846. }
  1847. } );
  1848. function viewToString( item ) {
  1849. let result = '';
  1850. if ( item instanceof ViewText ) {
  1851. result = item.data;
  1852. } else {
  1853. // ViewElement or ViewDocumentFragment.
  1854. for ( const child of item.getChildren() ) {
  1855. result += viewToString( child );
  1856. }
  1857. if ( item instanceof ViewElement ) {
  1858. result = '<' + item.name + viewAttributesToString( item ) + '>' + result + '</' + item.name + '>';
  1859. }
  1860. }
  1861. return result;
  1862. }
  1863. function viewAttributesToString( item ) {
  1864. let result = '';
  1865. for ( const key of item.getAttributeKeys() ) {
  1866. const value = item.getAttribute( key );
  1867. if ( value ) {
  1868. result += ' ' + key + '="' + value + '"';
  1869. }
  1870. }
  1871. return result;
  1872. }