tableutils.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
  6. import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  7. import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
  8. import { downcastInsertTable } from '../src/converters/downcast';
  9. import upcastTable from '../src/converters/upcasttable';
  10. import { formatTable, formattedModelTable, modelTable } from './_utils/utils';
  11. import TableUtils from '../src/tableutils';
  12. describe( 'TableUtils', () => {
  13. let editor, model, root, tableUtils;
  14. beforeEach( () => {
  15. return ModelTestEditor.create( {
  16. plugins: [ TableUtils ]
  17. } ).then( newEditor => {
  18. editor = newEditor;
  19. model = editor.model;
  20. root = model.document.getRoot( 'main' );
  21. tableUtils = editor.plugins.get( TableUtils );
  22. const conversion = editor.conversion;
  23. const schema = model.schema;
  24. schema.register( 'table', {
  25. allowWhere: '$block',
  26. allowAttributes: [ 'headingRows' ],
  27. isBlock: true,
  28. isObject: true
  29. } );
  30. schema.register( 'tableRow', {
  31. allowIn: 'table',
  32. allowAttributes: [],
  33. isBlock: true,
  34. isLimit: true
  35. } );
  36. schema.register( 'tableCell', {
  37. allowIn: 'tableRow',
  38. allowContentOf: '$block',
  39. allowAttributes: [ 'colspan', 'rowspan' ],
  40. isBlock: true,
  41. isLimit: true
  42. } );
  43. model.schema.register( 'p', { inheritAllFrom: '$block' } );
  44. // Table conversion.
  45. conversion.for( 'upcast' ).add( upcastTable() );
  46. conversion.for( 'downcast' ).add( downcastInsertTable() );
  47. // Table row upcast only since downcast conversion is done in `downcastTable()`.
  48. conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableRow', view: 'tr' } ) );
  49. // Table cell conversion.
  50. conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) );
  51. conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) );
  52. conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
  53. conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
  54. } );
  55. } );
  56. afterEach( () => {
  57. return editor.destroy();
  58. } );
  59. describe( 'getCellLocation()', () => {
  60. it( 'should return proper table cell location', () => {
  61. setData( model, modelTable( [
  62. [ { rowspan: 2, colspan: 2, contents: '00[]' }, '02' ],
  63. [ '12' ]
  64. ] ) );
  65. expect( tableUtils.getCellLocation( root.getNodeByPath( [ 0, 0, 0 ] ) ) ).to.deep.equal( { row: 0, column: 0 } );
  66. expect( tableUtils.getCellLocation( root.getNodeByPath( [ 0, 0, 1 ] ) ) ).to.deep.equal( { row: 0, column: 2 } );
  67. expect( tableUtils.getCellLocation( root.getNodeByPath( [ 0, 1, 0 ] ) ) ).to.deep.equal( { row: 1, column: 2 } );
  68. } );
  69. } );
  70. describe( 'insertRows()', () => {
  71. it( 'should insert row in given table at given index', () => {
  72. setData( model, modelTable( [
  73. [ '11[]', '12' ],
  74. [ '21', '22' ]
  75. ] ) );
  76. tableUtils.insertRows( root.getNodeByPath( [ 0 ] ), { at: 1 } );
  77. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  78. [ '11[]', '12' ],
  79. [ '', '' ],
  80. [ '21', '22' ]
  81. ] ) );
  82. } );
  83. it( 'should insert row in given table at default index', () => {
  84. setData( model, modelTable( [
  85. [ '11[]', '12' ],
  86. [ '21', '22' ]
  87. ] ) );
  88. tableUtils.insertRows( root.getNodeByPath( [ 0 ] ) );
  89. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  90. [ '', '' ],
  91. [ '11[]', '12' ],
  92. [ '21', '22' ]
  93. ] ) );
  94. } );
  95. it( 'should update table heading rows attribute when inserting row in headings section', () => {
  96. setData( model, modelTable( [
  97. [ '11[]', '12' ],
  98. [ '21', '22' ],
  99. [ '31', '32' ]
  100. ], { headingRows: 2 } ) );
  101. tableUtils.insertRows( root.getNodeByPath( [ 0 ] ), { at: 1 } );
  102. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  103. [ '11[]', '12' ],
  104. [ '', '' ],
  105. [ '21', '22' ],
  106. [ '31', '32' ]
  107. ], { headingRows: 3 } ) );
  108. } );
  109. it( 'should not update table heading rows attribute when inserting row after headings section', () => {
  110. setData( model, modelTable( [
  111. [ '11[]', '12' ],
  112. [ '21', '22' ],
  113. [ '31', '32' ]
  114. ], { headingRows: 2 } ) );
  115. tableUtils.insertRows( root.getNodeByPath( [ 0 ] ), { at: 2 } );
  116. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  117. [ '11[]', '12' ],
  118. [ '21', '22' ],
  119. [ '', '' ],
  120. [ '31', '32' ]
  121. ], { headingRows: 2 } ) );
  122. } );
  123. it( 'should expand rowspan of a cell that overlaps inserted rows', () => {
  124. setData( model, modelTable( [
  125. [ { colspan: 2, contents: '11[]' }, '13', '14' ],
  126. [ { colspan: 2, rowspan: 4, contents: '21' }, '23', '24' ],
  127. [ '33', '34' ]
  128. ], { headingColumns: 3, headingRows: 1 } ) );
  129. tableUtils.insertRows( root.getNodeByPath( [ 0 ] ), { at: 2, rows: 3 } );
  130. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  131. [ { colspan: 2, contents: '11[]' }, '13', '14' ],
  132. [ { colspan: 2, rowspan: 7, contents: '21' }, '23', '24' ],
  133. [ '', '' ],
  134. [ '', '' ],
  135. [ '', '' ],
  136. [ '33', '34' ]
  137. ], { headingColumns: 3, headingRows: 1 } ) );
  138. } );
  139. it( 'should not expand rowspan of a cell that does not overlaps inserted rows', () => {
  140. setData( model, modelTable( [
  141. [ { rowspan: 2, contents: '11[]' }, '12', '13' ],
  142. [ '22', '23' ],
  143. [ '31', '32', '33' ]
  144. ], { headingColumns: 3, headingRows: 1 } ) );
  145. tableUtils.insertRows( root.getNodeByPath( [ 0 ] ), { at: 2, rows: 3 } );
  146. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  147. [ { rowspan: 2, contents: '11[]' }, '12', '13' ],
  148. [ '22', '23' ],
  149. [ '', '', '' ],
  150. [ '', '', '' ],
  151. [ '', '', '' ],
  152. [ '31', '32', '33' ]
  153. ], { headingColumns: 3, headingRows: 1 } ) );
  154. } );
  155. it( 'should properly calculate columns if next row has colspans', () => {
  156. setData( model, modelTable( [
  157. [ { rowspan: 2, contents: '11[]' }, '12', '13' ],
  158. [ '22', '23' ],
  159. [ { colspan: 3, contents: '31' } ]
  160. ], { headingColumns: 3, headingRows: 1 } ) );
  161. tableUtils.insertRows( root.getNodeByPath( [ 0 ] ), { at: 2, rows: 3 } );
  162. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  163. [ { rowspan: 2, contents: '11[]' }, '12', '13' ],
  164. [ '22', '23' ],
  165. [ '', '', '' ],
  166. [ '', '', '' ],
  167. [ '', '', '' ],
  168. [ { colspan: 3, contents: '31' } ]
  169. ], { headingColumns: 3, headingRows: 1 } ) );
  170. } );
  171. it( 'should insert rows at the end of a table', () => {
  172. setData( model, modelTable( [
  173. [ '11[]', '12' ],
  174. [ '21', '22' ]
  175. ] ) );
  176. tableUtils.insertRows( root.getNodeByPath( [ 0 ] ), { at: 2, rows: 3 } );
  177. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  178. [ '11[]', '12' ],
  179. [ '21', '22' ],
  180. [ '', '' ],
  181. [ '', '' ],
  182. [ '', '' ]
  183. ] ) );
  184. } );
  185. } );
  186. describe( 'insertColumns()', () => {
  187. it( 'should insert column in given table at given index', () => {
  188. setData( model, modelTable( [
  189. [ '11[]', '12' ],
  190. [ '21', '22' ]
  191. ] ) );
  192. tableUtils.insertColumns( root.getNodeByPath( [ 0 ] ), { at: 1 } );
  193. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  194. [ '11[]', '', '12' ],
  195. [ '21', '', '22' ]
  196. ] ) );
  197. } );
  198. it( 'should insert column in given table with default values', () => {
  199. setData( model, modelTable( [
  200. [ '11[]', '12' ],
  201. [ '21', '22' ]
  202. ] ) );
  203. tableUtils.insertColumns( root.getNodeByPath( [ 0 ] ) );
  204. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  205. [ '', '11[]', '12' ],
  206. [ '', '21', '22' ]
  207. ] ) );
  208. } );
  209. it( 'should insert column in given table at default index', () => {
  210. setData( model, modelTable( [
  211. [ '11[]', '12' ],
  212. [ '21', '22' ]
  213. ] ) );
  214. tableUtils.insertColumns( root.getNodeByPath( [ 0 ] ) );
  215. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  216. [ '', '11[]', '12' ],
  217. [ '', '21', '22' ]
  218. ] ) );
  219. } );
  220. it( 'should insert columns at the end of a row', () => {
  221. setData( model, modelTable( [
  222. [ '00[]', '01' ],
  223. [ { colspan: 2, contents: '10' } ],
  224. [ '20', { rowspan: 2, contents: '21' } ],
  225. [ '30' ]
  226. ] ) );
  227. tableUtils.insertColumns( root.getNodeByPath( [ 0 ] ), { at: 2, columns: 2 } );
  228. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  229. [ '00[]', '01', '', '' ],
  230. [ { colspan: 2, contents: '10' }, '', '' ],
  231. [ '20', { rowspan: 2, contents: '21' }, '', '' ],
  232. [ '30', '', '' ]
  233. ] ) );
  234. } );
  235. it( 'should insert columns at the beginning of a row', () => {
  236. setData( model, modelTable( [
  237. [ '00[]', '01' ],
  238. [ { colspan: 2, contents: '10' } ],
  239. [ '20', { rowspan: 2, contents: '21' } ],
  240. [ { rowspan: 2, contents: '30' } ],
  241. [ '41' ]
  242. ] ) );
  243. tableUtils.insertColumns( root.getNodeByPath( [ 0 ] ), { at: 0, columns: 2 } );
  244. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  245. [ '', '', '00[]', '01' ],
  246. [ '', '', { colspan: 2, contents: '10' } ],
  247. [ '', '', '20', { rowspan: 2, contents: '21' } ],
  248. [ '', '', { rowspan: 2, contents: '30' } ],
  249. [ '', '', '41' ]
  250. ] ) );
  251. } );
  252. it( 'should update table heading columns attribute when inserting column in headings section', () => {
  253. setData( model, modelTable( [
  254. [ '11[]', '12' ],
  255. [ '21', '22' ],
  256. [ '31', '32' ]
  257. ], { headingColumns: 2 } ) );
  258. tableUtils.insertColumns( root.getNodeByPath( [ 0 ] ), { at: 1 } );
  259. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  260. [ '11[]', '', '12' ],
  261. [ '21', '', '22' ],
  262. [ '31', '', '32' ]
  263. ], { headingColumns: 3 } ) );
  264. } );
  265. it( 'should not update table heading columns attribute when inserting column after headings section', () => {
  266. setData( model, modelTable( [
  267. [ '11[]', '12', '13' ],
  268. [ '21', '22', '23' ],
  269. [ '31', '32', '33' ]
  270. ], { headingColumns: 2 } ) );
  271. tableUtils.insertColumns( root.getNodeByPath( [ 0 ] ), { at: 2 } );
  272. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  273. [ '11[]', '12', '', '13' ],
  274. [ '21', '22', '', '23' ],
  275. [ '31', '32', '', '33' ]
  276. ], { headingColumns: 2 } ) );
  277. } );
  278. it( 'should expand spanned columns', () => {
  279. setData( model, modelTable( [
  280. [ '11[]', '12' ],
  281. [ { colspan: 2, contents: '21' } ],
  282. [ '31', '32' ]
  283. ], { headingColumns: 2 } ) );
  284. tableUtils.insertColumns( root.getNodeByPath( [ 0 ] ), { at: 1 } );
  285. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  286. [ '11[]', '', '12' ],
  287. [ { colspan: 3, contents: '21' } ],
  288. [ '31', '', '32' ]
  289. ], { headingColumns: 3 } ) );
  290. } );
  291. it( 'should skip wide spanned columns', () => {
  292. setData( model, modelTable( [
  293. [ '11[]', '12', '13', '14', '15' ],
  294. [ '21', '22', { colspan: 2, contents: '23' }, '25' ],
  295. [ { colspan: 4, contents: '31' }, { colspan: 2, contents: '34' } ]
  296. ], { headingColumns: 4 } ) );
  297. tableUtils.insertColumns( root.getNodeByPath( [ 0 ] ), { at: 2, columns: 2 } );
  298. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  299. [ '11[]', '12', '', '', '13', '14', '15' ],
  300. [ '21', '22', '', '', { colspan: 2, contents: '23' }, '25' ],
  301. [ { colspan: 6, contents: '31' }, { colspan: 2, contents: '34' } ]
  302. ], { headingColumns: 6 } ) );
  303. } );
  304. it( 'should skip row spanned cells', () => {
  305. setData( model, modelTable( [
  306. [ { colspan: 2, rowspan: 2, contents: '00[]' }, '02' ],
  307. [ '12' ],
  308. [ '20', '21', '22' ]
  309. ], { headingColumns: 2 } ) );
  310. tableUtils.insertColumns( root.getNodeByPath( [ 0 ] ), { at: 1, columns: 2 } );
  311. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  312. [ { colspan: 4, rowspan: 2, contents: '00[]' }, '02' ],
  313. [ '12' ],
  314. [ '20', '', '', '21', '22' ]
  315. ], { headingColumns: 4 } ) );
  316. } );
  317. } );
  318. describe( 'splitCellHorizontally()', () => {
  319. it( 'should split table cell to given table cells number', () => {
  320. setData( model, modelTable( [
  321. [ '00', '01', '02' ],
  322. [ '10', '[]11', '12' ],
  323. [ '20', { colspan: 2, contents: '21' } ],
  324. [ { colspan: 2, contents: '30' }, '32' ]
  325. ] ) );
  326. tableUtils.splitCellHorizontally( root.getNodeByPath( [ 0, 1, 1 ] ), 3 );
  327. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  328. [ '00', { colspan: 3, contents: '01' }, '02' ],
  329. [ '10', '[]11', '', '', '12' ],
  330. [ '20', { colspan: 4, contents: '21' } ],
  331. [ { colspan: 4, contents: '30' }, '32' ]
  332. ] ) );
  333. } );
  334. it( 'should split table cell for two table cells as a default', () => {
  335. setData( model, modelTable( [
  336. [ '00', '01', '02' ],
  337. [ '10', '[]11', '12' ],
  338. [ '20', { colspan: 2, contents: '21' } ],
  339. [ { colspan: 2, contents: '30' }, '32' ]
  340. ] ) );
  341. tableUtils.splitCellHorizontally( root.getNodeByPath( [ 0, 1, 1 ] ) );
  342. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  343. [ '00', { colspan: 2, contents: '01' }, '02' ],
  344. [ '10', '[]11', '', '12' ],
  345. [ '20', { colspan: 3, contents: '21' } ],
  346. [ { colspan: 3, contents: '30' }, '32' ]
  347. ] ) );
  348. } );
  349. it( 'should unsplit table cell if split is equal to colspan', () => {
  350. setData( model, modelTable( [
  351. [ '00', '01', '02' ],
  352. [ '10', '11', '12' ],
  353. [ '20', { colspan: 2, contents: '21[]' } ],
  354. [ { colspan: 2, contents: '30' }, '32' ]
  355. ] ) );
  356. tableUtils.splitCellHorizontally( root.getNodeByPath( [ 0, 2, 1 ] ), 2 );
  357. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  358. [ '00', '01', '02' ],
  359. [ '10', '11', '12' ],
  360. [ '20', '21[]', '' ],
  361. [ { colspan: 2, contents: '30' }, '32' ]
  362. ] ) );
  363. } );
  364. it( 'should properly unsplit table cell if split is uneven', () => {
  365. setData( model, modelTable( [
  366. [ '00', '01', '02' ],
  367. [ { colspan: 3, contents: '10[]' } ]
  368. ] ) );
  369. tableUtils.splitCellHorizontally( root.getNodeByPath( [ 0, 1, 0 ] ), 2 );
  370. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  371. [ '00', '01', '02' ],
  372. [ { colspan: 2, contents: '10[]' }, '' ]
  373. ] ) );
  374. } );
  375. it( 'should properly set colspan of inserted cells', () => {
  376. setData( model, modelTable( [
  377. [ '00', '01', '02', '03' ],
  378. [ { colspan: 4, contents: '10[]' } ]
  379. ] ) );
  380. tableUtils.splitCellHorizontally( root.getNodeByPath( [ 0, 1, 0 ] ), 2 );
  381. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  382. [ '00', '01', '02', '03' ],
  383. [ { colspan: 2, contents: '10[]' }, { colspan: 2, contents: '' } ]
  384. ] ) );
  385. } );
  386. it( 'should keep rowspan attribute for newly inserted cells', () => {
  387. setData( model, modelTable( [
  388. [ '00', '01', '02', '03', '04', '05' ],
  389. [ { colspan: 5, rowspan: 2, contents: '10[]' }, '15' ],
  390. [ '25' ]
  391. ] ) );
  392. tableUtils.splitCellHorizontally( root.getNodeByPath( [ 0, 1, 0 ] ), 2 );
  393. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  394. [ '00', '01', '02', '03', '04', '05' ],
  395. [ { colspan: 3, rowspan: 2, contents: '10[]' }, { colspan: 2, rowspan: 2, contents: '' }, '15' ],
  396. [ '25' ]
  397. ] ) );
  398. } );
  399. } );
  400. describe( 'splitCellVertically()', () => {
  401. it( 'should split table cell to default table cells number', () => {
  402. setData( model, modelTable( [
  403. [ '00', '01', '02' ],
  404. [ '10', '[]11', '12' ],
  405. [ '20', '21', '22' ]
  406. ] ) );
  407. tableUtils.splitCellVertically( root.getNodeByPath( [ 0, 1, 1 ] ) );
  408. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  409. [ '00', '01', '02' ],
  410. [ { rowspan: 2, contents: '10' }, '[]11', { rowspan: 2, contents: '12' } ],
  411. [ '' ],
  412. [ '20', '21', '22' ]
  413. ] ) );
  414. } );
  415. it( 'should split table cell to given table cells number', () => {
  416. setData( model, modelTable( [
  417. [ '00', '01', '02' ],
  418. [ '10', '[]11', '12' ],
  419. [ '20', '21', '22' ]
  420. ] ) );
  421. tableUtils.splitCellVertically( root.getNodeByPath( [ 0, 1, 1 ] ), 4 );
  422. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  423. [ '00', '01', '02' ],
  424. [ { rowspan: 4, contents: '10' }, '[]11', { rowspan: 4, contents: '12' } ],
  425. [ '' ],
  426. [ '' ],
  427. [ '' ],
  428. [ '20', '21', '22' ]
  429. ] ) );
  430. } );
  431. it( 'should properly update rowspanned cells overlapping selected cell', () => {
  432. setData( model, modelTable( [
  433. [ { rowspan: 2, contents: '00' }, '01', { rowspan: 3, contents: '02' } ],
  434. [ '[]11' ],
  435. [ '20', '21' ]
  436. ] ) );
  437. tableUtils.splitCellVertically( root.getNodeByPath( [ 0, 1, 0 ] ), 3 );
  438. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  439. [ { rowspan: 4, contents: '00' }, '01', { rowspan: 5, contents: '02' } ],
  440. [ '[]11' ],
  441. [ '' ],
  442. [ '' ],
  443. [ '20', '21' ]
  444. ] ) );
  445. } );
  446. } );
  447. describe( 'getColumns()', () => {
  448. it( 'should return proper number of columns', () => {
  449. setData( model, modelTable( [
  450. [ '00', { colspan: 3, contents: '01' }, '04' ]
  451. ] ) );
  452. expect( tableUtils.getColumns( root.getNodeByPath( [ 0 ] ) ) ).to.equal( 5 );
  453. } );
  454. } );
  455. } );