mergecellscommand.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  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 { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  7. import MergeCellsCommand from '../../src/commands/mergecellscommand';
  8. import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from '../_utils/utils';
  9. import TableUtils from '../../src/tableutils';
  10. import TableSelection from '../../src/tableselection';
  11. import Range from '../../../ckeditor5-engine/src/model/range';
  12. describe( 'MergeCellsCommand', () => {
  13. let editor, model, command, root;
  14. beforeEach( () => {
  15. return ModelTestEditor
  16. .create( {
  17. plugins: [ TableUtils, TableSelection ]
  18. } )
  19. .then( newEditor => {
  20. editor = newEditor;
  21. model = editor.model;
  22. root = model.document.getRoot( 'main' );
  23. command = new MergeCellsCommand( editor );
  24. defaultSchema( model.schema );
  25. defaultConversion( editor.conversion );
  26. } );
  27. } );
  28. afterEach( () => {
  29. return editor.destroy();
  30. } );
  31. describe( 'isEnabled', () => {
  32. it( 'should be false if collapsed selection in table cell', () => {
  33. setData( model, modelTable( [
  34. [ '00[]', '01' ]
  35. ] ) );
  36. expect( command.isEnabled ).to.be.false;
  37. } );
  38. it( 'should be false if only one table cell is selected', () => {
  39. setData( model, modelTable( [
  40. [ '00', '01' ]
  41. ] ) );
  42. selectNodes( [ [ 0, 0, 0 ] ] );
  43. expect( command.isEnabled ).to.be.false;
  44. } );
  45. it( 'should be true if at least two adjacent table cells are selected', () => {
  46. setData( model, modelTable( [
  47. [ '00', '01' ]
  48. ] ) );
  49. selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] );
  50. expect( command.isEnabled ).to.be.true;
  51. } );
  52. it( 'should be true if many table cells are selected', () => {
  53. setData( model, modelTable( [
  54. [ '00', '01', '02', '03' ],
  55. [ '10', '11', '12', '13' ],
  56. [ '20', '21', '22', '23' ],
  57. [ '30', '31', '32', '33' ]
  58. ] ) );
  59. selectNodes( [
  60. [ 0, 0, 1 ], [ 0, 0, 2 ],
  61. [ 0, 1, 1 ], [ 0, 1, 2 ],
  62. [ 0, 2, 1 ], [ 0, 2, 2 ],
  63. [ 0, 3, 1 ], [ 0, 3, 2 ]
  64. ] );
  65. expect( command.isEnabled ).to.be.true;
  66. } );
  67. it( 'should be false if at least one table cell is not selected from an area', () => {
  68. setData( model, modelTable( [
  69. [ '00', '01', '02', '03' ],
  70. [ '10', '11', '12', '13' ],
  71. [ '20', '21', '22', '23' ],
  72. [ '30', '31', '32', '33' ]
  73. ] ) );
  74. selectNodes( [
  75. [ 0, 0, 1 ], [ 0, 0, 2 ],
  76. [ 0, 1, 2 ], // one table cell not selected from this row
  77. [ 0, 2, 1 ], [ 0, 2, 2 ],
  78. [ 0, 3, 1 ], [ 0, 3, 2 ]
  79. ] );
  80. expect( command.isEnabled ).to.be.false;
  81. } );
  82. it( 'should be false if table cells are not in adjacent rows', () => {
  83. setData( model, modelTable( [
  84. [ '00', '01' ],
  85. [ '10', '11' ]
  86. ] ) );
  87. selectNodes( [
  88. [ 0, 1, 0 ],
  89. [ 0, 0, 1 ]
  90. ] );
  91. expect( command.isEnabled ).to.be.false;
  92. } );
  93. it( 'should be false if table cells are not in adjacent columns', () => {
  94. setData( model, modelTable( [
  95. [ '00', '01', '02' ]
  96. ] ) );
  97. selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 2 ] ] );
  98. expect( command.isEnabled ).to.be.false;
  99. } );
  100. it( 'should be false if any range is collapsed in selection', () => {
  101. setData( model, modelTable( [
  102. [ '00', '01', '02' ]
  103. ] ) );
  104. selectNodes( [
  105. [ 0, 0, 0, 0, 0 ], // The "00" text node
  106. [ 0, 0, 1 ]
  107. ] );
  108. expect( command.isEnabled ).to.be.false;
  109. } );
  110. it( 'should be false if any ranges are on different tables', () => {
  111. setData( model,
  112. modelTable( [ [ '00', '01' ] ] ) +
  113. modelTable( [ [ 'aa', 'ab' ] ] )
  114. );
  115. selectNodes( [
  116. [ 0, 0, 0 ], // first table
  117. [ 1, 0, 1 ] // second table
  118. ] );
  119. expect( command.isEnabled ).to.be.false;
  120. } );
  121. it( 'should be false if any table cell with colspan attribute extends over selection area', () => {
  122. setData( model, modelTable( [
  123. [ '00', { colspan: 2, contents: '01' } ],
  124. [ '10', '11', '12' ]
  125. ] ) );
  126. selectNodes( [
  127. [ 0, 0, 0 ], [ 0, 0, 1 ],
  128. [ 0, 1, 0 ], [ 0, 1, 1 ]
  129. ] );
  130. expect( command.isEnabled ).to.be.false;
  131. } );
  132. it( 'should be true if none table cell with colspan attribute extends over selection area', () => {
  133. setData( model, modelTable( [
  134. [ '00', { colspan: 2, contents: '01' } ],
  135. [ '10', '11', '12' ]
  136. ] ) );
  137. selectNodes( [
  138. [ 0, 0, 0 ], [ 0, 0, 1 ],
  139. [ 0, 1, 0 ], [ 0, 1, 1 ],
  140. [ 0, 1, 2 ]
  141. ] );
  142. expect( command.isEnabled ).to.be.true;
  143. } );
  144. it( 'should be true if first table cell is inside selection area', () => {
  145. setData( model, modelTable( [
  146. [ { colspan: 2, rowspan: 2, contents: '00' }, '02', '03' ],
  147. [ '12', '13' ]
  148. ] ) );
  149. selectNodes( [
  150. [ 0, 0, 0 ], [ 0, 0, 1 ],
  151. [ 0, 1, 0 ]
  152. ] );
  153. expect( command.isEnabled ).to.be.true;
  154. } );
  155. it( 'should be false if any table cell with rowspan attribute extends over selection area', () => {
  156. setData( model, modelTable( [
  157. [ '00', { rowspan: 2, contents: '01' } ],
  158. [ '10' ]
  159. ] ) );
  160. selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] );
  161. expect( command.isEnabled ).to.be.false;
  162. } );
  163. it( 'should be true if none table cell with rowspan attribute extends over selection area', () => {
  164. setData( model, modelTable( [
  165. [ '00', { rowspan: 2, contents: '01' } ],
  166. [ '10' ]
  167. ] ) );
  168. selectNodes( [
  169. [ 0, 0, 0 ], [ 0, 0, 1 ],
  170. [ 0, 1, 0 ]
  171. ] );
  172. expect( command.isEnabled ).to.be.true;
  173. } );
  174. it( 'should be false if not in a cell', () => {
  175. setData( model, '<paragraph>11[]</paragraph>' );
  176. expect( command.isEnabled ).to.be.false;
  177. } );
  178. } );
  179. describe( 'execute()', () => {
  180. it( 'should merge table cells', () => {
  181. setData( model, modelTable( [
  182. [ '[]00', '01' ]
  183. ] ) );
  184. selectNodes( [
  185. [ 0, 0, 0 ], [ 0, 0, 1 ]
  186. ] );
  187. command.execute();
  188. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  189. [ { colspan: 2, contents: '[<paragraph>00</paragraph><paragraph>01</paragraph>]' } ]
  190. ] ) );
  191. } );
  192. it( 'should merge table cells - extend colspan attribute', () => {
  193. setData( model, modelTable( [
  194. [ { colspan: 2, contents: '00' }, '02', '03' ],
  195. [ '10', '11', '12', '13' ]
  196. ] ) );
  197. selectNodes( [
  198. [ 0, 0, 0 ], [ 0, 0, 1 ],
  199. [ 0, 1, 0 ], [ 0, 1, 1 ], [ 0, 1, 2 ]
  200. ] );
  201. command.execute();
  202. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  203. [ {
  204. colspan: 3,
  205. rowspan: 2,
  206. contents: '[<paragraph>00</paragraph>' +
  207. '<paragraph>02</paragraph>' +
  208. '<paragraph>10</paragraph>' +
  209. '<paragraph>11</paragraph>' +
  210. '<paragraph>12</paragraph>]'
  211. }, '03' ],
  212. [ '13' ]
  213. ] ) );
  214. } );
  215. it( 'should merge to a single paragraph - every cell is empty', () => {
  216. setData( model, modelTable( [
  217. [ '[]', '' ]
  218. ] ) );
  219. selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] );
  220. command.execute();
  221. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  222. [ { colspan: 2, contents: '[<paragraph></paragraph>]' } ]
  223. ] ) );
  224. } );
  225. it( 'should merge to a single paragraph - merged cell is empty', () => {
  226. setData( model, modelTable( [
  227. [ 'foo', '' ]
  228. ] ) );
  229. selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] );
  230. command.execute();
  231. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  232. [ { colspan: 2, contents: '[<paragraph>foo</paragraph>]' } ]
  233. ] ) );
  234. } );
  235. it( 'should merge to a single paragraph - cell to which others are merged is empty', () => {
  236. setData( model, modelTable( [
  237. [ '', 'foo' ]
  238. ] ) );
  239. selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] );
  240. command.execute();
  241. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  242. [ { colspan: 2, contents: '[<paragraph>foo</paragraph>]' } ]
  243. ] ) );
  244. } );
  245. it( 'should not merge empty blocks other then <paragraph> to a single block', () => {
  246. model.schema.register( 'block', {
  247. allowWhere: '$block',
  248. allowContentOf: '$block',
  249. isBlock: true
  250. } );
  251. setData( model, modelTable( [
  252. [ '<block>[]</block>', '<block></block>' ]
  253. ] ) );
  254. selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] );
  255. command.execute();
  256. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  257. [ { colspan: 2, contents: '[<block></block><block></block>]' } ]
  258. ] ) );
  259. } );
  260. describe( 'removing empty row', () => {
  261. it( 'should remove empty row if merging all table cells from that row', () => {
  262. setData( model, modelTable( [
  263. [ '00' ],
  264. [ '10' ],
  265. [ '20' ]
  266. ] ) );
  267. selectNodes( [
  268. [ 0, 0, 0 ],
  269. [ 0, 1, 0 ],
  270. [ 0, 2, 0 ]
  271. ] );
  272. command.execute();
  273. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  274. [
  275. '[<paragraph>00</paragraph><paragraph>10</paragraph><paragraph>20</paragraph>]'
  276. ]
  277. ] ) );
  278. } );
  279. it( 'should decrease rowspan if cell overlaps removed row', () => {
  280. setData( model, modelTable( [
  281. [ '00', { rowspan: 2, contents: '01' }, { rowspan: 3, contents: '02' } ],
  282. [ '10' ],
  283. [ '20', '21' ]
  284. ] ) );
  285. selectNodes( [
  286. [ 0, 0, 0 ],
  287. [ 0, 1, 0 ],
  288. [ 0, 2, 0 ]
  289. ] );
  290. command.execute();
  291. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  292. [
  293. { rowspan: 2, contents: '[<paragraph>00</paragraph><paragraph>10</paragraph><paragraph>20</paragraph>]' },
  294. '01',
  295. { rowspan: 2, contents: '02' }
  296. ],
  297. [ '21' ]
  298. ] ) );
  299. } );
  300. it( 'should not decrease rowspan if cell from previous row does not overlaps removed row', () => {
  301. setData( model, modelTable( [
  302. [ '00', { rowspan: 2, contents: '01' } ],
  303. [ '10' ],
  304. [ '20', '21' ],
  305. [ '30', '31' ]
  306. ] ) );
  307. selectNodes( [
  308. [ 0, 2, 0 ], [ 0, 2, 1 ],
  309. [ 0, 3, 0 ], [ 0, 3, 1 ]
  310. ] );
  311. command.execute();
  312. expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
  313. [ '00', { rowspan: 2, contents: '01' } ],
  314. [ '10' ],
  315. [
  316. {
  317. colspan: 2,
  318. contents: '[<paragraph>20</paragraph><paragraph>21</paragraph>' +
  319. '<paragraph>30</paragraph><paragraph>31</paragraph>]'
  320. }
  321. ]
  322. ] ) );
  323. } );
  324. } );
  325. } );
  326. function selectNodes( paths ) {
  327. const ranges = paths.map( path => Range.createOn( root.getNodeByPath( path ) ) );
  328. model.change( writer => {
  329. writer.setSelection( ranges );
  330. } );
  331. }
  332. } );