tableediting.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913
  1. /**
  2. * @license Copyright (c) 2003-2019, 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 { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
  10. import ImageEditing from '@ckeditor/ckeditor5-image/src/image/imageediting';
  11. import TableEditing from '../src/tableediting';
  12. import { formatTable, formattedModelTable, formattedViewTable, modelTable } from './_utils/utils';
  13. import InsertRowCommand from '../src/commands/insertrowcommand';
  14. import InsertTableCommand from '../src/commands/inserttablecommand';
  15. import InsertColumnCommand from '../src/commands/insertcolumncommand';
  16. import RemoveRowCommand from '../src/commands/removerowcommand';
  17. import RemoveColumnCommand from '../src/commands/removecolumncommand';
  18. import SplitCellCommand from '../src/commands/splitcellcommand';
  19. import MergeCellsCommand from '../src/commands/mergecellscommand';
  20. import SetHeaderRowCommand from '../src/commands/setheaderrowcommand';
  21. import SetHeaderColumnCommand from '../src/commands/setheadercolumncommand';
  22. import MediaEmbedEditing from '@ckeditor/ckeditor5-media-embed/src/mediaembedediting';
  23. describe( 'TableEditing', () => {
  24. let editor, model;
  25. beforeEach( () => {
  26. return VirtualTestEditor
  27. .create( {
  28. plugins: [ TableEditing, Paragraph, ImageEditing, MediaEmbedEditing ]
  29. } )
  30. .then( newEditor => {
  31. editor = newEditor;
  32. model = editor.model;
  33. } );
  34. } );
  35. afterEach( () => {
  36. editor.destroy();
  37. } );
  38. it( 'should have pluginName', () => {
  39. expect( TableEditing.pluginName ).to.equal( 'TableEditing' );
  40. } );
  41. it( 'should set proper schema rules', () => {
  42. // Table:
  43. expect( model.schema.isRegistered( 'table' ) ).to.be.true;
  44. expect( model.schema.isObject( 'table' ) ).to.be.true;
  45. expect( model.schema.isBlock( 'table' ) ).to.be.true;
  46. expect( model.schema.isLimit( 'table' ) ).to.be.true;
  47. expect( model.schema.checkChild( [ '$root' ], 'table' ) ).to.be.true;
  48. expect( model.schema.checkAttribute( [ '$root', 'table' ], 'headingRows' ) ).to.be.true;
  49. expect( model.schema.checkAttribute( [ '$root', 'table' ], 'headingColumns' ) ).to.be.true;
  50. // Table row:
  51. expect( model.schema.isRegistered( 'tableRow' ) ).to.be.true;
  52. expect( model.schema.isLimit( 'tableRow' ) ).to.be.true;
  53. expect( model.schema.checkChild( [ '$root' ], 'tableRow' ) ).to.be.false;
  54. expect( model.schema.checkChild( [ 'table' ], 'tableRow' ) ).to.be.true;
  55. // Table cell:
  56. expect( model.schema.isRegistered( 'tableCell' ) ).to.be.true;
  57. expect( model.schema.isLimit( 'tableCell' ) ).to.be.true;
  58. expect( model.schema.checkChild( [ '$root' ], 'tableCell' ) ).to.be.false;
  59. expect( model.schema.checkChild( [ 'table' ], 'tableCell' ) ).to.be.false;
  60. expect( model.schema.checkChild( [ 'tableRow' ], 'tableCell' ) ).to.be.true;
  61. expect( model.schema.checkChild( [ 'tableCell' ], 'tableCell' ) ).to.be.false;
  62. expect( model.schema.checkAttribute( [ 'tableCell' ], 'colspan' ) ).to.be.true;
  63. expect( model.schema.checkAttribute( [ 'tableCell' ], 'rowspan' ) ).to.be.true;
  64. // Table cell contents:
  65. expect( model.schema.checkChild( [ '$root', 'table', 'tableRow', 'tableCell' ], '$text' ) ).to.be.false;
  66. expect( model.schema.checkChild( [ '$root', 'table', 'tableRow', 'tableCell' ], '$block' ) ).to.be.true;
  67. expect( model.schema.checkChild( [ '$root', 'table', 'tableRow', 'tableCell' ], 'table' ) ).to.be.false;
  68. expect( model.schema.checkChild( [ '$root', 'table', 'tableRow', 'tableCell' ], 'image' ) ).to.be.true;
  69. } );
  70. it( 'adds insertTable command', () => {
  71. expect( editor.commands.get( 'insertTable' ) ).to.be.instanceOf( InsertTableCommand );
  72. } );
  73. it( 'adds insertRowAbove command', () => {
  74. expect( editor.commands.get( 'insertTableRowAbove' ) ).to.be.instanceOf( InsertRowCommand );
  75. } );
  76. it( 'adds insertRowBelow command', () => {
  77. expect( editor.commands.get( 'insertTableRowBelow' ) ).to.be.instanceOf( InsertRowCommand );
  78. } );
  79. it( 'adds insertColumnLeft command', () => {
  80. expect( editor.commands.get( 'insertTableColumnLeft' ) ).to.be.instanceOf( InsertColumnCommand );
  81. } );
  82. it( 'adds insertColumnRight command', () => {
  83. expect( editor.commands.get( 'insertTableColumnRight' ) ).to.be.instanceOf( InsertColumnCommand );
  84. } );
  85. it( 'adds removeRow command', () => {
  86. expect( editor.commands.get( 'removeTableRow' ) ).to.be.instanceOf( RemoveRowCommand );
  87. } );
  88. it( 'adds removeColumn command', () => {
  89. expect( editor.commands.get( 'removeTableColumn' ) ).to.be.instanceOf( RemoveColumnCommand );
  90. } );
  91. it( 'adds splitCellVertically command', () => {
  92. expect( editor.commands.get( 'splitTableCellVertically' ) ).to.be.instanceOf( SplitCellCommand );
  93. } );
  94. it( 'adds splitCellHorizontally command', () => {
  95. expect( editor.commands.get( 'splitTableCellHorizontally' ) ).to.be.instanceOf( SplitCellCommand );
  96. } );
  97. it( 'adds mergeTableCells command', () => {
  98. expect( editor.commands.get( 'mergeTableCells' ) ).to.be.instanceOf( MergeCellsCommand );
  99. } );
  100. it( 'adds setColumnHeader command', () => {
  101. expect( editor.commands.get( 'setTableColumnHeader' ) ).to.be.instanceOf( SetHeaderColumnCommand );
  102. } );
  103. it( 'adds setRowHeader command', () => {
  104. expect( editor.commands.get( 'setTableRowHeader' ) ).to.be.instanceOf( SetHeaderRowCommand );
  105. } );
  106. describe( 'conversion in data pipeline', () => {
  107. describe( 'model to view', () => {
  108. it( 'should create tbody section', () => {
  109. setModelData( model, '<table><tableRow><tableCell><paragraph>foo[]</paragraph></tableCell></tableRow></table>' );
  110. expect( editor.getData() ).to.equal(
  111. '<figure class="table">' +
  112. '<table>' +
  113. '<tbody>' +
  114. '<tr><td>foo</td></tr>' +
  115. '</tbody>' +
  116. '</table>' +
  117. '</figure>'
  118. );
  119. } );
  120. it( 'should create thead section', () => {
  121. setModelData(
  122. model,
  123. '<table headingRows="1"><tableRow><tableCell><paragraph>foo[]</paragraph></tableCell></tableRow></table>'
  124. );
  125. expect( editor.getData() ).to.equal(
  126. '<figure class="table">' +
  127. '<table>' +
  128. '<thead>' +
  129. '<tr><th>foo</th></tr>' +
  130. '</thead>' +
  131. '</table>' +
  132. '</figure>'
  133. );
  134. } );
  135. } );
  136. describe( 'view to model', () => {
  137. it( 'should convert table', () => {
  138. editor.setData( '<table><tbody><tr><td>foo</td></tr></tbody></table>' );
  139. expect( getModelData( model, { withoutSelection: true } ) )
  140. .to.equal( '<table><tableRow><tableCell><paragraph>foo</paragraph></tableCell></tableRow></table>' );
  141. } );
  142. it( 'should convert table with image', () => {
  143. editor.setData( '<table><tbody><tr><td><img src="sample.png"></td></tr></tbody></table>' );
  144. expect( getModelData( model, { withoutSelection: true } ) )
  145. .to.equal( '<table><tableRow><tableCell><image src="sample.png"></image></tableCell></tableRow></table>' );
  146. } );
  147. it( 'should insert a paragraph when the cell content is unsupported', () => {
  148. editor.setData(
  149. '<table><tbody><tr><td><foo></foo></td></tr></tbody></table>'
  150. );
  151. expect( getModelData( model, { withoutSelection: true } ) )
  152. .to.equal( '<table><tableRow><tableCell><paragraph></paragraph></tableCell></tableRow></table>' );
  153. } );
  154. it( 'should convert a table with media', () => {
  155. editor.setData(
  156. '<table><tbody><tr><td><oembed url="https://www.youtube.com/watch?v=H08tGjXNHO4"></oembed></td></tr></tbody></table>'
  157. );
  158. expect( getModelData( model, { withoutSelection: true } ) )
  159. .to.equal( '<table><tableRow><tableCell>' +
  160. '<media url="https://www.youtube.com/watch?v=H08tGjXNHO4"></media>' +
  161. '</tableCell></tableRow></table>' );
  162. } );
  163. } );
  164. } );
  165. describe( 'caret movement', () => {
  166. let domEvtDataStub;
  167. beforeEach( () => {
  168. domEvtDataStub = {
  169. keyCode: getCode( 'Tab' ),
  170. preventDefault: sinon.spy(),
  171. stopPropagation: sinon.spy()
  172. };
  173. } );
  174. it( 'should do nothing if not tab pressed', () => {
  175. setModelData( model, modelTable( [
  176. [ '11', '12[]' ]
  177. ] ) );
  178. domEvtDataStub.keyCode = getCode( 'a' );
  179. editor.editing.view.document.fire( 'keydown', domEvtDataStub );
  180. sinon.assert.notCalled( domEvtDataStub.preventDefault );
  181. sinon.assert.notCalled( domEvtDataStub.stopPropagation );
  182. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  183. [ '11', '12[]' ]
  184. ] ) );
  185. } );
  186. it( 'should do nothing if Ctrl+Tab is pressed', () => {
  187. setModelData( model, modelTable( [
  188. [ '11', '12[]' ]
  189. ] ) );
  190. domEvtDataStub.ctrlKey = true;
  191. editor.editing.view.document.fire( 'keydown', domEvtDataStub );
  192. sinon.assert.notCalled( domEvtDataStub.preventDefault );
  193. sinon.assert.notCalled( domEvtDataStub.stopPropagation );
  194. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  195. [ '11', '12[]' ]
  196. ] ) );
  197. } );
  198. describe( 'on TAB', () => {
  199. it( 'should do nothing if selection is not in a table', () => {
  200. setModelData( model, '<paragraph>[]</paragraph>' + modelTable( [ [ '11', '12' ] ] ) );
  201. editor.editing.view.document.fire( 'keydown', domEvtDataStub );
  202. sinon.assert.notCalled( domEvtDataStub.preventDefault );
  203. sinon.assert.notCalled( domEvtDataStub.stopPropagation );
  204. expect( formatTable( getModelData( model ) ) )
  205. .to.equal( '<paragraph>[]</paragraph>' + formattedModelTable( [ [ '11', '12' ] ] ) );
  206. } );
  207. it( 'should move to next cell', () => {
  208. setModelData( model, modelTable( [
  209. [ '11[]', '12' ]
  210. ] ) );
  211. editor.editing.view.document.fire( 'keydown', domEvtDataStub );
  212. sinon.assert.calledOnce( domEvtDataStub.preventDefault );
  213. sinon.assert.calledOnce( domEvtDataStub.stopPropagation );
  214. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  215. [ '11', '[12]' ]
  216. ] ) );
  217. } );
  218. it( 'should create another row and move to first cell in new row', () => {
  219. setModelData( model, modelTable( [
  220. [ '11', '[12]' ]
  221. ] ) );
  222. editor.editing.view.document.fire( 'keydown', domEvtDataStub );
  223. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  224. [ '11', '12' ],
  225. [ '[]', '' ]
  226. ] ) );
  227. } );
  228. it( 'should not create another row and not move the caret if insertTableRowBelow command is disabled', () => {
  229. setModelData( model, modelTable( [
  230. [ '11', '12[]' ]
  231. ] ) );
  232. const insertTableRowBelowCommand = editor.commands.get( 'insertTableRowBelow' );
  233. insertTableRowBelowCommand.forceDisabled( 'test' );
  234. editor.editing.view.document.fire( 'keydown', domEvtDataStub );
  235. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  236. [ '11', '12[]' ]
  237. ] ) );
  238. } );
  239. it( 'should move to the first cell of next row if on end of a row', () => {
  240. setModelData( model, modelTable( [
  241. [ '11', '12[]' ],
  242. [ '21', '22' ]
  243. ] ) );
  244. editor.editing.view.document.fire( 'keydown', domEvtDataStub );
  245. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  246. [ '11', '12' ],
  247. [ '[21]', '22' ]
  248. ] ) );
  249. } );
  250. it( 'should move to the next table cell if part of block content is selected', () => {
  251. setModelData( model, modelTable( [
  252. [ '11', '<paragraph>12</paragraph><paragraph>[foo]</paragraph><paragraph>bar</paragraph>', '13' ]
  253. ] ) );
  254. editor.editing.view.document.fire( 'keydown', domEvtDataStub );
  255. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  256. [
  257. '11',
  258. '<paragraph>12</paragraph><paragraph>foo</paragraph><paragraph>bar</paragraph>',
  259. '[13]'
  260. ]
  261. ] ) );
  262. } );
  263. it( 'should move to next cell with an image', () => {
  264. setModelData( model, modelTable( [
  265. [ '11[]', '<paragraph>foo</paragraph><image></image>' ]
  266. ] ) );
  267. editor.editing.view.document.fire( 'keydown', domEvtDataStub );
  268. sinon.assert.calledOnce( domEvtDataStub.preventDefault );
  269. sinon.assert.calledOnce( domEvtDataStub.stopPropagation );
  270. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  271. [ '11', '<paragraph>[foo</paragraph><image></image>]' ]
  272. ] ) );
  273. } );
  274. it( 'should move to next cell with an blockQuote', () => {
  275. model.schema.register( 'blockQuote', {
  276. allowWhere: '$block',
  277. allowContentOf: '$root'
  278. } );
  279. editor.conversion.elementToElement( { model: 'blockQuote', view: 'blockquote' } );
  280. setModelData( model, modelTable( [
  281. [ '11[]', '<blockQuote><paragraph>foo</paragraph></blockQuote>' ]
  282. ] ) );
  283. editor.editing.view.document.fire( 'keydown', domEvtDataStub );
  284. sinon.assert.calledOnce( domEvtDataStub.preventDefault );
  285. sinon.assert.calledOnce( domEvtDataStub.stopPropagation );
  286. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  287. [ '11', '<blockQuote><paragraph>[foo]</paragraph></blockQuote>' ]
  288. ] ) );
  289. } );
  290. it( 'should listen with lower priority then its children', () => {
  291. // Cancel TAB event.
  292. editor.keystrokes.set( 'Tab', ( data, cancel ) => cancel() );
  293. setModelData( model, modelTable( [
  294. [ '11[]', '12' ]
  295. ] ) );
  296. editor.editing.view.document.fire( 'keydown', domEvtDataStub );
  297. sinon.assert.calledOnce( domEvtDataStub.preventDefault );
  298. sinon.assert.calledOnce( domEvtDataStub.stopPropagation );
  299. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  300. [ '11[]', '12' ]
  301. ] ) );
  302. } );
  303. describe( 'on table widget selected', () => {
  304. beforeEach( () => {
  305. editor.model.schema.register( 'block', {
  306. allowWhere: '$block',
  307. allowContentOf: '$block',
  308. isObject: true
  309. } );
  310. editor.conversion.elementToElement( { model: 'block', view: 'block' } );
  311. } );
  312. it( 'should move caret to the first table cell on TAB', () => {
  313. const spy = sinon.spy();
  314. editor.keystrokes.set( 'Tab', spy, { priority: 'lowest' } );
  315. setModelData( model, '[' + modelTable( [
  316. [ '11', '12' ]
  317. ] ) + ']' );
  318. editor.editing.view.document.fire( 'keydown', domEvtDataStub );
  319. sinon.assert.calledOnce( domEvtDataStub.preventDefault );
  320. sinon.assert.calledOnce( domEvtDataStub.stopPropagation );
  321. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  322. [ '[11]', '12' ]
  323. ] ) );
  324. // Should cancel event - so no other tab handler is called.
  325. sinon.assert.notCalled( spy );
  326. } );
  327. it( 'shouldn\'t do anything on other blocks', () => {
  328. const spy = sinon.spy();
  329. editor.editing.view.document.on( 'keydown', spy );
  330. setModelData( model, '[<block>foo</block>]' );
  331. editor.editing.view.document.fire( 'keydown', domEvtDataStub );
  332. sinon.assert.notCalled( domEvtDataStub.preventDefault );
  333. sinon.assert.notCalled( domEvtDataStub.stopPropagation );
  334. expect( formatTable( getModelData( model ) ) ).to.equal( '[<block>foo</block>]' );
  335. // Should not cancel event.
  336. sinon.assert.calledOnce( spy );
  337. } );
  338. } );
  339. } );
  340. describe( 'on SHIFT+TAB', () => {
  341. beforeEach( () => {
  342. domEvtDataStub.shiftKey = true;
  343. } );
  344. it( 'should do nothing if selection is not in a table', () => {
  345. setModelData( model, '<paragraph>[]</paragraph>' + modelTable( [
  346. [ '11', '12' ]
  347. ] ) );
  348. domEvtDataStub.keyCode = getCode( 'Tab' );
  349. domEvtDataStub.shiftKey = true;
  350. editor.editing.view.document.fire( 'keydown', domEvtDataStub );
  351. sinon.assert.notCalled( domEvtDataStub.preventDefault );
  352. sinon.assert.notCalled( domEvtDataStub.stopPropagation );
  353. expect( formatTable( getModelData( model ) ) )
  354. .to.equal( '<paragraph>[]</paragraph>' + formattedModelTable( [ [ '11', '12' ] ] ) );
  355. } );
  356. it( 'should move to previous cell', () => {
  357. setModelData( model, modelTable( [
  358. [ '11', '12[]' ]
  359. ] ) );
  360. editor.editing.view.document.fire( 'keydown', domEvtDataStub );
  361. sinon.assert.calledOnce( domEvtDataStub.preventDefault );
  362. sinon.assert.calledOnce( domEvtDataStub.stopPropagation );
  363. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  364. [ '[11]', '12' ]
  365. ] ) );
  366. } );
  367. it( 'should not move if caret is in first table cell', () => {
  368. setModelData( model, '<paragraph>foo</paragraph>' + modelTable( [
  369. [ '[]11', '12' ]
  370. ] ) );
  371. editor.editing.view.document.fire( 'keydown', domEvtDataStub );
  372. expect( formatTable( getModelData( model ) ) ).to.equal(
  373. '<paragraph>foo</paragraph>' + formattedModelTable( [ [ '[]11', '12' ] ] )
  374. );
  375. } );
  376. it( 'should move to the last cell of previous row if on beginning of a row', () => {
  377. setModelData( model, modelTable( [
  378. [ '11', '12' ],
  379. [ '[]21', '22' ]
  380. ] ) );
  381. editor.editing.view.document.fire( 'keydown', domEvtDataStub );
  382. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  383. [ '11', '[12]' ],
  384. [ '21', '22' ]
  385. ] ) );
  386. } );
  387. it( 'should move to the previous table cell if part of block content is selected', () => {
  388. setModelData( model, modelTable( [
  389. [ '11', '<paragraph>12</paragraph><paragraph>[foo]</paragraph><paragraph>bar</paragraph>', '13' ]
  390. ] ) );
  391. editor.editing.view.document.fire( 'keydown', domEvtDataStub );
  392. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  393. [
  394. '[11]',
  395. '<paragraph>12</paragraph><paragraph>foo</paragraph><paragraph>bar</paragraph>',
  396. '13'
  397. ]
  398. ] ) );
  399. } );
  400. it( 'should move to previous cell with an image', () => {
  401. setModelData( model, modelTable( [
  402. [ '<paragraph>foo</paragraph><image></image>', 'bar[]' ]
  403. ] ) );
  404. editor.editing.view.document.fire( 'keydown', domEvtDataStub );
  405. sinon.assert.calledOnce( domEvtDataStub.preventDefault );
  406. sinon.assert.calledOnce( domEvtDataStub.stopPropagation );
  407. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  408. [ '<paragraph>[foo</paragraph><image></image>]', 'bar' ]
  409. ] ) );
  410. } );
  411. } );
  412. } );
  413. describe( 'enter key', () => {
  414. let evtDataStub, viewDocument;
  415. beforeEach( () => {
  416. evtDataStub = {
  417. preventDefault: sinon.spy(),
  418. stopPropagation: sinon.spy(),
  419. isSoft: false
  420. };
  421. return VirtualTestEditor
  422. .create( {
  423. plugins: [ TableEditing, Paragraph ]
  424. } )
  425. .then( newEditor => {
  426. editor = newEditor;
  427. sinon.stub( editor, 'execute' );
  428. viewDocument = editor.editing.view.document;
  429. model = editor.model;
  430. } );
  431. } );
  432. it( 'should do nothing if not in table cell', () => {
  433. setModelData( model, '<paragraph>[]foo</paragraph>' );
  434. viewDocument.fire( 'enter', evtDataStub );
  435. sinon.assert.notCalled( editor.execute );
  436. expect( formatTable( getModelData( model ) ) ).to.equal( '<paragraph>[]foo</paragraph>' );
  437. } );
  438. it( 'should do nothing if table cell has already a block content', () => {
  439. setModelData( model, modelTable( [
  440. [ '<paragraph>[]11</paragraph>' ]
  441. ] ) );
  442. viewDocument.fire( 'enter', evtDataStub );
  443. sinon.assert.notCalled( editor.execute );
  444. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  445. [ '<paragraph>[]11</paragraph>' ]
  446. ] ) );
  447. } );
  448. it( 'should do nothing if table cell with a block content is selected as a whole', () => {
  449. setModelData( model, modelTable( [
  450. [ '<paragraph>[1</paragraph><paragraph>1]</paragraph>' ]
  451. ] ) );
  452. viewDocument.fire( 'enter', evtDataStub );
  453. sinon.assert.notCalled( editor.execute );
  454. setModelData( model, modelTable( [
  455. [ '<paragraph>[1</paragraph><paragraph>1]</paragraph>' ]
  456. ] ) );
  457. } );
  458. it( 'should allow default behavior of Shift+Enter pressed', () => {
  459. setModelData( model, modelTable( [
  460. [ '[]11' ]
  461. ] ) );
  462. evtDataStub.isSoft = true;
  463. viewDocument.fire( 'enter', evtDataStub );
  464. sinon.assert.notCalled( editor.execute );
  465. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  466. [ '[]11' ]
  467. ] ) );
  468. } );
  469. } );
  470. describe( 'table selection', () => {
  471. let view, domEvtDataStub;
  472. beforeEach( () => {
  473. view = editor.editing.view;
  474. domEvtDataStub = {
  475. domEvent: {
  476. buttons: 1
  477. },
  478. target: undefined,
  479. preventDefault: sinon.spy(),
  480. stopPropagation: sinon.spy()
  481. };
  482. } );
  483. it( 'should not start table selection when mouse move is inside one table cell', () => {
  484. setModelData( model, modelTable( [
  485. [ '[]00', '01' ],
  486. [ '10', '11' ]
  487. ] ) );
  488. selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 );
  489. view.document.fire( 'mousedown', domEvtDataStub );
  490. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  491. [ '[]00', '01' ],
  492. [ '10', '11' ]
  493. ] ) );
  494. view.document.fire( 'mousemove', domEvtDataStub );
  495. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  496. [ '[]00', '01' ],
  497. [ '10', '11' ]
  498. ] ) );
  499. } );
  500. it( 'should start table selection when mouse move expands over two cells', () => {
  501. setModelData( model, modelTable( [
  502. [ '[]00', '01' ],
  503. [ '10', '11' ]
  504. ] ) );
  505. selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 );
  506. view.document.fire( 'mousedown', domEvtDataStub );
  507. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  508. [ '[]00', '01' ],
  509. [ '10', '11' ]
  510. ] ) );
  511. selectTableCell( domEvtDataStub, view, 0, 0, 0, 1 );
  512. view.document.fire( 'mousemove', domEvtDataStub );
  513. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  514. [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ],
  515. [ '10', '11' ]
  516. ] ) );
  517. expect( formatTable( getViewData( view ) ) ).to.equal( formattedViewTable( [
  518. [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ],
  519. [ '10', '11' ]
  520. ], { asWidget: true } ) );
  521. } );
  522. it( 'should select rectangular table cells when mouse moved to diagonal cell (up -> down)', () => {
  523. setModelData( model, modelTable( [
  524. [ '[]00', '01' ],
  525. [ '10', '11' ]
  526. ] ) );
  527. selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 );
  528. view.document.fire( 'mousedown', domEvtDataStub );
  529. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  530. [ '[]00', '01' ],
  531. [ '10', '11' ]
  532. ] ) );
  533. selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 );
  534. view.document.fire( 'mousemove', domEvtDataStub );
  535. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  536. [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ],
  537. [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true } ]
  538. ] ) );
  539. expect( formatTable( getViewData( view ) ) ).to.equal( formattedViewTable( [
  540. [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ],
  541. [ { contents: '10', class: 'selected', isSelected: true }, { contents: '11', class: 'selected', isSelected: true } ]
  542. ], { asWidget: true } ) );
  543. } );
  544. it( 'should select rectangular table cells when mouse moved to diagonal cell (down -> up)', () => {
  545. setModelData( model, modelTable( [
  546. [ '00', '01' ],
  547. [ '10', '[]11' ]
  548. ] ) );
  549. selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 );
  550. view.document.fire( 'mousedown', domEvtDataStub );
  551. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  552. [ '00', '01' ],
  553. [ '10', '[]11' ]
  554. ] ) );
  555. selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 );
  556. view.document.fire( 'mousemove', domEvtDataStub );
  557. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  558. [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ],
  559. [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true } ]
  560. ] ) );
  561. expect( formatTable( getViewData( view ) ) ).to.equal( formattedViewTable( [
  562. [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ],
  563. [ { contents: '10', class: 'selected', isSelected: true }, { contents: '11', class: 'selected', isSelected: true } ]
  564. ], { asWidget: true } ) );
  565. } );
  566. it( 'should update view selection after changing selection rect', () => {
  567. setModelData( model, modelTable( [
  568. [ '[]00', '01', '02' ],
  569. [ '10', '11', '12' ],
  570. [ '20', '21', '22' ]
  571. ] ) );
  572. selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 );
  573. view.document.fire( 'mousedown', domEvtDataStub );
  574. selectTableCell( domEvtDataStub, view, 0, 0, 2, 2 );
  575. view.document.fire( 'mousemove', domEvtDataStub );
  576. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  577. [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true }, { contents: '02', isSelected: true } ],
  578. [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true }, { contents: '12', isSelected: true } ],
  579. [ { contents: '20', isSelected: true }, { contents: '21', isSelected: true }, { contents: '22', isSelected: true } ]
  580. ] ) );
  581. expect( formatTable( getViewData( view ) ) ).to.equal( formattedViewTable( [
  582. [
  583. { contents: '00', class: 'selected', isSelected: true },
  584. { contents: '01', class: 'selected', isSelected: true },
  585. { contents: '02', class: 'selected', isSelected: true }
  586. ],
  587. [
  588. { contents: '10', class: 'selected', isSelected: true },
  589. { contents: '11', class: 'selected', isSelected: true },
  590. { contents: '12', class: 'selected', isSelected: true }
  591. ],
  592. [
  593. { contents: '20', class: 'selected', isSelected: true },
  594. { contents: '21', class: 'selected', isSelected: true },
  595. { contents: '22', class: 'selected', isSelected: true }
  596. ]
  597. ], { asWidget: true } ) );
  598. selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 );
  599. view.document.fire( 'mousemove', domEvtDataStub );
  600. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  601. [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true }, '02' ],
  602. [ { contents: '10', isSelected: true }, { contents: '11', isSelected: true }, '12' ],
  603. [ '20', '21', '22' ]
  604. ] ) );
  605. expect( formatTable( getViewData( view ) ) ).to.equal( formattedViewTable( [
  606. [
  607. { contents: '00', class: 'selected', isSelected: true },
  608. { contents: '01', class: 'selected', isSelected: true },
  609. '02'
  610. ],
  611. [
  612. { contents: '10', class: 'selected', isSelected: true },
  613. { contents: '11', class: 'selected', isSelected: true },
  614. '12'
  615. ],
  616. [
  617. '20',
  618. '21',
  619. '22'
  620. ]
  621. ], { asWidget: true } ) );
  622. } );
  623. it( 'should stop selecting after "mouseup" event', () => {
  624. setModelData( model, modelTable( [
  625. [ '[]00', '01' ],
  626. [ '10', '11' ]
  627. ] ) );
  628. selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 );
  629. view.document.fire( 'mousedown', domEvtDataStub );
  630. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  631. [ '[]00', '01' ],
  632. [ '10', '11' ]
  633. ] ) );
  634. selectTableCell( domEvtDataStub, view, 0, 0, 0, 1 );
  635. view.document.fire( 'mousemove', domEvtDataStub );
  636. view.document.fire( 'mouseup', domEvtDataStub );
  637. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  638. [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ],
  639. [ '10', '11' ]
  640. ] ) );
  641. expect( formatTable( getViewData( view ) ) ).to.equal( formattedViewTable( [
  642. [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ],
  643. [ '10', '11' ]
  644. ], { asWidget: true } ) );
  645. selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 );
  646. view.document.fire( 'mousemove', domEvtDataStub );
  647. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  648. [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ],
  649. [ '10', '11' ]
  650. ] ) );
  651. } );
  652. it( 'should stop selection mode on "mouseleve" event', () => {
  653. setModelData( model, modelTable( [
  654. [ '[]00', '01' ],
  655. [ '10', '11' ]
  656. ] ) );
  657. selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 );
  658. view.document.fire( 'mousedown', domEvtDataStub );
  659. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  660. [ '[]00', '01' ],
  661. [ '10', '11' ]
  662. ] ) );
  663. selectTableCell( domEvtDataStub, view, 0, 0, 0, 1 );
  664. view.document.fire( 'mousemove', domEvtDataStub );
  665. view.document.fire( 'mouseleave', domEvtDataStub );
  666. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  667. [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ],
  668. [ '10', '11' ]
  669. ] ) );
  670. expect( formatTable( getViewData( view ) ) ).to.equal( formattedViewTable( [
  671. [ { contents: '00', class: 'selected', isSelected: true }, { contents: '01', class: 'selected', isSelected: true } ],
  672. [ '10', '11' ]
  673. ], { asWidget: true } ) );
  674. selectTableCell( domEvtDataStub, view, 0, 0, 1, 1 );
  675. view.document.fire( 'mousemove', domEvtDataStub );
  676. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  677. [ { contents: '00', isSelected: true }, { contents: '01', isSelected: true } ],
  678. [ '10', '11' ]
  679. ] ) );
  680. } );
  681. it( 'should clear view table selection after mouse click outside table', () => {
  682. setModelData( model, modelTable( [
  683. [ '[]00', '01' ],
  684. [ '10', '11' ]
  685. ] ) + '<paragraph>foo</paragraph>' );
  686. selectTableCell( domEvtDataStub, view, 0, 0, 0, 0 );
  687. view.document.fire( 'mousedown', domEvtDataStub );
  688. expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
  689. [ '[]00', '01' ],
  690. [ '10', '11' ]
  691. ] ) + '<paragraph>foo</paragraph>' );
  692. selectTableCell( domEvtDataStub, view, 0, 0, 0, 1 );
  693. view.document.fire( 'mousemove', domEvtDataStub );
  694. domEvtDataStub.target = view.document.getRoot().getChild( 1 );
  695. view.document.fire( 'mousemove', domEvtDataStub );
  696. view.document.fire( 'mousedown', domEvtDataStub );
  697. view.document.fire( 'mouseup', domEvtDataStub );
  698. // The click in the DOM would trigger selection change and it will set the selection:
  699. model.change( writer => {
  700. writer.setSelection( writer.createRange( writer.createPositionAt( model.document.getRoot().getChild( 1 ), 0 ) ) );
  701. } );
  702. expect( formatTable( getViewData( view ) ) ).to.equal( formattedViewTable( [
  703. [ '00', '01' ],
  704. [ '10', '11' ]
  705. ], { asWidget: true } ) + '<p>{}foo</p>' );
  706. } );
  707. } );
  708. } );
  709. function selectTableCell( domEvtDataStub, view, tableIndex, sectionIndex, rowInSectionIndex, tableCellIndex ) {
  710. domEvtDataStub.target = view.document.getRoot()
  711. .getChild( tableIndex )
  712. .getChild( 1 ) // Table is second in widget
  713. .getChild( sectionIndex )
  714. .getChild( rowInSectionIndex )
  715. .getChild( tableCellIndex );
  716. }