8
0

downcast.js 37 KB

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