8
0

attributeelementconverters.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import ModelDocument from '../../src/model/document';
  6. import ModelElement from '../../src/model/element';
  7. import ModelText from '../../src/model/text';
  8. import ModelRange from '../../src/model/range';
  9. import ViewDocument from '../../src/view/document';
  10. import ViewElement from '../../src/view/element';
  11. import ViewAttributeElement from '../../src/view/attributeelement';
  12. import ViewText from '../../src/view/text';
  13. import Mapper from '../../src/conversion/mapper';
  14. import ModelConversionDispatcher from '../../src/conversion/modelconversiondispatcher';
  15. import {
  16. insertText,
  17. remove
  18. } from '../../src/conversion/model-to-view-converters';
  19. import { attributeElementToViewConverter, viewToAttributeElementConverter } from '../../src/conversion/attributeelementconverters';
  20. import { convertText } from '../../src/conversion/view-to-model-converters';
  21. import ViewConversionDispatcher from '../../src/conversion/viewconversiondispatcher';
  22. import ModelSchema from '../../src/model/schema';
  23. import ModelWalker from '../../src/model/treewalker';
  24. import ModelTextProxy from '../../src/model/textproxy';
  25. function viewAttributesToString( item ) {
  26. let result = '';
  27. for ( const key of item.getAttributeKeys() ) {
  28. const value = item.getAttribute( key );
  29. if ( value ) {
  30. result += ' ' + key + '="' + value + '"';
  31. }
  32. }
  33. return result;
  34. }
  35. function modelToString( item ) {
  36. let result = '';
  37. if ( item instanceof ModelTextProxy ) {
  38. const attributes = modelAttributesToString( item );
  39. result = attributes ? '<$text' + attributes + '>' + item.data + '</$text>' : item.data;
  40. } else {
  41. const walker = new ModelWalker( { boundaries: ModelRange.createIn( item ), shallow: true } );
  42. for ( const value of walker ) {
  43. result += modelToString( value.item );
  44. }
  45. if ( item instanceof ModelElement ) {
  46. const attributes = modelAttributesToString( item );
  47. result = '<' + item.name + attributes + '>' + result + '</' + item.name + '>';
  48. }
  49. }
  50. return result;
  51. }
  52. function modelAttributesToString( item ) {
  53. let result = '';
  54. for ( const attr of item.getAttributes() ) {
  55. result += ' ' + attr[ 0 ] + '="' + attr[ 1 ] + '"';
  56. }
  57. return result;
  58. }
  59. function viewToString( item ) {
  60. let result = '';
  61. if ( item instanceof ViewText ) {
  62. result = item.data;
  63. } else {
  64. // ViewElement or ViewDocumentFragment.
  65. for ( const child of item.getChildren() ) {
  66. result += viewToString( child );
  67. }
  68. if ( item instanceof ViewElement ) {
  69. result = '<' + item.name + viewAttributesToString( item ) + '>' + result + '</' + item.name + '>';
  70. }
  71. }
  72. return result;
  73. }
  74. describe( 'Attribute converter', () => {
  75. let dispatcher, mapper, modelDoc, modelRoot, viewDoc, viewRoot, viewSelection, batch;
  76. beforeEach( () => {
  77. modelDoc = new ModelDocument();
  78. modelRoot = modelDoc.createRoot( 'root', 'root' );
  79. batch = modelDoc.batch();
  80. viewDoc = new ViewDocument();
  81. viewRoot = viewDoc.createRoot( 'div' );
  82. viewSelection = viewDoc.selection;
  83. mapper = new Mapper();
  84. mapper.bindElements( modelRoot, viewRoot );
  85. dispatcher = new ModelConversionDispatcher( modelDoc, { mapper, viewSelection } );
  86. dispatcher.on( 'insert:$text', insertText() );
  87. dispatcher.on( 'remove', remove() );
  88. } );
  89. afterEach( () => {
  90. viewDoc.destroy();
  91. } );
  92. function testConversion( definition, expectedConversion ) {
  93. attributeElementToViewConverter( 'foo', definition, [ dispatcher ] );
  94. const modelElement = new ModelText( 'foo', { foo: 'bar' } );
  95. modelRoot.appendChildren( modelElement );
  96. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  97. expect( viewToString( viewRoot ) ).to.equal( expectedConversion );
  98. batch.removeAttribute( 'bold', modelRoot );
  99. dispatcher.convertAttribute( 'removeAttribute', ModelRange.createIn( modelRoot ), 'foo', 'bar', null );
  100. expect( viewToString( viewRoot ) ).to.equal( '<div>foo</div>' );
  101. }
  102. describe( 'model attribute to view element conversion', () => {
  103. it( 'using passed view element name', () => {
  104. testConversion( { model: 'bar', view: 'strong' }, '<div><strong>foo</strong></div>' );
  105. } );
  106. it( 'using passed view element object', () => {
  107. testConversion( { model: 'bar', view: { name: 'strong' } }, '<div><strong>foo</strong></div>' );
  108. } );
  109. it( 'using passed view element object with styles object', () => {
  110. testConversion( {
  111. model: 'bar',
  112. view: { name: 'span', styles: { 'font-weight': 'bold' } }
  113. }, '<div><span style="font-weight:bold;">foo</span></div>' );
  114. } );
  115. it( 'using passed view element object with class string', () => {
  116. testConversion( { model: 'bar', view: { name: 'span', classes: 'foo' } }, '<div><span class="foo">foo</span></div>' );
  117. } );
  118. it( 'using passed view element object with class array', () => {
  119. testConversion( {
  120. model: 'bar',
  121. view: { name: 'span', classes: [ 'foo', 'foo-bar' ] }
  122. }, '<div><span class="foo foo-bar">foo</span></div>' );
  123. } );
  124. it( 'using passed view element object with attributes', () => {
  125. testConversion( {
  126. model: 'bar',
  127. view: { name: 'span', attributes: { 'data-foo': 'bar' } }
  128. }, '<div><span data-foo="bar">foo</span></div>' );
  129. } );
  130. it( 'should do nothing for undefined value', () => {
  131. attributeElementToViewConverter( 'foo', { model: 'bar', view: 'strong' }, [ dispatcher ] );
  132. const modelElement = new ModelText( 'foo', { foo: 'baz' } );
  133. modelRoot.appendChildren( modelElement );
  134. dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
  135. expect( viewToString( viewRoot ) ).to.equal( '<div>foo</div>' );
  136. } );
  137. } );
  138. describe( 'view element to model attribute conversion', () => {
  139. let dispatcher, schema, additionalData, batch;
  140. const modelDocument = new ModelDocument();
  141. beforeEach( () => {
  142. batch = modelDocument.batch();
  143. // `additionalData` parameter for `.convert` calls.
  144. additionalData = { context: [ '$root' ] };
  145. schema = new ModelSchema();
  146. schema.registerItem( 'div', '$block' );
  147. schema.allow( { name: '$inline', attributes: [ 'foo' ], inside: '$root' } );
  148. schema.allow( { name: '$text', inside: '$root' } );
  149. dispatcher = new ViewConversionDispatcher( { schema } );
  150. dispatcher.on( 'text', convertText() );
  151. } );
  152. it( 'should convert from view element to model attribute', () => {
  153. viewToAttributeElementConverter( 'foo', { model: 'bar', view: 'strong' }, [ dispatcher ] );
  154. const conversionResult = dispatcher.convert(
  155. new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), batch, additionalData
  156. );
  157. expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
  158. } );
  159. it( 'should convert from view element to model attribute', () => {
  160. viewToAttributeElementConverter( 'foo', { model: 'bar', view: { name: 'strong' } }, [ dispatcher ] );
  161. const conversionResult = dispatcher.convert(
  162. new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), batch, additionalData
  163. );
  164. expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
  165. } );
  166. it( 'should convert from view element to model attribute', () => {
  167. viewToAttributeElementConverter( 'foo', { model: 'bar', view: { name: 'span', classes: 'foo' } }, [ dispatcher ] );
  168. const conversionResult = dispatcher.convert(
  169. new ViewAttributeElement( 'span', { class: 'foo' }, new ViewText( 'foo' ) ), batch, additionalData
  170. );
  171. expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
  172. } );
  173. it( 'should convert from view element to model attribute', () => {
  174. viewToAttributeElementConverter( 'foo', { model: 'bar', view: { name: 'span', classes: [ 'foo', 'bar' ] } }, [ dispatcher ] );
  175. const conversionResult = dispatcher.convert(
  176. new ViewAttributeElement( 'span', { class: 'foo bar' }, new ViewText( 'foo' ) ), batch, additionalData
  177. );
  178. expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
  179. } );
  180. it( 'should convert from view element to model attribute', () => {
  181. viewToAttributeElementConverter( 'foo', {
  182. model: 'bar',
  183. view: { name: 'span', styles: { 'font-weight': 'bold' } }
  184. }, [ dispatcher ] );
  185. const conversionResult = dispatcher.convert(
  186. new ViewAttributeElement( 'span', { style: 'font-weight:bold' }, new ViewText( 'foo' ) ), batch, additionalData
  187. );
  188. expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
  189. } );
  190. it( 'should convert from view element to model attribute', () => {
  191. viewToAttributeElementConverter( 'foo', {
  192. model: 'bar',
  193. view: { name: 'span', attributes: { 'data-foo': 'bar' } }
  194. }, [ dispatcher ] );
  195. const conversionResult = dispatcher.convert(
  196. new ViewAttributeElement( 'span', { 'data-foo': 'bar' }, new ViewText( 'foo' ) ), batch, additionalData
  197. );
  198. expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
  199. } );
  200. it( 'should convert from view element to model attribute', () => {
  201. viewToAttributeElementConverter( 'foo', {
  202. model: 'bar',
  203. view: 'strong',
  204. acceptsAlso: [
  205. { name: 'span', classes: [ 'foo', 'bar' ] },
  206. { name: 'span', attributes: { 'data-foo': 'bar' } }
  207. ]
  208. }, [ dispatcher ] );
  209. const conversionResult = dispatcher.convert(
  210. new ViewAttributeElement( 'span', { 'data-foo': 'bar' }, new ViewText( 'foo' ) ), batch, additionalData
  211. );
  212. expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
  213. } );
  214. it( 'should convert from view element to model attribute', () => {
  215. viewToAttributeElementConverter( 'foo', { model: 'baz', view: 'strong' }, [ dispatcher ] );
  216. viewToAttributeElementConverter( 'foo', { model: 'bar', view: { name: 'strong', priority: 'high' } }, [ dispatcher ] );
  217. const conversionResult = dispatcher.convert(
  218. new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), batch, additionalData
  219. );
  220. expect( modelToString( conversionResult ) ).to.equal( '<$text foo="bar">foo</$text>' );
  221. } );
  222. } );
  223. } );