tableselection.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  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 TableWalker from './tablewalker';
  10. import TableUtils from './tableutils';
  11. import { setupTableSelectionHighlighting } from './tableselection/converters';
  12. import MouseSelectionHandler from './tableselection/mouseselectionhandler';
  13. /**
  14. * The table selection plugin.
  15. *
  16. * It introduces the ability to select table cells. Table selection is described by two nodes: start and end.
  17. * Both are the oposite corners of an rectangle that spans over them.
  18. *
  19. * Consider a table:
  20. *
  21. * 0 1 2 3
  22. * +---+---+---+---+
  23. * 0 | a | b | c | d |
  24. * +-------+ +---+
  25. * 1 | e | f | | g |
  26. * +---+---+---+---+
  27. * 2 | h | i | j |
  28. * +---+---+---+---+
  29. *
  30. * Setting table selection start as table cell "b" and end as table cell "g" will select table cells: "b", "c", "d", "f", and "g".
  31. * The cells that spans over multiple rows or columns can extend over the selection rectangle. For instance setting a selection from
  32. * table cell "a" to table cell "i" will create a selection in which table cell "i" will be extended over a rectangular of the selected
  33. * cell: "a", "b", "e", "f", "h", and "i".
  34. *
  35. * @extends module:core/plugin~Plugin
  36. */
  37. export default class TableSelection extends Plugin {
  38. /**
  39. * @inheritDoc
  40. */
  41. static get pluginName() {
  42. return 'TableSelection';
  43. }
  44. /**
  45. * @inheritDoc
  46. */
  47. static get requires() {
  48. return [ TableUtils ];
  49. }
  50. /**
  51. * @inheritDoc
  52. */
  53. constructor( editor ) {
  54. super( editor );
  55. /**
  56. * A mouse selection handler.
  57. *
  58. * @private
  59. * @readonly
  60. * @member {module:table/tableselection/mouseselectionhandler~MouseSelectionHandler}
  61. */
  62. this._mouseHandler = new MouseSelectionHandler( this, this.editor.editing );
  63. /**
  64. * A table utilities.
  65. *
  66. * @private
  67. * @readonly
  68. * @member {module:table/tableutils~TableUtils}
  69. */
  70. }
  71. /**
  72. * Flag indicating that there are selected table cells and the selection has more than one table cell.
  73. *
  74. * @type {Boolean}
  75. */
  76. get hasMultiCellSelection() {
  77. return !!this._startElement && !!this._endElement && this._startElement !== this._endElement;
  78. }
  79. /**
  80. * @inheritDoc
  81. */
  82. init() {
  83. const editor = this.editor;
  84. const selection = editor.model.document.selection;
  85. this._tableUtils = editor.plugins.get( 'TableUtils' );
  86. setupTableSelectionHighlighting( editor, this );
  87. selection.on( 'change:range', () => this._clearSelectionOnExternalChange( selection ) );
  88. this.listenTo( editor.editing.view.document, 'copy', ( evt, data ) => {
  89. if ( !this.hasMultiCellSelection ) {
  90. return;
  91. }
  92. const dataTransfer = data.dataTransfer;
  93. data.preventDefault();
  94. evt.stop();
  95. const content = editor.data.toView( this.getSelectedTableAsFragment() );
  96. editor.editing.view.document.fire( 'clipboardOutput', { dataTransfer, content, method: evt.name } );
  97. }, { priority: 'normal' } );
  98. this.listenTo( editor.editing.view.document, 'cut', ( evt, data ) => {
  99. if ( this.hasMultiCellSelection ) {
  100. data.preventDefault();
  101. evt.stop();
  102. }
  103. }, { priority: 'high' } );
  104. }
  105. /**
  106. * @inheritDoc
  107. */
  108. destroy() {
  109. super.destroy();
  110. this._mouseHandler.stopListening();
  111. }
  112. /**
  113. * Starts a selection process.
  114. *
  115. * This method enables the table selection process.
  116. *
  117. * editor.plugins.get( 'TableSelection' ).startSelectingFrom( tableCell );
  118. *
  119. * @param {module:engine/model/element~Element} tableCell
  120. */
  121. startSelectingFrom( tableCell ) {
  122. this.clearSelection();
  123. this._startElement = tableCell;
  124. this._endElement = tableCell;
  125. }
  126. /**
  127. * Updates current table selection end element. Table selection is defined by #start and #end element.
  128. * This method updates the #end element. Must be preceded by {@link #startSelectingFrom}.
  129. *
  130. * editor.plugins.get( 'TableSelection' ).startSelectingFrom( startTableCell );
  131. *
  132. * editor.plugins.get( 'TableSelection' ).setSelectingTo( endTableCell );
  133. *
  134. * @param {module:engine/model/element~Element} tableCell
  135. */
  136. setSelectingTo( tableCell ) {
  137. if ( !this._startElement ) {
  138. this._startElement = tableCell;
  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 ( tableCell && tableCell.parent.parent === this._startElement.parent.parent ) {
  161. this._endElement = tableCell;
  162. }
  163. this._updateModelSelection();
  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. }
  178. getSelectedTableAsFragment() {
  179. return this.editor.model.change( writer => {
  180. const fragment = writer.createDocumentFragment();
  181. const table = writer.createElement( 'table' );
  182. writer.insert( table, fragment, 0 );
  183. const rowsMap = new Map();
  184. for ( const tableCell of this.getSelectedTableCells() ) {
  185. const row = tableCell.parent;
  186. if ( !rowsMap.has( row ) ) {
  187. const newRow = row._clone();
  188. writer.append( newRow, table );
  189. rowsMap.set( row, newRow );
  190. }
  191. writer.append( tableCell._clone( true ), rowsMap.get( row ) );
  192. }
  193. return fragment;
  194. } );
  195. }
  196. /**
  197. * Returns iterator for selected table cells.
  198. *
  199. * tableSelection.startSelectingFrom( startTableCell );
  200. * tableSelection.stopSelection( endTableCell );
  201. *
  202. * const selectedTableCells = Array.from( tableSelection.getSelectedTableCells() );
  203. * // The above array will consist a rectangular table selection.
  204. *
  205. * @returns {Iterable.<module:engine/model/element~Element>}
  206. */
  207. * getSelectedTableCells() {
  208. if ( !this.hasMultiCellSelection ) {
  209. return;
  210. }
  211. const startLocation = this._tableUtils.getCellLocation( this._startElement );
  212. const endLocation = this._tableUtils.getCellLocation( this._endElement );
  213. const startRow = startLocation.row > endLocation.row ? endLocation.row : startLocation.row;
  214. const endRow = startLocation.row > endLocation.row ? startLocation.row : endLocation.row;
  215. const startColumn = startLocation.column > endLocation.column ? endLocation.column : startLocation.column;
  216. const endColumn = startLocation.column > endLocation.column ? startLocation.column : endLocation.column;
  217. for ( const cellInfo of new TableWalker( this._startElement.parent.parent, { startRow, endRow } ) ) {
  218. if ( cellInfo.column >= startColumn && cellInfo.column <= endColumn ) {
  219. yield cellInfo.cell;
  220. }
  221. }
  222. }
  223. /**
  224. * Set proper model selection for currently selected table cells.
  225. *
  226. * @private
  227. */
  228. _updateModelSelection() {
  229. if ( !this.hasMultiCellSelection ) {
  230. return;
  231. }
  232. const editor = this.editor;
  233. const model = editor.model;
  234. const modelRanges = [];
  235. for ( const tableCell of this.getSelectedTableCells() ) {
  236. modelRanges.push( model.createRangeOn( tableCell ) );
  237. }
  238. // Update model's selection
  239. model.change( writer => {
  240. writer.setSelection( modelRanges );
  241. } );
  242. }
  243. /**
  244. * Checks if selection has changed from an external source and it is required to clear internal state.
  245. *
  246. * @param {module:engine/model/documentselection~DocumentSelection} selection
  247. * @private
  248. */
  249. _clearSelectionOnExternalChange( selection ) {
  250. if ( selection.rangeCount <= 1 && this.hasMultiCellSelection ) {
  251. this.clearSelection();
  252. }
  253. }
  254. }