downcasthelpers.js 114 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /* globals console */
  6. import EditingController from '../../src/controller/editingcontroller';
  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 ViewDocument from '../../src/view/document';
  16. import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
  17. import DowncastHelpers, {
  18. clearAttributes,
  19. convertCollapsedSelection,
  20. convertRangeSelection,
  21. createViewElementFromHighlightDescriptor,
  22. insertText
  23. } from '../../src/conversion/downcasthelpers';
  24. import Mapper from '../../src/conversion/mapper';
  25. import DowncastDispatcher from '../../src/conversion/downcastdispatcher';
  26. import { stringify as stringifyView } from '../../src/dev-utils/view';
  27. import View from '../../src/view/view';
  28. import createViewRoot from '../view/_utils/createroot';
  29. import { setData as setModelData } from '../../src/dev-utils/model';
  30. import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
  31. import { StylesProcessor } from '../../src/view/stylesmap';
  32. import DowncastWriter from '../../src/view/downcastwriter';
  33. describe( 'DowncastHelpers', () => {
  34. let model, modelRoot, viewRoot, downcastHelpers, controller, modelRootStart;
  35. beforeEach( () => {
  36. model = new Model();
  37. const modelDoc = model.document;
  38. modelRoot = modelDoc.createRoot();
  39. controller = new EditingController( model, new StylesProcessor() );
  40. // Set name of view root the same as dom root.
  41. // This is a mock of attaching view root to dom root.
  42. controller.view.document.getRoot()._name = 'div';
  43. viewRoot = controller.view.document.getRoot();
  44. downcastHelpers = new DowncastHelpers( [ controller.downcastDispatcher ] );
  45. modelRootStart = model.createPositionAt( modelRoot, 0 );
  46. } );
  47. describe( 'elementToElement()', () => {
  48. it( 'should be chainable', () => {
  49. expect( downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ) ).to.equal( downcastHelpers );
  50. } );
  51. it( 'config.view is a string', () => {
  52. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  53. model.change( writer => {
  54. writer.insertElement( 'paragraph', modelRoot, 0 );
  55. } );
  56. expectResult( '<p></p>' );
  57. } );
  58. it( 'can be overwritten using converterPriority', () => {
  59. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  60. downcastHelpers.elementToElement( { model: 'paragraph', view: 'foo', converterPriority: 'high' } );
  61. model.change( writer => {
  62. writer.insertElement( 'paragraph', modelRoot, 0 );
  63. } );
  64. expectResult( '<foo></foo>' );
  65. } );
  66. it( 'config.view is a view element definition', () => {
  67. downcastHelpers.elementToElement( {
  68. model: 'fancyParagraph',
  69. view: {
  70. name: 'p',
  71. classes: 'fancy'
  72. }
  73. } );
  74. model.change( writer => {
  75. writer.insertElement( 'fancyParagraph', modelRoot, 0 );
  76. } );
  77. expectResult( '<p class="fancy"></p>' );
  78. } );
  79. it( 'config.view is a function', () => {
  80. downcastHelpers.elementToElement( {
  81. model: 'heading',
  82. view: ( modelElement, { writer } ) => writer.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) )
  83. } );
  84. model.change( writer => {
  85. writer.insertElement( 'heading', { level: 2 }, modelRoot, 0 );
  86. } );
  87. expectResult( '<h2></h2>' );
  88. } );
  89. describe( 'config.triggerBy', () => {
  90. describe( 'with simple block view structure', () => {
  91. beforeEach( () => {
  92. model.schema.register( 'simpleBlock', {
  93. allowIn: '$root',
  94. allowAttributes: [ 'toStyle', 'toClass' ]
  95. } );
  96. downcastHelpers.elementToElement( {
  97. model: 'simpleBlock',
  98. view: ( modelElement, { writer } ) => {
  99. return writer.createContainerElement( 'div', getViewAttributes( modelElement ) );
  100. },
  101. triggerBy: [
  102. 'attribute:toStyle:simpleBlock',
  103. 'attribute:toClass:simpleBlock'
  104. ]
  105. } );
  106. } );
  107. it( 'should convert on insert', () => {
  108. model.change( writer => {
  109. writer.insertElement( 'simpleBlock', modelRoot, 0 );
  110. } );
  111. expectResult( '<div></div>' );
  112. } );
  113. it( 'should convert on attribute set', () => {
  114. setModelData( model, '<simpleBlock></simpleBlock>' );
  115. model.change( writer => {
  116. writer.setAttribute( 'toStyle', 'display:block', modelRoot.getChild( 0 ) );
  117. } );
  118. expectResult( '<div style="display:block"></div>' );
  119. } );
  120. it( 'should convert on attribute change', () => {
  121. setModelData( model, '<simpleBlock toStyle="display:block"></simpleBlock>' );
  122. model.change( writer => {
  123. writer.setAttribute( 'toStyle', 'display:inline', modelRoot.getChild( 0 ) );
  124. } );
  125. expectResult( '<div style="display:inline"></div>' );
  126. } );
  127. it( 'should convert on attribute remove', () => {
  128. setModelData( model, '<simpleBlock toStyle="display:block"></simpleBlock>' );
  129. model.change( writer => {
  130. writer.removeAttribute( 'toStyle', modelRoot.getChild( 0 ) );
  131. } );
  132. expectResult( '<div></div>' );
  133. } );
  134. it( 'should convert on one attribute add and other remove', () => {
  135. setModelData( model, '<simpleBlock toStyle="display:block"></simpleBlock>' );
  136. model.change( writer => {
  137. writer.removeAttribute( 'toStyle', modelRoot.getChild( 0 ) );
  138. writer.setAttribute( 'toClass', true, modelRoot.getChild( 0 ) );
  139. } );
  140. expectResult( '<div class="is-classy"></div>' );
  141. } );
  142. it( 'should do nothing if non-triggerBy attribute has changed', () => {
  143. setModelData( model, '<simpleBlock></simpleBlock>' );
  144. model.change( writer => {
  145. writer.setAttribute( 'notTriggered', true, modelRoot.getChild( 0 ) );
  146. } );
  147. expectResult( '<div></div>' );
  148. } );
  149. } );
  150. describe( 'with complex view structure - no children allowed', () => {
  151. beforeEach( () => {
  152. model.schema.register( 'complex', {
  153. allowIn: '$root',
  154. allowAttributes: [ 'toStyle', 'toClass' ]
  155. } );
  156. downcastHelpers.elementToElement( {
  157. model: 'complex',
  158. view: ( modelElement, { writer } ) => {
  159. const outer = writer.createContainerElement( 'div', { class: 'complex-outer' } );
  160. const inner = writer.createContainerElement( 'div', getViewAttributes( modelElement ) );
  161. writer.insert( writer.createPositionAt( outer, 0 ), inner );
  162. return outer;
  163. },
  164. triggerBy: [
  165. 'attribute:toStyle:complex',
  166. 'attribute:toClass:complex'
  167. ]
  168. } );
  169. } );
  170. it( 'should convert on insert', () => {
  171. model.change( writer => {
  172. writer.insertElement( 'complex', modelRoot, 0 );
  173. } );
  174. expectResult( '<div class="complex-outer"><div></div></div>' );
  175. } );
  176. it( 'should convert on attribute set', () => {
  177. setModelData( model, '<complex></complex>' );
  178. model.change( writer => {
  179. writer.setAttribute( 'toStyle', 'display:block', modelRoot.getChild( 0 ) );
  180. } );
  181. expectResult( '<div class="complex-outer"><div style="display:block"></div></div>' );
  182. } );
  183. it( 'should convert on attribute change', () => {
  184. setModelData( model, '<complex toStyle="display:block"></complex>' );
  185. model.change( writer => {
  186. writer.setAttribute( 'toStyle', 'display:inline', modelRoot.getChild( 0 ) );
  187. } );
  188. expectResult( '<div class="complex-outer"><div style="display:inline"></div></div>' );
  189. } );
  190. it( 'should convert on attribute remove', () => {
  191. setModelData( model, '<complex toStyle="display:block"></complex>' );
  192. model.change( writer => {
  193. writer.removeAttribute( 'toStyle', modelRoot.getChild( 0 ) );
  194. } );
  195. expectResult( '<div class="complex-outer"><div></div></div>' );
  196. } );
  197. it( 'should convert on one attribute add and other remove', () => {
  198. setModelData( model, '<complex toStyle="display:block"></complex>' );
  199. model.change( writer => {
  200. writer.removeAttribute( 'toStyle', modelRoot.getChild( 0 ) );
  201. writer.setAttribute( 'toClass', true, modelRoot.getChild( 0 ) );
  202. } );
  203. expectResult( '<div class="complex-outer"><div class="is-classy"></div></div>' );
  204. } );
  205. it( 'should do nothing if non-triggerBy attribute has changed', () => {
  206. setModelData( model, '<complex></complex>' );
  207. model.change( writer => {
  208. writer.setAttribute( 'notTriggered', true, modelRoot.getChild( 0 ) );
  209. } );
  210. expectResult( '<div class="complex-outer"><div></div></div>' );
  211. } );
  212. } );
  213. describe( 'with complex view structure (without slots)', () => {
  214. beforeEach( () => {
  215. model.schema.register( 'complex', {
  216. allowIn: '$root',
  217. allowAttributes: [ 'toStyle', 'toClass' ]
  218. } );
  219. downcastHelpers.elementToElement( {
  220. model: 'complex',
  221. view: ( modelElement, { writer, mapper } ) => {
  222. const outer = writer.createContainerElement( 'c-outer' );
  223. const inner = writer.createContainerElement( 'c-inner', getViewAttributes( modelElement ) );
  224. writer.insert( writer.createPositionAt( outer, 0 ), inner );
  225. mapper.bindElements( modelElement, outer ); // Need for nested mapping
  226. mapper.bindElements( modelElement, inner );
  227. return outer;
  228. },
  229. triggerBy: [
  230. 'attribute:toStyle:complex',
  231. 'attribute:toClass:complex'
  232. ]
  233. } );
  234. model.schema.register( 'paragraph', {
  235. inheritAllFrom: '$block',
  236. allowIn: 'complex'
  237. } );
  238. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  239. } );
  240. it( 'should convert on insert', () => {
  241. model.change( writer => {
  242. writer.insertElement( 'complex', modelRoot, 0 );
  243. } );
  244. expectResult( '<c-outer><c-inner></c-inner></c-outer>' );
  245. } );
  246. it( 'should convert on attribute set', () => {
  247. setModelData( model, '<complex></complex>' );
  248. model.change( writer => {
  249. writer.setAttribute( 'toStyle', 'display:block', modelRoot.getChild( 0 ) );
  250. } );
  251. expectResult( '<c-outer><c-inner style="display:block"></c-inner></c-outer>' );
  252. } );
  253. it( 'should convert on attribute remove', () => {
  254. setModelData( model, '<complex toStyle="display:block"></complex>' );
  255. model.change( writer => {
  256. writer.removeAttribute( 'toStyle', modelRoot.getChild( 0 ) );
  257. } );
  258. expectResult( '<c-outer><c-inner></c-inner></c-outer>' );
  259. } );
  260. it( 'should convert on one attribute add and other remove', () => {
  261. setModelData( model, '<complex toStyle="display:block"></complex>' );
  262. model.change( writer => {
  263. writer.removeAttribute( 'toStyle', modelRoot.getChild( 0 ) );
  264. writer.setAttribute( 'toClass', true, modelRoot.getChild( 0 ) );
  265. } );
  266. expectResult( '<c-outer><c-inner class="is-classy"></c-inner></c-outer>' );
  267. } );
  268. it( 'should do nothing if non-triggerBy attribute has changed', () => {
  269. setModelData( model, '<complex></complex>' );
  270. model.change( writer => {
  271. writer.setAttribute( 'notTriggered', true, modelRoot.getChild( 0 ) );
  272. } );
  273. expectResult( '<c-outer><c-inner></c-inner></c-outer>' );
  274. } );
  275. describe( 'memoization', () => {
  276. it( 'should create new element on re-converting element', () => {
  277. setModelData( model, '<complex></complex>' );
  278. const renderedView = viewRoot.getChild( 0 );
  279. model.change( writer => {
  280. writer.setAttribute( 'toStyle', 'display:block', modelRoot.getChild( 0 ) );
  281. } );
  282. const viewAfterReRender = viewRoot.getChild( 0 );
  283. expect( viewAfterReRender ).to.not.equal( renderedView );
  284. } );
  285. // Skipped, as it would require two-level mapping. See https://github.com/ckeditor/ckeditor5/issues/1589.
  286. // Doable as a similar case works in table scenario for table cells (table is refreshed).
  287. it.skip( 'should not re-create child elements on re-converting element', () => {
  288. setModelData( model, '<complex><paragraph>Foo bar baz</paragraph></complex>' );
  289. expectResult( '<c-outer><c-inner><p>Foo bar baz</p></c-inner></c-outer>' );
  290. const renderedViewView = viewRoot.getChild( 0 ).getChild( 0 );
  291. model.change( writer => {
  292. writer.setAttribute( 'toStyle', 'display:block', modelRoot.getChild( 0 ) );
  293. } );
  294. const viewAfterReRender = viewRoot.getChild( 0 ).getChild( 0 );
  295. expect( viewAfterReRender ).to.equal( renderedViewView );
  296. } );
  297. } );
  298. } );
  299. describe( 'with complex view structure (slot conversion)', () => {
  300. beforeEach( () => {
  301. model.schema.register( 'complex', {
  302. allowIn: '$root',
  303. allowAttributes: [ 'classForMain', 'classForWrap', 'attributeToElement' ]
  304. } );
  305. downcastHelpers.elementToElement( {
  306. model: 'complex',
  307. view: ( modelElement, { writer, mapper } ) => {
  308. const classForMain = !!modelElement.getAttribute( 'classForMain' );
  309. const classForWrap = !!modelElement.getAttribute( 'classForWrap' );
  310. const attributeToElement = !!modelElement.getAttribute( 'attributeToElement' );
  311. const outer = writer.createContainerElement( 'div', {
  312. class: `complex-slots${ classForMain ? ' with-class' : '' }`
  313. } );
  314. const inner = writer.createContainerElement( 'div', {
  315. class: `slots${ classForWrap ? ' with-class' : '' }`
  316. } );
  317. if ( attributeToElement ) {
  318. const optional = writer.createEmptyElement( 'div', { class: 'optional' } );
  319. writer.insert( writer.createPositionAt( outer, 0 ), optional );
  320. }
  321. writer.insert( writer.createPositionAt( outer, 'end' ), inner );
  322. mapper.bindElements( modelElement, inner );
  323. for ( const slot of modelElement.getChildren() ) {
  324. const viewSlot = writer.createContainerElement( 'div', { class: 'slot' } );
  325. writer.insert( writer.createPositionAt( inner, slot.index ), viewSlot );
  326. mapper.bindSlotElements( slot, viewSlot );
  327. }
  328. return outer;
  329. },
  330. triggerBy: [
  331. 'attribute:classForMain:complex',
  332. 'attribute:classForWrap:complex',
  333. 'attribute:attributeToElement:complex',
  334. 'insert:slot',
  335. 'remove:slot'
  336. ]
  337. } );
  338. model.schema.register( 'slot', {
  339. allowIn: 'complex'
  340. } );
  341. model.schema.register( 'paragraph', {
  342. inheritAllFrom: '$block',
  343. allowIn: 'slot'
  344. } );
  345. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  346. } );
  347. it( 'should convert on insert', () => {
  348. model.change( writer => {
  349. writer.insertElement( 'complex', modelRoot, 0 );
  350. } );
  351. expectResult( '<div class="complex-slots"><div class="slots"></div></div>' );
  352. } );
  353. it( 'should convert on attribute set (main element)', () => {
  354. setModelData( model, '<complex></complex>' );
  355. model.change( writer => {
  356. writer.setAttribute( 'classForMain', true, modelRoot.getChild( 0 ) );
  357. } );
  358. expectResult( '<div class="complex-slots with-class"><div class="slots"></div></div>' );
  359. } );
  360. it( 'should convert on attribute set (other element)', () => {
  361. setModelData( model, '<complex></complex>' );
  362. model.change( writer => {
  363. writer.setAttribute( 'classForWrap', true, modelRoot.getChild( 0 ) );
  364. } );
  365. expectResult( '<div class="complex-slots"><div class="slots with-class"></div></div>' );
  366. } );
  367. it( 'should convert on attribute set (insert new view element)', () => {
  368. setModelData( model, '<complex></complex>' );
  369. model.change( writer => {
  370. writer.setAttribute( 'attributeToElement', true, modelRoot.getChild( 0 ) );
  371. } );
  372. expectResult( '<div class="complex-slots"><div class="optional"></div><div class="slots"></div></div>' );
  373. } );
  374. it( 'should convert element with slots', () => {
  375. setModelData( model,
  376. '<complex>' +
  377. '<slot><paragraph>foo</paragraph></slot>' +
  378. '<slot><paragraph>bar</paragraph></slot>' +
  379. '</complex>' );
  380. expectResult(
  381. '<div class="complex-slots">' +
  382. '<div class="slots">' +
  383. '<div class="slot"><p>foo</p></div>' +
  384. '<div class="slot"><p>bar</p></div>' +
  385. '</div>' +
  386. '</div>'
  387. );
  388. } );
  389. it( 'should convert element on adding slot', () => {
  390. setModelData( model,
  391. '<complex>' +
  392. '<slot><paragraph>foo</paragraph></slot>' +
  393. '<slot><paragraph>bar</paragraph></slot>' +
  394. '</complex>' );
  395. model.change( writer => {
  396. insertBazSlot( writer, modelRoot );
  397. } );
  398. expectResult(
  399. '<div class="complex-slots">' +
  400. '<div class="slots">' +
  401. '<div class="slot"><p>foo</p></div>' +
  402. '<div class="slot"><p>bar</p></div>' +
  403. '<div class="slot"><p>baz</p></div>' +
  404. '</div>' +
  405. '</div>'
  406. );
  407. } );
  408. it( 'should convert element on removing slot', () => {
  409. setModelData( model,
  410. '<complex>' +
  411. '<slot><paragraph>foo</paragraph></slot>' +
  412. '<slot><paragraph>bar</paragraph></slot>' +
  413. '</complex>' );
  414. model.change( writer => {
  415. writer.remove( modelRoot.getChild( 0 ).getChild( 0 ) );
  416. } );
  417. expectResult(
  418. '<div class="complex-slots">' +
  419. '<div class="slots">' +
  420. '<div class="slot"><p>bar</p></div>' +
  421. '</div>' +
  422. '</div>'
  423. );
  424. } );
  425. it( 'should convert element on multiple triggers (remove + insert)', () => {
  426. setModelData( model,
  427. '<complex>' +
  428. '<slot><paragraph>foo</paragraph></slot>' +
  429. '<slot><paragraph>bar</paragraph></slot>' +
  430. '</complex>' );
  431. model.change( writer => {
  432. writer.remove( modelRoot.getChild( 0 ).getChild( 0 ) );
  433. insertBazSlot( writer, modelRoot );
  434. } );
  435. expectResult(
  436. '<div class="complex-slots">' +
  437. '<div class="slots">' +
  438. '<div class="slot"><p>bar</p></div>' +
  439. '<div class="slot"><p>baz</p></div>' +
  440. '</div>' +
  441. '</div>'
  442. );
  443. } );
  444. it( 'should convert element on multiple triggers (remove + attribute)', () => {
  445. setModelData( model,
  446. '<complex>' +
  447. '<slot><paragraph>foo</paragraph></slot>' +
  448. '<slot><paragraph>bar</paragraph></slot>' +
  449. '</complex>' );
  450. model.change( writer => {
  451. writer.remove( modelRoot.getChild( 0 ).getChild( 0 ) );
  452. writer.setAttribute( 'classForMain', true, modelRoot.getChild( 0 ) );
  453. } );
  454. expectResult(
  455. '<div class="complex-slots with-class">' +
  456. '<div class="slots">' +
  457. '<div class="slot"><p>bar</p></div>' +
  458. '</div>' +
  459. '</div>'
  460. );
  461. } );
  462. it( 'should convert element on multiple triggers (insert + attribute)', () => {
  463. setModelData( model,
  464. '<complex>' +
  465. '<slot><paragraph>foo</paragraph></slot>' +
  466. '<slot><paragraph>bar</paragraph></slot>' +
  467. '</complex>' );
  468. model.change( writer => {
  469. insertBazSlot( writer, modelRoot );
  470. writer.setAttribute( 'classForMain', true, modelRoot.getChild( 0 ) );
  471. } );
  472. expectResult(
  473. '<div class="complex-slots with-class">' +
  474. '<div class="slots">' +
  475. '<div class="slot"><p>foo</p></div>' +
  476. '<div class="slot"><p>bar</p></div>' +
  477. '<div class="slot"><p>baz</p></div>' +
  478. '</div>' +
  479. '</div>'
  480. );
  481. } );
  482. it( 'should not trigger refresh on adding a slot to an element without triggerBy conversion', () => {
  483. model.schema.register( 'other', {
  484. allowIn: '$root'
  485. } );
  486. model.schema.extend( 'slot', {
  487. allowIn: 'other'
  488. } );
  489. downcastHelpers.elementToElement( {
  490. model: 'other',
  491. view: {
  492. name: 'div',
  493. classes: 'other'
  494. }
  495. } );
  496. downcastHelpers.elementToElement( {
  497. model: 'slot',
  498. view: {
  499. name: 'div',
  500. classes: 'slot'
  501. }
  502. } );
  503. setModelData( model,
  504. '<other>' +
  505. '<slot><paragraph>foo</paragraph></slot>' +
  506. '<slot><paragraph>bar</paragraph></slot>' +
  507. '</other>'
  508. );
  509. const otherView = viewRoot.getChild( 0 );
  510. model.change( writer => {
  511. insertBazSlot( writer, modelRoot );
  512. } );
  513. expectResult(
  514. '<div class="other">' +
  515. '<div class="slot"><p>foo</p></div>' +
  516. '<div class="slot"><p>bar</p></div>' +
  517. '<div class="slot"><p>baz</p></div>' +
  518. '</div>'
  519. );
  520. const otherViewAfter = viewRoot.getChild( 0 );
  521. expect( otherView, 'the view should not be refreshed' ).to.equal( otherViewAfter );
  522. } );
  523. describe( 'memoization', () => {
  524. it( 'should create new element on re-converting element', () => {
  525. setModelData( model, '<complex>' +
  526. '<slot><paragraph>foo</paragraph></slot>' +
  527. '<slot><paragraph>bar</paragraph></slot>' +
  528. '</complex>'
  529. );
  530. const complexView = viewRoot.getChild( 0 );
  531. model.change( writer => {
  532. writer.setAttribute( 'classForMain', true, modelRoot.getChild( 0 ) );
  533. } );
  534. const viewAfterReRender = viewRoot.getChild( 0 );
  535. expect( viewAfterReRender, 'the view should be refreshed' ).to.not.equal( complexView );
  536. } );
  537. it( 'should not re-create slot\'s child elements on re-converting main element (attribute changed)', () => {
  538. setModelData( model, '<complex>' +
  539. '<slot><paragraph>foo</paragraph></slot>' +
  540. '<slot><paragraph>bar</paragraph></slot>' +
  541. '</complex>'
  542. );
  543. const [ main, slotOne, slotOneChild, slotTwo, slotTwoChild ] = getNodes();
  544. model.change( writer => {
  545. writer.setAttribute( 'classForMain', true, modelRoot.getChild( 0 ) );
  546. } );
  547. const [ mainAfter, slotOneAfter, slotOneChildAfter, slotTwoAfter, slotTwoChildAfter ] = getNodes();
  548. expect( mainAfter, 'main view' ).to.not.equal( main );
  549. expect( slotOneAfter, 'first slot view' ).to.not.equal( slotOne );
  550. expect( slotTwoAfter, 'second slot view' ).to.not.equal( slotTwo );
  551. expect( slotOneChildAfter, 'first slot paragraph view' ).to.equal( slotOneChild );
  552. expect( slotTwoChildAfter, 'second slot paragraph view' ).to.equal( slotTwoChild );
  553. } );
  554. it( 'should not re-create slot\'s child elements on re-converting main element (slot added)', () => {
  555. setModelData( model, '<complex>' +
  556. '<slot><paragraph>foo</paragraph></slot>' +
  557. '<slot><paragraph>bar</paragraph></slot>' +
  558. '</complex>'
  559. );
  560. const [ main, slotOne, slotOneChild, slotTwo, slotTwoChild ] = getNodes();
  561. model.change( writer => {
  562. const slot = writer.createElement( 'slot' );
  563. const paragraph = writer.createElement( 'paragraph' );
  564. writer.insertText( 'baz', paragraph, 0 );
  565. writer.insert( paragraph, slot, 0 );
  566. writer.insert( slot, modelRoot.getChild( 0 ), 'end' );
  567. } );
  568. const [
  569. mainAfter,
  570. slotOneAfter, slotOneChildAfter,
  571. slotTwoAfter, slotTwoChildAfter,
  572. slot3, slot3Child
  573. ] = getNodes();
  574. expect( mainAfter, 'main view' ).to.not.equal( main );
  575. expect( slotOneAfter, 'first slot view' ).to.not.equal( slotOne );
  576. expect( slotTwoAfter, 'second slot view' ).to.not.equal( slotTwo );
  577. expect( slotOneChildAfter, 'first slot paragraph view' ).to.equal( slotOneChild );
  578. expect( slotTwoChildAfter, 'second slot paragraph view' ).to.equal( slotTwoChild );
  579. expect( slot3, 'third slot view' ).to.not.be.undefined;
  580. expect( slot3Child, 'third slot paragraph view' ).to.not.be.undefined;
  581. } );
  582. /**
  583. * Returns a generator that yields elements as [ mainView, slot1, childOfSlot1, slot2, childOfSlot2, ... ].
  584. */
  585. function* getNodes() {
  586. const main = viewRoot.getChild( 0 );
  587. yield main;
  588. const slotWrap = main.getChild( 0 );
  589. for ( const slot of slotWrap.getChildren() ) {
  590. const slotOneChild = slot.getChild( 0 );
  591. yield slot;
  592. yield slotOneChild;
  593. }
  594. }
  595. } );
  596. } );
  597. // Skipped, as it would require two-level mapping. See https://github.com/ckeditor/ckeditor5/issues/1589.
  598. describe.skip( 'with complex view structure (slot conversion atomic converters for some changes)', () => {
  599. beforeEach( () => {
  600. model.schema.register( 'complex', {
  601. allowIn: '$root',
  602. allowAttributes: [ 'classForMain', 'classForWrap', 'attributeToElement' ]
  603. } );
  604. function createViewSlot( slot, { writer, mapper } ) {
  605. const viewSlot = writer.createContainerElement( 'div', { class: 'slot' } );
  606. mapper.bindSlotElements( slot, viewSlot );
  607. return viewSlot;
  608. }
  609. downcastHelpers.elementToElement( {
  610. model: 'complex',
  611. view: ( modelElement, { writer, mapper, consumable } ) => {
  612. const classForMain = !!modelElement.getAttribute( 'classForMain' );
  613. const classForWrap = !!modelElement.getAttribute( 'classForWrap' );
  614. const attributeToElement = !!modelElement.getAttribute( 'attributeToElement' );
  615. const outer = writer.createContainerElement( 'div', {
  616. class: `complex-slots${ classForMain ? ' with-class' : '' }`
  617. } );
  618. const inner = writer.createContainerElement( 'div', {
  619. class: `slots${ classForWrap ? ' with-class' : '' }`
  620. } );
  621. if ( attributeToElement ) {
  622. const optional = writer.createEmptyElement( 'div', { class: 'optional' } );
  623. writer.insert( writer.createPositionAt( outer, 0 ), optional );
  624. }
  625. writer.insert( writer.createPositionAt( outer, 'end' ), inner );
  626. mapper.bindElements( modelElement, outer );
  627. mapper.bindElements( modelElement, inner );
  628. for ( const slot of modelElement.getChildren() ) {
  629. const viewSlot = createViewSlot( slot, { writer, mapper } );
  630. writer.insert( writer.createPositionAt( inner, slot.index ), viewSlot );
  631. consumable.consume( slot, 'insert' );
  632. }
  633. return outer;
  634. },
  635. triggerBy: [
  636. 'attribute:classForMain:complex',
  637. 'attribute:classForWrap:complex',
  638. 'attribute:attributeToElement:complex'
  639. // Contrary to the previous test - do not act on slot insert/remove: 'insert:slot', 'remove:slot'.
  640. ]
  641. } );
  642. downcastHelpers.elementToElement( {
  643. model: 'slot',
  644. view: createViewSlot
  645. } );
  646. model.schema.register( 'slot', {
  647. allowIn: 'complex'
  648. } );
  649. model.schema.register( 'paragraph', {
  650. inheritAllFrom: '$block',
  651. allowIn: 'slot'
  652. } );
  653. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  654. } );
  655. it( 'should convert on insert', () => {
  656. model.change( writer => {
  657. writer.insertElement( 'complex', modelRoot, 0 );
  658. } );
  659. expectResult( '<div class="complex-slots"><div class="slots"></div></div>' );
  660. } );
  661. it( 'should convert on attribute set (main element)', () => {
  662. setModelData( model, '<complex></complex>' );
  663. model.change( writer => {
  664. writer.setAttribute( 'classForMain', true, modelRoot.getChild( 0 ) );
  665. } );
  666. expectResult( '<div class="complex-slots with-class"><div class="slots"></div></div>' );
  667. } );
  668. it( 'should convert on attribute set (other element)', () => {
  669. setModelData( model, '<complex></complex>' );
  670. model.change( writer => {
  671. writer.setAttribute( 'classForWrap', true, modelRoot.getChild( 0 ) );
  672. } );
  673. expectResult( '<div class="complex-slots"><div class="slots with-class"></div></div>' );
  674. } );
  675. it( 'should convert on attribute set (insert new view element)', () => {
  676. setModelData( model, '<complex></complex>' );
  677. model.change( writer => {
  678. writer.setAttribute( 'attributeToElement', true, modelRoot.getChild( 0 ) );
  679. } );
  680. expectResult( '<div class="complex-slots"><div class="optional"></div><div class="slots"></div></div>' );
  681. } );
  682. it( 'should convert element with slots', () => {
  683. setModelData( model,
  684. '<complex>' +
  685. '<slot><paragraph>foo</paragraph></slot>' +
  686. '<slot><paragraph>bar</paragraph></slot>' +
  687. '</complex>' );
  688. expectResult(
  689. '<div class="complex-slots">' +
  690. '<div class="slots">' +
  691. '<div class="slot"><p>foo</p></div>' +
  692. '<div class="slot"><p>bar</p></div>' +
  693. '</div>' +
  694. '</div>'
  695. );
  696. } );
  697. it( 'should not convert element on adding slot', () => {
  698. setModelData( model,
  699. '<complex>' +
  700. '<slot><paragraph>foo</paragraph></slot>' +
  701. '<slot><paragraph>bar</paragraph></slot>' +
  702. '</complex>' );
  703. model.change( writer => {
  704. const slot = writer.createElement( 'slot' );
  705. const paragraph = writer.createElement( 'paragraph' );
  706. writer.insertText( 'baz', paragraph, 0 );
  707. writer.insert( paragraph, slot, 0 );
  708. writer.insert( slot, modelRoot.getChild( 0 ), 'end' );
  709. } );
  710. expectResult(
  711. '<div class="complex-slots">' +
  712. '<div class="slots">' +
  713. '<div class="slot"><p>foo</p></div>' +
  714. '<div class="slot"><p>bar</p></div>' +
  715. '<div class="slot"><p>baz</p></div>' +
  716. '</div>' +
  717. '</div>'
  718. );
  719. } );
  720. it( 'should not convert element on removing slot', () => {
  721. setModelData( model,
  722. '<complex>' +
  723. '<slot><paragraph>foo</paragraph></slot>' +
  724. '<slot><paragraph>bar</paragraph></slot>' +
  725. '</complex>' );
  726. model.change( writer => {
  727. writer.remove( modelRoot.getChild( 0 ).getChild( 0 ) );
  728. } );
  729. expectResult(
  730. '<div class="complex-slots">' +
  731. '<div class="slots">' +
  732. '<div class="slot"><p>bar</p></div>' +
  733. '</div>' +
  734. '</div>'
  735. );
  736. } );
  737. it( 'should convert element on a trigger and block atomic converters (remove + attribute)', () => {
  738. setModelData( model,
  739. '<complex>' +
  740. '<slot><paragraph>foo</paragraph></slot>' +
  741. '<slot><paragraph>bar</paragraph></slot>' +
  742. '</complex>' );
  743. model.change( writer => {
  744. writer.remove( modelRoot.getChild( 0 ).getChild( 0 ) );
  745. writer.setAttribute( 'classForMain', true, modelRoot.getChild( 0 ) );
  746. } );
  747. expectResult(
  748. '<div class="complex-slots with-class">' +
  749. '<div class="slots">' +
  750. '<div class="slot"><p>bar</p></div>' +
  751. '</div>' +
  752. '</div>'
  753. );
  754. } );
  755. it( 'should convert element on a trigger and block atomic converters (insert + attribute)', () => {
  756. setModelData( model,
  757. '<complex>' +
  758. '<slot><paragraph>foo</paragraph></slot>' +
  759. '<slot><paragraph>bar</paragraph></slot>' +
  760. '</complex>' );
  761. model.change( writer => {
  762. writer.insert( modelRoot.getChild( 0 ).getChild( 0 ) );
  763. writer.setAttribute( 'classForMain', true, modelRoot.getChild( 0 ) );
  764. } );
  765. expectResult(
  766. '<div class="complex-slots with-class">' +
  767. '<div class="slots">' +
  768. '<div class="slot"><p>foo</p></div>' +
  769. '<div class="slot"><p>bar</p></div>' +
  770. '<div class="slot"><p>baz</p></div>' +
  771. '</div>' +
  772. '</div>'
  773. );
  774. } );
  775. } );
  776. function getViewAttributes( modelElement ) {
  777. const toStyle = modelElement.hasAttribute( 'toStyle' ) && { style: modelElement.getAttribute( 'toStyle' ) };
  778. const toClass = modelElement.hasAttribute( 'toClass' ) && { class: 'is-classy' };
  779. return {
  780. ...toStyle,
  781. ...toClass
  782. };
  783. }
  784. function insertBazSlot( writer, modelRoot ) {
  785. const slot = writer.createElement( 'slot' );
  786. const paragraph = writer.createElement( 'paragraph' );
  787. writer.insertText( 'baz', paragraph, 0 );
  788. writer.insert( paragraph, slot, 0 );
  789. writer.insert( slot, modelRoot.getChild( 0 ), 'end' );
  790. }
  791. } );
  792. } );
  793. describe( 'attributeToElement()', () => {
  794. beforeEach( () => {
  795. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  796. } );
  797. it( 'should be chainable', () => {
  798. expect( downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } ) ).to.equal( downcastHelpers );
  799. } );
  800. it( 'config.view is a string', () => {
  801. downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } );
  802. model.change( writer => {
  803. writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
  804. } );
  805. expectResult( '<strong>foo</strong>' );
  806. } );
  807. it( 'can be overwritten using converterPriority', () => {
  808. downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } );
  809. downcastHelpers.attributeToElement( { model: 'bold', view: 'b', converterPriority: 'high' } );
  810. model.change( writer => {
  811. writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
  812. } );
  813. expectResult( '<b>foo</b>' );
  814. } );
  815. it( 'config.view is a view element definition', () => {
  816. downcastHelpers.attributeToElement( {
  817. model: 'invert',
  818. view: {
  819. name: 'span',
  820. classes: [ 'font-light', 'bg-dark' ]
  821. }
  822. } );
  823. model.change( writer => {
  824. writer.insertText( 'foo', { invert: true }, modelRoot, 0 );
  825. } );
  826. expectResult( '<span class="bg-dark font-light">foo</span>' );
  827. expect( viewRoot.getChild( 0 ).priority ).to.equal( ViewAttributeElement.DEFAULT_PRIORITY );
  828. } );
  829. it( 'config.view allows specifying the element\'s priority', () => {
  830. downcastHelpers.attributeToElement( {
  831. model: 'invert',
  832. view: {
  833. name: 'span',
  834. priority: 5
  835. }
  836. } );
  837. model.change( writer => {
  838. writer.insertText( 'foo', { invert: true }, modelRoot, 0 );
  839. } );
  840. expect( viewRoot.getChild( 0 ).priority ).to.equal( 5 );
  841. } );
  842. it( 'model attribute value is enum', () => {
  843. downcastHelpers.attributeToElement( {
  844. model: {
  845. key: 'fontSize',
  846. values: [ 'big', 'small' ]
  847. },
  848. view: {
  849. big: {
  850. name: 'span',
  851. styles: {
  852. 'font-size': '1.2em'
  853. }
  854. },
  855. small: {
  856. name: 'span',
  857. styles: {
  858. 'font-size': '0.8em'
  859. },
  860. priority: 5
  861. }
  862. }
  863. } );
  864. model.change( writer => {
  865. writer.insertText( 'foo', { fontSize: 'big' }, modelRoot, 0 );
  866. } );
  867. expect( viewRoot.getChild( 0 ).priority ).to.equal( ViewAttributeElement.DEFAULT_PRIORITY );
  868. expectResult( '<span style="font-size:1.2em">foo</span>' );
  869. model.change( writer => {
  870. writer.setAttribute( 'fontSize', 'small', modelRoot.getChild( 0 ) );
  871. } );
  872. expectResult( '<span style="font-size:0.8em">foo</span>' );
  873. expect( viewRoot.getChild( 0 ).priority ).to.equal( 5 );
  874. model.change( writer => {
  875. writer.removeAttribute( 'fontSize', modelRoot.getChild( 0 ) );
  876. } );
  877. expectResult( 'foo' );
  878. } );
  879. it( 'config.view is a function', () => {
  880. downcastHelpers.attributeToElement( {
  881. model: 'bold',
  882. view: ( modelAttributeValue, { writer } ) => {
  883. return writer.createAttributeElement( 'span', { style: 'font-weight:' + modelAttributeValue } );
  884. }
  885. } );
  886. model.change( writer => {
  887. writer.insertText( 'foo', { bold: '500' }, modelRoot, 0 );
  888. } );
  889. expectResult( '<span style="font-weight:500">foo</span>' );
  890. } );
  891. it( 'config.model.name is given', () => {
  892. downcastHelpers.attributeToElement( {
  893. model: {
  894. key: 'color',
  895. name: '$text'
  896. },
  897. view: ( modelAttributeValue, { writer } ) => {
  898. return writer.createAttributeElement( 'span', { style: 'color:' + modelAttributeValue } );
  899. }
  900. } );
  901. downcastHelpers.elementToElement( {
  902. model: 'smiley',
  903. view: ( modelElement, { writer } ) => {
  904. return writer.createEmptyElement( 'img', {
  905. src: 'smile.jpg',
  906. class: 'smiley'
  907. } );
  908. }
  909. } );
  910. model.change( writer => {
  911. writer.insertText( 'foo', { color: '#FF0000' }, modelRoot, 0 );
  912. writer.insertElement( 'smiley', { color: '#FF0000' }, modelRoot, 3 );
  913. } );
  914. expectResult( '<span style="color:#FF0000">foo</span><img class="smiley" src="smile.jpg"></img>' );
  915. } );
  916. it( 'should not convert if creator returned null', () => {
  917. downcastHelpers.elementToElement( { model: 'div', view: () => null } );
  918. const modelElement = new ModelElement( 'div' );
  919. model.change( writer => {
  920. writer.insert( modelElement, modelRootStart );
  921. } );
  922. expect( viewToString( viewRoot ) ).to.equal( '<div></div>' );
  923. } );
  924. it( 'should convert insert/change/remove of attribute in model into wrapping element in a view', () => {
  925. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) );
  926. downcastHelpers.attributeToElement( {
  927. model: 'bold',
  928. view: ( modelAttributeValue, { writer } ) => writer.createAttributeElement( 'b' )
  929. } );
  930. model.change( writer => {
  931. writer.insert( modelElement, modelRootStart );
  932. } );
  933. expect( viewToString( viewRoot ) ).to.equal( '<div><p><b>foobar</b></p></div>' );
  934. model.change( writer => {
  935. writer.removeAttribute( 'bold', writer.createRangeIn( modelElement ) );
  936. } );
  937. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  938. } );
  939. it( 'should convert insert/remove of attribute in model with wrapping element generating function as a parameter', () => {
  940. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { style: 'bold' } ) );
  941. downcastHelpers.attributeToElement( {
  942. model: 'style',
  943. view: ( modelAttributeValue, { writer } ) => {
  944. if ( modelAttributeValue == 'bold' ) {
  945. return writer.createAttributeElement( 'b' );
  946. }
  947. }
  948. } );
  949. model.change( writer => {
  950. writer.insert( modelElement, modelRootStart );
  951. } );
  952. expect( viewToString( viewRoot ) ).to.equal( '<div><p><b>foobar</b></p></div>' );
  953. model.change( writer => {
  954. writer.removeAttribute( 'style', writer.createRangeIn( modelElement ) );
  955. } );
  956. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  957. } );
  958. it( 'should update range on re-wrapping attribute (#475)', () => {
  959. const modelElement = new ModelElement( 'paragraph', null, [
  960. new ModelText( 'x' ),
  961. new ModelText( 'foo', { link: 'http://foo.com' } ),
  962. new ModelText( 'x' )
  963. ] );
  964. downcastHelpers.attributeToElement( {
  965. model: 'link',
  966. view: ( modelAttributeValue, { writer } ) => {
  967. return writer.createAttributeElement( 'a', { href: modelAttributeValue } );
  968. }
  969. } );
  970. model.change( writer => {
  971. writer.insert( modelElement, modelRootStart );
  972. } );
  973. expect( viewToString( viewRoot ) ).to.equal( '<div><p>x<a href="http://foo.com">foo</a>x</p></div>' );
  974. // Set new attribute on old link but also on non-linked characters.
  975. model.change( writer => {
  976. writer.setAttribute( 'link', 'http://foobar.com', writer.createRangeIn( modelElement ) );
  977. } );
  978. expect( viewToString( viewRoot ) ).to.equal( '<div><p><a href="http://foobar.com">xfoox</a></p></div>' );
  979. } );
  980. it( 'should support unicode', () => {
  981. const modelElement = new ModelElement( 'paragraph', null, [ 'நி', new ModelText( 'லைக்', { bold: true } ), 'கு' ] );
  982. downcastHelpers.attributeToElement( {
  983. model: 'bold',
  984. view: ( modelAttributeValue, { writer } ) => writer.createAttributeElement( 'b' )
  985. } );
  986. model.change( writer => {
  987. writer.insert( modelElement, modelRootStart );
  988. } );
  989. expect( viewToString( viewRoot ) ).to.equal( '<div><p>நி<b>லைக்</b>கு</p></div>' );
  990. model.change( writer => {
  991. writer.removeAttribute( 'bold', writer.createRangeIn( modelElement ) );
  992. } );
  993. expect( viewToString( viewRoot ) ).to.equal( '<div><p>நிலைக்கு</p></div>' );
  994. } );
  995. it( 'should be possible to override ', () => {
  996. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) );
  997. downcastHelpers.attributeToElement( {
  998. model: 'bold',
  999. view: ( modelAttributeValue, { writer } ) => writer.createAttributeElement( 'b' )
  1000. } );
  1001. downcastHelpers.attributeToElement( {
  1002. model: 'bold',
  1003. view: ( modelAttributeValue, { writer } ) => writer.createAttributeElement( 'strong' ),
  1004. converterPriority: 'high'
  1005. } );
  1006. model.change( writer => {
  1007. writer.insert( modelElement, modelRootStart );
  1008. } );
  1009. expect( viewToString( viewRoot ) ).to.equal( '<div><p><strong>foobar</strong></p></div>' );
  1010. } );
  1011. it( 'should not convert and not consume if creator function returned null', () => {
  1012. sinon.spy( controller.downcastDispatcher, 'fire' );
  1013. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { italic: true } ) );
  1014. downcastHelpers.attributeToElement( {
  1015. model: 'italic',
  1016. view: () => null
  1017. } );
  1018. const spy = sinon.spy();
  1019. controller.downcastDispatcher.on( 'attribute:italic', spy );
  1020. model.change( writer => {
  1021. writer.insert( modelElement, modelRootStart );
  1022. } );
  1023. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  1024. expect( controller.downcastDispatcher.fire.calledWith( 'attribute:italic:$text' ) ).to.be.true;
  1025. expect( spy.called ).to.be.true;
  1026. } );
  1027. } );
  1028. describe( 'attributeToAttribute()', () => {
  1029. testUtils.createSinonSandbox();
  1030. beforeEach( () => {
  1031. downcastHelpers.elementToElement( { model: 'image', view: 'img' } );
  1032. downcastHelpers.elementToElement( {
  1033. model: 'paragraph',
  1034. view: ( modelItem, { writer } ) => writer.createContainerElement( 'p' )
  1035. } );
  1036. downcastHelpers.attributeToAttribute( {
  1037. model: 'class',
  1038. view: 'class'
  1039. } );
  1040. } );
  1041. it( 'should be chainable', () => {
  1042. expect( downcastHelpers.attributeToAttribute( { model: 'source', view: 'src' } ) ).to.equal( downcastHelpers );
  1043. } );
  1044. it( 'config.view is a string', () => {
  1045. downcastHelpers.attributeToAttribute( { model: 'source', view: 'src' } );
  1046. model.change( writer => {
  1047. writer.insertElement( 'image', { source: 'foo.jpg' }, modelRoot, 0 );
  1048. } );
  1049. expectResult( '<img src="foo.jpg"></img>' );
  1050. model.change( writer => {
  1051. writer.removeAttribute( 'source', modelRoot.getChild( 0 ) );
  1052. } );
  1053. expectResult( '<img></img>' );
  1054. } );
  1055. it( 'can be overwritten using converterPriority', () => {
  1056. downcastHelpers.attributeToAttribute( { model: 'source', view: 'href' } );
  1057. downcastHelpers.attributeToAttribute( { model: 'source', view: 'src', converterPriority: 'high' } );
  1058. model.change( writer => {
  1059. writer.insertElement( 'image', { source: 'foo.jpg' }, modelRoot, 0 );
  1060. } );
  1061. expectResult( '<img src="foo.jpg"></img>' );
  1062. } );
  1063. it( 'model element name specified', () => {
  1064. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  1065. downcastHelpers.attributeToAttribute( {
  1066. model: {
  1067. name: 'image',
  1068. key: 'source'
  1069. },
  1070. view: 'src'
  1071. } );
  1072. model.change( writer => {
  1073. writer.insertElement( 'image', { source: 'foo.jpg' }, modelRoot, 0 );
  1074. } );
  1075. expectResult( '<img src="foo.jpg"></img>' );
  1076. model.change( writer => {
  1077. writer.rename( modelRoot.getChild( 0 ), 'paragraph' );
  1078. } );
  1079. expectResult( '<p></p>' );
  1080. } );
  1081. it( 'config.view is an object, model attribute value is enum', () => {
  1082. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  1083. downcastHelpers.attributeToAttribute( {
  1084. model: {
  1085. key: 'styled',
  1086. values: [ 'dark', 'light' ]
  1087. },
  1088. view: {
  1089. dark: {
  1090. key: 'class',
  1091. value: [ 'styled', 'styled-dark' ]
  1092. },
  1093. light: {
  1094. key: 'class',
  1095. value: [ 'styled', 'styled-light' ]
  1096. }
  1097. }
  1098. } );
  1099. model.change( writer => {
  1100. writer.insertElement( 'paragraph', { styled: 'dark' }, modelRoot, 0 );
  1101. } );
  1102. expectResult( '<p class="styled styled-dark"></p>' );
  1103. model.change( writer => {
  1104. writer.setAttribute( 'styled', 'light', modelRoot.getChild( 0 ) );
  1105. } );
  1106. expectResult( '<p class="styled styled-light"></p>' );
  1107. model.change( writer => {
  1108. writer.removeAttribute( 'styled', modelRoot.getChild( 0 ) );
  1109. } );
  1110. expectResult( '<p></p>' );
  1111. } );
  1112. it( 'config.view is an object, model attribute value is enum, view has style', () => {
  1113. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  1114. downcastHelpers.attributeToAttribute( {
  1115. model: {
  1116. key: 'align',
  1117. values: [ 'right', 'center' ]
  1118. },
  1119. view: {
  1120. right: {
  1121. key: 'style',
  1122. value: {
  1123. 'text-align': 'right'
  1124. }
  1125. },
  1126. center: {
  1127. key: 'style',
  1128. value: {
  1129. 'text-align': 'center'
  1130. }
  1131. }
  1132. }
  1133. } );
  1134. model.change( writer => {
  1135. writer.insertElement( 'paragraph', { align: 'right' }, modelRoot, 0 );
  1136. } );
  1137. expectResult( '<p style="text-align:right"></p>' );
  1138. model.change( writer => {
  1139. writer.setAttribute( 'align', 'center', modelRoot.getChild( 0 ) );
  1140. } );
  1141. expectResult( '<p style="text-align:center"></p>' );
  1142. model.change( writer => {
  1143. writer.removeAttribute( 'align', modelRoot.getChild( 0 ) );
  1144. } );
  1145. expectResult( '<p></p>' );
  1146. } );
  1147. it( 'config.view is an object, only name and key are provided', () => {
  1148. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  1149. downcastHelpers.attributeToAttribute( {
  1150. model: {
  1151. name: 'paragraph',
  1152. key: 'class'
  1153. },
  1154. view: {
  1155. name: 'paragraph',
  1156. key: 'class'
  1157. }
  1158. } );
  1159. model.change( writer => {
  1160. writer.insertElement( 'paragraph', { class: 'dark' }, modelRoot, 0 );
  1161. } );
  1162. expectResult( '<p class="dark"></p>' );
  1163. model.change( writer => {
  1164. writer.setAttribute( 'class', 'light', modelRoot.getChild( 0 ) );
  1165. } );
  1166. expectResult( '<p class="light"></p>' );
  1167. model.change( writer => {
  1168. writer.removeAttribute( 'class', modelRoot.getChild( 0 ) );
  1169. } );
  1170. expectResult( '<p></p>' );
  1171. } );
  1172. it( 'config.view is a function', () => {
  1173. downcastHelpers.attributeToAttribute( {
  1174. model: 'styled',
  1175. view: ( attributeValue, conversionApi ) => {
  1176. // To ensure conversion API is provided.
  1177. expect( conversionApi.writer ).to.instanceof( DowncastWriter );
  1178. return { key: 'class', value: 'styled-' + attributeValue };
  1179. }
  1180. } );
  1181. model.change( writer => {
  1182. writer.insertElement( 'image', { styled: 'pull-out' }, modelRoot, 0 );
  1183. } );
  1184. expectResult( '<img class="styled-pull-out"></img>' );
  1185. } );
  1186. // #1587
  1187. it( 'config.view and config.model as strings in generic conversion (elements only)', () => {
  1188. const consoleWarnStub = testUtils.sinon.stub( console, 'warn' );
  1189. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  1190. downcastHelpers.attributeToAttribute( { model: 'test', view: 'test' } );
  1191. model.change( writer => {
  1192. writer.insertElement( 'paragraph', { test: '1' }, modelRoot, 0 );
  1193. writer.insertElement( 'paragraph', { test: '2' }, modelRoot, 1 );
  1194. } );
  1195. expectResult( '<p test="1"></p><p test="2"></p>' );
  1196. expect( consoleWarnStub.callCount ).to.equal( 0 );
  1197. model.change( writer => {
  1198. writer.removeAttribute( 'test', modelRoot.getChild( 1 ) );
  1199. } );
  1200. expectResult( '<p test="1"></p><p></p>' );
  1201. } );
  1202. // #1587
  1203. it( 'config.view and config.model as strings in generic conversion (elements + text)', () => {
  1204. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  1205. downcastHelpers.attributeToAttribute( { model: 'test', view: 'test' } );
  1206. expectToThrowCKEditorError( () => {
  1207. model.change( writer => {
  1208. writer.insertElement( 'paragraph', modelRoot, 0 );
  1209. writer.insertElement( 'paragraph', { test: '1' }, modelRoot, 1 );
  1210. writer.insertText( 'Foo', { test: '2' }, modelRoot.getChild( 0 ), 0 );
  1211. writer.insertText( 'Bar', { test: '3' }, modelRoot.getChild( 1 ), 0 );
  1212. } );
  1213. }, /^conversion-attribute-to-attribute-on-text/ );
  1214. } );
  1215. it( 'should convert attribute insert/change/remove on a model node', () => {
  1216. const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) );
  1217. model.change( writer => {
  1218. writer.insert( modelElement, modelRootStart );
  1219. } );
  1220. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="foo">foobar</p></div>' );
  1221. model.change( writer => {
  1222. writer.setAttribute( 'class', 'bar', modelElement );
  1223. } );
  1224. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="bar">foobar</p></div>' );
  1225. model.change( writer => {
  1226. writer.removeAttribute( 'class', modelElement );
  1227. } );
  1228. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  1229. } );
  1230. it( 'should be possible to override setAttribute', () => {
  1231. downcastHelpers.attributeToAttribute( {
  1232. model: 'class',
  1233. view: ( evt, data, conversionApi ) => {
  1234. conversionApi.consumable.consume( data.item, 'attribute:class' );
  1235. },
  1236. converterPriority: 'high'
  1237. } );
  1238. model.change( writer => {
  1239. const modelElement = new ModelElement( 'paragraph', { classes: 'foo' }, new ModelText( 'foobar' ) );
  1240. writer.insert( modelElement, modelRootStart );
  1241. } );
  1242. // No attribute set.
  1243. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  1244. } );
  1245. it( 'should not convert or consume if element creator returned null', () => {
  1246. const callback = sinon.stub().returns( null );
  1247. downcastHelpers.attributeToAttribute( {
  1248. model: 'class',
  1249. view: callback,
  1250. converterPriority: 'high'
  1251. } );
  1252. const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) );
  1253. model.change( writer => {
  1254. writer.insert( modelElement, modelRootStart );
  1255. } );
  1256. expect( viewToString( viewRoot ) ).to.equal( '<div><p class="foo">foobar</p></div>' );
  1257. sinon.assert.called( callback );
  1258. } );
  1259. } );
  1260. describe( 'markerToElement()', () => {
  1261. let modelText, modelElement, range;
  1262. it( 'should be chainable', () => {
  1263. expect( downcastHelpers.markerToElement( { model: 'search', view: 'marker-search' } ) ).to.equal( downcastHelpers );
  1264. } );
  1265. it( 'config.view is a string', () => {
  1266. downcastHelpers.markerToElement( { model: 'search', view: 'marker-search' } );
  1267. model.change( writer => {
  1268. writer.insertText( 'foo', modelRoot, 0 );
  1269. const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 2 ) );
  1270. writer.addMarker( 'search', { range, usingOperation: false } );
  1271. } );
  1272. expectResult( 'f<marker-search></marker-search>o<marker-search></marker-search>o' );
  1273. } );
  1274. it( 'can be overwritten using converterPriority', () => {
  1275. downcastHelpers.markerToElement( { model: 'search', view: 'marker-search' } );
  1276. downcastHelpers.markerToElement( { model: 'search', view: 'search', converterPriority: 'high' } );
  1277. model.change( writer => {
  1278. writer.insertText( 'foo', modelRoot, 0 );
  1279. const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 2 ) );
  1280. writer.addMarker( 'search', { range, usingOperation: false } );
  1281. } );
  1282. expectResult( 'f<search></search>o<search></search>o' );
  1283. } );
  1284. it( 'config.view is a view element definition', () => {
  1285. downcastHelpers.markerToElement( {
  1286. model: 'search',
  1287. view: {
  1288. name: 'span',
  1289. attributes: {
  1290. 'data-marker': 'search'
  1291. }
  1292. }
  1293. } );
  1294. model.change( writer => {
  1295. writer.insertText( 'foo', modelRoot, 0 );
  1296. const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 2 ) );
  1297. writer.addMarker( 'search', { range, usingOperation: false } );
  1298. } );
  1299. expectResult( 'f<span data-marker="search"></span>o<span data-marker="search"></span>o' );
  1300. } );
  1301. it( 'config.view is a function', () => {
  1302. downcastHelpers.markerToElement( {
  1303. model: 'search',
  1304. view: ( data, { writer } ) => {
  1305. return writer.createUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } );
  1306. }
  1307. } );
  1308. model.change( writer => {
  1309. writer.insertText( 'foo', modelRoot, 0 );
  1310. const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 2 ) );
  1311. writer.addMarker( 'search', { range, usingOperation: false } );
  1312. } );
  1313. expectResult( 'f<span data-marker="search" data-start="true"></span>o<span data-marker="search" data-start="false"></span>o' );
  1314. } );
  1315. describe( 'collapsed range', () => {
  1316. beforeEach( () => {
  1317. modelText = new ModelText( 'foobar' );
  1318. modelElement = new ModelElement( 'paragraph', null, modelText );
  1319. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  1320. model.change( writer => {
  1321. writer.insert( modelElement, modelRootStart );
  1322. } );
  1323. range = model.createRange( model.createPositionAt( modelElement, 3 ), model.createPositionAt( modelElement, 3 ) );
  1324. } );
  1325. it( 'should insert and remove ui element', () => {
  1326. downcastHelpers.markerToElement( {
  1327. model: 'marker',
  1328. view: ( data, { writer } ) => writer.createUIElement( 'span', { 'class': 'marker' } )
  1329. } );
  1330. model.change( writer => {
  1331. writer.addMarker( 'marker', { range, usingOperation: false } );
  1332. } );
  1333. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo<span class="marker"></span>bar</p></div>' );
  1334. model.change( writer => {
  1335. writer.removeMarker( 'marker' );
  1336. } );
  1337. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  1338. } );
  1339. it( 'should not convert if consumable was consumed', () => {
  1340. sinon.spy( controller.downcastDispatcher, 'fire' );
  1341. downcastHelpers.markerToElement( {
  1342. model: 'marker',
  1343. view: ( data, { writer } ) => writer.createUIElement( 'span', { 'class': 'marker' } )
  1344. } );
  1345. controller.downcastDispatcher.on( 'addMarker:marker', ( evt, data, conversionApi ) => {
  1346. conversionApi.consumable.consume( data.markerRange, 'addMarker:marker' );
  1347. }, { priority: 'high' } );
  1348. model.change( writer => {
  1349. writer.addMarker( 'marker', { range, usingOperation: false } );
  1350. } );
  1351. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  1352. expect( controller.downcastDispatcher.fire.calledWith( 'addMarker:marker' ) );
  1353. } );
  1354. it( 'should not convert if creator returned null', () => {
  1355. downcastHelpers.markerToElement( {
  1356. model: 'marker',
  1357. view: () => null
  1358. } );
  1359. model.change( writer => {
  1360. writer.addMarker( 'marker', { range, usingOperation: false } );
  1361. } );
  1362. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  1363. model.change( writer => {
  1364. writer.removeMarker( 'marker' );
  1365. } );
  1366. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  1367. } );
  1368. } );
  1369. describe( 'non-collapsed range', () => {
  1370. beforeEach( () => {
  1371. modelText = new ModelText( 'foobar' );
  1372. modelElement = new ModelElement( 'paragraph', null, modelText );
  1373. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  1374. model.change( writer => {
  1375. writer.insert( modelElement, modelRootStart );
  1376. } );
  1377. range = model.createRange( model.createPositionAt( modelElement, 2 ), model.createPositionAt( modelElement, 5 ) );
  1378. } );
  1379. it( 'should insert and remove ui element - element as a creator', () => {
  1380. downcastHelpers.markerToElement( {
  1381. model: 'marker',
  1382. view: ( data, { writer } ) => writer.createUIElement( 'span', { 'class': 'marker' } )
  1383. } );
  1384. model.change( writer => {
  1385. writer.addMarker( 'marker', { range, usingOperation: false } );
  1386. } );
  1387. expect( viewToString( viewRoot ) )
  1388. .to.equal( '<div><p>fo<span class="marker"></span>oba<span class="marker"></span>r</p></div>' );
  1389. model.change( writer => {
  1390. writer.removeMarker( 'marker' );
  1391. } );
  1392. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  1393. } );
  1394. it( 'should insert and remove ui element - function as a creator', () => {
  1395. downcastHelpers.markerToElement( {
  1396. model: 'marker',
  1397. view: ( data, { writer } ) => writer.createUIElement( 'span', { 'class': data.markerName } )
  1398. } );
  1399. model.change( writer => {
  1400. writer.addMarker( 'marker', { range, usingOperation: false } );
  1401. } );
  1402. expect( viewToString( viewRoot ) )
  1403. .to.equal( '<div><p>fo<span class="marker"></span>oba<span class="marker"></span>r</p></div>' );
  1404. model.change( writer => {
  1405. writer.removeMarker( 'marker' );
  1406. } );
  1407. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  1408. } );
  1409. it( 'should insert and remove different opening and ending element', () => {
  1410. downcastHelpers.markerToElement( {
  1411. model: 'marker',
  1412. view: ( data, { writer } ) => {
  1413. if ( data.isOpening ) {
  1414. return writer.createUIElement( 'span', { 'class': data.markerName, 'data-start': true } );
  1415. }
  1416. return writer.createUIElement( 'span', { 'class': data.markerName, 'data-end': true } );
  1417. }
  1418. } );
  1419. model.change( writer => {
  1420. writer.addMarker( 'marker', { range, usingOperation: false } );
  1421. } );
  1422. expect( viewToString( viewRoot ) ).to.equal(
  1423. '<div><p>fo<span class="marker" data-start="true"></span>oba<span class="marker" data-end="true"></span>r</p></div>'
  1424. );
  1425. model.change( writer => {
  1426. writer.removeMarker( 'marker' );
  1427. } );
  1428. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  1429. } );
  1430. it( 'should not convert if consumable was consumed', () => {
  1431. sinon.spy( controller.downcastDispatcher, 'fire' );
  1432. downcastHelpers.markerToElement( {
  1433. model: 'marker',
  1434. view: ( data, { writer } ) => writer.createUIElement( 'span', { 'class': 'marker' } )
  1435. } );
  1436. controller.downcastDispatcher.on( 'addMarker:marker', ( evt, data, conversionApi ) => {
  1437. conversionApi.consumable.consume( data.item, 'addMarker:marker' );
  1438. }, { priority: 'high' } );
  1439. model.change( writer => {
  1440. writer.addMarker( 'marker', { range, usingOperation: false } );
  1441. } );
  1442. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  1443. expect( controller.downcastDispatcher.fire.calledWith( 'addMarker:marker' ) );
  1444. } );
  1445. } );
  1446. } );
  1447. describe( 'markerToData()', () => {
  1448. let root;
  1449. beforeEach( () => {
  1450. root = model.document.getRoot();
  1451. model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
  1452. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  1453. } );
  1454. it( 'should be chainable', () => {
  1455. expect( downcastHelpers.markerToData( { model: 'search' } ) ).to.equal( downcastHelpers );
  1456. } );
  1457. it( 'default conversion, inside text, non-collapsed, no name', () => {
  1458. downcastHelpers.markerToData( { model: 'search' } );
  1459. setModelData( model, '<paragraph>Fo[ob]ar</paragraph>' );
  1460. model.change( writer => {
  1461. writer.addMarker( 'search', { range: model.document.selection.getFirstRange(), usingOperation: false } );
  1462. } );
  1463. expectResult( '<p>Fo<search-start></search-start>ob<search-end></search-end>ar</p>' );
  1464. model.change( writer => {
  1465. writer.removeMarker( 'search' );
  1466. } );
  1467. expectResult( '<p>Foobar</p>' );
  1468. } );
  1469. it( 'default conversion, inside text, non-collapsed, name', () => {
  1470. downcastHelpers.markerToData( { model: 'group' } );
  1471. setModelData( model, '<paragraph>Fo[ob]ar</paragraph>' );
  1472. model.change( writer => {
  1473. writer.addMarker( 'group:foo:bar:baz', { range: model.document.selection.getFirstRange(), usingOperation: false } );
  1474. } );
  1475. expectResult( '<p>Fo<group-start name="foo:bar:baz"></group-start>ob<group-end name="foo:bar:baz"></group-end>ar</p>' );
  1476. model.change( writer => {
  1477. writer.removeMarker( 'group:foo:bar:baz' );
  1478. } );
  1479. expectResult( '<p>Foobar</p>' );
  1480. } );
  1481. it( 'default conversion, inside text, collapsed, no name', () => {
  1482. downcastHelpers.markerToData( { model: 'search' } );
  1483. setModelData( model, '<paragraph>Foo[]bar</paragraph>' );
  1484. model.change( writer => {
  1485. writer.addMarker( 'search', { range: model.document.selection.getFirstRange(), usingOperation: false } );
  1486. } );
  1487. expectResult( '<p>Foo<search-start></search-start><search-end></search-end>bar</p>' );
  1488. model.change( writer => {
  1489. writer.removeMarker( 'search' );
  1490. } );
  1491. expectResult( '<p>Foobar</p>' );
  1492. } );
  1493. it( 'default conversion, inside text, collapsed, multiple markers, no name', () => {
  1494. downcastHelpers.markerToData( { model: 'group' } );
  1495. setModelData( model, '<paragraph>Foo[]bar</paragraph>' );
  1496. model.change( writer => {
  1497. writer.addMarker( 'group:foo', { range: model.document.selection.getFirstRange(), usingOperation: false } );
  1498. writer.addMarker( 'group:abc', { range: model.document.selection.getFirstRange(), usingOperation: false } );
  1499. } );
  1500. expectResult(
  1501. '<p>' +
  1502. 'Foo' +
  1503. '<group-start name="abc"></group-start><group-end name="abc"></group-end>' +
  1504. '<group-start name="foo"></group-start><group-end name="foo"></group-end>' +
  1505. 'bar' +
  1506. '</p>'
  1507. );
  1508. model.change( writer => {
  1509. writer.removeMarker( 'group:foo' );
  1510. writer.removeMarker( 'group:abc' );
  1511. } );
  1512. expectResult( '<p>Foobar</p>' );
  1513. } );
  1514. it( 'default conversion, on two elements, no name', () => {
  1515. downcastHelpers.markerToData( { model: 'search' } );
  1516. setModelData( model, '<paragraph>Foo</paragraph><paragraph>Bar</paragraph>' );
  1517. model.change( writer => {
  1518. const range = writer.createRangeIn( root );
  1519. writer.addMarker( 'search', { range, usingOperation: false } );
  1520. } );
  1521. expectResult( '<p data-search-start-before="">Foo</p><p data-search-end-after="">Bar</p>' );
  1522. model.change( writer => {
  1523. writer.removeMarker( 'search' );
  1524. } );
  1525. expectResult( '<p>Foo</p><p>Bar</p>' );
  1526. } );
  1527. it( 'default conversion, on two elements, name', () => {
  1528. downcastHelpers.markerToData( { model: 'group' } );
  1529. setModelData( model, '<paragraph>Foo</paragraph><paragraph>Bar</paragraph>' );
  1530. model.change( writer => {
  1531. const range = writer.createRangeIn( root );
  1532. writer.addMarker( 'group:foo:bar:baz', { range, usingOperation: false } );
  1533. } );
  1534. expectResult( '<p data-group-start-before="foo:bar:baz">Foo</p><p data-group-end-after="foo:bar:baz">Bar</p>' );
  1535. model.change( writer => {
  1536. writer.removeMarker( 'group:foo:bar:baz' );
  1537. } );
  1538. expectResult( '<p>Foo</p><p>Bar</p>' );
  1539. } );
  1540. it( 'default conversion, on one element, name', () => {
  1541. downcastHelpers.markerToData( { model: 'group' } );
  1542. setModelData( model, '<paragraph>Foobar</paragraph>' );
  1543. model.change( writer => {
  1544. const range = writer.createRangeIn( root );
  1545. writer.addMarker( 'group:foo:bar:baz', { range, usingOperation: false } );
  1546. } );
  1547. expectResult( '<p data-group-end-after="foo:bar:baz" data-group-start-before="foo:bar:baz">Foobar</p>' );
  1548. model.change( writer => {
  1549. writer.removeMarker( 'group:foo:bar:baz' );
  1550. } );
  1551. expectResult( '<p>Foobar</p>' );
  1552. } );
  1553. it( 'default conversion, collapsed before element, name', () => {
  1554. downcastHelpers.markerToData( { model: 'group' } );
  1555. setModelData( model, '<paragraph>Foobar</paragraph>' );
  1556. model.change( writer => {
  1557. // Collapsed before <paragraph>.
  1558. const range = writer.createRange(
  1559. writer.createPositionFromPath( root, [ 0 ] )
  1560. );
  1561. writer.addMarker( 'group:foo:bar:baz', { range, usingOperation: false } );
  1562. } );
  1563. expectResult( '<p data-group-end-before="foo:bar:baz" data-group-start-before="foo:bar:baz">Foobar</p>' );
  1564. model.change( writer => {
  1565. writer.removeMarker( 'group:foo:bar:baz' );
  1566. } );
  1567. expectResult( '<p>Foobar</p>' );
  1568. } );
  1569. it( 'default conversion, collapsed after element, name', () => {
  1570. downcastHelpers.markerToData( { model: 'group' } );
  1571. setModelData( model, '<paragraph>Foobar</paragraph>' );
  1572. model.change( writer => {
  1573. // Collapsed before <paragraph>.
  1574. const range = writer.createRange(
  1575. writer.createPositionFromPath( root, [ 1 ] )
  1576. );
  1577. writer.addMarker( 'group:foo:bar:baz', { range, usingOperation: false } );
  1578. } );
  1579. expectResult( '<p data-group-end-after="foo:bar:baz" data-group-start-after="foo:bar:baz">Foobar</p>' );
  1580. model.change( writer => {
  1581. writer.removeMarker( 'group:foo:bar:baz' );
  1582. } );
  1583. expectResult( '<p>Foobar</p>' );
  1584. } );
  1585. it( 'default conversion, mixed, multiple markers, name', () => {
  1586. downcastHelpers.markerToData( { model: 'group' } );
  1587. setModelData( model, '<paragraph>Foo</paragraph><paragraph>Bar</paragraph>' );
  1588. model.change( writer => {
  1589. const range = writer.createRange(
  1590. writer.createPositionFromPath( root, [ 0 ] ),
  1591. writer.createPositionFromPath( root, [ 1, 2 ] )
  1592. );
  1593. writer.addMarker( 'group:foo:bar', { range, usingOperation: false } );
  1594. writer.addMarker( 'group:abc:xyz', { range, usingOperation: false } );
  1595. } );
  1596. expectResult(
  1597. '<p data-group-start-before="abc:xyz,foo:bar">Foo</p>' +
  1598. '<p>Ba<group-end name="abc:xyz"></group-end><group-end name="foo:bar"></group-end>r</p>'
  1599. );
  1600. model.change( writer => {
  1601. writer.removeMarker( 'group:foo:bar' );
  1602. writer.removeMarker( 'group:abc:xyz' );
  1603. } );
  1604. expectResult( '<p>Foo</p><p>Bar</p>' );
  1605. } );
  1606. it( 'default conversion, mixed #2, multiple markers, name', () => {
  1607. downcastHelpers.markerToData( { model: 'group' } );
  1608. setModelData( model, '<paragraph>Foo</paragraph><paragraph>Bar</paragraph>' );
  1609. model.change( writer => {
  1610. const range = writer.createRange(
  1611. writer.createPositionFromPath( root, [ 0, 1 ] ),
  1612. writer.createPositionFromPath( root, [ 2 ] )
  1613. );
  1614. writer.addMarker( 'group:foo:bar', { range, usingOperation: false } );
  1615. writer.addMarker( 'group:abc:xyz', { range, usingOperation: false } );
  1616. } );
  1617. expectResult(
  1618. '<p>F<group-start name="abc:xyz"></group-start><group-start name="foo:bar"></group-start>oo</p>' +
  1619. '<p data-group-end-after="abc:xyz,foo:bar">Bar</p>'
  1620. );
  1621. model.change( writer => {
  1622. writer.removeMarker( 'group:foo:bar' );
  1623. writer.removeMarker( 'group:abc:xyz' );
  1624. } );
  1625. expectResult( '<p>Foo</p><p>Bar</p>' );
  1626. } );
  1627. it( 'default conversion, mixed #3, multiple markers, name', () => {
  1628. downcastHelpers.markerToData( { model: 'group' } );
  1629. setModelData( model, '<paragraph>Foo</paragraph>' );
  1630. model.change( writer => {
  1631. const range = writer.createRange(
  1632. writer.createPositionFromPath( root, [ 0 ] ),
  1633. writer.createPositionFromPath( root, [ 0, 2 ] )
  1634. );
  1635. writer.addMarker( 'group:foo:bar', { range, usingOperation: false } );
  1636. writer.addMarker( 'group:abc:xyz', { range, usingOperation: false } );
  1637. } );
  1638. expectResult(
  1639. '<p data-group-start-before="abc:xyz,foo:bar">' +
  1640. 'Fo<group-end name="abc:xyz"></group-end><group-end name="foo:bar"></group-end>o' +
  1641. '</p>'
  1642. );
  1643. model.change( writer => {
  1644. writer.removeMarker( 'group:foo:bar' );
  1645. writer.removeMarker( 'group:abc:xyz' );
  1646. } );
  1647. expectResult( '<p>Foo</p>' );
  1648. } );
  1649. it( 'default conversion, mixed #4, multiple markers, name', () => {
  1650. downcastHelpers.markerToData( { model: 'group' } );
  1651. setModelData( model, '<paragraph>Foo</paragraph>' );
  1652. model.change( writer => {
  1653. const range = writer.createRange(
  1654. writer.createPositionFromPath( root, [ 0, 2 ] ),
  1655. writer.createPositionFromPath( root, [ 1 ] )
  1656. );
  1657. writer.addMarker( 'group:foo:bar', { range, usingOperation: false } );
  1658. writer.addMarker( 'group:abc:xyz', { range, usingOperation: false } );
  1659. } );
  1660. expectResult(
  1661. '<p data-group-end-after="abc:xyz,foo:bar">' +
  1662. 'Fo<group-start name="abc:xyz"></group-start><group-start name="foo:bar"></group-start>o' +
  1663. '</p>'
  1664. );
  1665. model.change( writer => {
  1666. writer.removeMarker( 'group:foo:bar' );
  1667. writer.removeMarker( 'group:abc:xyz' );
  1668. } );
  1669. expectResult( '<p>Foo</p>' );
  1670. } );
  1671. it( 'conversion callback, mixed, multiple markers, name', () => {
  1672. const customData = {
  1673. foo: 'bar',
  1674. abc: 'xyz'
  1675. };
  1676. downcastHelpers.markerToData( {
  1677. model: 'group',
  1678. view: ( markerName, conversionApi ) => {
  1679. const namePart = markerName.split( ':' )[ 1 ];
  1680. // To ensure conversion API is provided.
  1681. expect( conversionApi.writer ).to.instanceof( DowncastWriter );
  1682. return {
  1683. group: 'g',
  1684. name: namePart + '_' + customData[ namePart ]
  1685. };
  1686. }
  1687. } );
  1688. setModelData( model, '<paragraph>Foo</paragraph><paragraph>Bar</paragraph>' );
  1689. model.change( writer => {
  1690. const range = writer.createRange(
  1691. writer.createPositionFromPath( root, [ 0 ] ),
  1692. writer.createPositionFromPath( root, [ 1, 2 ] )
  1693. );
  1694. writer.addMarker( 'group:foo', { range, usingOperation: false } );
  1695. writer.addMarker( 'group:abc', { range, usingOperation: false } );
  1696. } );
  1697. expectResult(
  1698. '<p data-g-start-before="abc_xyz,foo_bar">Foo</p>' +
  1699. '<p>Ba<g-end name="abc_xyz"></g-end><g-end name="foo_bar"></g-end>r</p>'
  1700. );
  1701. model.change( writer => {
  1702. writer.removeMarker( 'group:foo' );
  1703. writer.removeMarker( 'group:abc' );
  1704. } );
  1705. expectResult( '<p>Foo</p><p>Bar</p>' );
  1706. } );
  1707. it( 'conversion callback, mixed #2, multiple markers, name', () => {
  1708. const customData = {
  1709. foo: 'bar',
  1710. abc: 'xyz'
  1711. };
  1712. downcastHelpers.markerToData( {
  1713. model: 'group',
  1714. view: markerName => {
  1715. const namePart = markerName.split( ':' )[ 1 ];
  1716. return {
  1717. group: 'g',
  1718. name: namePart + '_' + customData[ namePart ]
  1719. };
  1720. }
  1721. } );
  1722. setModelData( model, '<paragraph>Foo</paragraph><paragraph>Bar</paragraph>' );
  1723. model.change( writer => {
  1724. const range = writer.createRange(
  1725. writer.createPositionFromPath( root, [ 0, 1 ] ),
  1726. writer.createPositionFromPath( root, [ 2 ] )
  1727. );
  1728. writer.addMarker( 'group:foo', { range, usingOperation: false } );
  1729. writer.addMarker( 'group:abc', { range, usingOperation: false } );
  1730. } );
  1731. expectResult(
  1732. '<p>F<g-start name="abc_xyz"></g-start><g-start name="foo_bar"></g-start>oo</p>' +
  1733. '<p data-g-end-after="abc_xyz,foo_bar">Bar</p>'
  1734. );
  1735. model.change( writer => {
  1736. writer.removeMarker( 'group:foo' );
  1737. writer.removeMarker( 'group:abc' );
  1738. } );
  1739. expectResult( '<p>Foo</p><p>Bar</p>' );
  1740. } );
  1741. it( 'can be overwritten using converterPriority', () => {
  1742. downcastHelpers.markerToData( {
  1743. model: 'group'
  1744. } );
  1745. downcastHelpers.markerToData( {
  1746. model: 'group',
  1747. view: markerName => {
  1748. const name = markerName.split( ':' )[ 1 ];
  1749. return {
  1750. group: 'g',
  1751. name
  1752. };
  1753. },
  1754. converterPriority: 'high'
  1755. } );
  1756. setModelData( model, '<paragraph>F[ooba]r</paragraph>' );
  1757. model.change( writer => {
  1758. writer.addMarker( 'group:foo', { range: model.document.selection.getFirstRange(), usingOperation: false } );
  1759. } );
  1760. expectResult(
  1761. '<p>F<g-start name="foo"></g-start>ooba<g-end name="foo"></g-end>r</p>'
  1762. );
  1763. model.change( writer => {
  1764. writer.removeMarker( 'group:foo' );
  1765. } );
  1766. expectResult( '<p>Foobar</p>' );
  1767. } );
  1768. it( 'can be overwritten by custom callback', () => {
  1769. downcastHelpers.markerToData( {
  1770. model: 'group'
  1771. } );
  1772. downcastHelpers.add( dispatcher => {
  1773. dispatcher.on( 'addMarker:group', ( evt, data, conversionApi ) => {
  1774. conversionApi.consumable.consume( data.markerRange, evt.name );
  1775. }, { priority: 'high' } );
  1776. } );
  1777. setModelData( model, '<paragraph>Foo[]bar</paragraph>' );
  1778. model.change( writer => {
  1779. writer.addMarker( 'group:foo', { range: model.document.selection.getFirstRange(), usingOperation: false } );
  1780. } );
  1781. expectResult( '<p>Foobar</p>' );
  1782. model.change( writer => {
  1783. writer.removeMarker( 'group:foo' );
  1784. } );
  1785. expectResult( '<p>Foobar</p>' );
  1786. } );
  1787. it( 'should not perform conversion if the callback returned falsy value', () => {
  1788. downcastHelpers.markerToData( {
  1789. model: 'group',
  1790. view: () => false
  1791. } );
  1792. setModelData( model, '<paragraph>F[ooba]r</paragraph>' );
  1793. model.change( writer => {
  1794. writer.addMarker( 'group:foo', { range: model.document.selection.getFirstRange(), usingOperation: false } );
  1795. } );
  1796. expectResult( '<p>Foobar</p>' );
  1797. model.change( writer => {
  1798. writer.removeMarker( 'group:foo' );
  1799. } );
  1800. expectResult( '<p>Foobar</p>' );
  1801. } );
  1802. } );
  1803. describe( 'markerToHighlight()', () => {
  1804. it( 'should be chainable', () => {
  1805. expect( downcastHelpers.markerToHighlight( { model: 'comment', view: { classes: 'comment' } } ) ).to.equal( downcastHelpers );
  1806. } );
  1807. it( 'config.view is a highlight descriptor', () => {
  1808. downcastHelpers.markerToHighlight( { model: 'comment', view: { classes: 'comment' } } );
  1809. model.change( writer => {
  1810. writer.insertText( 'foo', modelRoot, 0 );
  1811. const range = writer.createRange( writer.createPositionAt( modelRoot, 0 ), writer.createPositionAt( modelRoot, 3 ) );
  1812. writer.addMarker( 'comment', { range, usingOperation: false } );
  1813. } );
  1814. expectResult( '<span class="comment">foo</span>' );
  1815. } );
  1816. it( 'can be overwritten using converterPriority', () => {
  1817. downcastHelpers.markerToHighlight( { model: 'comment', view: { classes: 'comment' } } );
  1818. downcastHelpers.markerToHighlight( { model: 'comment', view: { classes: 'new-comment' }, converterPriority: 'high' } );
  1819. model.change( writer => {
  1820. writer.insertText( 'foo', modelRoot, 0 );
  1821. const range = writer.createRange( writer.createPositionAt( modelRoot, 0 ), writer.createPositionAt( modelRoot, 3 ) );
  1822. writer.addMarker( 'comment', { range, usingOperation: false } );
  1823. } );
  1824. expectResult( '<span class="new-comment">foo</span>' );
  1825. } );
  1826. it( 'config.view is a function', () => {
  1827. downcastHelpers.markerToHighlight( {
  1828. model: 'comment',
  1829. view: ( data, conversionApi ) => {
  1830. const commentType = data.markerName.split( ':' )[ 1 ];
  1831. // To ensure conversion API is provided.
  1832. expect( conversionApi.writer ).to.instanceof( DowncastWriter );
  1833. return {
  1834. classes: [ 'comment', 'comment-' + commentType ]
  1835. };
  1836. }
  1837. } );
  1838. model.change( writer => {
  1839. writer.insertText( 'foo', modelRoot, 0 );
  1840. const range = writer.createRange( writer.createPositionAt( modelRoot, 0 ), writer.createPositionAt( modelRoot, 3 ) );
  1841. writer.addMarker( 'comment:abc', { range, usingOperation: false } );
  1842. } );
  1843. expectResult( '<span class="comment comment-abc">foo</span>' );
  1844. } );
  1845. describe( 'highlight', () => {
  1846. const highlightConfig = {
  1847. model: 'marker',
  1848. view: {
  1849. classes: 'highlight-class',
  1850. attributes: { title: 'title' }
  1851. },
  1852. converterPriority: 7
  1853. };
  1854. describe( 'on text', () => {
  1855. let markerRange;
  1856. beforeEach( () => {
  1857. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  1858. const modelElement1 = new ModelElement( 'paragraph', null, new ModelText( 'foo' ) );
  1859. const modelElement2 = new ModelElement( 'paragraph', null, new ModelText( 'bar' ) );
  1860. model.change( writer => {
  1861. writer.insert( [ modelElement1, modelElement2 ], modelRootStart );
  1862. } );
  1863. markerRange = model.createRangeIn( modelRoot );
  1864. } );
  1865. it( 'should wrap and unwrap text nodes', () => {
  1866. downcastHelpers.markerToHighlight( highlightConfig );
  1867. model.change( writer => {
  1868. writer.addMarker( 'marker', { range: markerRange, usingOperation: false } );
  1869. } );
  1870. expect( viewToString( viewRoot ) ).to.equal(
  1871. '<div>' +
  1872. '<p>' +
  1873. '<span class="highlight-class" title="title">foo</span>' +
  1874. '</p>' +
  1875. '<p>' +
  1876. '<span class="highlight-class" title="title">bar</span>' +
  1877. '</p>' +
  1878. '</div>'
  1879. );
  1880. model.change( writer => {
  1881. writer.removeMarker( 'marker' );
  1882. } );
  1883. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  1884. } );
  1885. it( 'should be possible to overwrite', () => {
  1886. downcastHelpers.markerToHighlight( highlightConfig );
  1887. downcastHelpers.markerToHighlight( {
  1888. model: 'marker',
  1889. view: { classes: 'override-class' },
  1890. converterPriority: 'high'
  1891. } );
  1892. model.change( writer => {
  1893. writer.addMarker( 'marker', { range: markerRange, usingOperation: false } );
  1894. } );
  1895. expect( viewToString( viewRoot ) ).to.equal(
  1896. '<div>' +
  1897. '<p>' +
  1898. '<span class="override-class">foo</span>' +
  1899. '</p>' +
  1900. '<p>' +
  1901. '<span class="override-class">bar</span>' +
  1902. '</p>' +
  1903. '</div>'
  1904. );
  1905. model.change( writer => {
  1906. writer.removeMarker( 'marker' );
  1907. } );
  1908. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  1909. } );
  1910. it( 'should do nothing if descriptor is not provided or generating function returns null', () => {
  1911. downcastHelpers.markerToHighlight( {
  1912. model: 'marker',
  1913. view: () => null,
  1914. converterPriority: 'high'
  1915. } );
  1916. model.change( writer => {
  1917. writer.addMarker( 'marker', { range: markerRange, usingOperation: false } );
  1918. } );
  1919. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  1920. model.change( writer => {
  1921. writer.removeMarker( 'marker' );
  1922. } );
  1923. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  1924. } );
  1925. it( 'should do nothing if collapsed marker is converted', () => {
  1926. downcastHelpers.markerToHighlight( {
  1927. model: 'marker',
  1928. view: { classes: 'foo' },
  1929. converterPriority: 'high'
  1930. } );
  1931. markerRange = model.createRange( model.createPositionAt( modelRoot, 0 ), model.createPositionAt( modelRoot, 0 ) );
  1932. model.change( writer => {
  1933. writer.addMarker( 'marker', { range: markerRange, usingOperation: false } );
  1934. } );
  1935. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  1936. model.change( () => {
  1937. model.markers._remove( 'marker' );
  1938. } );
  1939. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  1940. } );
  1941. it( 'should correctly wrap and unwrap multiple, intersecting markers', () => {
  1942. downcastHelpers.markerToHighlight( {
  1943. model: 'markerFoo',
  1944. view: { classes: 'foo' }
  1945. } );
  1946. downcastHelpers.markerToHighlight( {
  1947. model: 'markerBar',
  1948. view: { classes: 'bar' }
  1949. } );
  1950. downcastHelpers.markerToHighlight( {
  1951. model: 'markerXyz',
  1952. view: { classes: 'xyz' }
  1953. } );
  1954. const p1 = modelRoot.getChild( 0 );
  1955. const p2 = modelRoot.getChild( 1 );
  1956. model.change( writer => {
  1957. const range = writer.createRange( writer.createPositionAt( p1, 0 ), writer.createPositionAt( p1, 3 ) );
  1958. writer.addMarker( 'markerFoo', { range, usingOperation: false } );
  1959. } );
  1960. expect( viewToString( viewRoot ) ).to.equal(
  1961. '<div>' +
  1962. '<p>' +
  1963. '<span class="foo">foo</span>' +
  1964. '</p>' +
  1965. '<p>bar</p>' +
  1966. '</div>'
  1967. );
  1968. model.change( writer => {
  1969. const range = writer.createRange( writer.createPositionAt( p1, 1 ), writer.createPositionAt( p2, 2 ) );
  1970. writer.addMarker( 'markerBar', { range, usingOperation: false } );
  1971. } );
  1972. expect( viewToString( viewRoot ) ).to.equal(
  1973. '<div>' +
  1974. '<p>' +
  1975. '<span class="foo">f</span>' +
  1976. '<span class="bar">' +
  1977. '<span class="foo">oo</span>' +
  1978. '</span>' +
  1979. '</p>' +
  1980. '<p>' +
  1981. '<span class="bar">ba</span>' +
  1982. 'r' +
  1983. '</p>' +
  1984. '</div>'
  1985. );
  1986. model.change( writer => {
  1987. const range = writer.createRange( writer.createPositionAt( p1, 2 ), writer.createPositionAt( p2, 3 ) );
  1988. writer.addMarker( 'markerXyz', { range, usingOperation: false } );
  1989. } );
  1990. expect( viewToString( viewRoot ) ).to.equal(
  1991. '<div>' +
  1992. '<p>' +
  1993. '<span class="foo">f</span>' +
  1994. '<span class="bar">' +
  1995. '<span class="foo">' +
  1996. 'o' +
  1997. '<span class="xyz">o</span>' +
  1998. '</span>' +
  1999. '</span>' +
  2000. '</p>' +
  2001. '<p>' +
  2002. '<span class="bar">' +
  2003. '<span class="xyz">ba</span>' +
  2004. '</span>' +
  2005. '<span class="xyz">r</span>' +
  2006. '</p>' +
  2007. '</div>'
  2008. );
  2009. model.change( writer => {
  2010. writer.removeMarker( 'markerBar' );
  2011. } );
  2012. expect( viewToString( viewRoot ) ).to.equal(
  2013. '<div>' +
  2014. '<p>' +
  2015. '<span class="foo">' +
  2016. 'fo' +
  2017. '<span class="xyz">o</span>' +
  2018. '</span>' +
  2019. '</p>' +
  2020. '<p>' +
  2021. '<span class="xyz">bar</span>' +
  2022. '</p>' +
  2023. '</div>'
  2024. );
  2025. model.change( writer => {
  2026. writer.removeMarker( 'markerFoo' );
  2027. } );
  2028. expect( viewToString( viewRoot ) ).to.equal(
  2029. '<div>' +
  2030. '<p>' +
  2031. 'fo' +
  2032. '<span class="xyz">o</span>' +
  2033. '</p>' +
  2034. '<p>' +
  2035. '<span class="xyz">bar</span>' +
  2036. '</p>' +
  2037. '</div>'
  2038. );
  2039. model.change( writer => {
  2040. writer.removeMarker( 'markerXyz' );
  2041. } );
  2042. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  2043. } );
  2044. it( 'should do nothing if marker is applied and removed on empty-ish range', () => {
  2045. downcastHelpers.markerToHighlight( highlightConfig );
  2046. const p1 = modelRoot.getChild( 0 );
  2047. const p2 = modelRoot.getChild( 1 );
  2048. const markerRange = model.createRange( model.createPositionAt( p1, 3 ), model.createPositionAt( p2, 0 ) );
  2049. model.change( writer => {
  2050. writer.addMarker( 'marker', { range: markerRange, usingOperation: false } );
  2051. } );
  2052. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  2053. model.change( writer => {
  2054. writer.removeMarker( 'marker', { range: markerRange, usingOperation: false } );
  2055. } );
  2056. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p><p>bar</p></div>' );
  2057. } );
  2058. } );
  2059. describe( 'on element', () => {
  2060. const highlightConfig = {
  2061. model: 'marker',
  2062. view: {
  2063. classes: 'highlight-class',
  2064. attributes: { title: 'title' },
  2065. id: 'customId'
  2066. },
  2067. converterPriority: 7
  2068. };
  2069. let markerRange, viewDocument;
  2070. beforeEach( () => {
  2071. viewDocument = new ViewDocument( new StylesProcessor() );
  2072. downcastHelpers.elementToElement( {
  2073. model: 'div',
  2074. view: () => {
  2075. const viewContainer = new ViewContainerElement( viewDocument, 'div' );
  2076. viewContainer._setCustomProperty( 'addHighlight', ( element, descriptor, writer ) => {
  2077. writer.addClass( descriptor.classes, element );
  2078. } );
  2079. viewContainer._setCustomProperty( 'removeHighlight', ( element, id, writer ) => {
  2080. writer.setAttribute( 'class', '', element );
  2081. } );
  2082. return viewContainer;
  2083. }
  2084. } );
  2085. const modelElement = new ModelElement( 'div', null, new ModelText( 'foo' ) );
  2086. model.change( writer => {
  2087. writer.insert( modelElement, modelRootStart );
  2088. } );
  2089. markerRange = model.createRangeOn( modelElement );
  2090. downcastHelpers.markerToHighlight( highlightConfig );
  2091. } );
  2092. it( 'should use addHighlight and removeHighlight on elements and not convert children nodes', () => {
  2093. model.change( writer => {
  2094. writer.addMarker( 'marker', { range: markerRange, usingOperation: false } );
  2095. } );
  2096. expect( viewToString( viewRoot ) ).to.equal(
  2097. '<div>' +
  2098. '<div class="highlight-class">' +
  2099. 'foo' +
  2100. '</div>' +
  2101. '</div>'
  2102. );
  2103. model.change( writer => {
  2104. writer.removeMarker( 'marker' );
  2105. } );
  2106. expect( viewToString( viewRoot ) ).to.equal( '<div><div>foo</div></div>' );
  2107. } );
  2108. it( 'should be possible to override', () => {
  2109. downcastHelpers.markerToHighlight( {
  2110. model: 'marker',
  2111. view: { classes: 'override-class' },
  2112. converterPriority: 'high'
  2113. } );
  2114. model.change( writer => {
  2115. writer.addMarker( 'marker', { range: markerRange, usingOperation: false } );
  2116. } );
  2117. expect( viewToString( viewRoot ) ).to.equal(
  2118. '<div>' +
  2119. '<div class="override-class">' +
  2120. 'foo' +
  2121. '</div>' +
  2122. '</div>'
  2123. );
  2124. model.change( writer => {
  2125. writer.removeMarker( 'marker' );
  2126. } );
  2127. expect( viewToString( viewRoot ) ).to.equal( '<div><div>foo</div></div>' );
  2128. } );
  2129. it( 'should use default priority and id if not provided', () => {
  2130. const viewDiv = viewRoot.getChild( 0 );
  2131. downcastHelpers.markerToHighlight( {
  2132. model: 'marker2',
  2133. view: () => null,
  2134. converterPriority: 'high'
  2135. } );
  2136. viewDiv._setCustomProperty( 'addHighlight', ( element, descriptor ) => {
  2137. expect( descriptor.priority ).to.equal( ViewAttributeElement.DEFAULT_PRIORITY );
  2138. expect( descriptor.id ).to.equal( 'marker:foo-bar-baz' );
  2139. } );
  2140. viewDiv._setCustomProperty( 'removeHighlight', ( element, id ) => {
  2141. expect( id ).to.equal( 'marker:foo-bar-baz' );
  2142. } );
  2143. model.change( writer => {
  2144. writer.addMarker( 'marker2', { range: markerRange, usingOperation: false } );
  2145. } );
  2146. } );
  2147. it( 'should do nothing if descriptor is not provided', () => {
  2148. downcastHelpers.markerToHighlight( {
  2149. model: 'marker2',
  2150. view: () => null
  2151. } );
  2152. model.change( writer => {
  2153. writer.addMarker( 'marker2', { range: markerRange, usingOperation: false } );
  2154. } );
  2155. expect( viewToString( viewRoot ) ).to.equal( '<div><div>foo</div></div>' );
  2156. model.change( writer => {
  2157. writer.removeMarker( 'marker2' );
  2158. } );
  2159. expect( viewToString( viewRoot ) ).to.equal( '<div><div>foo</div></div>' );
  2160. } );
  2161. } );
  2162. } );
  2163. } );
  2164. function expectResult( string ) {
  2165. expect( stringifyView( viewRoot, null, { ignoreRoot: true } ) ).to.equal( string );
  2166. }
  2167. } );
  2168. describe( 'downcast converters', () => {
  2169. let dispatcher, modelDoc, modelRoot, viewRoot, controller, modelRootStart, model;
  2170. beforeEach( () => {
  2171. model = new Model();
  2172. modelDoc = model.document;
  2173. modelRoot = modelDoc.createRoot();
  2174. controller = new EditingController( model, new StylesProcessor() );
  2175. viewRoot = controller.view.document.getRoot();
  2176. // Set name of view root the same as dom root.
  2177. // This is a mock of attaching view root to dom root.
  2178. controller.view.document.getRoot()._name = 'div';
  2179. dispatcher = controller.downcastDispatcher;
  2180. const downcastHelpers = new DowncastHelpers( [ dispatcher ] );
  2181. downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
  2182. modelRootStart = model.createPositionAt( modelRoot, 0 );
  2183. } );
  2184. describe( 'insertText()', () => {
  2185. it( 'should downcast text', () => {
  2186. model.change( writer => {
  2187. writer.insert( new ModelText( 'foobar' ), modelRootStart );
  2188. } );
  2189. expect( viewToString( viewRoot ) ).to.equal( '<div>foobar</div>' );
  2190. } );
  2191. it( 'should support unicode', () => {
  2192. model.change( writer => {
  2193. writer.insert( new ModelText( 'நிலைக்கு' ), modelRootStart );
  2194. } );
  2195. expect( viewToString( viewRoot ) ).to.equal( '<div>நிலைக்கு</div>' );
  2196. } );
  2197. it( 'should be possible to override it', () => {
  2198. dispatcher.on( 'insert:$text', ( evt, data, conversionApi ) => {
  2199. conversionApi.consumable.consume( data.item, 'insert' );
  2200. }, { converterPriority: 'high' } );
  2201. model.change( writer => {
  2202. writer.insert( new ModelText( 'foobar' ), modelRootStart );
  2203. } );
  2204. expect( viewToString( viewRoot ) ).to.equal( '<div></div>' );
  2205. } );
  2206. } );
  2207. // Remove converter is by default already added in `EditingController` instance.
  2208. describe( 'remove()', () => {
  2209. let viewDocument;
  2210. beforeEach( () => {
  2211. viewDocument = new ViewDocument( new StylesProcessor() );
  2212. } );
  2213. it( 'should remove items from view accordingly to changes in model #1', () => {
  2214. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar' ) );
  2215. model.change( writer => {
  2216. writer.insert( modelElement, modelRootStart );
  2217. } );
  2218. model.change( writer => {
  2219. writer.remove(
  2220. writer.createRange( writer.createPositionAt( modelElement, 2 ), writer.createPositionAt( modelElement, 4 ) )
  2221. );
  2222. } );
  2223. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foar</p></div>' );
  2224. } );
  2225. it( 'should be possible to overwrite', () => {
  2226. dispatcher.on( 'remove', evt => evt.stop(), { priority: 'high' } );
  2227. const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar' ) );
  2228. model.change( writer => {
  2229. writer.insert( modelElement, modelRootStart );
  2230. } );
  2231. model.change( writer => {
  2232. writer.remove(
  2233. writer.createRange( writer.createPositionAt( modelElement, 2 ), writer.createPositionAt( modelElement, 4 ) )
  2234. );
  2235. } );
  2236. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
  2237. } );
  2238. it( 'should support unicode', () => {
  2239. const modelElement = new ModelElement( 'paragraph', null, 'நிலைக்கு' );
  2240. model.change( writer => {
  2241. writer.insert( modelElement, modelRootStart );
  2242. } );
  2243. model.change( writer => {
  2244. writer.remove(
  2245. writer.createRange( writer.createPositionAt( modelElement, 0 ), writer.createPositionAt( modelElement, 6 ) )
  2246. );
  2247. } );
  2248. expect( viewToString( viewRoot ) ).to.equal( '<div><p>கு</p></div>' );
  2249. } );
  2250. it( 'should not remove view ui elements that are placed next to removed content', () => {
  2251. modelRoot._appendChild( new ModelText( 'fozbar' ) );
  2252. viewRoot._appendChild( [
  2253. new ViewText( viewDocument, 'foz' ),
  2254. new ViewUIElement( viewDocument, 'span' ),
  2255. new ViewText( viewDocument, 'bar' )
  2256. ] );
  2257. // Remove 'b'.
  2258. model.change( writer => {
  2259. writer.remove(
  2260. writer.createRange( writer.createPositionAt( modelRoot, 3 ), writer.createPositionAt( modelRoot, 4 ) )
  2261. );
  2262. } );
  2263. expect( viewToString( viewRoot ) ).to.equal( '<div>foz<span></span>ar</div>' );
  2264. // Remove 'z'.
  2265. model.change( writer => {
  2266. writer.remove(
  2267. writer.createRange( writer.createPositionAt( modelRoot, 2 ), writer.createPositionAt( modelRoot, 3 ) )
  2268. );
  2269. } );
  2270. expect( viewToString( viewRoot ) ).to.equal( '<div>fo<span></span>ar</div>' );
  2271. } );
  2272. it( 'should remove correct amount of text when it is split by view ui element', () => {
  2273. modelRoot._appendChild( new ModelText( 'fozbar' ) );
  2274. viewRoot._appendChild( [
  2275. new ViewText( viewDocument, 'foz' ),
  2276. new ViewUIElement( viewDocument, 'span' ),
  2277. new ViewText( viewDocument, 'bar' )
  2278. ] );
  2279. // Remove 'z<span></span>b'.
  2280. model.change( writer => {
  2281. writer.remove(
  2282. writer.createRange( writer.createPositionAt( modelRoot, 2 ), writer.createPositionAt( modelRoot, 4 ) )
  2283. );
  2284. } );
  2285. expect( viewToString( viewRoot ) ).to.equal( '<div>foar</div>' );
  2286. } );
  2287. it( 'should unbind elements', () => {
  2288. const modelElement = new ModelElement( 'paragraph' );
  2289. model.change( writer => {
  2290. writer.insert( modelElement, modelRootStart );
  2291. } );
  2292. const viewElement = controller.mapper.toViewElement( modelElement );
  2293. expect( viewElement ).not.to.be.undefined;
  2294. expect( controller.mapper.toModelElement( viewElement ) ).to.equal( modelElement );
  2295. model.change( writer => {
  2296. writer.remove( modelElement );
  2297. } );
  2298. expect( controller.mapper.toViewElement( modelElement ) ).to.be.undefined;
  2299. expect( controller.mapper.toModelElement( viewElement ) ).to.be.undefined;
  2300. } );
  2301. it( 'should not break when remove() is used as part of unwrapping', () => {
  2302. const modelP = new ModelElement( 'paragraph', null, new ModelText( 'foo' ) );
  2303. const modelWidget = new ModelElement( 'widget', null, modelP );
  2304. const downcastHelpers = new DowncastHelpers( [ dispatcher ] );
  2305. downcastHelpers.elementToElement( { model: 'widget', view: 'widget' } );
  2306. model.change( writer => {
  2307. writer.insert( modelWidget, modelRootStart );
  2308. } );
  2309. expect( viewToString( viewRoot ) ).to.equal( '<div><widget><p>foo</p></widget></div>' );
  2310. const viewP = controller.mapper.toViewElement( modelP );
  2311. expect( viewP ).not.to.be.undefined;
  2312. model.change( writer => {
  2313. writer.unwrap( modelWidget );
  2314. } );
  2315. expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo</p></div>' );
  2316. // `modelP` is now bound with newly created view element.
  2317. expect( controller.mapper.toViewElement( modelP ) ).not.to.equal( viewP );
  2318. // `viewP` is no longer bound with model element.
  2319. expect( controller.mapper.toModelElement( viewP ) ).to.be.undefined;
  2320. // View element from view root is bound to `modelP`.
  2321. expect( controller.mapper.toModelElement( viewRoot.getChild( 0 ) ) ).to.equal( modelP );
  2322. } );
  2323. it( 'should work correctly if container element after ui element is removed', () => {
  2324. // Prepare a model and view structure.
  2325. // This is done outside of conversion to put view ui elements inside easily.
  2326. const modelP1 = new ModelElement( 'paragraph' );
  2327. const modelP2 = new ModelElement( 'paragraph' );
  2328. const viewP1 = new ViewContainerElement( viewDocument, 'p' );
  2329. const viewUi1 = new ViewUIElement( viewDocument, 'span' );
  2330. const viewUi2 = new ViewUIElement( viewDocument, 'span' );
  2331. const viewP2 = new ViewContainerElement( viewDocument, 'p' );
  2332. modelRoot._appendChild( [ modelP1, modelP2 ] );
  2333. viewRoot._appendChild( [ viewP1, viewUi1, viewUi2, viewP2 ] );
  2334. controller.mapper.bindElements( modelP1, viewP1 );
  2335. controller.mapper.bindElements( modelP2, viewP2 );
  2336. // Remove second paragraph element.
  2337. model.change( writer => {
  2338. writer.remove( writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 2 ) ) );
  2339. } );
  2340. expect( viewToString( viewRoot ) ).to.equal( '<div><p></p><span></span><span></span></div>' );
  2341. } );
  2342. it( 'should work correctly if container element after text node is removed', () => {
  2343. const modelText = new ModelText( 'foo' );
  2344. const modelP = new ModelElement( 'paragraph' );
  2345. model.change( writer => {
  2346. writer.insert( [ modelText, modelP ], modelRootStart );
  2347. } );
  2348. model.change( writer => {
  2349. writer.remove( modelP );
  2350. } );
  2351. expect( viewToString( viewRoot ) ).to.equal( '<div>foo</div>' );
  2352. } );
  2353. } );
  2354. describe( 'createViewElementFromHighlightDescriptor()', () => {
  2355. let viewWriter;
  2356. beforeEach( () => {
  2357. viewWriter = new DowncastWriter( controller.view.document );
  2358. } );
  2359. it( 'should return attribute element from descriptor object', () => {
  2360. const descriptor = {
  2361. classes: 'foo-class',
  2362. attributes: { one: '1', two: '2' },
  2363. priority: 7
  2364. };
  2365. const element = createViewElementFromHighlightDescriptor( viewWriter, descriptor );
  2366. expect( element.is( 'attributeElement' ) ).to.be.true;
  2367. expect( element.name ).to.equal( 'span' );
  2368. expect( element.priority ).to.equal( 7 );
  2369. expect( element.hasClass( 'foo-class' ) ).to.be.true;
  2370. for ( const key of Object.keys( descriptor.attributes ) ) {
  2371. expect( element.getAttribute( key ) ).to.equal( descriptor.attributes[ key ] );
  2372. }
  2373. } );
  2374. it( 'should return attribute element from descriptor object - array with classes', () => {
  2375. const descriptor = {
  2376. classes: [ 'foo-class', 'bar-class' ],
  2377. attributes: { one: '1', two: '2' },
  2378. priority: 7
  2379. };
  2380. const element = createViewElementFromHighlightDescriptor( viewWriter, descriptor );
  2381. expect( element.is( 'attributeElement' ) ).to.be.true;
  2382. expect( element.name ).to.equal( 'span' );
  2383. expect( element.priority ).to.equal( 7 );
  2384. expect( element.hasClass( 'foo-class' ) ).to.be.true;
  2385. expect( element.hasClass( 'bar-class' ) ).to.be.true;
  2386. for ( const key of Object.keys( descriptor.attributes ) ) {
  2387. expect( element.getAttribute( key ) ).to.equal( descriptor.attributes[ key ] );
  2388. }
  2389. } );
  2390. it( 'should create element without class', () => {
  2391. const descriptor = {
  2392. attributes: { one: '1', two: '2' },
  2393. priority: 7
  2394. };
  2395. const element = createViewElementFromHighlightDescriptor( viewWriter, descriptor );
  2396. expect( element.is( 'attributeElement' ) ).to.be.true;
  2397. expect( element.name ).to.equal( 'span' );
  2398. expect( element.priority ).to.equal( 7 );
  2399. for ( const key of Object.keys( descriptor.attributes ) ) {
  2400. expect( element.getAttribute( key ) ).to.equal( descriptor.attributes[ key ] );
  2401. }
  2402. } );
  2403. it( 'should create element without priority', () => {
  2404. const descriptor = {
  2405. classes: 'foo-class',
  2406. attributes: { one: '1', two: '2' }
  2407. };
  2408. const element = createViewElementFromHighlightDescriptor( viewWriter, descriptor );
  2409. expect( element.is( 'attributeElement' ) ).to.be.true;
  2410. expect( element.name ).to.equal( 'span' );
  2411. expect( element.priority ).to.equal( ViewAttributeElement.DEFAULT_PRIORITY );
  2412. expect( element.hasClass( 'foo-class' ) ).to.be.true;
  2413. for ( const key of Object.keys( descriptor.attributes ) ) {
  2414. expect( element.getAttribute( key ) ).to.equal( descriptor.attributes[ key ] );
  2415. }
  2416. } );
  2417. it( 'should create element without attributes', () => {
  2418. const descriptor = {
  2419. classes: 'foo-class',
  2420. priority: 7
  2421. };
  2422. const element = createViewElementFromHighlightDescriptor( viewWriter, descriptor );
  2423. expect( element.is( 'attributeElement' ) ).to.be.true;
  2424. expect( element.name ).to.equal( 'span' );
  2425. expect( element.priority ).to.equal( 7 );
  2426. expect( element.hasClass( 'foo-class' ) ).to.be.true;
  2427. } );
  2428. } );
  2429. } );
  2430. describe( 'downcast selection converters', () => {
  2431. let dispatcher, mapper, model, view, modelDoc, modelRoot, docSelection, viewDoc, viewRoot, viewSelection, downcastHelpers;
  2432. beforeEach( () => {
  2433. model = new Model();
  2434. modelDoc = model.document;
  2435. modelRoot = modelDoc.createRoot();
  2436. docSelection = modelDoc.selection;
  2437. model.schema.extend( '$text', { allowIn: '$root' } );
  2438. view = new View( new StylesProcessor() );
  2439. viewDoc = view.document;
  2440. viewRoot = createViewRoot( viewDoc );
  2441. viewSelection = viewDoc.selection;
  2442. mapper = new Mapper();
  2443. mapper.bindElements( modelRoot, viewRoot );
  2444. dispatcher = new DowncastDispatcher( { mapper, viewSelection } );
  2445. dispatcher.on( 'insert:$text', insertText() );
  2446. downcastHelpers = new DowncastHelpers( [ dispatcher ] );
  2447. downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } );
  2448. downcastHelpers.markerToHighlight( { model: 'marker', view: { classes: 'marker' }, converterPriority: 1 } );
  2449. // Default selection converters.
  2450. dispatcher.on( 'selection', clearAttributes(), { priority: 'low' } );
  2451. dispatcher.on( 'selection', convertRangeSelection(), { priority: 'low' } );
  2452. dispatcher.on( 'selection', convertCollapsedSelection(), { priority: 'low' } );
  2453. } );
  2454. afterEach( () => {
  2455. view.destroy();
  2456. } );
  2457. describe( 'default converters', () => {
  2458. describe( 'range selection', () => {
  2459. it( 'in same container', () => {
  2460. testSelection(
  2461. [ 1, 4 ],
  2462. 'foobar',
  2463. 'f{oob}ar'
  2464. );
  2465. } );
  2466. it( 'in same container with unicode characters', () => {
  2467. testSelection(
  2468. [ 2, 6 ],
  2469. 'நிலைக்கு',
  2470. 'நி{லைக்}கு'
  2471. );
  2472. } );
  2473. it( 'in same container, over attribute', () => {
  2474. testSelection(
  2475. [ 1, 5 ],
  2476. 'fo<$text bold="true">ob</$text>ar',
  2477. 'f{o<strong>ob</strong>a}r'
  2478. );
  2479. } );
  2480. it( 'in same container, next to attribute', () => {
  2481. testSelection(
  2482. [ 1, 2 ],
  2483. 'fo<$text bold="true">ob</$text>ar',
  2484. 'f{o}<strong>ob</strong>ar'
  2485. );
  2486. } );
  2487. it( 'in same attribute', () => {
  2488. testSelection(
  2489. [ 2, 4 ],
  2490. 'f<$text bold="true">ooba</$text>r',
  2491. 'f<strong>o{ob}a</strong>r'
  2492. );
  2493. } );
  2494. it( 'in same attribute, selection same as attribute', () => {
  2495. testSelection(
  2496. [ 2, 4 ],
  2497. 'fo<$text bold="true">ob</$text>ar',
  2498. 'fo{<strong>ob</strong>}ar'
  2499. );
  2500. } );
  2501. it( 'starts in text node, ends in attribute #1', () => {
  2502. testSelection(
  2503. [ 1, 3 ],
  2504. 'fo<$text bold="true">ob</$text>ar',
  2505. 'f{o<strong>o}b</strong>ar'
  2506. );
  2507. } );
  2508. it( 'starts in text node, ends in attribute #2', () => {
  2509. testSelection(
  2510. [ 1, 4 ],
  2511. 'fo<$text bold="true">ob</$text>ar',
  2512. 'f{o<strong>ob</strong>}ar'
  2513. );
  2514. } );
  2515. it( 'starts in attribute, ends in text node', () => {
  2516. testSelection(
  2517. [ 3, 5 ],
  2518. 'fo<$text bold="true">ob</$text>ar',
  2519. 'fo<strong>o{b</strong>a}r'
  2520. );
  2521. } );
  2522. it( 'consumes consumable values properly', () => {
  2523. // Add callback that will fire before default ones.
  2524. // This should prevent default callback doing anything.
  2525. dispatcher.on( 'selection', ( evt, data, conversionApi ) => {
  2526. expect( conversionApi.consumable.consume( data.selection, 'selection' ) ).to.be.true;
  2527. }, { priority: 'high' } );
  2528. // Similar test case as the first in this suite.
  2529. testSelection(
  2530. [ 1, 4 ],
  2531. 'foobar',
  2532. 'foobar' // No selection in view.
  2533. );
  2534. } );
  2535. it( 'should convert backward selection', () => {
  2536. testSelection(
  2537. [ 1, 3, 'backward' ],
  2538. 'foobar',
  2539. 'f{oo}bar'
  2540. );
  2541. expect( viewSelection.focus.offset ).to.equal( 1 );
  2542. } );
  2543. } );
  2544. describe( 'collapsed selection', () => {
  2545. let marker, viewDocument;
  2546. beforeEach( () => {
  2547. viewDocument = new ViewDocument( new StylesProcessor() );
  2548. } );
  2549. it( 'in container', () => {
  2550. testSelection(
  2551. [ 1, 1 ],
  2552. 'foobar',
  2553. 'f{}oobar'
  2554. );
  2555. } );
  2556. it( 'in attribute', () => {
  2557. testSelection(
  2558. [ 3, 3 ],
  2559. 'f<$text bold="true">ooba</$text>r',
  2560. 'f<strong>oo{}ba</strong>r'
  2561. );
  2562. } );
  2563. it( 'in attribute and marker', () => {
  2564. setModelData( model, 'fo<$text bold="true">ob</$text>ar' );
  2565. model.change( writer => {
  2566. const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) );
  2567. marker = writer.addMarker( 'marker', { range, usingOperation: false } );
  2568. writer.setSelection( modelRoot, 3 );
  2569. } );
  2570. // Remove view children manually (without firing additional conversion).
  2571. viewRoot._removeChildren( 0, viewRoot.childCount );
  2572. // Convert model to view.
  2573. view.change( writer => {
  2574. dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer );
  2575. dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer );
  2576. dispatcher.convertSelection( docSelection, model.markers, writer );
  2577. } );
  2578. // Stringify view and check if it is same as expected.
  2579. expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ).to.equal(
  2580. '<div>f<span class="marker">o<strong>o{}b</strong>a</span>r</div>'
  2581. );
  2582. } );
  2583. it( 'in attribute and marker - no attribute', () => {
  2584. setModelData( model, 'fo<$text bold="true">ob</$text>ar' );
  2585. model.change( writer => {
  2586. const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) );
  2587. marker = writer.addMarker( 'marker', { range, usingOperation: false } );
  2588. writer.setSelection( modelRoot, 3 );
  2589. writer.removeSelectionAttribute( 'bold' );
  2590. } );
  2591. // Remove view children manually (without firing additional conversion).
  2592. viewRoot._removeChildren( 0, viewRoot.childCount );
  2593. // Convert model to view.
  2594. view.change( writer => {
  2595. dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer );
  2596. dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer );
  2597. dispatcher.convertSelection( docSelection, model.markers, writer );
  2598. } );
  2599. // Stringify view and check if it is same as expected.
  2600. expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
  2601. .to.equal( '<div>f<span class="marker">o<strong>o</strong>[]<strong>b</strong>a</span>r</div>' );
  2602. } );
  2603. it( 'in marker - using highlight descriptor creator', () => {
  2604. downcastHelpers.markerToHighlight( {
  2605. model: 'marker2',
  2606. view: data => ( { classes: data.markerName } )
  2607. } );
  2608. setModelData( model, 'foobar' );
  2609. model.change( writer => {
  2610. const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) );
  2611. marker = writer.addMarker( 'marker2', { range, usingOperation: false } );
  2612. writer.setSelection( modelRoot, 3 );
  2613. } );
  2614. // Remove view children manually (without firing additional conversion).
  2615. viewRoot._removeChildren( 0, viewRoot.childCount );
  2616. // Convert model to view.
  2617. view.change( writer => {
  2618. dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer );
  2619. dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer );
  2620. dispatcher.convertSelection( docSelection, model.markers, writer );
  2621. } );
  2622. // Stringify view and check if it is same as expected.
  2623. expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
  2624. .to.equal( '<div>f<span class="marker2">oo{}ba</span>r</div>' );
  2625. } );
  2626. it( 'should do nothing if creator return null', () => {
  2627. downcastHelpers.markerToHighlight( {
  2628. model: 'marker3',
  2629. view: () => null
  2630. } );
  2631. setModelData( model, 'foobar' );
  2632. model.change( writer => {
  2633. const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) );
  2634. marker = writer.addMarker( 'marker3', { range, usingOperation: false } );
  2635. writer.setSelection( modelRoot, 3 );
  2636. } );
  2637. // Remove view children manually (without firing additional conversion).
  2638. viewRoot._removeChildren( 0, viewRoot.childCount );
  2639. // Convert model to view.
  2640. view.change( writer => {
  2641. dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer );
  2642. dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer );
  2643. dispatcher.convertSelection( docSelection, model.markers, writer );
  2644. } );
  2645. // Stringify view and check if it is same as expected.
  2646. expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
  2647. .to.equal( '<div>foo{}bar</div>' );
  2648. } );
  2649. // #1072 - if the container has only ui elements, collapsed selection attribute should be rendered after those ui elements.
  2650. it( 'selection with attribute before ui element - no non-ui children', () => {
  2651. setModelData( model, '' );
  2652. // Add two ui elements to view.
  2653. viewRoot._appendChild( [
  2654. new ViewUIElement( viewDocument, 'span' ),
  2655. new ViewUIElement( viewDocument, 'span' )
  2656. ] );
  2657. model.change( writer => {
  2658. writer.setSelection( writer.createRange( writer.createPositionFromPath( modelRoot, [ 0 ] ) ) );
  2659. writer.setSelectionAttribute( 'bold', true );
  2660. } );
  2661. // Convert model to view.
  2662. view.change( writer => {
  2663. dispatcher.convertSelection( docSelection, model.markers, writer );
  2664. } );
  2665. // Stringify view and check if it is same as expected.
  2666. expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
  2667. .to.equal( '<div><span></span><span></span><strong>[]</strong></div>' );
  2668. } );
  2669. // #1072.
  2670. it( 'selection with attribute before ui element - has non-ui children #1', () => {
  2671. setModelData( model, 'x' );
  2672. model.change( writer => {
  2673. writer.setSelection( writer.createRange( writer.createPositionFromPath( modelRoot, [ 1 ] ) ) );
  2674. writer.setSelectionAttribute( 'bold', true );
  2675. } );
  2676. // Convert model to view.
  2677. view.change( writer => {
  2678. dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer );
  2679. // Add ui element to view.
  2680. const uiElement = new ViewUIElement( viewDocument, 'span' );
  2681. viewRoot._insertChild( 1, uiElement );
  2682. dispatcher.convertSelection( docSelection, model.markers, writer );
  2683. } );
  2684. // Stringify view and check if it is same as expected.
  2685. expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
  2686. .to.equal( '<div>x<strong>[]</strong><span></span></div>' );
  2687. } );
  2688. // #1072.
  2689. it( 'selection with attribute before ui element - has non-ui children #2', () => {
  2690. setModelData( model, '<$text bold="true">x</$text>y' );
  2691. model.change( writer => {
  2692. writer.setSelection( writer.createRange( writer.createPositionFromPath( modelRoot, [ 1 ] ) ) );
  2693. writer.setSelectionAttribute( 'bold', true );
  2694. } );
  2695. // Convert model to view.
  2696. view.change( writer => {
  2697. dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer );
  2698. // Add ui element to view.
  2699. const uiElement = new ViewUIElement( viewDocument, 'span' );
  2700. viewRoot._insertChild( 1, uiElement, writer );
  2701. dispatcher.convertSelection( docSelection, model.markers, writer );
  2702. } );
  2703. // Stringify view and check if it is same as expected.
  2704. expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
  2705. .to.equal( '<div><strong>x{}</strong><span></span>y</div>' );
  2706. } );
  2707. it( 'consumes consumable values properly', () => {
  2708. // Add callbacks that will fire before default ones.
  2709. // This should prevent default callbacks doing anything.
  2710. dispatcher.on( 'selection', ( evt, data, conversionApi ) => {
  2711. expect( conversionApi.consumable.consume( data.selection, 'selection' ) ).to.be.true;
  2712. }, { priority: 'high' } );
  2713. dispatcher.on( 'attribute:bold', ( evt, data, conversionApi ) => {
  2714. expect( conversionApi.consumable.consume( data.item, 'attribute:bold' ) ).to.be.true;
  2715. }, { priority: 'high' } );
  2716. // Similar test case as above.
  2717. testSelection(
  2718. [ 3, 3 ],
  2719. 'f<$text bold="true">ooba</$text>r',
  2720. 'foobar' // No selection in view and no attribute.
  2721. );
  2722. } );
  2723. } );
  2724. } );
  2725. describe( 'clean-up', () => {
  2726. describe( 'convertRangeSelection', () => {
  2727. it( 'should remove all ranges before adding new range', () => {
  2728. testSelection(
  2729. [ 0, 2 ],
  2730. 'foobar',
  2731. '{fo}obar'
  2732. );
  2733. testSelection(
  2734. [ 3, 5 ],
  2735. 'foobar',
  2736. 'foo{ba}r'
  2737. );
  2738. expect( viewSelection.rangeCount ).to.equal( 1 );
  2739. } );
  2740. } );
  2741. describe( 'convertCollapsedSelection', () => {
  2742. it( 'should remove all ranges before adding new range', () => {
  2743. testSelection(
  2744. [ 2, 2 ],
  2745. 'foobar',
  2746. 'fo{}obar'
  2747. );
  2748. testSelection(
  2749. [ 3, 3 ],
  2750. 'foobar',
  2751. 'foo{}bar'
  2752. );
  2753. expect( viewSelection.rangeCount ).to.equal( 1 );
  2754. } );
  2755. } );
  2756. describe( 'clearAttributes', () => {
  2757. it( 'should remove all ranges before adding new range', () => {
  2758. testSelection(
  2759. [ 3, 3 ],
  2760. 'foobar',
  2761. 'foo<strong>[]</strong>bar',
  2762. { bold: 'true' }
  2763. );
  2764. view.change( writer => {
  2765. const modelRange = model.createRange( model.createPositionAt( modelRoot, 1 ), model.createPositionAt( modelRoot, 1 ) );
  2766. model.change( writer => {
  2767. writer.setSelection( modelRange );
  2768. } );
  2769. dispatcher.convertSelection( modelDoc.selection, model.markers, writer );
  2770. } );
  2771. expect( viewSelection.rangeCount ).to.equal( 1 );
  2772. const viewString = stringifyView( viewRoot, viewSelection, { showType: false } );
  2773. expect( viewString ).to.equal( '<div>f{}oobar</div>' );
  2774. } );
  2775. it( 'should do nothing if the attribute element had been already removed', () => {
  2776. testSelection(
  2777. [ 3, 3 ],
  2778. 'foobar',
  2779. 'foo<strong>[]</strong>bar',
  2780. { bold: 'true' }
  2781. );
  2782. view.change( writer => {
  2783. // Remove <strong></strong> manually.
  2784. writer.mergeAttributes( viewSelection.getFirstPosition() );
  2785. const modelRange = model.createRange( model.createPositionAt( modelRoot, 1 ), model.createPositionAt( modelRoot, 1 ) );
  2786. model.change( writer => {
  2787. writer.setSelection( modelRange );
  2788. } );
  2789. dispatcher.convertSelection( modelDoc.selection, model.markers, writer );
  2790. } );
  2791. expect( viewSelection.rangeCount ).to.equal( 1 );
  2792. const viewString = stringifyView( viewRoot, viewSelection, { showType: false } );
  2793. expect( viewString ).to.equal( '<div>f{}oobar</div>' );
  2794. } );
  2795. it( 'should clear fake selection', () => {
  2796. const modelRange = model.createRange( model.createPositionAt( modelRoot, 1 ), model.createPositionAt( modelRoot, 1 ) );
  2797. view.change( writer => {
  2798. writer.setSelection( modelRange, { fake: true } );
  2799. dispatcher.convertSelection( docSelection, model.markers, writer );
  2800. } );
  2801. expect( viewSelection.isFake ).to.be.false;
  2802. } );
  2803. } );
  2804. } );
  2805. describe( 'table cell selection converter', () => {
  2806. beforeEach( () => {
  2807. model.schema.register( 'table', { isLimit: true } );
  2808. model.schema.register( 'tr', { isLimit: true } );
  2809. model.schema.register( 'td', { isLimit: true } );
  2810. model.schema.extend( 'table', { allowIn: '$root' } );
  2811. model.schema.extend( 'tr', { allowIn: 'table' } );
  2812. model.schema.extend( 'td', { allowIn: 'tr' } );
  2813. model.schema.extend( '$text', { allowIn: 'td' } );
  2814. const downcastHelpers = new DowncastHelpers( [ dispatcher ] );
  2815. // "Universal" converter to convert table structure.
  2816. downcastHelpers.elementToElement( { model: 'table', view: 'table' } );
  2817. downcastHelpers.elementToElement( { model: 'tr', view: 'tr' } );
  2818. downcastHelpers.elementToElement( { model: 'td', view: 'td' } );
  2819. // Special converter for table cells.
  2820. dispatcher.on( 'selection', ( evt, data, conversionApi ) => {
  2821. const selection = data.selection;
  2822. if ( !conversionApi.consumable.test( selection, 'selection' ) || selection.isCollapsed ) {
  2823. return;
  2824. }
  2825. for ( const range of selection.getRanges() ) {
  2826. const node = range.start.parent;
  2827. if ( !!node && node.is( 'element', 'td' ) ) {
  2828. conversionApi.consumable.consume( selection, 'selection' );
  2829. const viewNode = conversionApi.mapper.toViewElement( node );
  2830. conversionApi.writer.addClass( 'selected', viewNode );
  2831. }
  2832. }
  2833. }, { priority: 'high' } );
  2834. } );
  2835. it( 'should not be used to convert selection that is not on table cell', () => {
  2836. testSelection(
  2837. [ 1, 5 ],
  2838. 'f{o<$text bold="true">ob</$text>a}r',
  2839. 'f{o<strong>ob</strong>a}r'
  2840. );
  2841. } );
  2842. it( 'should add a class to the selected table cell', () => {
  2843. testSelection(
  2844. // table tr#0 td#0 [foo, table tr#0 td#0 bar]
  2845. [ [ 0, 0, 0, 0 ], [ 0, 0, 0, 3 ] ],
  2846. '<table><tr><td>foo</td></tr><tr><td>bar</td></tr></table>',
  2847. '<table><tr><td class="selected">foo</td></tr><tr><td>bar</td></tr></table>'
  2848. );
  2849. } );
  2850. it( 'should not be used if selection contains more than just a table cell', () => {
  2851. testSelection(
  2852. // table tr td#1 f{oo bar, table tr#2 bar]
  2853. [ [ 0, 0, 0, 1 ], [ 0, 0, 1, 3 ] ],
  2854. '<table><tr><td>foo</td><td>bar</td></tr></table>',
  2855. '[<table><tr><td>foo</td><td>bar</td></tr></table>]'
  2856. );
  2857. } );
  2858. } );
  2859. // Tests if the selection got correctly converted.
  2860. // Because `setData` might use selection converters itself to set the selection, we can't use it
  2861. // to set the selection (because then we would test converters using converters).
  2862. // Instead, the `test` function expects to be passed `selectionPaths` which is an array containing two numbers or two arrays,
  2863. // that are offsets or paths of selection positions in root element.
  2864. function testSelection( selectionPaths, modelInput, expectedView, selectionAttributes = {} ) {
  2865. // Parse passed `modelInput` string and set it as current model.
  2866. setModelData( model, modelInput );
  2867. // Manually set selection ranges using passed `selectionPaths`.
  2868. const startPath = typeof selectionPaths[ 0 ] == 'number' ? [ selectionPaths[ 0 ] ] : selectionPaths[ 0 ];
  2869. const endPath = typeof selectionPaths[ 1 ] == 'number' ? [ selectionPaths[ 1 ] ] : selectionPaths[ 1 ];
  2870. const startPos = model.createPositionFromPath( modelRoot, startPath );
  2871. const endPos = model.createPositionFromPath( modelRoot, endPath );
  2872. const isBackward = selectionPaths[ 2 ] === 'backward';
  2873. model.change( writer => {
  2874. writer.setSelection( writer.createRange( startPos, endPos ), { backward: isBackward } );
  2875. // And add or remove passed attributes.
  2876. for ( const key in selectionAttributes ) {
  2877. const value = selectionAttributes[ key ];
  2878. if ( value ) {
  2879. writer.setSelectionAttribute( key, value );
  2880. } else {
  2881. writer.removeSelectionAttribute( key );
  2882. }
  2883. }
  2884. } );
  2885. // Remove view children manually (without firing additional conversion).
  2886. viewRoot._removeChildren( 0, viewRoot.childCount );
  2887. // Convert model to view.
  2888. view.change( writer => {
  2889. dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer );
  2890. dispatcher.convertSelection( docSelection, model.markers, writer );
  2891. } );
  2892. // Stringify view and check if it is same as expected.
  2893. expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ).to.equal( '<div>' + expectedView + '</div>' );
  2894. }
  2895. } );
  2896. function viewToString( item ) {
  2897. let result = '';
  2898. if ( item instanceof ViewText ) {
  2899. result = item.data;
  2900. } else {
  2901. // ViewElement or ViewDocumentFragment.
  2902. for ( const child of item.getChildren() ) {
  2903. result += viewToString( child );
  2904. }
  2905. if ( item instanceof ViewElement ) {
  2906. result = '<' + item.name + viewAttributesToString( item ) + '>' + result + '</' + item.name + '>';
  2907. }
  2908. }
  2909. return result;
  2910. }
  2911. function viewAttributesToString( item ) {
  2912. let result = '';
  2913. for ( const key of item.getAttributeKeys() ) {
  2914. const value = item.getAttribute( key );
  2915. if ( value ) {
  2916. result += ' ' + key + '="' + value + '"';
  2917. }
  2918. }
  2919. return result;
  2920. }