tablemouse.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  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. /**
  6. * @module table/tablemouse
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import TableSelection from './tableselection';
  10. import MouseEventsObserver from './tablemouse/mouseeventsobserver';
  11. import { findAncestor } from './utils/common';
  12. import { getTableCellsContainingSelection } from './utils/selection';
  13. /**
  14. * This plugin enables a table cells' selection with the mouse.
  15. * It is loaded automatically by the {@link module:table/table~Table} plugin.
  16. *
  17. * @extends module:core/plugin~Plugin
  18. */
  19. export default class TableMouse extends Plugin {
  20. /**
  21. * @inheritDoc
  22. */
  23. static get pluginName() {
  24. return 'TableMouse';
  25. }
  26. /**
  27. * @inheritDoc
  28. */
  29. static get requires() {
  30. return [ TableSelection ];
  31. }
  32. /**
  33. * @inheritDoc
  34. */
  35. init() {
  36. const editor = this.editor;
  37. // Currently the MouseObserver only handles `mouseup` events.
  38. // TODO move to the engine?
  39. editor.editing.view.addObserver( MouseEventsObserver );
  40. this._enableShiftClickSelection();
  41. this._enableMouseDragSelection();
  42. }
  43. /**
  44. * Enables making cells selection by <kbd>Shift</kbd>+click. Creates a selection from the cell which previously held
  45. * the selection to the cell which was clicked. It can be the same cell, in which case it selects a single cell.
  46. *
  47. * @private
  48. */
  49. _enableShiftClickSelection() {
  50. const editor = this.editor;
  51. let blockSelectionChange = false;
  52. const tableSelection = editor.plugins.get( TableSelection );
  53. this.listenTo( editor.editing.view.document, 'mousedown', ( evt, domEventData ) => {
  54. if ( !this.isEnabled || !tableSelection.isEnabled ) {
  55. return;
  56. }
  57. if ( !domEventData.domEvent.shiftKey ) {
  58. return;
  59. }
  60. const anchorCell = tableSelection.getAnchorCell() || getTableCellsContainingSelection( editor.model.document.selection )[ 0 ];
  61. if ( !anchorCell ) {
  62. return;
  63. }
  64. const targetCell = this._getModelTableCellFromDomEvent( domEventData );
  65. if ( targetCell && haveSameTableParent( anchorCell, targetCell ) ) {
  66. blockSelectionChange = true;
  67. tableSelection.setCellSelection( anchorCell, targetCell );
  68. domEventData.preventDefault();
  69. }
  70. } );
  71. this.listenTo( editor.editing.view.document, 'mouseup', () => {
  72. blockSelectionChange = false;
  73. } );
  74. // We need to ignore a `selectionChange` event that is fired after we render our new table cells selection.
  75. // When downcasting table cells selection to the view, we put the view selection in the last selected cell
  76. // in a place that may not be natively a "correct" location. This is – we put it directly in the `<td>` element.
  77. // All browsers fire the native `selectionchange` event.
  78. // However, all browsers except Safari return the selection in the exact place where we put it
  79. // (even though it's visually normalized). Safari returns `<td><p>^foo` that makes our selection observer
  80. // fire our `selectionChange` event (because the view selection that we set in the first step differs from the DOM selection).
  81. // Since `selectionChange` is fired, we automatically update the model selection that moves it that paragraph.
  82. // This breaks our dear cells selection.
  83. //
  84. // Theoretically this issue concerns only Safari that is the only browser that do normalize the selection.
  85. // However, to avoid code branching and to have a good coverage for this event blocker, I enabled it for all browsers.
  86. //
  87. // Note: I'm keeping the `blockSelectionChange` state separately for shift+click and mouse drag (exact same logic)
  88. // so I don't have to try to analyze whether they don't overlap in some weird cases. Probably they don't.
  89. // But I have other things to do, like writing this comment.
  90. this.listenTo( editor.editing.view.document, 'selectionChange', evt => {
  91. if ( blockSelectionChange ) {
  92. // @if CK_DEBUG // console.log( 'Blocked selectionChange to avoid breaking table cells selection.' );
  93. evt.stop();
  94. }
  95. }, { priority: 'highest' } );
  96. }
  97. /**
  98. * Enables making cells selection by dragging.
  99. *
  100. * The selection is made only on mousemove. Mouse tracking is started on mousedown.
  101. * However, the cells selection is enabled only after the mouse cursor left the anchor cell.
  102. * Thanks to that normal text selection within one cell works just fine. However, you can still select
  103. * just one cell by leaving the anchor cell and moving back to it.
  104. *
  105. * @private
  106. */
  107. _enableMouseDragSelection() {
  108. const editor = this.editor;
  109. let anchorCell, targetCell;
  110. let beganCellSelection = false;
  111. let blockSelectionChange = false;
  112. const tableSelection = editor.plugins.get( TableSelection );
  113. this.listenTo( editor.editing.view.document, 'mousedown', ( evt, domEventData ) => {
  114. if ( !this.isEnabled || !tableSelection.isEnabled ) {
  115. return;
  116. }
  117. // Make sure to not conflict with the shift+click listener and any other possible handler.
  118. if ( domEventData.domEvent.shiftKey || domEventData.domEvent.ctrlKey || domEventData.domEvent.altKey ) {
  119. return;
  120. }
  121. anchorCell = this._getModelTableCellFromDomEvent( domEventData );
  122. } );
  123. this.listenTo( editor.editing.view.document, 'mousemove', ( evt, domEventData ) => {
  124. if ( !domEventData.domEvent.buttons ) {
  125. return;
  126. }
  127. if ( !anchorCell ) {
  128. return;
  129. }
  130. const newTargetCell = this._getModelTableCellFromDomEvent( domEventData );
  131. if ( newTargetCell && haveSameTableParent( anchorCell, newTargetCell ) ) {
  132. targetCell = newTargetCell;
  133. // Switch to the cell selection mode after the mouse cursor left the anchor cell.
  134. // Switch off only on mouseup (makes selecting a single cell possible).
  135. if ( !beganCellSelection && targetCell != anchorCell ) {
  136. beganCellSelection = true;
  137. }
  138. }
  139. // Yep, not making a cell selection yet. See method docs.
  140. if ( !beganCellSelection ) {
  141. return;
  142. }
  143. blockSelectionChange = true;
  144. tableSelection.setCellSelection( anchorCell, targetCell );
  145. domEventData.preventDefault();
  146. } );
  147. this.listenTo( editor.editing.view.document, 'mouseup', () => {
  148. beganCellSelection = false;
  149. blockSelectionChange = false;
  150. anchorCell = null;
  151. targetCell = null;
  152. } );
  153. // See the explanation in `_enableShiftClickSelection()`.
  154. this.listenTo( editor.editing.view.document, 'selectionChange', evt => {
  155. if ( blockSelectionChange ) {
  156. // @if CK_DEBUG // console.log( 'Blocked selectionChange to avoid breaking table cells selection.' );
  157. evt.stop();
  158. }
  159. }, { priority: 'highest' } );
  160. }
  161. /**
  162. * Returns the model table cell element based on the target element of the passed DOM event.
  163. *
  164. * @private
  165. * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
  166. * @returns {module:engine/model/element~Element|undefined} Returns the table cell or `undefined`.
  167. */
  168. _getModelTableCellFromDomEvent( domEventData ) {
  169. // Note: Work with positions (not element mapping) because the target element can be an attribute or other non-mapped element.
  170. const viewTargetElement = domEventData.target;
  171. const viewPosition = this.editor.editing.view.createPositionAt( viewTargetElement, 0 );
  172. const modelPosition = this.editor.editing.mapper.toModelPosition( viewPosition );
  173. const modelElement = modelPosition.parent;
  174. if ( modelElement.is( 'tableCell' ) ) {
  175. return modelElement;
  176. }
  177. return findAncestor( 'tableCell', modelElement );
  178. }
  179. }
  180. function haveSameTableParent( cellA, cellB ) {
  181. return cellA.parent.parent == cellB.parent.parent;
  182. }