tablemouse.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  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 { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  9. import TableEditing from '../src/tableediting';
  10. import TableSelection from '../src/tableselection';
  11. import TableMouse from '../src/tablemouse';
  12. import { assertSelectedCells, modelTable } from './_utils/utils';
  13. import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
  14. import Typing from '@ckeditor/ckeditor5-typing/src/typing';
  15. describe( 'TableMouse', () => {
  16. let editorElement, editor, model, tableMouse, modelRoot, view, viewDocument;
  17. beforeEach( () => {
  18. editorElement = document.createElement( 'div' );
  19. document.body.appendChild( editorElement );
  20. } );
  21. afterEach( async () => {
  22. editorElement.remove();
  23. await editor.destroy();
  24. } );
  25. describe( 'plugin', () => {
  26. beforeEach( async () => {
  27. editor = await createEditor();
  28. } );
  29. it( 'should have pluginName', () => {
  30. expect( TableMouse.pluginName ).to.equal( 'TableMouse' );
  31. } );
  32. } );
  33. describe( 'selection by Shift+click', () => {
  34. beforeEach( async () => {
  35. editor = await createEditor();
  36. model = editor.model;
  37. modelRoot = model.document.getRoot();
  38. view = editor.editing.view;
  39. viewDocument = view.document;
  40. tableMouse = editor.plugins.get( TableMouse );
  41. setModelData( model, modelTable( [
  42. [ '11[]', '12', '13' ],
  43. [ '21', '22', '23' ],
  44. [ '31', '32', '33' ]
  45. ] ) );
  46. } );
  47. it( 'should do nothing if the plugin is disabled', () => {
  48. tableMouse.isEnabled = false;
  49. viewDocument.fire( 'mousedown', new DomEventData( view, {} ) );
  50. assertSelectedCells( model, [
  51. [ 0, 0, 0 ],
  52. [ 0, 0, 0 ],
  53. [ 0, 0, 0 ]
  54. ] );
  55. } );
  56. it( 'should do nothing if the TableSelection plugin is disabled', () => {
  57. editor.plugins.get( 'TableSelection' ).isEnabled = false;
  58. viewDocument.fire( 'mousedown', new DomEventData( view, {} ) );
  59. assertSelectedCells( model, [
  60. [ 0, 0, 0 ],
  61. [ 0, 0, 0 ],
  62. [ 0, 0, 0 ]
  63. ] );
  64. } );
  65. it( 'should abort if Shift key was not pressed', () => {
  66. viewDocument.fire( 'mousedown', new DomEventData( view, {
  67. shiftKey: false,
  68. target: view.domConverter.mapViewToDom(
  69. // figure > table > tbody > tr > td
  70. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 2 )
  71. )
  72. } ) );
  73. assertSelectedCells( model, [
  74. [ 0, 0, 0 ],
  75. [ 0, 0, 0 ],
  76. [ 0, 0, 0 ]
  77. ] );
  78. } );
  79. it( 'should abort if Shift+clicked an element outside a table', () => {
  80. const preventDefault = sinon.spy();
  81. model.change( writer => {
  82. const paragraph = writer.createElement( 'paragraph' );
  83. const text = writer.createText( 'foo' );
  84. writer.insert( text, paragraph );
  85. writer.insert( paragraph, model.document.getRoot(), 'end' );
  86. writer.setSelection( paragraph, 'end' );
  87. } );
  88. viewDocument.fire( 'mousedown', new DomEventData( view, {
  89. shiftKey: true,
  90. target: view.domConverter.mapViewToDom(
  91. viewDocument.getRoot().getChild( 1 )
  92. ),
  93. preventDefault
  94. } ) );
  95. assertSelectedCells( model, [
  96. [ 0, 0, 0 ],
  97. [ 0, 0, 0 ],
  98. [ 0, 0, 0 ]
  99. ] );
  100. expect( preventDefault.called ).to.equal( false );
  101. } );
  102. it( 'should abort if clicked a cell that belongs to another table', () => {
  103. const preventDefault = sinon.spy();
  104. setModelData( model, [
  105. modelTable( [
  106. [ '1.11[]', '1.12' ],
  107. [ '1.21', '1.22' ]
  108. ] ),
  109. modelTable( [
  110. [ '2.11', '2.12' ],
  111. [ '2.21', '2.22' ]
  112. ] )
  113. ].join( '' ) );
  114. const domEventDataMock = new DomEventData( view, {
  115. shiftKey: true,
  116. target: view.domConverter.mapViewToDom(
  117. // The second table: figure > table > tbody > tr > td
  118. viewDocument.getRoot().getChild( 1 ).getChild( 1 ).getChild( 0 ).getChild( 1 ).getChild( 1 )
  119. ),
  120. preventDefault
  121. } );
  122. viewDocument.fire( 'mousedown', domEventDataMock );
  123. assertSelectedCells( model, [
  124. [ 0, 0 ],
  125. [ 0, 0 ]
  126. ] );
  127. expect( preventDefault.called ).to.equal( false );
  128. } );
  129. it( 'should select all cells in first row', () => {
  130. const preventDefault = sinon.spy();
  131. const domEventDataMock = new DomEventData( view, {
  132. shiftKey: true,
  133. target: view.domConverter.mapViewToDom(
  134. // figure > table > tbody > tr > td
  135. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 2 )
  136. ),
  137. preventDefault
  138. } );
  139. viewDocument.fire( 'mousedown', domEventDataMock );
  140. assertSelectedCells( model, [
  141. [ 1, 1, 1 ],
  142. [ 0, 0, 0 ],
  143. [ 0, 0, 0 ]
  144. ] );
  145. expect( preventDefault.called ).to.equal( true );
  146. } );
  147. it( 'should use the anchor cell from the selection if possible', () => {
  148. const preventDefault = sinon.spy();
  149. const domEventDataMock = new DomEventData( view, {
  150. shiftKey: true,
  151. target: view.domConverter.mapViewToDom(
  152. // figure > table > tbody > tr > td
  153. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 2 )
  154. ),
  155. preventDefault
  156. } );
  157. editor.plugins.get( 'TableSelection' ).setCellSelection(
  158. modelRoot.getNodeByPath( [ 0, 1, 0 ] ),
  159. modelRoot.getNodeByPath( [ 0, 2, 1 ] )
  160. );
  161. assertSelectedCells( model, [
  162. [ 0, 0, 0 ],
  163. [ 1, 1, 0 ],
  164. [ 1, 1, 0 ]
  165. ] );
  166. viewDocument.fire( 'mousedown', domEventDataMock );
  167. assertSelectedCells( model, [
  168. [ 1, 1, 1 ],
  169. [ 1, 1, 1 ],
  170. [ 0, 0, 0 ]
  171. ] );
  172. expect( preventDefault.called ).to.equal( true );
  173. } );
  174. it( 'should ignore `selectionChange` event when selecting cells', () => {
  175. const consoleLog = sinon.stub( console, 'log' );
  176. const preventDefault = sinon.spy();
  177. const selectionChangeCallback = sinon.spy();
  178. // Adding a new callback to check whether it will be executed (whether `evt.stop()` is being called).
  179. viewDocument.on( 'selectionChange', selectionChangeCallback );
  180. // Shift+click a cell to create a selection. Should disable listening to `selectionChange`.
  181. viewDocument.fire( 'mousedown', new DomEventData( view, {
  182. shiftKey: true,
  183. target: view.domConverter.mapViewToDom(
  184. // figure > table > tbody > tr > td
  185. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 2 )
  186. ),
  187. preventDefault
  188. } ) );
  189. // Due to browsers "fixing" the selection (e.g. moving it to text nodes), after we set a selection
  190. // the browser fill fire native selectionchange, which triggers our selectionChange. We need to ignore it.
  191. // See a broader explanation in tablemouse.js.
  192. viewDocument.fire( 'selectionChange' );
  193. // The callback shouldn't be executed because
  194. // `selectionChange` event should be canceled.
  195. expect( selectionChangeCallback.called ).to.equal( false );
  196. expect( consoleLog.called ).to.equal( true );
  197. expect( consoleLog.firstCall.args[ 0 ] ).to.equal( 'Blocked selectionChange to avoid breaking table cells selection.' );
  198. // Enables listening to `selectionChange` event.
  199. viewDocument.fire( 'mouseup' );
  200. viewDocument.fire( 'selectionChange', {
  201. newSelection: view.document.selection
  202. } );
  203. expect( selectionChangeCallback.called ).to.equal( true );
  204. consoleLog.restore();
  205. } );
  206. } );
  207. describe( 'selection by mouse drag', () => {
  208. let preventDefault;
  209. beforeEach( async () => {
  210. editor = await createEditor();
  211. model = editor.model;
  212. modelRoot = model.document.getRoot();
  213. view = editor.editing.view;
  214. viewDocument = view.document;
  215. tableMouse = editor.plugins.get( TableMouse );
  216. setModelData( model, modelTable( [
  217. [ '11[]', '12', '13' ],
  218. [ '21', '22', '23' ],
  219. [ '31', '32', '33' ]
  220. ] ) );
  221. preventDefault = sinon.spy();
  222. } );
  223. it( 'should do nothing if the plugin is disabled', () => {
  224. tableMouse.isEnabled = false;
  225. const domEventDataMock = new DomEventData( view, {} );
  226. viewDocument.fire( 'mousedown', domEventDataMock );
  227. assertSelectedCells( model, [
  228. [ 0, 0, 0 ],
  229. [ 0, 0, 0 ],
  230. [ 0, 0, 0 ]
  231. ] );
  232. } );
  233. it( 'should do nothing if the TableSelection plugin is disabled', () => {
  234. editor.plugins.get( 'TableSelection' ).isEnabled = false;
  235. const domEventDataMock = new DomEventData( view, {} );
  236. viewDocument.fire( 'mousedown', domEventDataMock );
  237. assertSelectedCells( model, [
  238. [ 0, 0, 0 ],
  239. [ 0, 0, 0 ],
  240. [ 0, 0, 0 ]
  241. ] );
  242. } );
  243. it( 'should abort if Ctrl is pressed', () => {
  244. const domEventDataMock = new DomEventData( view, {
  245. ctrlKey: true
  246. } );
  247. viewDocument.fire( 'mousedown', domEventDataMock );
  248. assertSelectedCells( model, [
  249. [ 0, 0, 0 ],
  250. [ 0, 0, 0 ],
  251. [ 0, 0, 0 ]
  252. ] );
  253. } );
  254. it( 'should abort if Alt is pressed', () => {
  255. const domEventDataMock = new DomEventData( view, {
  256. altKey: true
  257. } );
  258. viewDocument.fire( 'mousedown', domEventDataMock );
  259. assertSelectedCells( model, [
  260. [ 0, 0, 0 ],
  261. [ 0, 0, 0 ],
  262. [ 0, 0, 0 ]
  263. ] );
  264. } );
  265. it( 'should do nothing if any of mouse buttons was not clicked', () => {
  266. viewDocument.fire( 'mousemove', new DomEventData( view, {
  267. buttons: 0
  268. } ) );
  269. assertSelectedCells( model, [
  270. [ 0, 0, 0 ],
  271. [ 0, 0, 0 ],
  272. [ 0, 0, 0 ]
  273. ] );
  274. } );
  275. it( 'should do nothing if started dragging outside of table', () => {
  276. model.change( writer => {
  277. const paragraph = writer.createElement( 'paragraph' );
  278. const text = writer.createText( 'foo' );
  279. writer.insert( text, paragraph );
  280. writer.insert( paragraph, model.document.getRoot(), 'end' );
  281. writer.setSelection( paragraph, 'end' );
  282. } );
  283. viewDocument.fire( 'mousedown', new DomEventData( view, {
  284. target: view.domConverter.mapViewToDom(
  285. viewDocument.getRoot().getChild( 1 )
  286. )
  287. } ) );
  288. viewDocument.fire( 'mousemove', new DomEventData( view, {
  289. buttons: 1
  290. } ) );
  291. assertSelectedCells( model, [
  292. [ 0, 0, 0 ],
  293. [ 0, 0, 0 ],
  294. [ 0, 0, 0 ]
  295. ] );
  296. } );
  297. it( 'should do nothing if ended dragging outside of table', () => {
  298. model.change( writer => {
  299. const paragraph = writer.createElement( 'paragraph' );
  300. const text = writer.createText( 'foo' );
  301. writer.insert( text, paragraph );
  302. writer.insert( paragraph, model.document.getRoot(), 'end' );
  303. writer.setSelection( paragraph, 'end' );
  304. } );
  305. viewDocument.fire( 'mousedown', new DomEventData( view, {
  306. target: view.domConverter.mapViewToDom(
  307. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 0 )
  308. )
  309. } ) );
  310. viewDocument.fire( 'mousemove', new DomEventData( view, {
  311. target: view.domConverter.mapViewToDom(
  312. viewDocument.getRoot().getChild( 1 )
  313. ),
  314. buttons: 1
  315. } ) );
  316. assertSelectedCells( model, [
  317. [ 0, 0, 0 ],
  318. [ 0, 0, 0 ],
  319. [ 0, 0, 0 ]
  320. ] );
  321. } );
  322. it( 'should do nothing if ended dragging inside another table', () => {
  323. setModelData( model, [
  324. modelTable( [
  325. [ '1.11[]', '1.12' ],
  326. [ '1.21', '1.22' ]
  327. ] ),
  328. modelTable( [
  329. [ '2.11', '2.12' ],
  330. [ '2.21', '2.22' ]
  331. ] )
  332. ].join( '' ) );
  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 ).getChild( 1 ).getChild( 0 ).getChild( 1 ).getChild( 1 )
  341. ),
  342. buttons: 1
  343. } ) );
  344. assertSelectedCells( model, [
  345. [ 0, 0 ],
  346. [ 0, 0 ]
  347. ] );
  348. } );
  349. it( 'should do nothing if ended in the same cell', () => {
  350. viewDocument.fire( 'mousedown', new DomEventData( view, {
  351. target: view.domConverter.mapViewToDom(
  352. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 0 )
  353. )
  354. } ) );
  355. viewDocument.fire( 'mousemove', new DomEventData( view, {
  356. target: view.domConverter.mapViewToDom(
  357. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 0 )
  358. ),
  359. buttons: 1
  360. } ) );
  361. assertSelectedCells( model, [
  362. [ 0, 0, 0 ],
  363. [ 0, 0, 0 ],
  364. [ 0, 0, 0 ]
  365. ] );
  366. } );
  367. it( 'should select started and ended dragging in the same cell but went over its border', () => {
  368. viewDocument.fire( 'mousedown', new DomEventData( view, {
  369. target: view.domConverter.mapViewToDom(
  370. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 0 )
  371. )
  372. } ) );
  373. // Select the next one.
  374. viewDocument.fire( 'mousemove', new DomEventData( view, {
  375. target: view.domConverter.mapViewToDom(
  376. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 1 )
  377. ),
  378. buttons: 1,
  379. preventDefault: sinon.spy()
  380. } ) );
  381. // And back to the "started" cell.
  382. viewDocument.fire( 'mousemove', new DomEventData( view, {
  383. target: view.domConverter.mapViewToDom(
  384. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 0 )
  385. ),
  386. buttons: 1,
  387. preventDefault: sinon.spy()
  388. } ) );
  389. viewDocument.fire( 'mouseup' );
  390. assertSelectedCells( model, [
  391. [ 1, 0, 0 ],
  392. [ 0, 0, 0 ],
  393. [ 0, 0, 0 ]
  394. ] );
  395. } );
  396. it( 'should select all cells in first row', () => {
  397. viewDocument.fire( 'mousedown', new DomEventData( view, {
  398. target: view.domConverter.mapViewToDom(
  399. // figure > table > tbody > tr > td
  400. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 0 )
  401. )
  402. } ) );
  403. viewDocument.fire( 'mousemove', new DomEventData( view, {
  404. target: view.domConverter.mapViewToDom(
  405. // figure > table > tbody > tr > td
  406. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 2 )
  407. ),
  408. buttons: 1,
  409. preventDefault
  410. } ) );
  411. viewDocument.fire( 'mouseup' );
  412. assertSelectedCells( model, [
  413. [ 1, 1, 1 ],
  414. [ 0, 0, 0 ],
  415. [ 0, 0, 0 ]
  416. ] );
  417. expect( preventDefault.called ).to.equal( true );
  418. } );
  419. it( 'should ignore `selectionChange` event when selecting cells ', () => {
  420. const consoleLog = sinon.stub( console, 'log' );
  421. const preventDefault = sinon.spy();
  422. const selectionChangeCallback = sinon.spy();
  423. // Adding a new callback to check whether it will be executed (whether `evt.stop()` is being called).
  424. viewDocument.on( 'selectionChange', selectionChangeCallback );
  425. // Click on a cell.
  426. viewDocument.fire( 'mousedown', new DomEventData( view, {
  427. target: view.domConverter.mapViewToDom(
  428. // figure > table > tbody > tr > td
  429. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 1 )
  430. )
  431. } ) );
  432. // Then move the mouse to another cell. Disables listening to `selectionChange`.
  433. viewDocument.fire( 'mousemove', new DomEventData( view, {
  434. buttons: 1,
  435. target: view.domConverter.mapViewToDom(
  436. // figure > table > tbody > tr > td
  437. viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 2 )
  438. ),
  439. preventDefault
  440. } ) );
  441. // See explanation why do we fire it in the similar test for Shift+click.
  442. viewDocument.fire( 'selectionChange' );
  443. // `selectionChange` event should be canceled.
  444. expect( selectionChangeCallback.called ).to.equal( false );
  445. expect( consoleLog.called ).to.equal( true );
  446. expect( consoleLog.firstCall.args[ 0 ] ).to.equal( 'Blocked selectionChange to avoid breaking table cells selection.' );
  447. // Enables listening to `selectionChange` event.
  448. viewDocument.fire( 'mouseup' );
  449. viewDocument.fire( 'selectionChange', {
  450. newSelection: view.document.selection
  451. } );
  452. expect( selectionChangeCallback.called ).to.equal( true );
  453. consoleLog.restore();
  454. } );
  455. } );
  456. function createEditor() {
  457. return ClassicTestEditor.create( editorElement, {
  458. plugins: [ TableEditing, TableSelection, TableMouse, Paragraph, Typing ]
  459. } );
  460. }
  461. } );