contextualballoon.js 8.1 KB

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