contextualballoon.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  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 ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
  6. import Table from '../../../src/table';
  7. import TableCellProperties from '../../../src/tablecellproperties';
  8. import global from '@ckeditor/ckeditor5-utils/src/dom/global';
  9. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  10. import View from '@ckeditor/ckeditor5-ui/src/view';
  11. import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview';
  12. import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  13. import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
  14. import { centeredBalloonPositionForLongWidgets } from '@ckeditor/ckeditor5-widget/src/utils';
  15. import { modelTable } from '../../_utils/utils';
  16. import { getTableCellsContainingSelection } from '../../../src/utils/selection';
  17. import { getBalloonCellPositionData, repositionContextualBalloon } from '../../../src/utils/ui/contextualballoon';
  18. describe( 'table utils', () => {
  19. let editor, editingView, balloon, editorElement;
  20. testUtils.createSinonSandbox();
  21. beforeEach( () => {
  22. editorElement = global.document.createElement( 'div' );
  23. global.document.body.appendChild( editorElement );
  24. return ClassicEditor
  25. .create( editorElement, {
  26. plugins: [ Table, TableCellProperties, Paragraph ]
  27. } )
  28. .then( newEditor => {
  29. editor = newEditor;
  30. editingView = editor.editing.view;
  31. balloon = editor.plugins.get( 'ContextualBalloon' );
  32. } );
  33. } );
  34. afterEach( () => {
  35. editorElement.remove();
  36. return editor.destroy();
  37. } );
  38. describe( 'ui - contextual balloon', () => {
  39. describe( 'repositionContextualBalloon()', () => {
  40. describe( 'with respect to the table cell', () => {
  41. it( 'should re-position the ContextualBalloon when the table cell is selected', () => {
  42. const spy = sinon.spy( balloon, 'updatePosition' );
  43. const defaultPositions = BalloonPanelView.defaultPositions;
  44. const view = new View();
  45. view.element = global.document.createElement( 'div' );
  46. balloon.add( {
  47. view,
  48. position: {
  49. target: global.document.body
  50. }
  51. } );
  52. setData( editor.model,
  53. '<table><tableRow>' +
  54. '<tableCell><paragraph>foo</paragraph></tableCell>' +
  55. '<tableCell><paragraph>[bar]</paragraph></tableCell>' +
  56. '</tableRow></table>' );
  57. repositionContextualBalloon( editor, 'cell' );
  58. const modelCell = getTableCellsContainingSelection( editor.model.document.selection )[ 0 ];
  59. const viewCell = editor.editing.mapper.toViewElement( modelCell );
  60. sinon.assert.calledWithExactly( spy, {
  61. target: editingView.domConverter.viewToDom( viewCell ),
  62. positions: [
  63. defaultPositions.northArrowSouth,
  64. defaultPositions.northArrowSouthWest,
  65. defaultPositions.northArrowSouthEast,
  66. defaultPositions.southArrowNorth,
  67. defaultPositions.southArrowNorthWest,
  68. defaultPositions.southArrowNorthEast
  69. ]
  70. } );
  71. } );
  72. it( 'should not engage with no table is selected', () => {
  73. const spy = sinon.spy( balloon, 'updatePosition' );
  74. setData( editor.model, '<paragraph>foo</paragraph>' );
  75. repositionContextualBalloon( editor, 'cell' );
  76. sinon.assert.notCalled( spy );
  77. } );
  78. } );
  79. describe( 'with respect to the entire table', () => {
  80. it( 'should re-position the ContextualBalloon when the table is selected', () => {
  81. const spy = sinon.spy( balloon, 'updatePosition' );
  82. const defaultPositions = BalloonPanelView.defaultPositions;
  83. const view = new View();
  84. view.element = global.document.createElement( 'div' );
  85. balloon.add( {
  86. view,
  87. position: {
  88. target: global.document.body
  89. }
  90. } );
  91. setData( editor.model,
  92. '<table><tableRow>' +
  93. '<tableCell><paragraph>foo</paragraph></tableCell>' +
  94. '<tableCell><paragraph>[bar]</paragraph></tableCell>' +
  95. '</tableRow></table>' );
  96. repositionContextualBalloon( editor, 'table' );
  97. const modelTable = editor.model.document.selection.getFirstPosition().findAncestor( 'table' );
  98. const viewTable = editor.editing.mapper.toViewElement( modelTable );
  99. sinon.assert.calledWithExactly( spy, {
  100. target: editingView.domConverter.viewToDom( viewTable ),
  101. positions: [
  102. defaultPositions.northArrowSouth,
  103. defaultPositions.northArrowSouthWest,
  104. defaultPositions.northArrowSouthEast,
  105. defaultPositions.southArrowNorth,
  106. defaultPositions.southArrowNorthWest,
  107. defaultPositions.southArrowNorthEast,
  108. centeredBalloonPositionForLongWidgets
  109. ]
  110. } );
  111. } );
  112. it( 'should not engage with no table is selected', () => {
  113. const spy = sinon.spy( balloon, 'updatePosition' );
  114. setData( editor.model, '<paragraph>foo</paragraph>' );
  115. repositionContextualBalloon( editor, 'table' );
  116. sinon.assert.notCalled( spy );
  117. } );
  118. } );
  119. } );
  120. describe( 'getBalloonCellPositionData()', () => {
  121. let modelRoot;
  122. beforeEach( () => {
  123. setData( editor.model, modelTable( [
  124. [ '11[]', '12', '13' ],
  125. [ '21', '22', '23' ],
  126. [ '31', '32', '33' ]
  127. ] ) );
  128. modelRoot = editor.model.document.getRoot();
  129. for ( let row = 0; row < 3; row++ ) {
  130. for ( let col = 0; col < 3; col++ ) {
  131. const modelCell = modelRoot.getNodeByPath( [ 0, row, col ] );
  132. const viewCell = editor.editing.mapper.toViewElement( modelCell );
  133. const cellDomElement = editingView.domConverter.viewToDom( viewCell );
  134. mockBoundingBox( cellDomElement, {
  135. top: 100 + row * 10,
  136. left: 100 + col * 10,
  137. height: 10,
  138. width: 10
  139. } );
  140. }
  141. }
  142. } );
  143. it( 'returns the position data', () => {
  144. const defaultPositions = BalloonPanelView.defaultPositions;
  145. const data = getBalloonCellPositionData( editor );
  146. const modelCell = getTableCellsContainingSelection( editor.model.document.selection )[ 0 ];
  147. const viewCell = editor.editing.mapper.toViewElement( modelCell );
  148. expect( data ).to.deep.equal( {
  149. target: editingView.domConverter.viewToDom( viewCell ),
  150. positions: [
  151. defaultPositions.northArrowSouth,
  152. defaultPositions.northArrowSouthWest,
  153. defaultPositions.northArrowSouthEast,
  154. defaultPositions.southArrowNorth,
  155. defaultPositions.southArrowNorthWest,
  156. defaultPositions.southArrowNorthEast
  157. ]
  158. } );
  159. } );
  160. it( 'returns the position data for multiple cells selected horizontally', () => {
  161. selectTableCells( [
  162. [ 0, 0 ],
  163. [ 0, 1 ]
  164. ] );
  165. const data = getBalloonCellPositionData( editor );
  166. const targetData = data.target();
  167. expect( targetData ).to.deep.equal( {
  168. top: 100,
  169. left: 100,
  170. right: 120,
  171. bottom: 110,
  172. width: 20,
  173. height: 10
  174. } );
  175. } );
  176. it( 'returns the position data for multiple cells selected vertically', () => {
  177. selectTableCells( [
  178. [ 0, 1 ],
  179. [ 1, 1 ]
  180. ] );
  181. const data = getBalloonCellPositionData( editor );
  182. const targetData = data.target();
  183. expect( targetData ).to.deep.equal( {
  184. top: 100,
  185. left: 110,
  186. right: 120,
  187. bottom: 120,
  188. width: 10,
  189. height: 20
  190. } );
  191. } );
  192. it( 'returns the position data for multiple cells selected', () => {
  193. selectTableCells( [
  194. [ 0, 1 ],
  195. [ 1, 0 ],
  196. [ 1, 1 ]
  197. ] );
  198. const data = getBalloonCellPositionData( editor );
  199. const targetData = data.target();
  200. expect( targetData ).to.deep.equal( {
  201. top: 100,
  202. left: 100,
  203. right: 120,
  204. bottom: 120,
  205. width: 20,
  206. height: 20
  207. } );
  208. } );
  209. function selectTableCells( paths ) {
  210. editor.model.change( writer => {
  211. writer.setSelection( paths.map( path => writer.createRangeOn( modelRoot.getNodeByPath( [ 0, ...path ] ) ) ) );
  212. } );
  213. }
  214. function mockBoundingBox( element, data ) {
  215. testUtils.sinon.stub( element, 'getBoundingClientRect' ).returns( {
  216. ...data,
  217. right: data.left + data.width,
  218. bottom: data.top + data.height
  219. } );
  220. }
  221. } );
  222. } );
  223. } );