8
0

tableselection.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  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/tableselection
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import MouseSelectionObserver from './tableselection/mouseselectionobserver';
  10. import TableWalker from './tablewalker';
  11. import { findAncestor } from './commands/utils';
  12. /**
  13. * The table selection plugin.
  14. *
  15. * It introduces the ability to select table cells using mouse.
  16. *
  17. * @extends module:core/plugin~Plugin
  18. */
  19. export default class TableSelection extends Plugin {
  20. /**
  21. * @inheritDoc
  22. */
  23. constructor( editor ) {
  24. super( editor );
  25. this._isSelecting = false;
  26. this._highlighted = new Set();
  27. }
  28. /**
  29. * Flag indicating that table selection is selecting valid ranges in table cell.
  30. *
  31. * @readonly
  32. * @member {Boolean} #isSelectingAndSomethingElse
  33. */
  34. get isSelectingAndSomethingElse() {
  35. return this._isSelecting && this._startElement && this._endElement && this._startElement !== this._endElement;
  36. }
  37. /**
  38. * @inheritDoc
  39. */
  40. init() {
  41. const editor = this.editor;
  42. this.tableUtils = editor.plugins.get( 'TableUtils' );
  43. const viewDocument = editor.editing.view.document;
  44. editor.editing.view.addObserver( MouseSelectionObserver );
  45. this.listenTo( viewDocument, 'keydown', () => {
  46. if ( this.isSelectingAndSomethingElse ) {
  47. this.stopSelection();
  48. const tableCell = this._startElement;
  49. this.clearSelection();
  50. editor.model.change( writer => {
  51. // Select the contents of table cell.
  52. writer.setSelection( tableCell, 'in' );
  53. } );
  54. }
  55. } );
  56. this.listenTo( viewDocument, 'mousedown', ( eventInfo, domEventData ) => {
  57. const tableCell = getModelTableCellFromViewEvent( domEventData, this.editor );
  58. if ( !tableCell ) {
  59. this.stopSelection();
  60. this.clearSelection();
  61. return;
  62. }
  63. this.startSelectingFrom( tableCell );
  64. } );
  65. this.listenTo( viewDocument, 'mousemove', ( eventInfo, domEventData ) => {
  66. if ( !this._isSelecting ) {
  67. return;
  68. }
  69. const tableCell = getModelTableCellFromViewEvent( domEventData, this.editor );
  70. if ( !tableCell ) {
  71. return;
  72. }
  73. this.setSelectingTo( tableCell );
  74. if ( this.isSelectingAndSomethingElse ) {
  75. domEventData.preventDefault();
  76. this._updateModelSelection();
  77. }
  78. } );
  79. this.listenTo( viewDocument, 'mouseup', ( eventInfo, domEventData ) => {
  80. if ( !this._isSelecting ) {
  81. return;
  82. }
  83. const tableCell = getModelTableCellFromViewEvent( domEventData, this.editor );
  84. this.stopSelection( tableCell );
  85. } );
  86. this.listenTo( viewDocument, 'mouseleave', () => {
  87. if ( !this._isSelecting ) {
  88. return;
  89. }
  90. this.stopSelection();
  91. } );
  92. editor.conversion.for( 'editingDowncast' ).add( dispatcher => dispatcher.on( 'selection', ( evt, data, conversionApi ) => {
  93. const viewWriter = conversionApi.writer;
  94. const viewSelection = viewWriter.document.selection;
  95. if ( this._isSelecting ) {
  96. this._clearHighlightedTableCells();
  97. for ( const tableCell of this.getSelectedTableCells() ) {
  98. const viewElement = conversionApi.mapper.toViewElement( tableCell );
  99. viewWriter.addClass( 'selected', viewElement );
  100. this._highlighted.add( viewElement );
  101. }
  102. const ranges = viewSelection.getRanges();
  103. const from = Array.from( ranges );
  104. viewWriter.setSelection( from, { fake: true, label: 'TABLE' } );
  105. } else {
  106. this._clearHighlightedTableCells();
  107. }
  108. }, { priority: 'lowest' } ) );
  109. }
  110. /**
  111. * Starts a selection process.
  112. *
  113. * This method enables the table selection process.
  114. *
  115. * editor.plugins.get( 'TableSelection' ).startSelectingFrom( tableCell );
  116. *
  117. * @param {module:engine/model/element~Element} tableCell
  118. */
  119. startSelectingFrom( tableCell ) {
  120. this.clearSelection();
  121. this._isSelecting = true;
  122. this._startElement = tableCell;
  123. this._endElement = tableCell;
  124. }
  125. /**
  126. * Updates current table selection end element. Table selection is defined by #start and #end element.
  127. * This method updates the #end element. Must be preceded by {@link #startSelectingFrom}.
  128. *
  129. * editor.plugins.get( 'TableSelection' ).startSelectingFrom( startTableCell );
  130. *
  131. * editor.plugins.get( 'TableSelection' ).setSelectingTo( endTableCell );
  132. *
  133. * @param {module:engine/model/element~Element} tableCell
  134. */
  135. setSelectingTo( tableCell ) {
  136. // Do not update if not in selection mode or no table cell is passed.
  137. if ( !this._isSelecting || !tableCell ) {
  138. return;
  139. }
  140. const table = this._startElement.parent.parent;
  141. // Do not add tableCell to selection if it is from other table or is already set as end element.
  142. if ( table !== tableCell.parent.parent || this._endElement === tableCell ) {
  143. return;
  144. }
  145. this._endElement = tableCell;
  146. this._updateModelSelection();
  147. }
  148. /**
  149. * Stops selection process (but do not clear the current selection). The selecting process is ended but the selection in model remains.
  150. *
  151. * editor.plugins.get( 'TableSelection' ).startSelectingFrom( startTableCell );
  152. * editor.plugins.get( 'TableSelection' ).setSelectingTo( endTableCell );
  153. * editor.plugins.get( 'TableSelection' ).stopSelection();
  154. *
  155. * To clear selection use {@link #clearSelection}.
  156. *
  157. * @param {module:engine/model/element~Element} [tableCell]
  158. */
  159. stopSelection( tableCell ) {
  160. if ( this._isSelecting && tableCell && tableCell.parent.parent === this._startElement.parent.parent ) {
  161. this._endElement = tableCell;
  162. }
  163. this._isSelecting = false;
  164. }
  165. /**
  166. * Stops current selection process and clears table selection.
  167. *
  168. * editor.plugins.get( 'TableSelection' ).startSelectingFrom( startTableCell );
  169. * editor.plugins.get( 'TableSelection' ).setSelectingTo( endTableCell );
  170. * editor.plugins.get( 'TableSelection' ).stopSelection();
  171. *
  172. * editor.plugins.get( 'TableSelection' ).clearSelection();
  173. */
  174. clearSelection() {
  175. this._startElement = undefined;
  176. this._endElement = undefined;
  177. this._isSelecting = false;
  178. this._clearHighlightedTableCells();
  179. this._highlighted.clear();
  180. }
  181. /**
  182. * Returns iterator that iterates over all selected table cells.
  183. *
  184. * tableSelection.startSelectingFrom( startTableCell );
  185. * tableSelection.stopSelection();
  186. *
  187. * const selectedTableCells = Array.from( tableSelection.getSelectedTableCells() );
  188. * // The above array will consist one table cell.
  189. *
  190. * @returns {Iterable.<module:engine/model/element~Element>}
  191. */
  192. * getSelectedTableCells() {
  193. if ( !this._startElement || !this._endElement ) {
  194. return;
  195. }
  196. const startLocation = this.tableUtils.getCellLocation( this._startElement );
  197. const endLocation = this.tableUtils.getCellLocation( this._endElement );
  198. const startRow = startLocation.row > endLocation.row ? endLocation.row : startLocation.row;
  199. const endRow = startLocation.row > endLocation.row ? startLocation.row : endLocation.row;
  200. const startColumn = startLocation.column > endLocation.column ? endLocation.column : startLocation.column;
  201. const endColumn = startLocation.column > endLocation.column ? startLocation.column : endLocation.column;
  202. for ( const cellInfo of new TableWalker( this._startElement.parent.parent, { startRow, endRow } ) ) {
  203. if ( cellInfo.column >= startColumn && cellInfo.column <= endColumn ) {
  204. yield cellInfo.cell;
  205. }
  206. }
  207. }
  208. /**
  209. * Set proper model selection for currently selected table cells.
  210. *
  211. * @private
  212. */
  213. _updateModelSelection() {
  214. const editor = this.editor;
  215. const model = editor.model;
  216. const modelRanges = [];
  217. for ( const tableCell of this.getSelectedTableCells() ) {
  218. modelRanges.push( model.createRangeOn( tableCell ) );
  219. }
  220. // Update model's selection
  221. model.change( writer => {
  222. writer.setSelection( modelRanges );
  223. } );
  224. }
  225. /**
  226. * Removes highlight from table cells.
  227. *
  228. * @TODO move to highlight handling.
  229. * @private
  230. */
  231. _clearHighlightedTableCells() {
  232. const previous = [ ...this._highlighted.values() ];
  233. this.editor.editing.view.change( writer => {
  234. for ( const previouslyHighlighted of previous ) {
  235. writer.removeClass( 'selected', previouslyHighlighted );
  236. }
  237. } );
  238. }
  239. }
  240. // Finds model table cell for given DOM event - ie. for 'mousedown'.
  241. function getModelTableCellFromViewEvent( domEventData, editor ) {
  242. const viewTargetElement = domEventData.target;
  243. const modelElement = editor.editing.mapper.toModelElement( viewTargetElement );
  244. if ( !modelElement ) {
  245. return;
  246. }
  247. return findAncestor( 'tableCell', editor.model.createPositionAt( modelElement, 0 ) );
  248. }