8
0

tablemouse.js 7.3 KB

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