tableediting.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  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 { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
  10. import Range from '@ckeditor/ckeditor5-engine/src/model/range';
  11. import upcastTable, { upcastTableCell } from './converters/upcasttable';
  12. import {
  13. downcastInsertCell,
  14. downcastInsertRow,
  15. downcastInsertTable,
  16. downcastRemoveRow,
  17. downcastTableHeadingColumnsChange,
  18. downcastTableHeadingRowsChange
  19. } from './converters/downcast';
  20. import InsertTableCommand from './commands/inserttablecommand';
  21. import InsertRowCommand from './commands/insertrowcommand';
  22. import InsertColumnCommand from './commands/insertcolumncommand';
  23. import SplitCellCommand from './commands/splitcellcommand';
  24. import MergeCellCommand from './commands/mergecellcommand';
  25. import RemoveRowCommand from './commands/removerowcommand';
  26. import RemoveColumnCommand from './commands/removecolumncommand';
  27. import SetHeaderRowCommand from './commands/setheaderrowcommand';
  28. import SetHeaderColumnCommand from './commands/setheadercolumncommand';
  29. import { findAncestor } from './commands/utils';
  30. import TableUtils from '../src/tableutils';
  31. import injectTablePostFixer from './converters/table-post-fixer';
  32. import Position from '@ckeditor/ckeditor5-engine/src/model/position';
  33. import injectTableCellPostFixer from './converters/tablecell-post-fixer';
  34. import TableSelection from './tableselection';
  35. import '../theme/tableediting.css';
  36. /**
  37. * The table editing feature.
  38. *
  39. * @extends module:core/plugin~Plugin
  40. */
  41. export default class TableEditing extends Plugin {
  42. /**
  43. * @inheritDoc
  44. */
  45. init() {
  46. const editor = this.editor;
  47. const model = editor.model;
  48. const schema = model.schema;
  49. const conversion = editor.conversion;
  50. const viewDocument = editor.editing.view.document;
  51. schema.register( 'table', {
  52. allowWhere: '$block',
  53. allowAttributes: [ 'headingRows', 'headingColumns' ],
  54. isLimit: true,
  55. isObject: true
  56. } );
  57. schema.register( 'tableRow', {
  58. allowIn: 'table',
  59. isLimit: true
  60. } );
  61. schema.register( 'tableCell', {
  62. allowIn: 'tableRow',
  63. allowAttributes: [ 'colspan', 'rowspan' ],
  64. isLimit: true
  65. } );
  66. // Allow all $block content inside table cell.
  67. schema.extend( '$block', { allowIn: 'tableCell' } );
  68. // Disallow table in table.
  69. schema.addChildCheck( ( context, childDefinition ) => {
  70. if ( childDefinition.name == 'table' && Array.from( context.getNames() ).includes( 'table' ) ) {
  71. return false;
  72. }
  73. } );
  74. // Disallow image in table cell.
  75. schema.addChildCheck( ( context, childDefinition ) => {
  76. if ( childDefinition.name == 'image' && Array.from( context.getNames() ).includes( 'table' ) ) {
  77. return false;
  78. }
  79. } );
  80. // Table conversion.
  81. conversion.for( 'upcast' ).add( upcastTable() );
  82. conversion.for( 'editingDowncast' ).add( downcastInsertTable( { asWidget: true } ) );
  83. conversion.for( 'dataDowncast' ).add( downcastInsertTable() );
  84. // Table row conversion.
  85. conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableRow', view: 'tr' } ) );
  86. conversion.for( 'editingDowncast' ).add( downcastInsertRow( { asWidget: true } ) );
  87. conversion.for( 'dataDowncast' ).add( downcastInsertRow() );
  88. conversion.for( 'downcast' ).add( downcastRemoveRow() );
  89. // Table cell conversion.
  90. conversion.for( 'upcast' ).add( upcastTableCell( 'td' ) );
  91. conversion.for( 'upcast' ).add( upcastTableCell( 'th' ) );
  92. conversion.for( 'editingDowncast' ).add( downcastInsertCell( { asWidget: true } ) );
  93. conversion.for( 'dataDowncast' ).add( downcastInsertCell() );
  94. // Table attributes conversion.
  95. conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
  96. conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
  97. // Table heading rows and cols conversion.
  98. conversion.for( 'editingDowncast' ).add( downcastTableHeadingColumnsChange( { asWidget: true } ) );
  99. conversion.for( 'dataDowncast' ).add( downcastTableHeadingColumnsChange() );
  100. conversion.for( 'editingDowncast' ).add( downcastTableHeadingRowsChange( { asWidget: true } ) );
  101. conversion.for( 'dataDowncast' ).add( downcastTableHeadingRowsChange() );
  102. injectTableCellPostFixer( editor.model, editor.editing );
  103. // Define all the commands.
  104. editor.commands.add( 'insertTable', new InsertTableCommand( editor ) );
  105. editor.commands.add( 'insertTableRowAbove', new InsertRowCommand( editor, { order: 'above' } ) );
  106. editor.commands.add( 'insertTableRowBelow', new InsertRowCommand( editor, { order: 'below' } ) );
  107. editor.commands.add( 'insertTableColumnBefore', new InsertColumnCommand( editor, { order: 'before' } ) );
  108. editor.commands.add( 'insertTableColumnAfter', new InsertColumnCommand( editor, { order: 'after' } ) );
  109. editor.commands.add( 'removeTableRow', new RemoveRowCommand( editor ) );
  110. editor.commands.add( 'removeTableColumn', new RemoveColumnCommand( editor ) );
  111. editor.commands.add( 'splitTableCellVertically', new SplitCellCommand( editor, { direction: 'vertically' } ) );
  112. editor.commands.add( 'splitTableCellHorizontally', new SplitCellCommand( editor, { direction: 'horizontally' } ) );
  113. editor.commands.add( 'mergeTableCellRight', new MergeCellCommand( editor, { direction: 'right' } ) );
  114. editor.commands.add( 'mergeTableCellLeft', new MergeCellCommand( editor, { direction: 'left' } ) );
  115. editor.commands.add( 'mergeTableCellDown', new MergeCellCommand( editor, { direction: 'down' } ) );
  116. editor.commands.add( 'mergeTableCellUp', new MergeCellCommand( editor, { direction: 'up' } ) );
  117. editor.commands.add( 'setTableColumnHeader', new SetHeaderColumnCommand( editor ) );
  118. editor.commands.add( 'setTableRowHeader', new SetHeaderRowCommand( editor ) );
  119. injectTablePostFixer( model );
  120. // Handle tab key navigation.
  121. this.editor.keystrokes.set( 'Tab', ( ...args ) => this._handleTabOnSelectedTable( ...args ), { priority: 'low' } );
  122. this.editor.keystrokes.set( 'Tab', this._getTabHandler( true ), { priority: 'low' } );
  123. this.editor.keystrokes.set( 'Shift+Tab', this._getTabHandler( false ), { priority: 'low' } );
  124. const tableSelection = editor.plugins.get( TableSelection );
  125. this.listenTo( viewDocument, 'mousedown', ( eventInfo, domEventData ) => {
  126. const tableCell = getTableCell( domEventData, this.editor );
  127. if ( !tableCell ) {
  128. return;
  129. }
  130. const { column, row } = editor.plugins.get( TableUtils ).getCellLocation( tableCell );
  131. const mode = getSelectionMode( domEventData, column, row );
  132. tableSelection.startSelection( tableCell, mode );
  133. domEventData.preventDefault();
  134. } );
  135. this.listenTo( viewDocument, 'mousemove', ( eventInfo, domEventData ) => {
  136. if ( !tableSelection.isSelecting ) {
  137. return;
  138. }
  139. const tableCell = getTableCell( domEventData, this.editor );
  140. if ( !tableCell ) {
  141. return;
  142. }
  143. tableSelection.updateSelection( tableCell );
  144. } );
  145. this.listenTo( viewDocument, 'mouseup', ( eventInfo, domEventData ) => {
  146. if ( !tableSelection.isSelecting ) {
  147. return;
  148. }
  149. const tableCell = getTableCell( domEventData, this.editor );
  150. tableSelection.stopSelection( tableCell );
  151. } );
  152. this.listenTo( viewDocument, 'blur', () => {
  153. tableSelection.clearSelection();
  154. } );
  155. viewDocument.selection.on( 'change', () => {
  156. for ( const range of viewDocument.selection.getRanges() ) {
  157. const node = range.start.nodeAfter;
  158. if ( node && ( node.is( 'td' ) || node.is( 'th' ) ) ) {
  159. editor.editing.view.change( writer => writer.addClass( 'selected', node ) );
  160. }
  161. }
  162. } );
  163. }
  164. /**
  165. * @inheritDoc
  166. */
  167. static get requires() {
  168. return [ TableUtils, TableSelection ];
  169. }
  170. /**
  171. * Handles {@link module:engine/view/document~Document#event:keydown keydown} events for the <kbd>Tab</kbd> key executed
  172. * when the table widget is selected.
  173. *
  174. * @private
  175. * @param {module:utils/eventinfo~EventInfo} eventInfo
  176. * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
  177. */
  178. _handleTabOnSelectedTable( domEventData, cancel ) {
  179. const editor = this.editor;
  180. const selection = editor.model.document.selection;
  181. if ( !selection.isCollapsed && selection.rangeCount === 1 && selection.getFirstRange().isFlat ) {
  182. const selectedElement = selection.getSelectedElement();
  183. if ( !selectedElement || !selectedElement.is( 'table' ) ) {
  184. return;
  185. }
  186. cancel();
  187. editor.model.change( writer => {
  188. writer.setSelection( Range.createIn( selectedElement.getChild( 0 ).getChild( 0 ) ) );
  189. } );
  190. }
  191. }
  192. /**
  193. * Returns a handler for {@link module:engine/view/document~Document#event:keydown keydown} events for the <kbd>Tab</kbd> key executed
  194. * inside table cell.
  195. *
  196. * @private
  197. * @param {Boolean} isForward Whether this handler will move selection to the next cell or previous.
  198. */
  199. _getTabHandler( isForward ) {
  200. const editor = this.editor;
  201. return ( domEventData, cancel ) => {
  202. const selection = editor.model.document.selection;
  203. const firstPosition = selection.getFirstPosition();
  204. const tableCell = findAncestor( 'tableCell', firstPosition );
  205. if ( !tableCell ) {
  206. return;
  207. }
  208. cancel();
  209. const tableRow = tableCell.parent;
  210. const table = tableRow.parent;
  211. const currentRowIndex = table.getChildIndex( tableRow );
  212. const currentCellIndex = tableRow.getChildIndex( tableCell );
  213. const isFirstCellInRow = currentCellIndex === 0;
  214. if ( !isForward && isFirstCellInRow && currentRowIndex === 0 ) {
  215. // It's the first cell of a table - don't do anything (stay in current position).
  216. return;
  217. }
  218. const isLastCellInRow = currentCellIndex === tableRow.childCount - 1;
  219. const isLastRow = currentRowIndex === table.childCount - 1;
  220. if ( isForward && isLastRow && isLastCellInRow ) {
  221. editor.plugins.get( TableUtils ).insertRows( table, { at: table.childCount } );
  222. }
  223. let cellToFocus;
  224. // Move to first cell in next row.
  225. if ( isForward && isLastCellInRow ) {
  226. const nextRow = table.getChild( currentRowIndex + 1 );
  227. cellToFocus = nextRow.getChild( 0 );
  228. }
  229. // Move to last cell in a previous row.
  230. else if ( !isForward && isFirstCellInRow ) {
  231. const previousRow = table.getChild( currentRowIndex - 1 );
  232. cellToFocus = previousRow.getChild( previousRow.childCount - 1 );
  233. }
  234. // Move to next/previous cell.
  235. else {
  236. cellToFocus = tableRow.getChild( currentCellIndex + ( isForward ? 1 : -1 ) );
  237. }
  238. editor.model.change( writer => {
  239. writer.setSelection( Range.createIn( cellToFocus ) );
  240. } );
  241. };
  242. }
  243. }
  244. function getTableCell( domEventData, editor ) {
  245. const element = domEventData.target;
  246. const modelElement = editor.editing.mapper.toModelElement( element );
  247. if ( !modelElement ) {
  248. return;
  249. }
  250. return findAncestor( 'tableCell', Position.createAt( modelElement ) );
  251. }
  252. function getSelectionMode( domEventData, column, row ) {
  253. let mode = 'block';
  254. const domEvent = domEventData.domEvent;
  255. const target = domEvent.target;
  256. if ( column == 0 && domEvent.offsetX < target.clientWidth / 2 ) {
  257. mode = 'row';
  258. } else if ( row == 0 && ( domEvent.offsetY < target.clientHeight / 2 ) ) {
  259. mode = 'column';
  260. }
  261. return mode;
  262. }