mergecellscommand.js 10 KB

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