downcast-helpers.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import {
  6. downcastElementToElement, downcastAttributeToElement, downcastAttributeToAttribute, downcastMarkerToElement, downcastMarkerToHighlight
  7. } from '../../src/conversion/downcast-helpers';
  8. import Conversion from '../../src/conversion/conversion';
  9. import EditingController from '../../src/controller/editingcontroller';
  10. import Model from '../../src/model/model';
  11. import ModelRange from '../../src/model/range';
  12. import ViewContainerElement from '../../src/view/containerelement';
  13. import ViewAttributeElement from '../../src/view/attributeelement';
  14. import ViewUIElement from '../../src/view/uielement';
  15. import { stringify } from '../../src/dev-utils/view';
  16. describe( 'downcast-helpers', () => {
  17. let conversion, model, modelRoot, viewRoot;
  18. beforeEach( () => {
  19. model = new Model();
  20. const modelDoc = model.document;
  21. modelRoot = modelDoc.createRoot();
  22. const controller = new EditingController( model );
  23. // Set name of view root the same as dom root.
  24. // This is a mock of attaching view root to dom root.
  25. controller.view.getRoot()._name = 'div';
  26. viewRoot = controller.view.getRoot();
  27. conversion = new Conversion();
  28. conversion.register( 'downcast', [ controller.downcastDispatcher ] );
  29. } );
  30. describe( 'downcastElementToElement', () => {
  31. it( 'config.view is a string', () => {
  32. const helper = downcastElementToElement( { model: 'paragraph', view: 'p' } );
  33. conversion.for( 'downcast' ).add( helper );
  34. model.change( writer => {
  35. writer.insertElement( 'paragraph', modelRoot, 0 );
  36. } );
  37. expectResult( '<p></p>' );
  38. } );
  39. it( 'can be overwritten using priority', () => {
  40. const helperA = downcastElementToElement( { model: 'paragraph', view: 'p' } );
  41. const helperB = downcastElementToElement( { model: 'paragraph', view: 'foo' }, 'high' );
  42. conversion.for( 'downcast' ).add( helperA ).add( helperB );
  43. model.change( writer => {
  44. writer.insertElement( 'paragraph', modelRoot, 0 );
  45. } );
  46. expectResult( '<foo></foo>' );
  47. } );
  48. it( 'config.view is an element instance', () => {
  49. const helper = downcastElementToElement( {
  50. model: 'paragraph',
  51. view: new ViewContainerElement( 'p' )
  52. } );
  53. conversion.for( 'downcast' ).add( helper );
  54. model.change( writer => {
  55. writer.insertElement( 'paragraph', modelRoot, 0 );
  56. } );
  57. expectResult( '<p></p>' );
  58. } );
  59. it( 'config.view is a view element definition', () => {
  60. const helper = downcastElementToElement( {
  61. model: 'fancyParagraph',
  62. view: {
  63. name: 'p',
  64. class: 'fancy'
  65. }
  66. } );
  67. conversion.for( 'downcast' ).add( helper );
  68. model.change( writer => {
  69. writer.insertElement( 'fancyParagraph', modelRoot, 0 );
  70. } );
  71. expectResult( '<p class="fancy"></p>' );
  72. } );
  73. it( 'config.view is a function', () => {
  74. const helper = downcastElementToElement( {
  75. model: 'heading',
  76. view: modelElement => new ViewContainerElement( 'h' + modelElement.getAttribute( 'level' ) )
  77. } );
  78. conversion.for( 'downcast' ).add( helper );
  79. model.change( writer => {
  80. writer.insertElement( 'heading', { level: 2 }, modelRoot, 0 );
  81. } );
  82. expectResult( '<h2></h2>' );
  83. } );
  84. } );
  85. describe( 'downcastAttributeToElement', () => {
  86. it( 'config.view is a string', () => {
  87. const helper = downcastAttributeToElement( 'bold', { view: 'strong' } );
  88. conversion.for( 'downcast' ).add( helper );
  89. model.change( writer => {
  90. writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
  91. } );
  92. expectResult( '<strong>foo</strong>' );
  93. } );
  94. it( 'can be overwritten using priority', () => {
  95. const helperA = downcastAttributeToElement( 'bold', { view: 'strong' } );
  96. const helperB = downcastAttributeToElement( 'bold', { view: 'b' }, 'high' );
  97. conversion.for( 'downcast' ).add( helperA ).add( helperB );
  98. model.change( writer => {
  99. writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
  100. } );
  101. expectResult( '<b>foo</b>' );
  102. } );
  103. it( 'config.view is an element instance', () => {
  104. const helper = downcastAttributeToElement( 'bold', {
  105. view: new ViewAttributeElement( 'strong' )
  106. } );
  107. conversion.for( 'downcast' ).add( helper );
  108. model.change( writer => {
  109. writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
  110. } );
  111. expectResult( '<strong>foo</strong>' );
  112. } );
  113. it( 'config.view is a view element definition', () => {
  114. const helper = downcastAttributeToElement( 'bold', {
  115. view: {
  116. name: 'span',
  117. class: 'bold'
  118. }
  119. } );
  120. conversion.for( 'downcast' ).add( helper );
  121. model.change( writer => {
  122. writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
  123. } );
  124. expectResult( '<span class="bold">foo</span>' );
  125. } );
  126. it( 'config.view is a view element definition, model attribute value specified', () => {
  127. const helper = downcastAttributeToElement( 'styled', {
  128. model: 'dark',
  129. view: {
  130. name: 'span',
  131. class: [ 'styled', 'styled-dark' ]
  132. }
  133. } );
  134. conversion.for( 'downcast' ).add( helper );
  135. model.change( writer => {
  136. writer.insertText( 'foo', { styled: 'dark' }, modelRoot, 0 );
  137. } );
  138. expectResult( '<span class="styled styled-dark">foo</span>' );
  139. model.change( writer => {
  140. writer.setAttribute( 'styled', 'xyz', modelRoot.getChild( 0 ) );
  141. } );
  142. expectResult( 'foo' );
  143. } );
  144. it( 'multiple config items', () => {
  145. const helper = downcastAttributeToElement( 'fontSize', [
  146. {
  147. model: 'big',
  148. view: {
  149. name: 'span',
  150. style: {
  151. 'font-size': '1.2em'
  152. }
  153. }
  154. },
  155. {
  156. model: 'small',
  157. view: {
  158. name: 'span',
  159. style: {
  160. 'font-size': '0.8em'
  161. }
  162. }
  163. }
  164. ] );
  165. conversion.for( 'downcast' ).add( helper );
  166. model.change( writer => {
  167. writer.insertText( 'foo', { fontSize: 'big' }, modelRoot, 0 );
  168. } );
  169. expectResult( '<span style="font-size:1.2em">foo</span>' );
  170. model.change( writer => {
  171. writer.setAttribute( 'fontSize', 'small', modelRoot.getChild( 0 ) );
  172. } );
  173. expectResult( '<span style="font-size:0.8em">foo</span>' );
  174. model.change( writer => {
  175. writer.removeAttribute( 'fontSize', modelRoot.getChild( 0 ) );
  176. } );
  177. expectResult( 'foo' );
  178. } );
  179. it( 'config.view is a function', () => {
  180. const helper = downcastAttributeToElement( 'bold', {
  181. view: attributeValue => new ViewAttributeElement( 'span', { style: 'font-weight:' + attributeValue } )
  182. } );
  183. conversion.for( 'downcast' ).add( helper );
  184. model.change( writer => {
  185. writer.insertText( 'foo', { bold: '500' }, modelRoot, 0 );
  186. } );
  187. expectResult( '<span style="font-weight:500">foo</span>' );
  188. } );
  189. } );
  190. describe( 'downcastAttributeToAttribute', () => {
  191. beforeEach( () => {
  192. conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'image', view: 'img' } ) );
  193. } );
  194. it( 'config not set', () => {
  195. const helper = downcastAttributeToAttribute( 'src' );
  196. conversion.for( 'downcast' ).add( helper );
  197. model.change( writer => {
  198. writer.insertElement( 'image', { src: 'foo.jpg' }, modelRoot, 0 );
  199. } );
  200. expectResult( '<img src="foo.jpg"></img>' );
  201. } );
  202. it( 'config.view is a string', () => {
  203. const helper = downcastAttributeToAttribute( 'source', { view: 'src' } );
  204. conversion.for( 'downcast' ).add( helper );
  205. model.change( writer => {
  206. writer.insertElement( 'image', { source: 'foo.jpg' }, modelRoot, 0 );
  207. } );
  208. expectResult( '<img src="foo.jpg"></img>' );
  209. } );
  210. it( 'can be overwritten using priority', () => {
  211. const helperA = downcastAttributeToAttribute( 'source', { view: 'href' } );
  212. const helperB = downcastAttributeToAttribute( 'source', { view: 'src' }, 'high' );
  213. conversion.for( 'downcast' ).add( helperA ).add( helperB );
  214. model.change( writer => {
  215. writer.insertElement( 'image', { source: 'foo.jpg' }, modelRoot, 0 );
  216. } );
  217. expectResult( '<img src="foo.jpg"></img>' );
  218. } );
  219. it( 'config.view is an object', () => {
  220. const helper = downcastAttributeToAttribute( 'stylish', { view: { key: 'class', value: 'styled' } } );
  221. conversion.for( 'downcast' ).add( helper );
  222. model.change( writer => {
  223. writer.insertElement( 'image', { stylish: true }, modelRoot, 0 );
  224. } );
  225. expectResult( '<img class="styled"></img>' );
  226. } );
  227. it( 'config.view is an object, model attribute value specified', () => {
  228. const helper = downcastAttributeToAttribute( 'styled', {
  229. model: 'dark',
  230. view: {
  231. key: 'class',
  232. value: 'styled-dark styled'
  233. }
  234. } );
  235. conversion.for( 'downcast' ).add( helper );
  236. model.change( writer => {
  237. writer.insertElement( 'image', { styled: 'dark' }, modelRoot, 0 );
  238. } );
  239. expectResult( '<img class="styled styled-dark"></img>' );
  240. model.change( writer => {
  241. writer.setAttribute( 'styled', 'xyz', modelRoot.getChild( 0 ) );
  242. } );
  243. expectResult( '<img></img>' );
  244. } );
  245. it( 'multiple config items', () => {
  246. const helper = downcastAttributeToAttribute( 'styled', [
  247. {
  248. model: 'dark',
  249. view: {
  250. key: 'class',
  251. value: 'styled-dark'
  252. }
  253. },
  254. {
  255. model: 'light',
  256. view: {
  257. key: 'class',
  258. value: 'styled-light'
  259. }
  260. }
  261. ] );
  262. conversion.for( 'downcast' ).add( helper );
  263. model.change( writer => {
  264. writer.insertElement( 'image', { styled: 'dark' }, modelRoot, 0 );
  265. } );
  266. expectResult( '<img class="styled-dark"></img>' );
  267. model.change( writer => {
  268. writer.setAttribute( 'styled', 'light', modelRoot.getChild( 0 ) );
  269. } );
  270. expectResult( '<img class="styled-light"></img>' );
  271. model.change( writer => {
  272. writer.setAttribute( 'styled', 'xyz', modelRoot.getChild( 0 ) );
  273. } );
  274. expectResult( '<img></img>' );
  275. } );
  276. it( 'config.view is a function', () => {
  277. const helper = downcastAttributeToAttribute( 'styled', {
  278. view: attributeValue => ( { key: 'class', value: 'styled-' + attributeValue } )
  279. } );
  280. conversion.for( 'downcast' ).add( helper );
  281. model.change( writer => {
  282. writer.insertElement( 'image', { styled: 'pull-out' }, modelRoot, 0 );
  283. } );
  284. expectResult( '<img class="styled-pull-out"></img>' );
  285. } );
  286. } );
  287. describe( 'downcastMarkerToElement', () => {
  288. it( 'config.view is a string', () => {
  289. const helper = downcastMarkerToElement( { model: 'search', view: 'marker-search' } );
  290. conversion.for( 'downcast' ).add( helper );
  291. model.change( writer => {
  292. writer.insertText( 'foo', modelRoot, 0 );
  293. writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
  294. } );
  295. expectResult( 'f<marker-search></marker-search>o<marker-search></marker-search>o' );
  296. } );
  297. it( 'can be overwritten using priority', () => {
  298. const helperA = downcastMarkerToElement( { model: 'search', view: 'marker-search' } );
  299. const helperB = downcastMarkerToElement( { model: 'search', view: 'search' }, 'high' );
  300. conversion.for( 'downcast' ).add( helperA ).add( helperB );
  301. model.change( writer => {
  302. writer.insertText( 'foo', modelRoot, 0 );
  303. writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
  304. } );
  305. expectResult( 'f<search></search>o<search></search>o' );
  306. } );
  307. it( 'config.view is an element instance', () => {
  308. const helper = downcastMarkerToElement( {
  309. model: 'search',
  310. view: new ViewUIElement( 'span', { 'data-marker': 'search' } )
  311. } );
  312. conversion.for( 'downcast' ).add( helper );
  313. model.change( writer => {
  314. writer.insertText( 'foo', modelRoot, 0 );
  315. writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
  316. } );
  317. expectResult( 'f<span data-marker="search"></span>o<span data-marker="search"></span>o' );
  318. } );
  319. it( 'config.view is a view element definition', () => {
  320. const helper = downcastMarkerToElement( {
  321. model: 'search',
  322. view: {
  323. name: 'span',
  324. attribute: {
  325. 'data-marker': 'search'
  326. }
  327. }
  328. } );
  329. conversion.for( 'downcast' ).add( helper );
  330. model.change( writer => {
  331. writer.insertText( 'foo', modelRoot, 0 );
  332. writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
  333. } );
  334. expectResult( 'f<span data-marker="search"></span>o<span data-marker="search"></span>o' );
  335. } );
  336. it( 'config.view is a function', () => {
  337. const helper = downcastMarkerToElement( {
  338. model: 'search',
  339. view: data => {
  340. return new ViewUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } );
  341. }
  342. } );
  343. conversion.for( 'downcast' ).add( helper );
  344. model.change( writer => {
  345. writer.insertText( 'foo', modelRoot, 0 );
  346. writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
  347. } );
  348. expectResult( 'f<span data-marker="search" data-start="true"></span>o<span data-marker="search" data-start="false"></span>o' );
  349. } );
  350. } );
  351. describe( 'downcastMarkerToHighlight', () => {
  352. it( 'config.view is a highlight descriptor', () => {
  353. const helper = downcastMarkerToHighlight( { model: 'comment', view: { class: 'comment' } } );
  354. conversion.for( 'downcast' ).add( helper );
  355. model.change( writer => {
  356. writer.insertText( 'foo', modelRoot, 0 );
  357. writer.setMarker( 'comment', ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 3 ) );
  358. } );
  359. expectResult( '<span class="comment">foo</span>' );
  360. } );
  361. it( 'can be overwritten using priority', () => {
  362. const helperA = downcastMarkerToHighlight( { model: 'comment', view: { class: 'comment' } } );
  363. const helperB = downcastMarkerToHighlight( { model: 'comment', view: { class: 'new-comment' } }, 'high' );
  364. conversion.for( 'downcast' ).add( helperA ).add( helperB );
  365. model.change( writer => {
  366. writer.insertText( 'foo', modelRoot, 0 );
  367. writer.setMarker( 'comment', ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 3 ) );
  368. } );
  369. expectResult( '<span class="new-comment">foo</span>' );
  370. } );
  371. it( 'config.view is a function', () => {
  372. const helper = downcastMarkerToHighlight( {
  373. model: 'comment',
  374. view: data => {
  375. const commentType = data.markerName.split( ':' )[ 1 ];
  376. return {
  377. class: [ 'comment', 'comment-' + commentType ]
  378. };
  379. }
  380. } );
  381. conversion.for( 'downcast' ).add( helper );
  382. model.change( writer => {
  383. writer.insertText( 'foo', modelRoot, 0 );
  384. writer.setMarker( 'comment:abc', ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 3 ) );
  385. } );
  386. expectResult( '<span class="comment comment-abc">foo</span>' );
  387. } );
  388. } );
  389. function expectResult( string ) {
  390. expect( stringify( viewRoot, null, { ignoreRoot: true } ) ).to.equal( string );
  391. }
  392. } );