8
0

downcast.js 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
  6. import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
  7. import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  8. import {
  9. downcastInsertCell,
  10. downcastInsertRow,
  11. downcastInsertTable,
  12. downcastRemoveRow,
  13. downcastTableHeadingColumnsChange,
  14. downcastTableHeadingRowsChange
  15. } from '../../src/converters/downcast';
  16. import { formatTable, formattedViewTable, modelTable } from '../_utils/utils';
  17. describe( 'downcast converters', () => {
  18. let editor, model, doc, root, viewDocument;
  19. beforeEach( () => {
  20. return VirtualTestEditor.create()
  21. .then( newEditor => {
  22. editor = newEditor;
  23. model = editor.model;
  24. doc = model.document;
  25. root = doc.getRoot( 'main' );
  26. viewDocument = editor.editing.view;
  27. const conversion = editor.conversion;
  28. const schema = model.schema;
  29. schema.register( 'table', {
  30. allowWhere: '$block',
  31. allowAttributes: [ 'headingRows', 'headingColumns' ],
  32. isObject: true
  33. } );
  34. schema.register( 'tableRow', { allowIn: 'table' } );
  35. schema.register( 'tableCell', {
  36. allowIn: 'tableRow',
  37. allowContentOf: '$block',
  38. allowAttributes: [ 'colspan', 'rowspan' ],
  39. isLimit: true
  40. } );
  41. conversion.for( 'downcast' ).add( downcastInsertTable() );
  42. conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
  43. conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
  44. // Insert conversion
  45. conversion.for( 'downcast' ).add( downcastInsertRow() );
  46. conversion.for( 'downcast' ).add( downcastInsertCell() );
  47. conversion.for( 'downcast' ).add( downcastRemoveRow() );
  48. conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange() );
  49. conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange() );
  50. } );
  51. } );
  52. describe( 'downcastInsertTable()', () => {
  53. it( 'should create table with tbody', () => {
  54. setModelData( model, modelTable( [ [ '' ] ] ) );
  55. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable(
  56. '<figure class="table">' +
  57. '<table>' +
  58. '<tbody>' +
  59. '<tr><td></td></tr>' +
  60. '</tbody>' +
  61. '</table>' +
  62. '</figure>'
  63. ) );
  64. } );
  65. it( 'should create table with tbody and thead', () => {
  66. setModelData( model, modelTable( [
  67. [ '00' ],
  68. [ '10' ]
  69. ], { headingRows: 1 } ) );
  70. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable(
  71. '<figure class="table">' +
  72. '<table>' +
  73. '<thead>' +
  74. '<tr><th>00</th></tr>' +
  75. '</thead>' +
  76. '<tbody>' +
  77. '<tr><td>10</td></tr>' +
  78. '</tbody>' +
  79. '</table>' +
  80. '</figure>'
  81. ) );
  82. } );
  83. it( 'should create table with thead', () => {
  84. setModelData( model, modelTable( [
  85. [ '00' ],
  86. [ '10' ]
  87. ], { headingRows: 2 } ) );
  88. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable(
  89. '<figure class="table">' +
  90. '<table>' +
  91. '<thead>' +
  92. '<tr><th>00</th></tr>' +
  93. '<tr><th>10</th></tr>' +
  94. '</thead>' +
  95. '</table>' +
  96. '</figure>'
  97. ) );
  98. } );
  99. it( 'should create table with heading columns and rows', () => {
  100. setModelData( model, modelTable( [
  101. [ '00', '01', '02', '03' ],
  102. [ '10', '11', '12', '13' ]
  103. ], { headingColumns: 3, headingRows: 1 } ) );
  104. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable(
  105. '<figure class="table">' +
  106. '<table>' +
  107. '<thead>' +
  108. '<tr><th>00</th><th>01</th><th>02</th><th>03</th></tr>' +
  109. '</thead>' +
  110. '<tbody>' +
  111. '<tr><th>10</th><th>11</th><th>12</th><td>13</td></tr>' +
  112. '</tbody>' +
  113. '</table>' +
  114. '</figure>'
  115. ) );
  116. } );
  117. it( 'should be possible to overwrite', () => {
  118. editor.conversion.elementToElement( { model: 'tableRow', view: 'tr', converterPriority: 'high' } );
  119. editor.conversion.elementToElement( { model: 'tableCell', view: 'td', converterPriority: 'high' } );
  120. editor.conversion.for( 'downcast' ).add( dispatcher => {
  121. dispatcher.on( 'insert:table', ( evt, data, conversionApi ) => {
  122. conversionApi.consumable.consume( data.item, 'insert' );
  123. const tableElement = conversionApi.writer.createContainerElement( 'table', { foo: 'bar' } );
  124. const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
  125. conversionApi.mapper.bindElements( data.item, tableElement );
  126. conversionApi.writer.insert( viewPosition, tableElement );
  127. }, { priority: 'high' } );
  128. } );
  129. setModelData( model, modelTable( [ [ '' ] ] ) );
  130. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable(
  131. '<table foo="bar">' +
  132. '<tr><td></td></tr>' +
  133. '</table>'
  134. ) );
  135. } );
  136. describe( 'headingColumns attribute', () => {
  137. it( 'should mark heading columns table cells', () => {
  138. setModelData( model, modelTable( [
  139. [ '00', '01', '02' ],
  140. [ '10', '11', '12' ]
  141. ], { headingColumns: 2 } ) );
  142. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable(
  143. '<figure class="table">' +
  144. '<table>' +
  145. '<tbody>' +
  146. '<tr><th>00</th><th>01</th><td>02</td></tr>' +
  147. '<tr><th>10</th><th>11</th><td>12</td></tr>' +
  148. '</tbody>' +
  149. '</table>' +
  150. '</figure>'
  151. ) );
  152. } );
  153. it( 'should mark heading columns table cells when one has colspan attribute', () => {
  154. setModelData( model, modelTable( [
  155. [ '00', '01', '02', '03' ],
  156. [ { colspan: 2, contents: '10' }, '12', '13' ]
  157. ], { headingColumns: 3 } ) );
  158. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable(
  159. '<figure class="table">' +
  160. '<table>' +
  161. '<tbody>' +
  162. '<tr><th>00</th><th>01</th><th>02</th><td>03</td></tr>' +
  163. '<tr><th colspan="2">10</th><th>12</th><td>13</td></tr>' +
  164. '</tbody>' +
  165. '</table>' +
  166. '</figure>'
  167. ) );
  168. } );
  169. it( 'should work with colspan and rowspan attributes on table cells', () => {
  170. // The table in this test looks like a table below:
  171. //
  172. // Row headings | Normal cells
  173. // |
  174. // +----+----+----+----+
  175. // | 00 | 01 | 02 | 03 |
  176. // | +----+ +----+
  177. // | | 11 | | 13 |
  178. // |----+----+ +----+
  179. // | 20 | | 23 |
  180. // | +----+----+
  181. // | | 32 | 33 |
  182. // +----+----+----+----+
  183. setModelData( model, modelTable( [
  184. [ { rowspan: 2, contents: '00' }, '01', { rowspan: 3, contents: '02' }, '03' ],
  185. [ '11', '13' ],
  186. [ { colspan: 2, rowspan: 2, contents: '20' }, '23' ],
  187. [ '32', '33' ]
  188. ], { headingColumns: 3 } ) );
  189. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable(
  190. '<figure class="table">' +
  191. '<table>' +
  192. '<tbody>' +
  193. '<tr><th rowspan="2">00</th><th>01</th><th rowspan="3">02</th><td>03</td></tr>' +
  194. '<tr><th>11</th><td>13</td></tr>' +
  195. '<tr><th colspan="2" rowspan="2">20</th><td>23</td></tr>' +
  196. '<tr><th>32</th><td>33</td></tr>' +
  197. '</tbody>' +
  198. '</table>' +
  199. '</figure>'
  200. ) );
  201. } );
  202. } );
  203. describe( 'asWidget', () => {
  204. beforeEach( () => {
  205. return VirtualTestEditor.create()
  206. .then( newEditor => {
  207. editor = newEditor;
  208. model = editor.model;
  209. doc = model.document;
  210. root = doc.getRoot( 'main' );
  211. viewDocument = editor.editing.view;
  212. const conversion = editor.conversion;
  213. const schema = model.schema;
  214. schema.register( 'table', {
  215. allowWhere: '$block',
  216. allowAttributes: [ 'headingRows', 'headingColumns' ],
  217. isObject: true
  218. } );
  219. schema.register( 'tableRow', { allowIn: 'table' } );
  220. schema.register( 'tableCell', {
  221. allowIn: 'tableRow',
  222. allowContentOf: '$block',
  223. allowAttributes: [ 'colspan', 'rowspan' ],
  224. isLimit: true
  225. } );
  226. conversion.for( 'downcast' ).add( downcastInsertTable( { asWidget: true } ) );
  227. conversion.for( 'downcast' ).add( downcastInsertRow( { asWidget: true } ) );
  228. conversion.for( 'downcast' ).add( downcastInsertCell( { asWidget: true } ) );
  229. conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
  230. conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
  231. } );
  232. } );
  233. it( 'should create table as a widget', () => {
  234. setModelData( model, modelTable( [ [ '' ] ] ) );
  235. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable(
  236. '<figure class="ck-widget table" contenteditable="false">' +
  237. '<table>' +
  238. '<tbody>' +
  239. '<tr><td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true"></td></tr>' +
  240. '</tbody>' +
  241. '</table>' +
  242. '</figure>'
  243. ) );
  244. } );
  245. } );
  246. } );
  247. describe( 'downcastInsertRow()', () => {
  248. it( 'should react to changed rows', () => {
  249. setModelData( model, modelTable( [
  250. [ '00', '01' ]
  251. ] ) );
  252. const table = root.getChild( 0 );
  253. model.change( writer => {
  254. const row = writer.createElement( 'tableRow' );
  255. writer.insert( row, table, 1 );
  256. writer.insertElement( 'tableCell', row, 'end' );
  257. writer.insertElement( 'tableCell', row, 'end' );
  258. } );
  259. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  260. [ '00', '01' ],
  261. [ '', '' ]
  262. ] ) );
  263. } );
  264. it( 'should properly consume already added rows', () => {
  265. setModelData( model, modelTable( [
  266. [ '00', '01' ]
  267. ] ) );
  268. const table = root.getChild( 0 );
  269. model.change( writer => {
  270. const row = writer.createElement( 'tableRow' );
  271. writer.insert( row, table, 1 );
  272. writer.insertElement( 'tableCell', row, 'end' );
  273. writer.insertElement( 'tableCell', row, 'end' );
  274. } );
  275. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  276. [ '00', '01' ],
  277. [ '', '' ]
  278. ] ) );
  279. model.change( writer => {
  280. const row = writer.createElement( 'tableRow' );
  281. writer.insert( row, table, 2 );
  282. writer.insertElement( 'tableCell', row, 'end' );
  283. writer.insertElement( 'tableCell', row, 'end' );
  284. } );
  285. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  286. [ '00', '01' ],
  287. [ '', '' ],
  288. [ '', '' ]
  289. ] ) );
  290. } );
  291. it( 'should insert row on proper index', () => {
  292. setModelData( model, modelTable( [
  293. [ '00', '01' ],
  294. [ '21', '22' ],
  295. [ '31', '32' ]
  296. ] ) );
  297. const table = root.getChild( 0 );
  298. model.change( writer => {
  299. const row = writer.createElement( 'tableRow' );
  300. writer.insert( row, table, 1 );
  301. writer.insertElement( 'tableCell', row, 'end' );
  302. writer.insertElement( 'tableCell', row, 'end' );
  303. } );
  304. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  305. [ '00', '01' ],
  306. [ '', '' ],
  307. [ '21', '22' ],
  308. [ '31', '32' ]
  309. ] ) );
  310. } );
  311. it( 'should insert row on proper index when table has heading rows defined - insert in body', () => {
  312. setModelData( model, modelTable( [
  313. [ '00', '01' ],
  314. [ '21', '22' ],
  315. [ '31', '32' ]
  316. ], { headingRows: 1 } ) );
  317. const table = root.getChild( 0 );
  318. model.change( writer => {
  319. const row = writer.createElement( 'tableRow' );
  320. writer.insert( row, table, 1 );
  321. writer.insertElement( 'tableCell', row, 'end' );
  322. writer.insertElement( 'tableCell', row, 'end' );
  323. } );
  324. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  325. [ '00', '01' ],
  326. [ '', '' ],
  327. [ '21', '22' ],
  328. [ '31', '32' ]
  329. ], { headingRows: 1 } ) );
  330. } );
  331. it( 'should insert row on proper index when table has heading rows defined - insert in heading', () => {
  332. setModelData( model, modelTable( [
  333. [ '00', '01' ],
  334. [ '21', '22' ],
  335. [ '31', '32' ]
  336. ], { headingRows: 2 } ) );
  337. const table = root.getChild( 0 );
  338. model.change( writer => {
  339. const row = writer.createElement( 'tableRow' );
  340. writer.insert( row, table, 1 );
  341. writer.insertElement( 'tableCell', row, 'end' );
  342. writer.insertElement( 'tableCell', row, 'end' );
  343. } );
  344. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  345. [ '00', '01' ],
  346. [ '', '' ],
  347. [ '21', '22' ],
  348. [ '31', '32' ]
  349. ], { headingRows: 3 } ) );
  350. } );
  351. it( 'should react to changed rows when previous rows\' cells has rowspans', () => {
  352. setModelData( model, modelTable( [
  353. [ { rowspan: 3, contents: '00' }, '01' ],
  354. [ '22' ]
  355. ] ) );
  356. const table = root.getChild( 0 );
  357. model.change( writer => {
  358. const row = writer.createElement( 'tableRow' );
  359. writer.insert( row, table, 2 );
  360. writer.insertElement( 'tableCell', row, 'end' );
  361. } );
  362. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  363. [ { rowspan: 3, contents: '00' }, '01' ],
  364. [ '22' ],
  365. [ '' ]
  366. ] ) );
  367. } );
  368. it( 'should properly create row headings', () => {
  369. setModelData( model, modelTable( [
  370. [ { rowspan: 3, contents: '00' }, '01' ],
  371. [ '22' ]
  372. ], { headingColumns: 1 } ) );
  373. const table = root.getChild( 0 );
  374. model.change( writer => {
  375. const firstRow = writer.createElement( 'tableRow' );
  376. writer.insert( firstRow, table, 2 );
  377. writer.insert( writer.createElement( 'tableCell' ), firstRow, 'end' );
  378. const secondRow = writer.createElement( 'tableRow' );
  379. writer.insert( secondRow, table, 3 );
  380. writer.insert( writer.createElement( 'tableCell' ), secondRow, 'end' );
  381. writer.insert( writer.createElement( 'tableCell' ), secondRow, 'end' );
  382. } );
  383. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  384. [ { rowspan: 3, contents: '00', isHeading: true }, '01' ],
  385. [ '22' ],
  386. [ '' ],
  387. [ { contents: '', isHeading: true }, '' ]
  388. ] ) );
  389. } );
  390. describe( 'asWidget', () => {
  391. beforeEach( () => {
  392. return VirtualTestEditor.create()
  393. .then( newEditor => {
  394. editor = newEditor;
  395. model = editor.model;
  396. doc = model.document;
  397. root = doc.getRoot( 'main' );
  398. viewDocument = editor.editing.view;
  399. const conversion = editor.conversion;
  400. const schema = model.schema;
  401. schema.register( 'table', {
  402. allowWhere: '$block',
  403. allowAttributes: [ 'headingRows', 'headingColumns' ],
  404. isObject: true
  405. } );
  406. schema.register( 'tableRow', { allowIn: 'table' } );
  407. schema.register( 'tableCell', {
  408. allowIn: 'tableRow',
  409. allowContentOf: '$block',
  410. allowAttributes: [ 'colspan', 'rowspan' ],
  411. isLimit: true
  412. } );
  413. conversion.for( 'downcast' ).add( downcastInsertTable( { asWidget: true } ) );
  414. conversion.for( 'downcast' ).add( downcastInsertRow( { asWidget: true } ) );
  415. conversion.for( 'downcast' ).add( downcastInsertCell( { asWidget: true } ) );
  416. conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
  417. conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
  418. } );
  419. } );
  420. it( 'should create table cell inside inserted row as a widget', () => {
  421. setModelData( model, modelTable( [ [ '00' ] ] ) );
  422. const table = root.getChild( 0 );
  423. model.change( writer => {
  424. const firstRow = writer.createElement( 'tableRow' );
  425. writer.insert( firstRow, table, 1 );
  426. writer.insert( writer.createElement( 'tableCell' ), firstRow, 'end' );
  427. } );
  428. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable(
  429. '<figure class="ck-widget table" contenteditable="false">' +
  430. '<table>' +
  431. '<tbody>' +
  432. '<tr><td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true">00</td></tr>' +
  433. '<tr><td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true"></td></tr>' +
  434. '</tbody>' +
  435. '</table>' +
  436. '</figure>'
  437. ) );
  438. } );
  439. } );
  440. } );
  441. describe( 'downcastInsertCell()', () => {
  442. it( 'should add tableCell on proper index in tr', () => {
  443. setModelData( model, modelTable( [
  444. [ '00', '01' ]
  445. ] ) );
  446. const table = root.getChild( 0 );
  447. model.change( writer => {
  448. const row = table.getChild( 0 );
  449. writer.insertElement( 'tableCell', row, 1 );
  450. } );
  451. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  452. [ '00', '', '01' ]
  453. ] ) );
  454. } );
  455. it( 'should add tableCell on proper index in tr when previous have colspans', () => {
  456. setModelData( model, modelTable( [
  457. [ { colspan: 2, contents: '00' }, '13' ]
  458. ] ) );
  459. const table = root.getChild( 0 );
  460. model.change( writer => {
  461. const row = table.getChild( 0 );
  462. writer.insertElement( 'tableCell', row, 1 );
  463. } );
  464. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  465. [ { colspan: 2, contents: '00' }, '', '13' ]
  466. ] ) );
  467. } );
  468. it( 'should add tableCell on proper index in tr when previous row have rowspans', () => {
  469. setModelData( model, modelTable( [
  470. [ { rowspan: 2, contents: '00' }, '13' ],
  471. [ '11', '12' ]
  472. ] ) );
  473. const table = root.getChild( 0 );
  474. model.change( writer => {
  475. writer.insertElement( 'tableCell', table.getChild( 0 ), 1 );
  476. writer.insertElement( 'tableCell', table.getChild( 1 ), 0 );
  477. } );
  478. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  479. [ { rowspan: 2, contents: '00' }, '', '13' ],
  480. [ '', '11', '12' ]
  481. ] ) );
  482. } );
  483. it( 'split cell simulation - simple', () => {
  484. setModelData( model, modelTable( [
  485. [ '00', '01' ],
  486. [ '10', '11' ]
  487. ] ) );
  488. const table = root.getChild( 0 );
  489. model.change( writer => {
  490. const firstRow = table.getChild( 0 );
  491. const secondRow = table.getChild( 1 );
  492. writer.insertElement( 'tableCell', firstRow, 1 );
  493. writer.setAttribute( 'colspan', 2, secondRow.getChild( 0 ) );
  494. } );
  495. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  496. [ '00', '', '01' ],
  497. [ { colspan: 2, contents: '10' }, '11' ]
  498. ] ) );
  499. } );
  500. it( 'merge simulation - simple', () => {
  501. setModelData( model, modelTable( [
  502. [ '00', '01' ],
  503. [ '10', '11' ]
  504. ] ) );
  505. const table = root.getChild( 0 );
  506. model.change( writer => {
  507. const firstRow = table.getChild( 0 );
  508. writer.setAttribute( 'colspan', 2, firstRow.getChild( 0 ) );
  509. writer.remove( firstRow.getChild( 1 ) );
  510. } );
  511. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  512. [ { colspan: 2, contents: '00' } ],
  513. [ '10', '11' ]
  514. ] ) );
  515. } );
  516. describe( 'asWidget', () => {
  517. beforeEach( () => {
  518. return VirtualTestEditor.create()
  519. .then( newEditor => {
  520. editor = newEditor;
  521. model = editor.model;
  522. doc = model.document;
  523. root = doc.getRoot( 'main' );
  524. viewDocument = editor.editing.view;
  525. const conversion = editor.conversion;
  526. const schema = model.schema;
  527. schema.register( 'table', {
  528. allowWhere: '$block',
  529. allowAttributes: [ 'headingRows', 'headingColumns' ],
  530. isObject: true
  531. } );
  532. schema.register( 'tableRow', { allowIn: 'table' } );
  533. schema.register( 'tableCell', {
  534. allowIn: 'tableRow',
  535. allowContentOf: '$block',
  536. allowAttributes: [ 'colspan', 'rowspan' ],
  537. isLimit: true
  538. } );
  539. conversion.for( 'downcast' ).add( downcastInsertTable( { asWidget: true } ) );
  540. conversion.for( 'downcast' ).add( downcastInsertRow( { asWidget: true } ) );
  541. conversion.for( 'downcast' ).add( downcastInsertCell( { asWidget: true } ) );
  542. conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
  543. conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
  544. } );
  545. } );
  546. it( 'should create inserted table cell as a widget', () => {
  547. setModelData( model, modelTable( [ [ '00' ] ] ) );
  548. const table = root.getChild( 0 );
  549. model.change( writer => {
  550. const row = table.getChild( 0 );
  551. writer.insert( writer.createElement( 'tableCell' ), row, 'end' );
  552. } );
  553. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable(
  554. '<figure class="ck-widget table" contenteditable="false">' +
  555. '<table>' +
  556. '<tbody>' +
  557. '<tr>' +
  558. '<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true">00</td>' +
  559. '<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true"></td>' +
  560. '</tr>' +
  561. '</tbody>' +
  562. '</table>' +
  563. '</figure>'
  564. ) );
  565. } );
  566. } );
  567. } );
  568. describe( 'downcastTableHeadingColumnsChange()', () => {
  569. it( 'should work for adding heading columns', () => {
  570. setModelData( model, modelTable( [
  571. [ '00', '01' ],
  572. [ '10', '11' ]
  573. ] ) );
  574. const table = root.getChild( 0 );
  575. model.change( writer => {
  576. writer.setAttribute( 'headingColumns', 1, table );
  577. } );
  578. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  579. [ { isHeading: true, contents: '00' }, '01' ],
  580. [ { isHeading: true, contents: '10' }, '11' ]
  581. ], { headingColumns: 1 } ) );
  582. } );
  583. it( 'should work for changing heading columns to a bigger number', () => {
  584. setModelData( model, modelTable( [
  585. [ '00', '01', '02', '03' ],
  586. [ '10', '11', '12', '13' ]
  587. ], { headingColumns: 1 } ) );
  588. const table = root.getChild( 0 );
  589. model.change( writer => {
  590. writer.setAttribute( 'headingColumns', 3, table );
  591. } );
  592. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  593. [ { isHeading: true, contents: '00' }, { isHeading: true, contents: '01' }, { isHeading: true, contents: '02' }, '03' ],
  594. [ { isHeading: true, contents: '10' }, { isHeading: true, contents: '11' }, { isHeading: true, contents: '12' }, '13' ]
  595. ] ) );
  596. } );
  597. it( 'should work for changing heading columns to a smaller number', () => {
  598. setModelData( model, modelTable( [
  599. [ { isHeading: true, contents: '00' }, { isHeading: true, contents: '01' }, { isHeading: true, contents: '02' }, '03' ],
  600. [ { isHeading: true, contents: '10' }, { isHeading: true, contents: '11' }, { isHeading: true, contents: '12' }, '13' ]
  601. ], { headingColumns: 3 } ) );
  602. const table = root.getChild( 0 );
  603. model.change( writer => {
  604. writer.setAttribute( 'headingColumns', 1, table );
  605. } );
  606. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  607. [ { isHeading: true, contents: '00' }, '01', '02', '03' ],
  608. [ { isHeading: true, contents: '10' }, '11', '12', '13' ]
  609. ], { headingColumns: 3 } ) );
  610. } );
  611. it( 'should work for removing heading columns', () => {
  612. setModelData( model, modelTable( [
  613. [ '00', '01' ],
  614. [ '10', '11' ]
  615. ], { headingColumns: 1 } ) );
  616. const table = root.getChild( 0 );
  617. model.change( writer => {
  618. writer.removeAttribute( 'headingColumns', table );
  619. } );
  620. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  621. [ '00', '01' ],
  622. [ '10', '11' ]
  623. ] ) );
  624. } );
  625. it( 'should be possible to overwrite', () => {
  626. editor.conversion.attributeToAttribute( { model: 'headingColumns', view: 'headingColumns', converterPriority: 'high' } );
  627. setModelData( model, modelTable( [ [ '00' ] ] ) );
  628. const table = root.getChild( 0 );
  629. model.change( writer => {
  630. writer.setAttribute( 'headingColumns', 1, table );
  631. } );
  632. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable(
  633. '<figure class="table" headingColumns="1">' +
  634. '<table>' +
  635. '<tbody>' +
  636. '<tr><td>00</td></tr>' +
  637. '</tbody>' +
  638. '</table>' +
  639. '</figure>'
  640. ) );
  641. } );
  642. it( 'should work with adding table cells', () => {
  643. setModelData( model, modelTable( [
  644. [ { rowspan: 2, contents: '00' }, '01', '13', '14' ],
  645. [ '11', '12', '13' ],
  646. [ { colspan: 2, contents: '20' }, '22', '23' ]
  647. ], { headingColumns: 2 } ) );
  648. const table = root.getChild( 0 );
  649. model.change( writer => {
  650. // Inserting column in heading columns so update table's attribute also
  651. writer.setAttribute( 'headingColumns', 3, table );
  652. writer.insertElement( 'tableCell', table.getChild( 0 ), 2 );
  653. writer.insertElement( 'tableCell', table.getChild( 1 ), 1 );
  654. writer.insertElement( 'tableCell', table.getChild( 2 ), 1 );
  655. } );
  656. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  657. [
  658. { isHeading: true, rowspan: 2, contents: '00' },
  659. { isHeading: true, contents: '01' },
  660. { isHeading: true, contents: '' },
  661. '13',
  662. '14'
  663. ],
  664. [
  665. { isHeading: true, contents: '11' },
  666. { isHeading: true, contents: '' },
  667. '12',
  668. '13'
  669. ],
  670. [
  671. { isHeading: true, colspan: 2, contents: '20' },
  672. { isHeading: true, contents: '' },
  673. '22',
  674. '23'
  675. ]
  676. ] ) );
  677. } );
  678. describe( 'asWidget', () => {
  679. beforeEach( () => {
  680. return VirtualTestEditor.create()
  681. .then( newEditor => {
  682. editor = newEditor;
  683. model = editor.model;
  684. doc = model.document;
  685. root = doc.getRoot( 'main' );
  686. viewDocument = editor.editing.view;
  687. const conversion = editor.conversion;
  688. const schema = model.schema;
  689. schema.register( 'table', {
  690. allowWhere: '$block',
  691. allowAttributes: [ 'headingRows', 'headingColumns' ],
  692. isObject: true
  693. } );
  694. schema.register( 'tableRow', { allowIn: 'table' } );
  695. schema.register( 'tableCell', {
  696. allowIn: 'tableRow',
  697. allowContentOf: '$block',
  698. allowAttributes: [ 'colspan', 'rowspan' ],
  699. isLimit: true
  700. } );
  701. conversion.for( 'downcast' ).add( downcastInsertTable( { asWidget: true } ) );
  702. conversion.for( 'downcast' ).add( downcastInsertRow( { asWidget: true } ) );
  703. conversion.for( 'downcast' ).add( downcastInsertCell( { asWidget: true } ) );
  704. conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange( { asWidget: true } ) );
  705. conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange( { asWidget: true } ) );
  706. conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
  707. conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
  708. } );
  709. } );
  710. it( 'should create renamed cell as a widget', () => {
  711. setModelData( model, modelTable( [ [ '00' ] ] ) );
  712. const table = root.getChild( 0 );
  713. model.change( writer => {
  714. writer.setAttribute( 'headingRows', 1, table );
  715. } );
  716. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable(
  717. '<figure class="ck-widget table" contenteditable="false">' +
  718. '<table>' +
  719. '<thead>' +
  720. '<tr><th class="ck-editor__editable ck-editor__nested-editable" contenteditable="true">00</th></tr>' +
  721. '</thead>' +
  722. '</table>' +
  723. '</figure>'
  724. ) );
  725. } );
  726. } );
  727. } );
  728. describe( 'downcastTableHeadingRowsChange()', () => {
  729. it( 'should work for adding heading rows', () => {
  730. setModelData( model, modelTable( [
  731. [ '00', '01' ],
  732. [ '10', '11' ],
  733. [ '20', '21' ]
  734. ] ) );
  735. const table = root.getChild( 0 );
  736. model.change( writer => {
  737. writer.setAttribute( 'headingRows', 2, table );
  738. } );
  739. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  740. [ '00', '01' ],
  741. [ '10', '11' ],
  742. [ '20', '21' ]
  743. ], { headingRows: 2 } ) );
  744. } );
  745. it( 'should work for changing number of heading rows to a bigger number', () => {
  746. setModelData( model, modelTable( [
  747. [ '00', '01' ],
  748. [ '10', '11' ],
  749. [ '20', '21' ]
  750. ], { headingRows: 1 } ) );
  751. const table = root.getChild( 0 );
  752. model.change( writer => {
  753. writer.setAttribute( 'headingRows', 2, table );
  754. } );
  755. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  756. [ '00', '01' ],
  757. [ '10', '11' ],
  758. [ '20', '21' ]
  759. ], { headingRows: 2 } ) );
  760. } );
  761. it( 'should work for changing number of heading rows to a smaller number', () => {
  762. setModelData( model, modelTable( [
  763. [ '00', '01' ],
  764. [ '10', '11' ],
  765. [ '20', '21' ],
  766. [ '30', '31' ]
  767. ], { headingRows: 3 } ) );
  768. const table = root.getChild( 0 );
  769. model.change( writer => {
  770. writer.setAttribute( 'headingRows', 2, table );
  771. } );
  772. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  773. [ '00', '01' ],
  774. [ '10', '11' ],
  775. [ '20', '21' ],
  776. [ '30', '31' ]
  777. ], { headingRows: 2 } ) );
  778. } );
  779. it( 'should work for removing heading rows', () => {
  780. setModelData( model, modelTable( [
  781. [ '00', '01' ],
  782. [ '10', '11' ]
  783. ], { headingRows: 2 } ) );
  784. const table = root.getChild( 0 );
  785. model.change( writer => {
  786. writer.removeAttribute( 'headingRows', table );
  787. } );
  788. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  789. [ '00', '01' ],
  790. [ '10', '11' ]
  791. ] ) );
  792. } );
  793. it( 'should work for making heading rows without tbody', () => {
  794. setModelData( model, modelTable( [
  795. [ '00', '01' ],
  796. [ '10', '11' ]
  797. ] ) );
  798. const table = root.getChild( 0 );
  799. model.change( writer => {
  800. writer.setAttribute( 'headingRows', 2, table );
  801. } );
  802. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  803. [ '00', '01' ],
  804. [ '10', '11' ]
  805. ], { headingRows: 2 } ) );
  806. } );
  807. it( 'should be possible to overwrite', () => {
  808. editor.conversion.attributeToAttribute( { model: 'headingRows', view: 'headingRows', converterPriority: 'high' } );
  809. setModelData( model, modelTable( [ [ '00' ] ] ) );
  810. const table = root.getChild( 0 );
  811. model.change( writer => {
  812. writer.setAttribute( 'headingRows', 1, table );
  813. } );
  814. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable(
  815. '<figure class="table" headingRows="1">' +
  816. '<table>' +
  817. '<tbody>' +
  818. '<tr><td>00</td></tr>' +
  819. '</tbody>' +
  820. '</table>' +
  821. '</figure>'
  822. ) );
  823. } );
  824. it( 'should work with adding table rows at the beginning of a table', () => {
  825. setModelData( model, modelTable( [
  826. [ '00', '01' ],
  827. [ '10', '11' ]
  828. ], { headingRows: 1 } ) );
  829. const table = root.getChild( 0 );
  830. model.change( writer => {
  831. writer.setAttribute( 'headingRows', 2, table );
  832. const tableRow = writer.createElement( 'tableRow' );
  833. writer.insert( tableRow, table, 0 );
  834. writer.insertElement( 'tableCell', tableRow, 'end' );
  835. writer.insertElement( 'tableCell', tableRow, 'end' );
  836. } );
  837. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  838. [ '', '' ],
  839. [ '00', '01' ],
  840. [ '10', '11' ]
  841. ], { headingRows: 2 } ) );
  842. } );
  843. describe( 'asWidget', () => {
  844. beforeEach( () => {
  845. return VirtualTestEditor.create()
  846. .then( newEditor => {
  847. editor = newEditor;
  848. model = editor.model;
  849. doc = model.document;
  850. root = doc.getRoot( 'main' );
  851. viewDocument = editor.editing.view;
  852. const conversion = editor.conversion;
  853. const schema = model.schema;
  854. schema.register( 'table', {
  855. allowWhere: '$block',
  856. allowAttributes: [ 'headingRows', 'headingColumns' ],
  857. isObject: true
  858. } );
  859. schema.register( 'tableRow', { allowIn: 'table' } );
  860. schema.register( 'tableCell', {
  861. allowIn: 'tableRow',
  862. allowContentOf: '$block',
  863. allowAttributes: [ 'colspan', 'rowspan' ],
  864. isLimit: true
  865. } );
  866. conversion.for( 'downcast' ).add( downcastInsertTable( { asWidget: true } ) );
  867. conversion.for( 'downcast' ).add( downcastInsertRow( { asWidget: true } ) );
  868. conversion.for( 'downcast' ).add( downcastInsertCell( { asWidget: true } ) );
  869. conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange( { asWidget: true } ) );
  870. conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange( { asWidget: true } ) );
  871. conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
  872. conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
  873. } );
  874. } );
  875. it( 'should create renamed cell as a widget', () => {
  876. setModelData( model, modelTable( [ [ '00' ] ] ) );
  877. const table = root.getChild( 0 );
  878. model.change( writer => {
  879. writer.setAttribute( 'headingColumns', 1, table );
  880. } );
  881. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable(
  882. '<figure class="ck-widget table" contenteditable="false">' +
  883. '<table>' +
  884. '<tbody>' +
  885. '<tr><th class="ck-editor__editable ck-editor__nested-editable" contenteditable="true">00</th></tr>' +
  886. '</tbody>' +
  887. '</table>' +
  888. '</figure>'
  889. ) );
  890. } );
  891. } );
  892. } );
  893. describe( 'downcastRemoveRow()', () => {
  894. it( 'should react to removed row from the beginning of a tbody', () => {
  895. setModelData( model, modelTable( [
  896. [ '00[]', '01' ],
  897. [ '10', '11' ]
  898. ] ) );
  899. const table = root.getChild( 0 );
  900. model.change( writer => {
  901. writer.remove( table.getChild( 1 ) );
  902. } );
  903. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  904. [ '00', '01' ]
  905. ] ) );
  906. } );
  907. it( 'should react to removed row form the end of a tbody', () => {
  908. setModelData( model, modelTable( [
  909. [ '00[]', '01' ],
  910. [ '10', '11' ]
  911. ] ) );
  912. const table = root.getChild( 0 );
  913. model.change( writer => {
  914. writer.remove( table.getChild( 0 ) );
  915. } );
  916. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  917. [ '10', '11' ]
  918. ] ) );
  919. } );
  920. it( 'should react to removed row from the beginning of a thead', () => {
  921. setModelData( model, modelTable( [
  922. [ '00[]', '01' ],
  923. [ '10', '11' ]
  924. ], { headingRows: 2 } ) );
  925. const table = root.getChild( 0 );
  926. model.change( writer => {
  927. writer.remove( table.getChild( 1 ) );
  928. } );
  929. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  930. [ '00', '01' ]
  931. ], { headingRows: 2 } ) );
  932. } );
  933. it( 'should react to removed row form the end of a thead', () => {
  934. setModelData( model, modelTable( [
  935. [ '00[]', '01' ],
  936. [ '10', '11' ]
  937. ], { headingRows: 2 } ) );
  938. const table = root.getChild( 0 );
  939. model.change( writer => {
  940. writer.remove( table.getChild( 0 ) );
  941. } );
  942. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  943. [ '10', '11' ]
  944. ], { headingRows: 2 } ) );
  945. } );
  946. it( 'should remove empty thead section if a last row was removed from thead', () => {
  947. setModelData( model, modelTable( [
  948. [ '00[]', '01' ],
  949. [ '10', '11' ]
  950. ], { headingRows: 1 } ) );
  951. const table = root.getChild( 0 );
  952. model.change( writer => {
  953. writer.setAttribute( 'headingRows', 0, table );
  954. writer.remove( table.getChild( 0 ) );
  955. } );
  956. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  957. [ '10', '11' ]
  958. ] ) );
  959. } );
  960. it( 'should remove empty tbody section if a last row was removed from tbody', () => {
  961. setModelData( model, modelTable( [
  962. [ '00[]', '01' ],
  963. [ '10', '11' ]
  964. ], { headingRows: 1 } ) );
  965. const table = root.getChild( 0 );
  966. model.change( writer => {
  967. writer.remove( table.getChild( 1 ) );
  968. } );
  969. expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
  970. [ '00', '01' ]
  971. ], { headingRows: 1 } ) );
  972. } );
  973. } );
  974. } );