tableselection.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  6. import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
  7. import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  8. import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
  9. import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
  10. import TableEditing from '../src/tableediting';
  11. import TableSelection from '../src/tableselection';
  12. import { modelTable, viewTable } from './_utils/utils';
  13. describe( 'table selection', () => {
  14. let editor, model, tableSelection, modelRoot;
  15. beforeEach( async () => {
  16. editor = await VirtualTestEditor.create( {
  17. plugins: [ TableEditing, TableSelection, Paragraph ]
  18. } );
  19. model = editor.model;
  20. modelRoot = model.document.getRoot();
  21. tableSelection = editor.plugins.get( TableSelection );
  22. setModelData( model, modelTable( [
  23. [ '11[]', '12', '13' ],
  24. [ '21', '22', '23' ],
  25. [ '31', '32', '33' ]
  26. ] ) );
  27. } );
  28. afterEach( async () => {
  29. await editor.destroy();
  30. } );
  31. describe( 'TableSelection', () => {
  32. describe( 'hasValidSelection()', () => {
  33. it( 'should be false if selection is not started', () => {
  34. expect( tableSelection.hasValidSelection ).to.be.false;
  35. } );
  36. it( 'should be true if selection is selecting two different cells', () => {
  37. tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
  38. tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
  39. expect( tableSelection.hasValidSelection ).to.be.true;
  40. } );
  41. it( 'should be false if selection start/end is selecting the same table cell', () => {
  42. tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
  43. tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
  44. expect( tableSelection.hasValidSelection ).to.be.false;
  45. } );
  46. it( 'should be false if selection has no end cell', () => {
  47. tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
  48. expect( tableSelection.hasValidSelection ).to.be.false;
  49. } );
  50. it( 'should be false if selection has ended', () => {
  51. tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
  52. tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
  53. tableSelection.stopSelection();
  54. expect( tableSelection.hasValidSelection ).to.be.false;
  55. } );
  56. } );
  57. describe( 'startSelectingFrom()', () => {
  58. it( 'should not change model selection', () => {
  59. const spy = sinon.spy();
  60. model.document.selection.on( 'change', spy );
  61. tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
  62. sinon.assert.notCalled( spy );
  63. } );
  64. } );
  65. describe( 'setSelectingTo()', () => {
  66. it( 'should not change model selection if selection is not started', () => {
  67. const spy = sinon.spy();
  68. model.document.selection.on( 'change', spy );
  69. tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
  70. sinon.assert.notCalled( spy );
  71. } );
  72. it( 'should change model selection if valid selection will be set', () => {
  73. const spy = sinon.spy();
  74. model.document.selection.on( 'change', spy );
  75. tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
  76. tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
  77. sinon.assert.calledOnce( spy );
  78. } );
  79. it( 'should not change model selection if passed table cell is from other table then start cell', () => {
  80. setModelData( model,
  81. modelTable( [
  82. [ '11[]', '12', '13' ],
  83. [ '21', '22', '23' ],
  84. [ '31', '32', '33' ]
  85. ] ) +
  86. modelTable( [
  87. [ 'a', 'b' ],
  88. [ 'c', 'd' ]
  89. ] )
  90. );
  91. const spy = sinon.spy();
  92. model.document.selection.on( 'change', spy );
  93. tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
  94. tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 1, 0, 1 ] ) );
  95. sinon.assert.notCalled( spy );
  96. } );
  97. it( 'should select two table cells', () => {
  98. tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
  99. tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
  100. assertSelectedCells( [
  101. [ 1, 1, 0 ],
  102. [ 0, 0, 0 ],
  103. [ 0, 0, 0 ]
  104. ] );
  105. } );
  106. it( 'should select four table cells for diagonal selection', () => {
  107. tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
  108. tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 1, 1 ] ) );
  109. assertSelectedCells( [
  110. [ 1, 1, 0 ],
  111. [ 1, 1, 0 ],
  112. [ 0, 0, 0 ]
  113. ] );
  114. } );
  115. it( 'should select row table cells', () => {
  116. tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
  117. tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 2 ] ) );
  118. assertSelectedCells( [
  119. [ 1, 1, 1 ],
  120. [ 0, 0, 0 ],
  121. [ 0, 0, 0 ]
  122. ] );
  123. } );
  124. it( 'should select column table cells', () => {
  125. tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
  126. tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 2, 1 ] ) );
  127. assertSelectedCells( [
  128. [ 0, 1, 0 ],
  129. [ 0, 1, 0 ],
  130. [ 0, 1, 0 ]
  131. ] );
  132. } );
  133. it( 'should create proper selection on consecutive changes', () => {
  134. tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 1, 1 ] ) );
  135. tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 2, 1 ] ) );
  136. assertSelectedCells( [
  137. [ 0, 0, 0 ],
  138. [ 0, 1, 0 ],
  139. [ 0, 1, 0 ]
  140. ] );
  141. tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
  142. assertSelectedCells( [
  143. [ 0, 1, 0 ],
  144. [ 0, 1, 0 ],
  145. [ 0, 0, 0 ]
  146. ] );
  147. tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 2, 2 ] ) );
  148. assertSelectedCells( [
  149. [ 0, 0, 0 ],
  150. [ 0, 1, 1 ],
  151. [ 0, 1, 1 ]
  152. ] );
  153. } );
  154. } );
  155. describe( 'stopSelection()', () => {} );
  156. describe( 'clearSelection()', () => {} );
  157. describe( '* getSelectedTableCells()', () => {} );
  158. } );
  159. describe( 'mouse selection', () => {
  160. let view, domEvtDataStub;
  161. beforeEach( () => {
  162. view = editor.editing.view;
  163. domEvtDataStub = {
  164. domEvent: {
  165. buttons: 1
  166. },
  167. target: undefined,
  168. preventDefault: sinon.spy(),
  169. stopPropagation: sinon.spy()
  170. };
  171. } );
  172. it( 'should not start table selection when mouse move is inside one table cell', () => {
  173. setModelData( model, modelTable( [
  174. [ '[]00', '01' ],
  175. [ '10', '11' ]
  176. ] ) );
  177. selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 );
  178. view.document.fire( 'mousedown', domEvtDataStub );
  179. assertEqualMarkup( getModelData( model ), modelTable( [
  180. [ '[]00', '01' ],
  181. [ '10', '11' ]
  182. ] ) );
  183. view.document.fire( 'mousemove', domEvtDataStub );
  184. assertEqualMarkup( getModelData( model ), modelTable( [
  185. [ '[]00', '01' ],
  186. [ '10', '11' ]
  187. ] ) );
  188. } );
  189. it( 'should start table selection when mouse move expands over two cells', () => {
  190. setModelData( model, modelTable( [
  191. [ '[]00', '01' ],
  192. [ '10', '11' ]
  193. ] ) );
  194. selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 );
  195. view.document.fire( 'mousedown', domEvtDataStub );
  196. assertEqualMarkup( getModelData( model ), modelTable( [
  197. [ '[]00', '01' ],
  198. [ '10', '11' ]
  199. ] ) );
  200. selectTableCell( domEvtDataStub, view, 0, 0, 0, 1 );
  201. view.document.fire( 'mousemove', domEvtDataStub );
  202. assertEqualMarkup( getModelData( model ), modelTable( [
  203. [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ],
  204. [ '10', '11' ]
  205. ] ) );
  206. assertEqualMarkup( getViewData( view ), viewTable( [
  207. [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ],
  208. [ '10', '11' ]
  209. ], { asWidget: true } ) );
  210. } );
  211. it( 'should select rectangular table cells when mouse moved to diagonal cell (up -> down)', () => {
  212. setModelData( model, modelTable( [
  213. [ '[]00', '01' ],
  214. [ '10', '11' ]
  215. ] ) );
  216. selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 );
  217. view.document.fire( 'mousedown', domEvtDataStub );
  218. assertEqualMarkup( getModelData( model ), modelTable( [
  219. [ '[]00', '01' ],
  220. [ '10', '11' ]
  221. ] ) );
  222. selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 );
  223. view.document.fire( 'mousemove', domEvtDataStub );
  224. assertEqualMarkup( getModelData( model ), modelTable( [
  225. [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ],
  226. [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true } ]
  227. ] ) );
  228. assertEqualMarkup( getViewData( view ), viewTable( [
  229. [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ],
  230. [ { contents: '10', class: 'selected', isSelected: true }, { contents: '11', class: 'selected', isSelected: true } ]
  231. ], { asWidget: true } ) );
  232. } );
  233. it( 'should select rectangular table cells when mouse moved to diagonal cell (down -> up)', () => {
  234. setModelData( model, modelTable( [
  235. [ '00', '01' ],
  236. [ '10', '[]11' ]
  237. ] ) );
  238. selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 );
  239. view.document.fire( 'mousedown', domEvtDataStub );
  240. assertEqualMarkup( getModelData( model ), modelTable( [
  241. [ '00', '01' ],
  242. [ '10', '[]11' ]
  243. ] ) );
  244. selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 );
  245. view.document.fire( 'mousemove', domEvtDataStub );
  246. assertEqualMarkup( getModelData( model ), modelTable( [
  247. [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ],
  248. [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true } ]
  249. ] ) );
  250. assertEqualMarkup( getViewData( view ), viewTable( [
  251. [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ],
  252. [ { contents: '10', class: 'selected', isSelected: true }, { contents: '11', class: 'selected', isSelected: true } ]
  253. ], { asWidget: true } ) );
  254. } );
  255. it( 'should update view selection after changing selection rect', () => {
  256. setModelData( model, modelTable( [
  257. [ '[]00', '01', '02' ],
  258. [ '10', '11', '12' ],
  259. [ '20', '21', '22' ]
  260. ] ) );
  261. selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 );
  262. view.document.fire( 'mousedown', domEvtDataStub );
  263. selectTableCell( domEvtDataStub, view, 0, 0, 2, 2 );
  264. view.document.fire( 'mousemove', domEvtDataStub );
  265. assertEqualMarkup( getModelData( model ), modelTable( [
  266. [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true }, { contents: '02', isSelected: true } ],
  267. [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true }, { contents: '12', isSelected: true } ],
  268. [ { contents: '20', isSelected: true }, { contents: '21', isSelected: true }, { contents: '22', isSelected: true } ]
  269. ] ) );
  270. assertEqualMarkup( getViewData( view ), viewTable( [
  271. [
  272. { contents: '00', class: 'selected', isSelected: true },
  273. { contents: '01', class: 'selected', isSelected: true },
  274. { contents: '02', class: 'selected', isSelected: true }
  275. ],
  276. [
  277. { contents: '10', class: 'selected', isSelected: true },
  278. { contents: '11', class: 'selected', isSelected: true },
  279. { contents: '12', class: 'selected', isSelected: true }
  280. ],
  281. [
  282. { contents: '20', class: 'selected', isSelected: true },
  283. { contents: '21', class: 'selected', isSelected: true },
  284. { contents: '22', class: 'selected', isSelected: true }
  285. ]
  286. ], { asWidget: true } ) );
  287. selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 );
  288. view.document.fire( 'mousemove', domEvtDataStub );
  289. assertEqualMarkup( getModelData( model ), modelTable( [
  290. [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true }, '02' ],
  291. [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true }, '12' ],
  292. [ '20', '21', '22' ]
  293. ] ) );
  294. assertEqualMarkup( getViewData( view ), viewTable( [
  295. [
  296. { contents: '00', class: 'selected', isSelected: true },
  297. { contents: '01', class: 'selected', isSelected: true },
  298. '02'
  299. ],
  300. [
  301. { contents: '10', class: 'selected', isSelected: true },
  302. { contents: '11', class: 'selected', isSelected: true },
  303. '12'
  304. ],
  305. [
  306. '20',
  307. '21',
  308. '22'
  309. ]
  310. ], { asWidget: true } ) );
  311. } );
  312. it( 'should stop selecting after "mouseup" event', () => {
  313. setModelData( model, modelTable( [
  314. [ '[]00', '01' ],
  315. [ '10', '11' ]
  316. ] ) );
  317. selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 );
  318. view.document.fire( 'mousedown', domEvtDataStub );
  319. assertEqualMarkup( getModelData( model ), modelTable( [
  320. [ '[]00', '01' ],
  321. [ '10', '11' ]
  322. ] ) );
  323. selectTableCell( domEvtDataStub, view, 0, 0, 0, 1 );
  324. view.document.fire( 'mousemove', domEvtDataStub );
  325. view.document.fire( 'mouseup', domEvtDataStub );
  326. assertEqualMarkup( getModelData( model ), modelTable( [
  327. [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ],
  328. [ '10', '11' ]
  329. ] ) );
  330. assertEqualMarkup( getViewData( view ), viewTable( [
  331. [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ],
  332. [ '10', '11' ]
  333. ], { asWidget: true } ) );
  334. selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 );
  335. view.document.fire( 'mousemove', domEvtDataStub );
  336. assertEqualMarkup( getModelData( model ), modelTable( [
  337. [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ],
  338. [ '10', '11' ]
  339. ] ) );
  340. } );
  341. it( 'should stop selection mode on "mouseleve" event', () => {
  342. setModelData( model, modelTable( [
  343. [ '[]00', '01' ],
  344. [ '10', '11' ]
  345. ] ) );
  346. selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 );
  347. view.document.fire( 'mousedown', domEvtDataStub );
  348. assertEqualMarkup( getModelData( model ), modelTable( [
  349. [ '[]00', '01' ],
  350. [ '10', '11' ]
  351. ] ) );
  352. selectTableCell( domEvtDataStub, view, 0, 0, 0, 1 );
  353. view.document.fire( 'mousemove', domEvtDataStub );
  354. view.document.fire( 'mouseleave', domEvtDataStub );
  355. assertEqualMarkup( getModelData( model ), modelTable( [
  356. [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ],
  357. [ '10', '11' ]
  358. ] ) );
  359. assertEqualMarkup( getViewData( view ), viewTable( [
  360. [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ],
  361. [ '10', '11' ]
  362. ], { asWidget: true } ) );
  363. selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 );
  364. view.document.fire( 'mousemove', domEvtDataStub );
  365. assertEqualMarkup( getModelData( model ), modelTable( [
  366. [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ],
  367. [ '10', '11' ]
  368. ] ) );
  369. } );
  370. it( 'should clear view table selection after mouse click outside table', () => {
  371. setModelData( model, modelTable( [
  372. [ '[]00', '01' ],
  373. [ '10', '11' ]
  374. ] ) + '<paragraph>foo</paragraph>' );
  375. selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 );
  376. view.document.fire( 'mousedown', domEvtDataStub );
  377. assertEqualMarkup( getModelData( model ), modelTable( [
  378. [ '[]00', '01' ],
  379. [ '10', '11' ]
  380. ] ) + '<paragraph>foo</paragraph>' );
  381. selectTableCell( domEvtDataStub, view, 0, 0, 0, 1 );
  382. view.document.fire( 'mousemove', domEvtDataStub );
  383. domEvtDataStub.target = view.document.getRoot().getChild( 1 );
  384. view.document.fire( 'mousemove', domEvtDataStub );
  385. view.document.fire( 'mousedown', domEvtDataStub );
  386. view.document.fire( 'mouseup', domEvtDataStub );
  387. // The click in the DOM would trigger selection change and it will set the selection:
  388. model.change( writer => {
  389. writer.setSelection( writer.createRange( writer.createPositionAt( model.document.getRoot().getChild( 1 ), 0 ) ) );
  390. } );
  391. assertEqualMarkup( getViewData( view ), viewTable( [
  392. [ '00', '01' ],
  393. [ '10', '11' ]
  394. ], { asWidget: true } ) + '<p>{}foo</p>' );
  395. } );
  396. } );
  397. // Helper method for asserting selected table cells.
  398. //
  399. // To check if a table has expected cells selected pass two dimensional array of truthy and falsy values:
  400. //
  401. // assertSelectedCells( [
  402. // [ 0, 1 ],
  403. // [ 0, 1 ]
  404. // ] );
  405. //
  406. // The above call will check if table has second column selected (assuming no spans).
  407. //
  408. // **Note**: This function operates on child indexes - not rows/columns.
  409. function assertSelectedCells( tableMap ) {
  410. const tableIndex = 0;
  411. for ( let rowIndex = 0; rowIndex < tableMap.length; rowIndex++ ) {
  412. const row = tableMap[ rowIndex ];
  413. for ( let cellIndex = 0; cellIndex < row.length; cellIndex++ ) {
  414. const expectSelected = row[ cellIndex ];
  415. if ( expectSelected ) {
  416. assertNodeIsSelected( [ tableIndex, rowIndex, cellIndex ] );
  417. } else {
  418. assertNodeIsNotSelected( [ tableIndex, rowIndex, cellIndex ] );
  419. }
  420. }
  421. }
  422. }
  423. function assertNodeIsSelected( path ) {
  424. const node = modelRoot.getNodeByPath( path );
  425. const selectionRanges = Array.from( model.document.selection.getRanges() );
  426. expect( selectionRanges.some( range => range.containsItem( node ) ), `Expected node [${ path }] to be selected` ).to.be.true;
  427. }
  428. function assertNodeIsNotSelected( path ) {
  429. const node = modelRoot.getNodeByPath( path );
  430. const selectionRanges = Array.from( model.document.selection.getRanges() );
  431. expect( selectionRanges.every( range => !range.containsItem( node ) ), `Expected node [${ path }] to be not selected` ).to.be.true;
  432. }
  433. } );
  434. function selectTableCell( domEvtDataStub, view, tableIndex, sectionIndex, rowInSectionIndex, tableCellIndex ) {
  435. domEvtDataStub.target = view.document.getRoot()
  436. .getChild( tableIndex )
  437. .getChild( 1 ) // Table is second in widget
  438. .getChild( sectionIndex )
  439. .getChild( rowInSectionIndex )
  440. .getChild( tableCellIndex );
  441. }