8
0

downcasthelpers.js 112 KB

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