8
0

mouseselectionobserver.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  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, modelRoot, view, viewDoc;
  15. beforeEach( async () => {
  16. editor = await VirtualTestEditor.create( {
  17. plugins: [ TableEditing, TableSelection, Paragraph ]
  18. } );
  19. model = editor.model;
  20. modelRoot = model.document.getRoot();
  21. view = editor.editing.view;
  22. viewDoc = view.document;
  23. setModelData( model, modelTable( [
  24. [ '11[]', '12', '13' ],
  25. [ '21', '22', '23' ],
  26. [ '31', '32', '33' ]
  27. ] ) );
  28. } );
  29. afterEach( async () => {
  30. await editor.destroy();
  31. } );
  32. describe( 'MouseSelectionObserver', () => {
  33. it( 'should not start table selection when mouse move is inside one table cell', () => {
  34. setModelData( model, modelTable( [
  35. [ '[]00', '01' ],
  36. [ '10', '11' ]
  37. ] ) );
  38. pressMouseButtonOver( getTableCell( '00' ) );
  39. assertEqualMarkup( getModelData( model ), modelTable( [
  40. [ '[]00', '01' ],
  41. [ '10', '11' ]
  42. ] ) );
  43. movePressedMouseOver( getTableCell( '00' ) );
  44. assertEqualMarkup( getModelData( model ), modelTable( [
  45. [ '[]00', '01' ],
  46. [ '10', '11' ]
  47. ] ) );
  48. } );
  49. it( 'should start table selection when mouse move expands over two cells', () => {
  50. setModelData( model, modelTable( [
  51. [ '[]00', '01' ],
  52. [ '10', '11' ]
  53. ] ) );
  54. pressMouseButtonOver( getTableCell( '00' ) );
  55. assertEqualMarkup( getModelData( model ), modelTable( [
  56. [ '[]00', '01' ],
  57. [ '10', '11' ]
  58. ] ) );
  59. movePressedMouseOver( getTableCell( '01' ) );
  60. assertSelectedCells( [
  61. [ 1, 1 ],
  62. [ 0, 0 ]
  63. ] );
  64. assertEqualMarkup( getViewData( view ), viewTable( [
  65. [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ],
  66. [ '10', '11' ]
  67. ], { asWidget: true } ) );
  68. } );
  69. it( 'should select rectangular table cells when mouse moved to diagonal cell (up -> down)', () => {
  70. setModelData( model, modelTable( [
  71. [ '[]00', '01' ],
  72. [ '10', '11' ]
  73. ] ) );
  74. pressMouseButtonOver( getTableCell( '00' ) );
  75. assertEqualMarkup( getModelData( model ), modelTable( [
  76. [ '[]00', '01' ],
  77. [ '10', '11' ]
  78. ] ) );
  79. movePressedMouseOver( getTableCell( '11' ) );
  80. assertSelectedCells( [
  81. [ 1, 1 ],
  82. [ 1, 1 ]
  83. ] );
  84. assertEqualMarkup( getViewData( view ), viewTable( [
  85. [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ],
  86. [ { contents: '10', class: 'selected', isSelected: true }, { contents: '11', class: 'selected', isSelected: true } ]
  87. ], { asWidget: true } ) );
  88. } );
  89. it( 'should select rectangular table cells when mouse moved to diagonal cell (down -> up)', () => {
  90. setModelData( model, modelTable( [
  91. [ '00', '01' ],
  92. [ '10', '[]11' ]
  93. ] ) );
  94. pressMouseButtonOver( getTableCell( '11' ) );
  95. assertEqualMarkup( getModelData( model ), modelTable( [
  96. [ '00', '01' ],
  97. [ '10', '[]11' ]
  98. ] ) );
  99. movePressedMouseOver( getTableCell( '00' ) );
  100. assertSelectedCells( [
  101. [ 1, 1 ],
  102. [ 1, 1 ]
  103. ] );
  104. assertEqualMarkup( getViewData( view ), viewTable( [
  105. [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ],
  106. [ { contents: '10', class: 'selected', isSelected: true }, { contents: '11', class: 'selected', isSelected: true } ]
  107. ], { asWidget: true } ) );
  108. } );
  109. it( 'should update view selection after changing selection rect', () => {
  110. setModelData( model, modelTable( [
  111. [ '[]00', '01', '02' ],
  112. [ '10', '11', '12' ],
  113. [ '20', '21', '22' ]
  114. ] ) );
  115. pressMouseButtonOver( getTableCell( '00' ) );
  116. movePressedMouseOver( getTableCell( '22' ) );
  117. assertSelectedCells( [
  118. [ 1, 1, 1 ],
  119. [ 1, 1, 1 ],
  120. [ 1, 1, 1 ]
  121. ] );
  122. assertEqualMarkup( getViewData( view ), viewTable( [
  123. [
  124. { contents: '00', class: 'selected', isSelected: true },
  125. { contents: '01', class: 'selected', isSelected: true },
  126. { contents: '02', class: 'selected', isSelected: true }
  127. ],
  128. [
  129. { contents: '10', class: 'selected', isSelected: true },
  130. { contents: '11', class: 'selected', isSelected: true },
  131. { contents: '12', class: 'selected', isSelected: true }
  132. ],
  133. [
  134. { contents: '20', class: 'selected', isSelected: true },
  135. { contents: '21', class: 'selected', isSelected: true },
  136. { contents: '22', class: 'selected', isSelected: true }
  137. ]
  138. ], { asWidget: true } ) );
  139. movePressedMouseOver( getTableCell( '11' ) );
  140. assertSelectedCells( [
  141. [ 1, 1, 0 ],
  142. [ 1, 1, 0 ],
  143. [ 0, 0, 0 ]
  144. ] );
  145. assertEqualMarkup( getViewData( view ), viewTable( [
  146. [
  147. { contents: '00', class: 'selected', isSelected: true },
  148. { contents: '01', class: 'selected', isSelected: true },
  149. '02'
  150. ],
  151. [
  152. { contents: '10', class: 'selected', isSelected: true },
  153. { contents: '11', class: 'selected', isSelected: true },
  154. '12'
  155. ],
  156. [
  157. '20',
  158. '21',
  159. '22'
  160. ]
  161. ], { asWidget: true } ) );
  162. } );
  163. it( 'should stop selecting after "mouseup" event', () => {
  164. setModelData( model, modelTable( [
  165. [ '[]00', '01' ],
  166. [ '10', '11' ]
  167. ] ) );
  168. pressMouseButtonOver( getTableCell( '00' ) );
  169. assertEqualMarkup( getModelData( model ), modelTable( [
  170. [ '[]00', '01' ],
  171. [ '10', '11' ]
  172. ] ) );
  173. movePressedMouseOver( getTableCell( '01' ) );
  174. releaseMouseButtonOver( getTableCell( '01' ) );
  175. assertSelectedCells( [
  176. [ 1, 1 ],
  177. [ 0, 0 ]
  178. ] );
  179. assertEqualMarkup( getViewData( view ), viewTable( [
  180. [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ],
  181. [ '10', '11' ]
  182. ], { asWidget: true } ) );
  183. moveReleasedMouseOver( getTableCell( '11' ) );
  184. assertSelectedCells( [
  185. [ 1, 1 ],
  186. [ 0, 0 ]
  187. ] );
  188. } );
  189. it( 'should do nothing on "mouseup" event', () => {
  190. setModelData( model, modelTable( [
  191. [ '[]00', '01' ],
  192. [ '10', '11' ]
  193. ] ) );
  194. releaseMouseButtonOver( getTableCell( '01' ) );
  195. assertSelectedCells( [
  196. [ 0, 0 ],
  197. [ 0, 0 ]
  198. ] );
  199. } );
  200. it( 'should stop selection mode on "mouseleve" event if next "mousemove" has no button pressed', () => {
  201. setModelData( model, modelTable( [
  202. [ '[]00', '01' ],
  203. [ '10', '11' ]
  204. ] ) );
  205. pressMouseButtonOver( getTableCell( '00' ) );
  206. assertEqualMarkup( getModelData( model ), modelTable( [
  207. [ '[]00', '01' ],
  208. [ '10', '11' ]
  209. ] ) );
  210. movePressedMouseOver( getTableCell( '01' ) );
  211. makeMouseLeave();
  212. assertSelectedCells( [
  213. [ 1, 1 ],
  214. [ 0, 0 ]
  215. ] );
  216. assertEqualMarkup( getViewData( view ), viewTable( [
  217. [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ],
  218. [ '10', '11' ]
  219. ], { asWidget: true } ) );
  220. moveReleasedMouseOver( getTableCell( '11' ) );
  221. assertSelectedCells( [
  222. [ 1, 1 ],
  223. [ 0, 0 ]
  224. ] );
  225. } );
  226. it( 'should continue selection mode on "mouseleve" and "mousemove" if mouse button is pressed', () => {
  227. setModelData( model, modelTable( [
  228. [ '[]00', '01' ],
  229. [ '10', '11' ]
  230. ] ) );
  231. pressMouseButtonOver( getTableCell( '00' ) );
  232. assertEqualMarkup( getModelData( model ), modelTable( [
  233. [ '[]00', '01' ],
  234. [ '10', '11' ]
  235. ] ) );
  236. movePressedMouseOver( getTableCell( '01' ) );
  237. makeMouseLeave();
  238. assertSelectedCells( [
  239. [ 1, 1 ],
  240. [ 0, 0 ]
  241. ] );
  242. assertEqualMarkup( getViewData( view ), viewTable( [
  243. [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ],
  244. [ '10', '11' ]
  245. ], { asWidget: true } ) );
  246. movePressedMouseOver( getTableCell( '11' ) );
  247. assertSelectedCells( [
  248. [ 1, 1 ],
  249. [ 1, 1 ]
  250. ] );
  251. } );
  252. it( 'should do nothing on "mouseleve" event', () => {
  253. setModelData( model, modelTable( [
  254. [ '[]00', '01' ],
  255. [ '10', '11' ]
  256. ] ) );
  257. makeMouseLeave();
  258. assertSelectedCells( [
  259. [ 0, 0 ],
  260. [ 0, 0 ]
  261. ] );
  262. } );
  263. it( 'should do nothing on "mousedown" event over ui element (click on selection handle)', () => {
  264. setModelData( model, modelTable( [
  265. [ '[]00', '01' ],
  266. [ '10', '11' ]
  267. ] ) );
  268. const uiElement = viewDoc.getRoot()
  269. .getChild( 0 )
  270. .getChild( 0 ); // selection handler;
  271. fireEvent( view, 'mousedown', addTarget( uiElement ), mouseButtonPressed );
  272. assertEqualMarkup( getModelData( model ), modelTable( [
  273. [ '[]00', '01' ],
  274. [ '10', '11' ]
  275. ] ) );
  276. } );
  277. it( 'should do nothing on "mousemove" event over ui element (click on selection handle)', () => {
  278. setModelData( model, modelTable( [
  279. [ '[]00', '01' ],
  280. [ '10', '11' ]
  281. ] ) );
  282. const uiElement = viewDoc.getRoot()
  283. .getChild( 0 )
  284. .getChild( 0 ); // selection handler;
  285. fireEvent( view, 'mousemove', addTarget( uiElement ), mouseButtonPressed );
  286. assertEqualMarkup( getModelData( model ), modelTable( [
  287. [ '[]00', '01' ],
  288. [ '10', '11' ]
  289. ] ) );
  290. } );
  291. it( 'should clear view table selection after mouse click outside table', () => {
  292. setModelData( model, modelTable( [
  293. [ '[]00', '01' ],
  294. [ '10', '11' ]
  295. ] ) + '<paragraph>foo</paragraph>' );
  296. pressMouseButtonOver( getTableCell( '00' ) );
  297. assertEqualMarkup( getModelData( model ), modelTable( [
  298. [ '[]00', '01' ],
  299. [ '10', '11' ]
  300. ] ) + '<paragraph>foo</paragraph>' );
  301. movePressedMouseOver( getTableCell( '01' ) );
  302. const paragraph = viewDoc.getRoot().getChild( 1 );
  303. fireEvent( view, 'mousemove', addTarget( paragraph ) );
  304. fireEvent( view, 'mousedown', addTarget( paragraph ) );
  305. fireEvent( view, 'mouseup', addTarget( paragraph ) );
  306. // The click in the DOM would trigger selection change and it will set the selection:
  307. model.change( writer => {
  308. writer.setSelection( writer.createRange( writer.createPositionAt( model.document.getRoot().getChild( 1 ), 0 ) ) );
  309. } );
  310. assertEqualMarkup( getViewData( view ), viewTable( [
  311. [ '00', '01' ],
  312. [ '10', '11' ]
  313. ], { asWidget: true } ) + '<p>{}foo</p>' );
  314. } );
  315. } );
  316. // Helper method for asserting selected table cells.
  317. //
  318. // To check if a table has expected cells selected pass two dimensional array of truthy and falsy values:
  319. //
  320. // assertSelectedCells( [
  321. // [ 0, 1 ],
  322. // [ 0, 1 ]
  323. // ] );
  324. //
  325. // The above call will check if table has second column selected (assuming no spans).
  326. //
  327. // **Note**: This function operates on child indexes - not rows/columns.
  328. function assertSelectedCells( tableMap ) {
  329. const tableIndex = 0;
  330. for ( let rowIndex = 0; rowIndex < tableMap.length; rowIndex++ ) {
  331. const row = tableMap[ rowIndex ];
  332. for ( let cellIndex = 0; cellIndex < row.length; cellIndex++ ) {
  333. const expectSelected = row[ cellIndex ];
  334. if ( expectSelected ) {
  335. assertNodeIsSelected( [ tableIndex, rowIndex, cellIndex ] );
  336. } else {
  337. assertNodeIsNotSelected( [ tableIndex, rowIndex, cellIndex ] );
  338. }
  339. }
  340. }
  341. }
  342. function assertNodeIsSelected( path ) {
  343. const node = modelRoot.getNodeByPath( path );
  344. const selectionRanges = Array.from( model.document.selection.getRanges() );
  345. expect( selectionRanges.some( range => range.containsItem( node ) ), `Expected node [${ path }] to be selected` ).to.be.true;
  346. }
  347. function assertNodeIsNotSelected( path ) {
  348. const node = modelRoot.getNodeByPath( path );
  349. const selectionRanges = Array.from( model.document.selection.getRanges() );
  350. expect( selectionRanges.every( range => !range.containsItem( node ) ), `Expected node [${ path }] to be not selected` ).to.be.true;
  351. }
  352. function getTableCell( data ) {
  353. for ( const value of view.createRangeIn( viewDoc.getRoot() ) ) {
  354. if ( value.type === 'text' && value.item.data === data ) {
  355. return value.item.parent.parent;
  356. }
  357. }
  358. }
  359. function makeMouseLeave() {
  360. fireEvent( view, 'mouseleave' );
  361. }
  362. function pressMouseButtonOver( target ) {
  363. fireEvent( view, 'mousedown', addTarget( target ), mouseButtonPressed );
  364. }
  365. function movePressedMouseOver( target ) {
  366. moveMouseOver( target, mouseButtonPressed );
  367. }
  368. function moveReleasedMouseOver( target ) {
  369. moveMouseOver( target, mouseButtonReleased );
  370. }
  371. function moveMouseOver( target, ...decorators ) {
  372. fireEvent( view, 'mousemove', addTarget( target ), ...decorators );
  373. }
  374. function releaseMouseButtonOver( target ) {
  375. fireEvent( view, 'mouseup', addTarget( target ), mouseButtonReleased );
  376. }
  377. function addTarget( target ) {
  378. return domEventData => {
  379. domEventData.target = target;
  380. };
  381. }
  382. function mouseButtonPressed( domEventData ) {
  383. domEventData.domEvent.buttons = 1;
  384. }
  385. function mouseButtonReleased( domEventData ) {
  386. domEventData.domEvent.buttons = 0;
  387. }
  388. function fireEvent( view, eventName, ...decorators ) {
  389. const domEvtDataStub = {
  390. domEvent: {
  391. buttons: 0
  392. },
  393. target: undefined,
  394. preventDefault: sinon.spy(),
  395. stopPropagation: sinon.spy()
  396. };
  397. for ( const decorator of decorators ) {
  398. decorator( domEvtDataStub );
  399. }
  400. viewDoc.fire( eventName, domEvtDataStub );
  401. }
  402. } );