tableselection.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986
  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. /* globals document, console */
  6. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  7. import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
  8. import {
  9. getData as getModelData,
  10. setData as setModelData,
  11. stringify as stringifyModel
  12. } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  13. import TableEditing from '../src/tableediting';
  14. import TableSelection from '../src/tableselection';
  15. import { assertSelectedCells, modelTable } from './_utils/utils';
  16. import DocumentFragment from '@ckeditor/ckeditor5-engine/src/model/documentfragment';
  17. import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
  18. import Typing from '@ckeditor/ckeditor5-typing/src/typing';
  19. import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
  20. describe( 'table selection', () => {
  21. let editorElement, editor, model, tableSelection, modelRoot, view, viewDocument;
  22. beforeEach( () => {
  23. editorElement = document.createElement( 'div' );
  24. document.body.appendChild( editorElement );
  25. } );
  26. afterEach( async () => {
  27. editorElement.remove();
  28. await editor.destroy();
  29. } );
  30. describe( 'init()', () => {
  31. beforeEach( async () => {
  32. editor = await createEditor();
  33. model = editor.model;
  34. modelRoot = model.document.getRoot();
  35. tableSelection = editor.plugins.get( TableSelection );
  36. setModelData( model, modelTable( [
  37. [ '11[]', '12', '13' ],
  38. [ '21', '22', '23' ],
  39. [ '31', '32', '33' ]
  40. ] ) );
  41. } );
  42. describe( 'plugin disabling support', () => {
  43. it( 'should collapse multi-cell selection when the plugin gets disabled', () => {
  44. const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
  45. const lastCell = modelRoot.getNodeByPath( [ 0, 1, 1 ] );
  46. tableSelection.setCellSelection(
  47. firstCell,
  48. lastCell
  49. );
  50. tableSelection.forceDisabled( 'foo' );
  51. const ranges = [ ...model.document.selection.getRanges() ];
  52. expect( ranges ).to.have.length( 1 );
  53. expect( ranges[ 0 ].isCollapsed ).to.be.true;
  54. expect( ranges[ 0 ].start.path ).to.deep.equal( [ 0, 0, 0, 0, 0 ] );
  55. } );
  56. it( 'should reenable table selection when reenabling the plugin', () => {
  57. tableSelection.forceDisabled( 'foo' );
  58. tableSelection.clearForceDisabled( 'foo' );
  59. const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
  60. const lastCell = modelRoot.getNodeByPath( [ 0, 1, 1 ] );
  61. tableSelection.setCellSelection(
  62. firstCell,
  63. lastCell
  64. );
  65. assertSelectedCells( model, [
  66. [ 1, 1, 0 ],
  67. [ 1, 1, 0 ],
  68. [ 0, 0, 0 ]
  69. ] );
  70. } );
  71. it( 'should do nothing if there were no multi-cell selections', () => {
  72. tableSelection.forceDisabled( 'foo' );
  73. const ranges = [ ...model.document.selection.getRanges() ];
  74. expect( ranges ).to.have.length( 1 );
  75. expect( ranges[ 0 ].isCollapsed ).to.be.true;
  76. expect( ranges[ 0 ].start.path ).to.deep.equal( [ 0, 0, 0, 0, 2 ] );
  77. } );
  78. } );
  79. } );
  80. describe( 'selection by Shift+click', () => {
  81. beforeEach( async () => {
  82. editor = await createEditor();
  83. model = editor.model;
  84. modelRoot = model.document.getRoot();
  85. view = editor.editing.view;
  86. viewDocument = view.document;
  87. tableSelection = editor.plugins.get( TableSelection );
  88. setModelData( model, modelTable( [
  89. [ '11[]', '12', '13' ],
  90. [ '21', '22', '23' ],
  91. [ '31', '32', '33' ]
  92. ] ) );
  93. } );
  94. it( 'should do nothing if the plugin is disabled', () => {
  95. tableSelection.isEnabled = false;
  96. viewDocument.fire( 'mousedown', new DomEventData( view, {} ) );
  97. assertSelectedCells( model, [
  98. [ 0, 0, 0 ],
  99. [ 0, 0, 0 ],
  100. [ 0, 0, 0 ]
  101. ] );
  102. } );
  103. it( 'should abort if Shift key was not pressed', () => {
  104. viewDocument.fire( 'mousedown', new DomEventData( view, {
  105. shiftKey: false,
  106. target: view.domConverter.mapViewToDom(
  107. // figure > table > tbody > tr > td
  108. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 2 )
  109. )
  110. } ) );
  111. assertSelectedCells( model, [
  112. [ 0, 0, 0 ],
  113. [ 0, 0, 0 ],
  114. [ 0, 0, 0 ]
  115. ] );
  116. } );
  117. it( 'should abort if Shift+clicked an element outside a table', () => {
  118. const preventDefault = sinon.spy();
  119. model.change( writer => {
  120. const paragraph = writer.createElement( 'paragraph' );
  121. const text = writer.createText( 'foo' );
  122. writer.insert( text, paragraph );
  123. writer.insert( paragraph, model.document.getRoot(), 'end' );
  124. writer.setSelection( paragraph, 'end' );
  125. } );
  126. viewDocument.fire( 'mousedown', new DomEventData( view, {
  127. shiftKey: true,
  128. target: view.domConverter.mapViewToDom(
  129. viewDocument.getRoot().getChild( 1 )
  130. ),
  131. preventDefault
  132. } ) );
  133. assertSelectedCells( model, [
  134. [ 0, 0, 0 ],
  135. [ 0, 0, 0 ],
  136. [ 0, 0, 0 ]
  137. ] );
  138. expect( preventDefault.called ).to.equal( false );
  139. } );
  140. it( 'should abort if clicked a cell that belongs to another table', () => {
  141. const preventDefault = sinon.spy();
  142. setModelData( model, [
  143. modelTable( [
  144. [ '1.11[]', '1.12' ],
  145. [ '1.21', '1.22' ]
  146. ] ),
  147. modelTable( [
  148. [ '2.11', '2.12' ],
  149. [ '2.21', '2.22' ]
  150. ] )
  151. ].join( '' ) );
  152. const domEventDataMock = new DomEventData( view, {
  153. shiftKey: true,
  154. target: view.domConverter.mapViewToDom(
  155. // The second table: figure > table > tbody > tr > td
  156. viewDocument.getRoot().getChild( 1 ).getChild( 1 ).getChild( 0 ).getChild( 1 ).getChild( 1 )
  157. ),
  158. preventDefault
  159. } );
  160. viewDocument.fire( 'mousedown', domEventDataMock );
  161. assertSelectedCells( model, [
  162. [ 0, 0 ],
  163. [ 0, 0 ]
  164. ] );
  165. expect( preventDefault.called ).to.equal( false );
  166. } );
  167. it( 'should select all cells in first row', () => {
  168. const preventDefault = sinon.spy();
  169. const domEventDataMock = new DomEventData( view, {
  170. shiftKey: true,
  171. target: view.domConverter.mapViewToDom(
  172. // figure > table > tbody > tr > td
  173. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 2 )
  174. ),
  175. preventDefault
  176. } );
  177. viewDocument.fire( 'mousedown', domEventDataMock );
  178. assertSelectedCells( model, [
  179. [ 1, 1, 1 ],
  180. [ 0, 0, 0 ],
  181. [ 0, 0, 0 ]
  182. ] );
  183. expect( preventDefault.called ).to.equal( true );
  184. } );
  185. it( 'should use the anchor cell from the selection if possible', () => {
  186. const preventDefault = sinon.spy();
  187. const domEventDataMock = new DomEventData( view, {
  188. shiftKey: true,
  189. target: view.domConverter.mapViewToDom(
  190. // figure > table > tbody > tr > td
  191. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 2 )
  192. ),
  193. preventDefault
  194. } );
  195. tableSelection.setCellSelection(
  196. modelRoot.getNodeByPath( [ 0, 1, 0 ] ),
  197. modelRoot.getNodeByPath( [ 0, 2, 1 ] )
  198. );
  199. assertSelectedCells( model, [
  200. [ 0, 0, 0 ],
  201. [ 1, 1, 0 ],
  202. [ 1, 1, 0 ]
  203. ] );
  204. viewDocument.fire( 'mousedown', domEventDataMock );
  205. assertSelectedCells( model, [
  206. [ 1, 1, 1 ],
  207. [ 1, 1, 1 ],
  208. [ 0, 0, 0 ]
  209. ] );
  210. expect( preventDefault.called ).to.equal( true );
  211. } );
  212. it( 'should ignore `selectionChange` event when selecting cells', () => {
  213. const consoleLog = sinon.stub( console, 'log' );
  214. const preventDefault = sinon.spy();
  215. const selectionChangeCallback = sinon.spy();
  216. // Adding a new callback to check whether it will be executed (whether `evt.stop()` is being called).
  217. viewDocument.on( 'selectionChange', selectionChangeCallback );
  218. // Shift+click a cell to create a selection. Should disable listening to `selectionChange`.
  219. viewDocument.fire( 'mousedown', new DomEventData( view, {
  220. shiftKey: true,
  221. target: view.domConverter.mapViewToDom(
  222. // figure > table > tbody > tr > td
  223. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 2 )
  224. ),
  225. preventDefault
  226. } ) );
  227. // Due to browsers "fixing" the selection (e.g. moving it to text nodes), after we set a selection
  228. // the browser fill fire native selectionchange, which triggers our selectionChange. We need to ignore it.
  229. // See a broader explanation in tableselection.js.
  230. viewDocument.fire( 'selectionChange' );
  231. // The callback shouldn't be executed because
  232. // `selectionChange` event should be canceled.
  233. expect( selectionChangeCallback.called ).to.equal( false );
  234. expect( consoleLog.called ).to.equal( true );
  235. expect( consoleLog.firstCall.args[ 0 ] ).to.equal( 'Blocked selectionChange to avoid breaking table cells selection.' );
  236. // Enables listening to `selectionChange` event.
  237. viewDocument.fire( 'mouseup' );
  238. viewDocument.fire( 'selectionChange', {
  239. newSelection: view.document.selection
  240. } );
  241. expect( selectionChangeCallback.called ).to.equal( true );
  242. consoleLog.restore();
  243. } );
  244. } );
  245. describe( 'selection by mouse drag', () => {
  246. let preventDefault;
  247. beforeEach( async () => {
  248. editor = await createEditor();
  249. model = editor.model;
  250. modelRoot = model.document.getRoot();
  251. view = editor.editing.view;
  252. viewDocument = view.document;
  253. tableSelection = editor.plugins.get( TableSelection );
  254. setModelData( model, modelTable( [
  255. [ '11[]', '12', '13' ],
  256. [ '21', '22', '23' ],
  257. [ '31', '32', '33' ]
  258. ] ) );
  259. preventDefault = sinon.spy();
  260. } );
  261. it( 'should do nothing if the plugin is disabled', () => {
  262. tableSelection.isEnabled = false;
  263. const domEventDataMock = new DomEventData( view, {} );
  264. viewDocument.fire( 'mousedown', domEventDataMock );
  265. assertSelectedCells( model, [
  266. [ 0, 0, 0 ],
  267. [ 0, 0, 0 ],
  268. [ 0, 0, 0 ]
  269. ] );
  270. } );
  271. it( 'should abort if Ctrl is pressed', () => {
  272. const domEventDataMock = new DomEventData( view, {
  273. ctrlKey: true
  274. } );
  275. viewDocument.fire( 'mousedown', domEventDataMock );
  276. assertSelectedCells( model, [
  277. [ 0, 0, 0 ],
  278. [ 0, 0, 0 ],
  279. [ 0, 0, 0 ]
  280. ] );
  281. } );
  282. it( 'should abort if Alt is pressed', () => {
  283. const domEventDataMock = new DomEventData( view, {
  284. altKey: true
  285. } );
  286. viewDocument.fire( 'mousedown', domEventDataMock );
  287. assertSelectedCells( model, [
  288. [ 0, 0, 0 ],
  289. [ 0, 0, 0 ],
  290. [ 0, 0, 0 ]
  291. ] );
  292. } );
  293. it( 'should do nothing if any of mouse buttons was not clicked', () => {
  294. viewDocument.fire( 'mousemove', new DomEventData( view, {
  295. buttons: 0
  296. } ) );
  297. assertSelectedCells( model, [
  298. [ 0, 0, 0 ],
  299. [ 0, 0, 0 ],
  300. [ 0, 0, 0 ]
  301. ] );
  302. } );
  303. it( 'should do nothing if started dragging outside of table', () => {
  304. model.change( writer => {
  305. const paragraph = writer.createElement( 'paragraph' );
  306. const text = writer.createText( 'foo' );
  307. writer.insert( text, paragraph );
  308. writer.insert( paragraph, model.document.getRoot(), 'end' );
  309. writer.setSelection( paragraph, 'end' );
  310. } );
  311. viewDocument.fire( 'mousedown', new DomEventData( view, {
  312. target: view.domConverter.mapViewToDom(
  313. viewDocument.getRoot().getChild( 1 )
  314. )
  315. } ) );
  316. viewDocument.fire( 'mousemove', new DomEventData( view, {
  317. buttons: 1
  318. } ) );
  319. assertSelectedCells( model, [
  320. [ 0, 0, 0 ],
  321. [ 0, 0, 0 ],
  322. [ 0, 0, 0 ]
  323. ] );
  324. } );
  325. it( 'should do nothing if ended dragging outside of table', () => {
  326. model.change( writer => {
  327. const paragraph = writer.createElement( 'paragraph' );
  328. const text = writer.createText( 'foo' );
  329. writer.insert( text, paragraph );
  330. writer.insert( paragraph, model.document.getRoot(), 'end' );
  331. writer.setSelection( paragraph, 'end' );
  332. } );
  333. viewDocument.fire( 'mousedown', new DomEventData( view, {
  334. target: view.domConverter.mapViewToDom(
  335. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 0 )
  336. )
  337. } ) );
  338. viewDocument.fire( 'mousemove', new DomEventData( view, {
  339. target: view.domConverter.mapViewToDom(
  340. viewDocument.getRoot().getChild( 1 )
  341. ),
  342. buttons: 1
  343. } ) );
  344. assertSelectedCells( model, [
  345. [ 0, 0, 0 ],
  346. [ 0, 0, 0 ],
  347. [ 0, 0, 0 ]
  348. ] );
  349. } );
  350. it( 'should do nothing if ended dragging inside another table', () => {
  351. setModelData( model, [
  352. modelTable( [
  353. [ '1.11[]', '1.12' ],
  354. [ '1.21', '1.22' ]
  355. ] ),
  356. modelTable( [
  357. [ '2.11', '2.12' ],
  358. [ '2.21', '2.22' ]
  359. ] )
  360. ].join( '' ) );
  361. viewDocument.fire( 'mousedown', new DomEventData( view, {
  362. target: view.domConverter.mapViewToDom(
  363. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 0 )
  364. )
  365. } ) );
  366. viewDocument.fire( 'mousemove', new DomEventData( view, {
  367. target: view.domConverter.mapViewToDom(
  368. viewDocument.getRoot().getChild( 1 ).getChild( 1 ).getChild( 0 ).getChild( 1 ).getChild( 1 )
  369. ),
  370. buttons: 1
  371. } ) );
  372. assertSelectedCells( model, [
  373. [ 0, 0 ],
  374. [ 0, 0 ]
  375. ] );
  376. } );
  377. it( 'should do nothing if ended in the same cell', () => {
  378. viewDocument.fire( 'mousedown', new DomEventData( view, {
  379. target: view.domConverter.mapViewToDom(
  380. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 0 )
  381. )
  382. } ) );
  383. viewDocument.fire( 'mousemove', new DomEventData( view, {
  384. target: view.domConverter.mapViewToDom(
  385. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 0 )
  386. ),
  387. buttons: 1
  388. } ) );
  389. assertSelectedCells( model, [
  390. [ 0, 0, 0 ],
  391. [ 0, 0, 0 ],
  392. [ 0, 0, 0 ]
  393. ] );
  394. } );
  395. it( 'should select started and ended dragging in the same cell but went over its border', () => {
  396. viewDocument.fire( 'mousedown', new DomEventData( view, {
  397. target: view.domConverter.mapViewToDom(
  398. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 0 )
  399. )
  400. } ) );
  401. // Select the next one.
  402. viewDocument.fire( 'mousemove', new DomEventData( view, {
  403. target: view.domConverter.mapViewToDom(
  404. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 1 )
  405. ),
  406. buttons: 1,
  407. preventDefault: sinon.spy()
  408. } ) );
  409. // And back to the "started" cell.
  410. viewDocument.fire( 'mousemove', new DomEventData( view, {
  411. target: view.domConverter.mapViewToDom(
  412. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 0 )
  413. ),
  414. buttons: 1,
  415. preventDefault: sinon.spy()
  416. } ) );
  417. viewDocument.fire( 'mouseup' );
  418. assertSelectedCells( model, [
  419. [ 1, 0, 0 ],
  420. [ 0, 0, 0 ],
  421. [ 0, 0, 0 ]
  422. ] );
  423. } );
  424. it( 'should select all cells in first row', () => {
  425. viewDocument.fire( 'mousedown', new DomEventData( view, {
  426. target: view.domConverter.mapViewToDom(
  427. // figure > table > tbody > tr > td
  428. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 0 )
  429. )
  430. } ) );
  431. viewDocument.fire( 'mousemove', new DomEventData( view, {
  432. target: view.domConverter.mapViewToDom(
  433. // figure > table > tbody > tr > td
  434. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 2 )
  435. ),
  436. buttons: 1,
  437. preventDefault
  438. } ) );
  439. viewDocument.fire( 'mouseup' );
  440. assertSelectedCells( model, [
  441. [ 1, 1, 1 ],
  442. [ 0, 0, 0 ],
  443. [ 0, 0, 0 ]
  444. ] );
  445. expect( preventDefault.called ).to.equal( true );
  446. } );
  447. it( 'should ignore `selectionChange` event when selecting cells ', () => {
  448. const consoleLog = sinon.stub( console, 'log' );
  449. const preventDefault = sinon.spy();
  450. const selectionChangeCallback = sinon.spy();
  451. // Adding a new callback to check whether it will be executed (whether `evt.stop()` is being called).
  452. viewDocument.on( 'selectionChange', selectionChangeCallback );
  453. // Click on a cell.
  454. viewDocument.fire( 'mousedown', new DomEventData( view, {
  455. target: view.domConverter.mapViewToDom(
  456. // figure > table > tbody > tr > td
  457. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 1 )
  458. )
  459. } ) );
  460. // Then move the mouse to another cell. Disables listening to `selectionChange`.
  461. viewDocument.fire( 'mousemove', new DomEventData( view, {
  462. buttons: 1,
  463. target: view.domConverter.mapViewToDom(
  464. // figure > table > tbody > tr > td
  465. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 2 )
  466. ),
  467. preventDefault
  468. } ) );
  469. // See explanation why do we fire it in the similar test for Shift+click.
  470. viewDocument.fire( 'selectionChange' );
  471. // `selectionChange` event should be canceled.
  472. expect( selectionChangeCallback.called ).to.equal( false );
  473. expect( consoleLog.called ).to.equal( true );
  474. expect( consoleLog.firstCall.args[ 0 ] ).to.equal( 'Blocked selectionChange to avoid breaking table cells selection.' );
  475. // Enables listening to `selectionChange` event.
  476. viewDocument.fire( 'mouseup' );
  477. viewDocument.fire( 'selectionChange', {
  478. newSelection: view.document.selection
  479. } );
  480. expect( selectionChangeCallback.called ).to.equal( true );
  481. consoleLog.restore();
  482. } );
  483. } );
  484. describe( 'getSelectedTableCells()', () => {
  485. beforeEach( async () => {
  486. editor = await createEditor();
  487. model = editor.model;
  488. modelRoot = model.document.getRoot();
  489. view = editor.editing.view;
  490. viewDocument = view.document;
  491. tableSelection = editor.plugins.get( TableSelection );
  492. setModelData( model, modelTable( [
  493. [ '11[]', '12', '13' ],
  494. [ '21', '22', '23' ],
  495. [ '31', '32', '33' ]
  496. ] ) );
  497. } );
  498. it( 'should return nothing if selection is not started', () => {
  499. expect( tableSelection.getSelectedTableCells() ).to.be.null;
  500. } );
  501. it( 'should return two table cells', () => {
  502. const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
  503. const lastCell = modelRoot.getNodeByPath( [ 0, 0, 1 ] );
  504. tableSelection.setCellSelection(
  505. firstCell,
  506. lastCell
  507. );
  508. expect( tableSelection.getSelectedTableCells() ).to.deep.equal( [
  509. firstCell, lastCell
  510. ] );
  511. } );
  512. it( 'should return four table cells for diagonal selection', () => {
  513. const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
  514. const lastCell = modelRoot.getNodeByPath( [ 0, 1, 1 ] );
  515. tableSelection.setCellSelection(
  516. firstCell,
  517. lastCell
  518. );
  519. expect( tableSelection.getSelectedTableCells() ).to.deep.equal( [
  520. firstCell, modelRoot.getNodeByPath( [ 0, 0, 1 ] ), modelRoot.getNodeByPath( [ 0, 1, 0 ] ), lastCell
  521. ] );
  522. } );
  523. it( 'should return row table cells', () => {
  524. const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
  525. const lastCell = modelRoot.getNodeByPath( [ 0, 0, 2 ] );
  526. tableSelection.setCellSelection(
  527. firstCell,
  528. lastCell
  529. );
  530. expect( tableSelection.getSelectedTableCells() ).to.deep.equal( [
  531. firstCell, modelRoot.getNodeByPath( [ 0, 0, 1 ] ), lastCell
  532. ] );
  533. } );
  534. it( 'should return column table cells', () => {
  535. const firstCell = modelRoot.getNodeByPath( [ 0, 0, 1 ] );
  536. const lastCell = modelRoot.getNodeByPath( [ 0, 2, 1 ] );
  537. tableSelection.setCellSelection( firstCell, lastCell );
  538. expect( tableSelection.getSelectedTableCells() ).to.deep.equal( [
  539. firstCell, modelRoot.getNodeByPath( [ 0, 1, 1 ] ), lastCell
  540. ] );
  541. } );
  542. it( 'should return cells in source order despite backward selection', () => {
  543. const firstCell = modelRoot.getNodeByPath( [ 0, 0, 2 ] );
  544. const lastCell = modelRoot.getNodeByPath( [ 0, 0, 1 ] );
  545. tableSelection.setCellSelection( firstCell, lastCell );
  546. expect( tableSelection.getSelectedTableCells() ).to.deep.equal( [
  547. lastCell, firstCell
  548. ] );
  549. } );
  550. } );
  551. describe( 'getSelectionAsFragment()', () => {
  552. beforeEach( async () => {
  553. editor = await createEditor();
  554. model = editor.model;
  555. modelRoot = model.document.getRoot();
  556. view = editor.editing.view;
  557. viewDocument = view.document;
  558. tableSelection = editor.plugins.get( TableSelection );
  559. setModelData( model, modelTable( [
  560. [ '11[]', '12', '13' ],
  561. [ '21', '22', '23' ],
  562. [ '31', '32', '33' ]
  563. ] ) );
  564. } );
  565. it( 'should return undefined if no table cells are selected', () => {
  566. expect( tableSelection.getSelectionAsFragment() ).to.be.null;
  567. } );
  568. it( 'should return document fragment for selected table cells', () => {
  569. tableSelection.setCellSelection(
  570. modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
  571. modelRoot.getNodeByPath( [ 0, 1, 1 ] )
  572. );
  573. expect( tableSelection.getSelectionAsFragment() ).to.be.instanceOf( DocumentFragment );
  574. } );
  575. it( 'should return cells in the source order in case of forward selection', () => {
  576. tableSelection.setCellSelection(
  577. modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
  578. modelRoot.getNodeByPath( [ 0, 1, 1 ] )
  579. );
  580. expect( stringifyModel( tableSelection.getSelectionAsFragment() ) ).to.equal( modelTable( [
  581. [ '11', '12' ],
  582. [ '21', '22' ]
  583. ] ) );
  584. } );
  585. it( 'should return cells in the source order in case of backward selection', () => {
  586. tableSelection.setCellSelection(
  587. modelRoot.getNodeByPath( [ 0, 1, 1 ] ),
  588. modelRoot.getNodeByPath( [ 0, 0, 0 ] )
  589. );
  590. expect( editor.model.document.selection.isBackward ).to.be.true;
  591. expect( stringifyModel( tableSelection.getSelectionAsFragment() ) ).to.equal( modelTable( [
  592. [ '11', '12' ],
  593. [ '21', '22' ]
  594. ] ) );
  595. } );
  596. } );
  597. describe( 'delete content', () => {
  598. beforeEach( async () => {
  599. editor = await createEditor();
  600. model = editor.model;
  601. modelRoot = model.document.getRoot();
  602. view = editor.editing.view;
  603. viewDocument = view.document;
  604. tableSelection = editor.plugins.get( TableSelection );
  605. setModelData( model, modelTable( [
  606. [ '11[]', '12', '13' ],
  607. [ '21', '22', '23' ],
  608. [ '31', '32', '33' ]
  609. ] ) );
  610. } );
  611. it( 'should put selection in the last selected cell after removing content (backward delete)', () => {
  612. tableSelection.setCellSelection(
  613. modelRoot.getChild( 0 ).getChild( 0 ).getChild( 0 ),
  614. modelRoot.getChild( 0 ).getChild( 1 ).getChild( 1 )
  615. );
  616. editor.execute( 'delete' );
  617. assertEqualMarkup( getModelData( model ), modelTable( [
  618. [ '', '', '13' ],
  619. [ '', '[]', '23' ],
  620. [ '31', '32', '33' ]
  621. ] ) );
  622. } );
  623. it( 'should put selection in the last selected cell after removing content (forward delete)', () => {
  624. tableSelection.setCellSelection(
  625. modelRoot.getChild( 0 ).getChild( 0 ).getChild( 0 ),
  626. modelRoot.getChild( 0 ).getChild( 1 ).getChild( 1 )
  627. );
  628. editor.execute( 'delete' );
  629. assertEqualMarkup( getModelData( model ), modelTable( [
  630. [ '', '', '13' ],
  631. [ '', '[]', '23' ],
  632. [ '31', '32', '33' ]
  633. ] ) );
  634. } );
  635. it( 'should clear single cell if selected', () => {
  636. tableSelection.setCellSelection(
  637. modelRoot.getChild( 0 ).getChild( 0 ).getChild( 0 ),
  638. modelRoot.getChild( 0 ).getChild( 0 ).getChild( 0 ),
  639. );
  640. editor.execute( 'forwardDelete' );
  641. assertEqualMarkup( getModelData( model ), modelTable( [
  642. [ '[]', '12', '13' ],
  643. [ '21', '22', '23' ],
  644. [ '31', '32', '33' ]
  645. ] ) );
  646. } );
  647. it( 'should work with document selection passed to Model#deleteContent()', () => {
  648. tableSelection.setCellSelection(
  649. modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
  650. modelRoot.getNodeByPath( [ 0, 1, 1 ] )
  651. );
  652. model.change( () => {
  653. model.deleteContent( model.document.selection );
  654. } );
  655. assertEqualMarkup( getModelData( model ), modelTable( [
  656. [ '', '', '13' ],
  657. [ '', '[]', '23' ],
  658. [ '31', '32', '33' ]
  659. ] ) );
  660. } );
  661. it( 'should work with any arbitrary selection passed to Model#deleteContent() (delete backwards)', () => {
  662. const selection = model.createSelection( [
  663. model.createRange(
  664. model.createPositionFromPath( modelRoot, [ 0, 0, 0 ] ),
  665. model.createPositionFromPath( modelRoot, [ 0, 0, 1 ] )
  666. ),
  667. model.createRange(
  668. model.createPositionFromPath( modelRoot, [ 0, 0, 1 ] ),
  669. model.createPositionFromPath( modelRoot, [ 0, 0, 2 ] )
  670. )
  671. ] );
  672. model.change( writer => {
  673. model.deleteContent( selection );
  674. writer.setSelection( selection );
  675. } );
  676. assertEqualMarkup( getModelData( model ), modelTable( [
  677. [ '', '[]', '13' ],
  678. [ '21', '22', '23' ],
  679. [ '31', '32', '33' ]
  680. ] ) );
  681. } );
  682. it( 'should work with any arbitrary selection passed to Model#deleteContent() (delete forwards)', () => {
  683. const selection = model.createSelection( [
  684. model.createRange(
  685. model.createPositionFromPath( modelRoot, [ 0, 0, 0 ] ),
  686. model.createPositionFromPath( modelRoot, [ 0, 0, 1 ] )
  687. ),
  688. model.createRange(
  689. model.createPositionFromPath( modelRoot, [ 0, 0, 1 ] ),
  690. model.createPositionFromPath( modelRoot, [ 0, 0, 2 ] )
  691. )
  692. ] );
  693. model.change( writer => {
  694. model.deleteContent( selection, {
  695. direction: 'forward'
  696. } );
  697. writer.setSelection( selection );
  698. } );
  699. assertEqualMarkup( getModelData( model ), modelTable( [
  700. [ '[]', '', '13' ],
  701. [ '21', '22', '23' ],
  702. [ '31', '32', '33' ]
  703. ] ) );
  704. } );
  705. } );
  706. describe( 'getAnchorCell() and getFocusCell()', () => {
  707. beforeEach( async () => {
  708. editor = await createEditor();
  709. model = editor.model;
  710. modelRoot = model.document.getRoot();
  711. tableSelection = editor.plugins.get( TableSelection );
  712. setModelData( model, modelTable( [
  713. [ '[]00', '01', '02' ],
  714. [ '10', '11', '12' ],
  715. [ '20', '21', '22' ]
  716. ] ) );
  717. } );
  718. it( 'should return null if no table cell is selected', () => {
  719. expect( tableSelection.getAnchorCell() ).to.be.null;
  720. expect( tableSelection.getFocusCell() ).to.be.null;
  721. } );
  722. it( 'getAnchorCell() should return the table cell from the first range in the selection', () => {
  723. const anchorCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
  724. const focusCell = modelRoot.getNodeByPath( [ 0, 1, 1 ] );
  725. tableSelection.setCellSelection( anchorCell, focusCell );
  726. expect( tableSelection.getAnchorCell() ).to.equal( anchorCell );
  727. } );
  728. it( 'getFocusCell() should return the table cell from the last range in the selection', () => {
  729. const anchorCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
  730. const focusCell = modelRoot.getNodeByPath( [ 0, 1, 1 ] );
  731. tableSelection.setCellSelection( anchorCell, focusCell );
  732. expect( tableSelection.getFocusCell() ).to.equal( focusCell );
  733. } );
  734. } );
  735. describe( 'the selection ranges order', () => {
  736. let selection, table;
  737. beforeEach( async () => {
  738. editor = await createEditor();
  739. model = editor.model;
  740. selection = model.document.selection;
  741. modelRoot = model.document.getRoot();
  742. tableSelection = editor.plugins.get( TableSelection );
  743. setModelData( model, modelTable( [
  744. [ '00', '01', '02' ],
  745. [ '10', '11', '12' ],
  746. [ '20', '21', '22' ]
  747. ] ) );
  748. table = modelRoot.getChild( 0 );
  749. } );
  750. it( 'should be to below right', () => {
  751. const anchorCell = table.getChild( 1 ).getChild( 1 );
  752. const focusCell = table.getChild( 2 ).getChild( 2 );
  753. tableSelection.setCellSelection( anchorCell, focusCell );
  754. assertSelection( anchorCell, focusCell, 4 );
  755. expect( tableSelection.getFocusCell() ).to.equal( focusCell );
  756. expect( tableSelection.getAnchorCell() ).to.equal( anchorCell );
  757. expect( selection.isBackward ).to.be.false;
  758. } );
  759. it( 'should be to below left', () => {
  760. const anchorCell = table.getChild( 1 ).getChild( 1 );
  761. const focusCell = table.getChild( 2 ).getChild( 0 );
  762. tableSelection.setCellSelection( anchorCell, focusCell );
  763. assertSelection( anchorCell, focusCell, 4 );
  764. expect( tableSelection.getFocusCell() ).to.equal( focusCell );
  765. expect( tableSelection.getAnchorCell() ).to.equal( anchorCell );
  766. expect( selection.isBackward ).to.be.true;
  767. } );
  768. it( 'should be to above left', () => {
  769. const anchorCell = table.getChild( 1 ).getChild( 1 );
  770. const focusCell = table.getChild( 0 ).getChild( 0 );
  771. tableSelection.setCellSelection( anchorCell, focusCell );
  772. assertSelection( anchorCell, focusCell, 4 );
  773. expect( tableSelection.getFocusCell() ).to.equal( focusCell );
  774. expect( tableSelection.getAnchorCell() ).to.equal( anchorCell );
  775. expect( selection.isBackward ).to.be.true;
  776. } );
  777. it( 'should be to above right', () => {
  778. const anchorCell = table.getChild( 1 ).getChild( 1 );
  779. const focusCell = table.getChild( 0 ).getChild( 2 );
  780. tableSelection.setCellSelection( anchorCell, focusCell );
  781. assertSelection( anchorCell, focusCell, 4 );
  782. expect( tableSelection.getFocusCell() ).to.equal( focusCell );
  783. expect( tableSelection.getAnchorCell() ).to.equal( anchorCell );
  784. expect( selection.isBackward ).to.be.true;
  785. } );
  786. function assertSelection( anchorCell, focusCell, count ) {
  787. const cells = [ ...selection.getRanges() ].map( range => range.getContainedElement() );
  788. expect( selection.rangeCount ).to.equal( count );
  789. expect( cells[ 0 ] ).to.equal( anchorCell );
  790. expect( cells[ cells.length - 1 ] ).to.equal( focusCell );
  791. }
  792. } );
  793. function createEditor() {
  794. return ClassicTestEditor.create( editorElement, {
  795. plugins: [ TableEditing, TableSelection, Paragraph, Typing ]
  796. } );
  797. }
  798. } );