tableselection.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module table/tableediting
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import Position from '@ckeditor/ckeditor5-engine/src/model/position';
  10. import TableWalker from './tablewalker';
  11. import TableUtils from './tableutils';
  12. import { findAncestor } from './commands/utils';
  13. import Range from '@ckeditor/ckeditor5-engine/src/model/range';
  14. export default class TableSelection extends Plugin {
  15. /**
  16. * @inheritDoc
  17. */
  18. static get pluginName() {
  19. return 'TableSelection';
  20. }
  21. /**
  22. * @inheritDoc
  23. */
  24. static get requires() {
  25. return [ TableUtils ];
  26. }
  27. constructor( editor ) {
  28. super( editor );
  29. this._isSelecting = false;
  30. this._highlighted = new Set();
  31. this.editor = editor;
  32. this.tableUtils = editor.plugins.get( TableUtils );
  33. }
  34. init() {
  35. const editor = this.editor;
  36. const viewDocument = editor.editing.view.document;
  37. this.listenTo( viewDocument, 'mousedown', ( eventInfo, domEventData ) => {
  38. const tableCell = getTableCell( domEventData, this.editor );
  39. if ( !tableCell ) {
  40. return;
  41. }
  42. this.startSelection( tableCell );
  43. } );
  44. this.listenTo( viewDocument, 'mousemove', ( eventInfo, domEventData ) => {
  45. if ( !this.isSelecting ) {
  46. return;
  47. }
  48. const tableCell = getTableCell( domEventData, this.editor );
  49. if ( !tableCell ) {
  50. return;
  51. }
  52. const wasOne = this.size === 1;
  53. this.updateSelection( tableCell );
  54. if ( this.size > 1 ) {
  55. domEventData.preventDefault();
  56. if ( wasOne ) {
  57. // TODO:
  58. // editor.editing.view.change( writer => {
  59. // // TODO const viewElement = editor.editing.mapper.toViewElement( this._startElement );
  60. //
  61. // // Set selection to the first selected table cell.
  62. // // writer.setSelection( ViewRange.createIn( viewElement ), {
  63. // // fake: true,
  64. // // label: 'fake selection over table cell'
  65. // // } );
  66. // } );
  67. }
  68. this.redrawSelection();
  69. }
  70. } );
  71. this.listenTo( viewDocument, 'mouseup', ( eventInfo, domEventData ) => {
  72. if ( !this.isSelecting ) {
  73. return;
  74. }
  75. const tableCell = getTableCell( domEventData, this.editor );
  76. this.stopSelection( tableCell );
  77. } );
  78. }
  79. get isSelecting() {
  80. return this._isSelecting;
  81. }
  82. get size() {
  83. return [ ...this.getSelection() ].length;
  84. }
  85. startSelection( tableCell ) {
  86. this.clearSelection();
  87. this._isSelecting = true;
  88. this._startElement = tableCell;
  89. this._endElement = tableCell;
  90. // todo: stop rendering
  91. this.editor.editing.view._renderer.renderSelection = false;
  92. }
  93. updateSelection( tableCell ) {
  94. // Do not update if not in selection mode or no table cell passed.
  95. if ( !this.isSelecting || !tableCell ) {
  96. return;
  97. }
  98. const table = this._startElement.parent.parent;
  99. // Do not add tableCell to selection if it is from other table or is already set as end element.
  100. if ( table !== tableCell.parent.parent || this._endElement === tableCell ) {
  101. return;
  102. }
  103. const headingRows = parseInt( table.getAttribute( 'headingRows' ) || 0 );
  104. const startInHeading = this._startElement.parent.index < headingRows;
  105. const updateCellInHeading = tableCell.parent.index < headingRows;
  106. // Only add cell to selection if they are in the same table section.
  107. if ( startInHeading === updateCellInHeading ) {
  108. this._endElement = tableCell;
  109. this.redrawSelection();
  110. }
  111. }
  112. stopSelection( tableCell ) {
  113. if ( this.isSelecting && tableCell && tableCell.parent.parent === this._startElement.parent.parent ) {
  114. this._endElement = tableCell;
  115. }
  116. this._isSelecting = false;
  117. this.editor.editing.view._renderer.renderSelection = true;
  118. }
  119. clearSelection() {
  120. this._startElement = undefined;
  121. this._endElement = undefined;
  122. this._isSelecting = false;
  123. this.clearPreviousSelection();
  124. this._highlighted.clear();
  125. this.editor.editing.view._renderer.renderSelection = true;
  126. }
  127. * getSelection() {
  128. if ( !this._startElement || !this._endElement ) {
  129. return;
  130. }
  131. yield* this._getBlockSelection();
  132. }
  133. * _getBlockSelection() {
  134. const startLocation = this.tableUtils.getCellLocation( this._startElement );
  135. const endLocation = this.tableUtils.getCellLocation( this._endElement );
  136. const startRow = startLocation.row > endLocation.row ? endLocation.row : startLocation.row;
  137. const endRow = startLocation.row > endLocation.row ? startLocation.row : endLocation.row;
  138. const startColumn = startLocation.column > endLocation.column ? endLocation.column : startLocation.column;
  139. const endColumn = startLocation.column > endLocation.column ? startLocation.column : endLocation.column;
  140. for ( const cellInfo of new TableWalker( this._startElement.parent.parent, { startRow, endRow } ) ) {
  141. if ( cellInfo.column >= startColumn && cellInfo.column <= endColumn ) {
  142. yield cellInfo.cell;
  143. }
  144. }
  145. }
  146. redrawSelection() {
  147. const editor = this.editor;
  148. const mapper = editor.editing.mapper;
  149. const view = editor.editing.view;
  150. const model = editor.model;
  151. const modelRanges = [];
  152. const selected = [ ...this.getSelection() ];
  153. const previous = [ ...this._highlighted.values() ];
  154. this._highlighted.clear();
  155. for ( const tableCell of selected ) {
  156. const viewElement = mapper.toViewElement( tableCell );
  157. modelRanges.push( Range.createOn( tableCell ) );
  158. this._highlighted.add( viewElement );
  159. }
  160. // Update model's selection
  161. model.change( writer => {
  162. writer.setSelection( modelRanges );
  163. } );
  164. view.change( writer => {
  165. for ( const previouslyHighlighted of previous ) {
  166. if ( !selected.includes( previouslyHighlighted ) ) {
  167. writer.removeClass( 'selected', previouslyHighlighted );
  168. }
  169. }
  170. for ( const currently of this._highlighted ) {
  171. writer.addClass( 'selected', currently );
  172. }
  173. } );
  174. }
  175. clearPreviousSelection() {
  176. const previous = [ ...this._highlighted.values() ];
  177. this.editor.editing.view.change( writer => {
  178. for ( const previouslyHighlighted of previous ) {
  179. writer.removeClass( 'selected', previouslyHighlighted );
  180. }
  181. } );
  182. }
  183. }
  184. function getTableCell( domEventData, editor ) {
  185. const element = domEventData.target;
  186. const modelElement = editor.editing.mapper.toModelElement( element );
  187. if ( !modelElement ) {
  188. return;
  189. }
  190. return findAncestor( 'tableCell', Position.createAt( modelElement ) );
  191. }