@ckeditor/ckeditor5-table 32.0.0 → 34.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +2 -2
- package/README.md +2 -1
- package/build/table.js +2 -2
- package/build/translations/el.js +1 -0
- package/build/translations/en-au.js +1 -1
- package/build/translations/es.js +1 -1
- package/build/translations/hr.js +1 -1
- package/build/translations/lv.js +1 -1
- package/build/translations/sk.js +1 -1
- package/build/translations/ur.js +1 -0
- package/ckeditor5-metadata.json +19 -0
- package/lang/translations/el.po +261 -0
- package/lang/translations/en-au.po +3 -3
- package/lang/translations/es.po +32 -32
- package/lang/translations/hr.po +3 -3
- package/lang/translations/lv.po +40 -40
- package/lang/translations/sk.po +3 -3
- package/lang/translations/ur.po +261 -0
- package/package.json +26 -21
- package/src/commands/insertcolumncommand.js +4 -4
- package/src/commands/insertrowcommand.js +4 -4
- package/src/commands/inserttablecommand.js +1 -5
- package/src/commands/mergecellcommand.js +4 -5
- package/src/commands/mergecellscommand.js +5 -4
- package/src/commands/removecolumncommand.js +8 -7
- package/src/commands/removerowcommand.js +5 -6
- package/src/commands/selectcolumncommand.js +4 -4
- package/src/commands/selectrowcommand.js +5 -5
- package/src/commands/setheadercolumncommand.js +5 -4
- package/src/commands/setheaderrowcommand.js +7 -4
- package/src/commands/splitcellcommand.js +4 -4
- package/src/converters/downcast.js +76 -407
- package/src/converters/{table-cell-refresh-post-fixer.js → table-cell-refresh-handler.js} +8 -19
- package/src/converters/table-headings-refresh-handler.js +68 -0
- package/src/index.js +3 -0
- package/src/plaintableoutput.js +151 -0
- package/src/tablecellproperties/commands/tablecellpropertycommand.js +4 -3
- package/src/tableclipboard.js +18 -15
- package/src/tablecolumnresize/constants.js +32 -0
- package/src/tablecolumnresize/converters.js +126 -0
- package/src/tablecolumnresize/tablecolumnresizeediting.js +758 -0
- package/src/tablecolumnresize/utils.js +367 -0
- package/src/tablecolumnresize.js +36 -0
- package/src/tableediting.js +51 -32
- package/src/tablekeyboard.js +73 -70
- package/src/tablemouse.js +6 -4
- package/src/tableselection.js +9 -8
- package/src/tableutils.js +310 -0
- package/theme/table.css +1 -1
- package/theme/tablecolumnresize.css +59 -0
- package/src/converters/table-heading-rows-refresh-post-fixer.js +0 -72
- package/src/utils/selection.js +0 -276
package/src/tablekeyboard.js
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
import TableSelection from './tableselection';
|
|
11
11
|
import TableWalker from './tablewalker';
|
|
12
|
+
import TableUtils from './tableutils';
|
|
12
13
|
|
|
13
14
|
import { Plugin } from 'ckeditor5/src/core';
|
|
14
15
|
import { getLocalizedArrowKeyCodeDirection } from 'ckeditor5/src/utils';
|
|
15
|
-
import { getSelectedTableCells, getTableCellsContainingSelection } from './utils/selection';
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* This plugin enables keyboard navigation for tables.
|
|
@@ -32,7 +32,7 @@ export default class TableKeyboard extends Plugin {
|
|
|
32
32
|
* @inheritDoc
|
|
33
33
|
*/
|
|
34
34
|
static get requires() {
|
|
35
|
-
return [ TableSelection ];
|
|
35
|
+
return [ TableSelection, TableUtils ];
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/**
|
|
@@ -42,23 +42,20 @@ export default class TableKeyboard extends Plugin {
|
|
|
42
42
|
const view = this.editor.editing.view;
|
|
43
43
|
const viewDocument = view.document;
|
|
44
44
|
|
|
45
|
-
// Handle Tab key navigation.
|
|
46
|
-
this.editor.keystrokes.set( 'Tab', ( ...args ) => this._handleTabOnSelectedTable( ...args ), { priority: 'low' } );
|
|
47
|
-
this.editor.keystrokes.set( 'Tab', this._getTabHandler( true ), { priority: 'low' } );
|
|
48
|
-
this.editor.keystrokes.set( 'Shift+Tab', this._getTabHandler( false ), { priority: 'low' } );
|
|
49
|
-
|
|
50
45
|
this.listenTo( viewDocument, 'arrowKey', ( ...args ) => this._onArrowKey( ...args ), { context: 'table' } );
|
|
46
|
+
this.listenTo( viewDocument, 'tab', ( ...args ) => this._handleTabOnSelectedTable( ...args ), { context: 'figure' } );
|
|
47
|
+
this.listenTo( viewDocument, 'tab', ( ...args ) => this._handleTab( ...args ), { context: [ 'th', 'td' ] } );
|
|
51
48
|
}
|
|
52
49
|
|
|
53
50
|
/**
|
|
54
|
-
* Handles {@link module:engine/view/document~Document#event:
|
|
51
|
+
* Handles {@link module:engine/view/document~Document#event:tab tab} events for the <kbd>Tab</kbd> key executed
|
|
55
52
|
* when the table widget is selected.
|
|
56
53
|
*
|
|
57
54
|
* @private
|
|
58
|
-
* @param {module:engine/view/observer/
|
|
59
|
-
* @param {
|
|
55
|
+
* @param {module:engine/view/observer/bubblingeventinfo~BubblingEventInfo} bubblingEventInfo
|
|
56
|
+
* @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
|
|
60
57
|
*/
|
|
61
|
-
_handleTabOnSelectedTable(
|
|
58
|
+
_handleTabOnSelectedTable( bubblingEventInfo, domEventData ) {
|
|
62
59
|
const editor = this.editor;
|
|
63
60
|
const selection = editor.model.document.selection;
|
|
64
61
|
const selectedElement = selection.getSelectedElement();
|
|
@@ -67,7 +64,9 @@ export default class TableKeyboard extends Plugin {
|
|
|
67
64
|
return;
|
|
68
65
|
}
|
|
69
66
|
|
|
70
|
-
|
|
67
|
+
domEventData.preventDefault();
|
|
68
|
+
domEventData.stopPropagation();
|
|
69
|
+
bubblingEventInfo.stop();
|
|
71
70
|
|
|
72
71
|
editor.model.change( writer => {
|
|
73
72
|
writer.setSelection( writer.createRangeIn( selectedElement.getChild( 0 ).getChild( 0 ) ) );
|
|
@@ -75,87 +74,90 @@ export default class TableKeyboard extends Plugin {
|
|
|
75
74
|
}
|
|
76
75
|
|
|
77
76
|
/**
|
|
78
|
-
*
|
|
77
|
+
* Handles {@link module:engine/view/document~Document#event:tab tab} events for the <kbd>Tab</kbd> key executed
|
|
79
78
|
* inside table cells.
|
|
80
79
|
*
|
|
81
80
|
* @private
|
|
82
|
-
* @param {
|
|
81
|
+
* @param {module:engine/view/observer/bubblingeventinfo~BubblingEventInfo} bubblingEventInfo
|
|
82
|
+
* @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
|
|
83
83
|
*/
|
|
84
|
-
|
|
84
|
+
_handleTab( bubblingEventInfo, domEventData ) {
|
|
85
85
|
const editor = this.editor;
|
|
86
|
+
const tableUtils = this.editor.plugins.get( TableUtils );
|
|
86
87
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
let tableCell = getTableCellsContainingSelection( selection )[ 0 ];
|
|
88
|
+
const selection = editor.model.document.selection;
|
|
89
|
+
const isForward = !domEventData.shiftKey;
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
tableCell = this.editor.plugins.get( 'TableSelection' ).getFocusCell();
|
|
93
|
-
}
|
|
91
|
+
let tableCell = tableUtils.getTableCellsContainingSelection( selection )[ 0 ];
|
|
94
92
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
if ( !tableCell ) {
|
|
94
|
+
tableCell = this.editor.plugins.get( 'TableSelection' ).getFocusCell();
|
|
95
|
+
}
|
|
98
96
|
|
|
99
|
-
|
|
97
|
+
if ( !tableCell ) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
domEventData.preventDefault();
|
|
102
|
+
domEventData.stopPropagation();
|
|
103
|
+
bubblingEventInfo.stop();
|
|
103
104
|
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
const tableRow = tableCell.parent;
|
|
106
|
+
const table = tableRow.parent;
|
|
106
107
|
|
|
107
|
-
|
|
108
|
+
const currentRowIndex = table.getChildIndex( tableRow );
|
|
109
|
+
const currentCellIndex = tableRow.getChildIndex( tableCell );
|
|
108
110
|
|
|
109
|
-
|
|
110
|
-
// Set the selection over the whole table if the selection was in the first table cell.
|
|
111
|
-
editor.model.change( writer => {
|
|
112
|
-
writer.setSelection( writer.createRangeOn( table ) );
|
|
113
|
-
} );
|
|
111
|
+
const isFirstCellInRow = currentCellIndex === 0;
|
|
114
112
|
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
if ( !isForward && isFirstCellInRow && currentRowIndex === 0 ) {
|
|
114
|
+
// Set the selection over the whole table if the selection was in the first table cell.
|
|
115
|
+
editor.model.change( writer => {
|
|
116
|
+
writer.setSelection( writer.createRangeOn( table ) );
|
|
117
|
+
} );
|
|
117
118
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const isLastRow = currentRowIndex === tableUtils.getRows( table ) - 1;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
121
|
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
const isLastCellInRow = currentCellIndex === tableRow.childCount - 1;
|
|
123
|
+
const isLastRow = currentRowIndex === tableUtils.getRows( table ) - 1;
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if ( currentRowIndex === tableUtils.getRows( table ) - 1 ) {
|
|
128
|
-
editor.model.change( writer => {
|
|
129
|
-
writer.setSelection( writer.createRangeOn( table ) );
|
|
130
|
-
} );
|
|
125
|
+
if ( isForward && isLastRow && isLastCellInRow ) {
|
|
126
|
+
editor.execute( 'insertTableRowBelow' );
|
|
131
127
|
|
|
132
|
-
|
|
133
|
-
|
|
128
|
+
// Check if the command actually added a row. If `insertTableRowBelow` execution didn't add a row (because it was disabled
|
|
129
|
+
// or it got overwritten) set the selection over the whole table to mirror the first cell case.
|
|
130
|
+
if ( currentRowIndex === tableUtils.getRows( table ) - 1 ) {
|
|
131
|
+
editor.model.change( writer => {
|
|
132
|
+
writer.setSelection( writer.createRangeOn( table ) );
|
|
133
|
+
} );
|
|
134
|
+
|
|
135
|
+
return;
|
|
134
136
|
}
|
|
137
|
+
}
|
|
135
138
|
|
|
136
|
-
|
|
139
|
+
let cellToFocus;
|
|
137
140
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
// Move to the first cell in the next row.
|
|
142
|
+
if ( isForward && isLastCellInRow ) {
|
|
143
|
+
const nextRow = table.getChild( currentRowIndex + 1 );
|
|
141
144
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
cellToFocus = nextRow.getChild( 0 );
|
|
146
|
+
}
|
|
147
|
+
// Move to the last cell in the previous row.
|
|
148
|
+
else if ( !isForward && isFirstCellInRow ) {
|
|
149
|
+
const previousRow = table.getChild( currentRowIndex - 1 );
|
|
147
150
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
151
|
+
cellToFocus = previousRow.getChild( previousRow.childCount - 1 );
|
|
152
|
+
}
|
|
153
|
+
// Move to the next/previous cell.
|
|
154
|
+
else {
|
|
155
|
+
cellToFocus = tableRow.getChild( currentCellIndex + ( isForward ? 1 : -1 ) );
|
|
156
|
+
}
|
|
154
157
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
};
|
|
158
|
+
editor.model.change( writer => {
|
|
159
|
+
writer.setSelection( writer.createRangeIn( cellToFocus ) );
|
|
160
|
+
} );
|
|
159
161
|
}
|
|
160
162
|
|
|
161
163
|
/**
|
|
@@ -188,13 +190,14 @@ export default class TableKeyboard extends Plugin {
|
|
|
188
190
|
* @returns {Boolean} Returns `true` if key was handled.
|
|
189
191
|
*/
|
|
190
192
|
_handleArrowKeys( direction, expandSelection ) {
|
|
193
|
+
const tableUtils = this.editor.plugins.get( TableUtils );
|
|
191
194
|
const model = this.editor.model;
|
|
192
195
|
const selection = model.document.selection;
|
|
193
196
|
const isForward = [ 'right', 'down' ].includes( direction );
|
|
194
197
|
|
|
195
198
|
// In case one or more table cells are selected (from outside),
|
|
196
199
|
// move the selection to a cell adjacent to the selected table fragment.
|
|
197
|
-
const selectedCells = getSelectedTableCells( selection );
|
|
200
|
+
const selectedCells = tableUtils.getSelectedTableCells( selection );
|
|
198
201
|
|
|
199
202
|
if ( selectedCells.length ) {
|
|
200
203
|
let focusCell;
|
package/src/tablemouse.js
CHANGED
|
@@ -11,8 +11,7 @@ import { Plugin } from 'ckeditor5/src/core';
|
|
|
11
11
|
|
|
12
12
|
import TableSelection from './tableselection';
|
|
13
13
|
import MouseEventsObserver from './tablemouse/mouseeventsobserver';
|
|
14
|
-
|
|
15
|
-
import { getTableCellsContainingSelection } from './utils/selection';
|
|
14
|
+
import TableUtils from './tableutils';
|
|
16
15
|
|
|
17
16
|
/**
|
|
18
17
|
* This plugin enables a table cells' selection with the mouse.
|
|
@@ -32,7 +31,7 @@ export default class TableMouse extends Plugin {
|
|
|
32
31
|
* @inheritDoc
|
|
33
32
|
*/
|
|
34
33
|
static get requires() {
|
|
35
|
-
return [ TableSelection ];
|
|
34
|
+
return [ TableSelection, TableUtils ];
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
/**
|
|
@@ -57,11 +56,14 @@ export default class TableMouse extends Plugin {
|
|
|
57
56
|
*/
|
|
58
57
|
_enableShiftClickSelection() {
|
|
59
58
|
const editor = this.editor;
|
|
59
|
+
const tableUtils = editor.plugins.get( TableUtils );
|
|
60
60
|
let blockSelectionChange = false;
|
|
61
61
|
|
|
62
62
|
const tableSelection = editor.plugins.get( TableSelection );
|
|
63
63
|
|
|
64
64
|
this.listenTo( editor.editing.view.document, 'mousedown', ( evt, domEventData ) => {
|
|
65
|
+
const selection = editor.model.document.selection;
|
|
66
|
+
|
|
65
67
|
if ( !this.isEnabled || !tableSelection.isEnabled ) {
|
|
66
68
|
return;
|
|
67
69
|
}
|
|
@@ -70,7 +72,7 @@ export default class TableMouse extends Plugin {
|
|
|
70
72
|
return;
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
const anchorCell = tableSelection.getAnchorCell() || getTableCellsContainingSelection(
|
|
75
|
+
const anchorCell = tableSelection.getAnchorCell() || tableUtils.getTableCellsContainingSelection( selection )[ 0 ];
|
|
74
76
|
|
|
75
77
|
if ( !anchorCell ) {
|
|
76
78
|
return;
|
package/src/tableselection.js
CHANGED
|
@@ -14,7 +14,6 @@ import TableWalker from './tablewalker';
|
|
|
14
14
|
import TableUtils from './tableutils';
|
|
15
15
|
|
|
16
16
|
import { cropTableToDimensions, adjustLastRowIndex, adjustLastColumnIndex } from './utils/structure';
|
|
17
|
-
import { getColumnIndexes, getRowIndexes, getSelectedTableCells, isSelectionRectangular } from './utils/selection';
|
|
18
17
|
|
|
19
18
|
import '../theme/tableselection.css';
|
|
20
19
|
|
|
@@ -36,7 +35,7 @@ export default class TableSelection extends Plugin {
|
|
|
36
35
|
* @inheritDoc
|
|
37
36
|
*/
|
|
38
37
|
static get requires() {
|
|
39
|
-
return [ TableUtils ];
|
|
38
|
+
return [ TableUtils, TableUtils ];
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
/**
|
|
@@ -58,9 +57,10 @@ export default class TableSelection extends Plugin {
|
|
|
58
57
|
* @returns {Array.<module:engine/model/element~Element>|null}
|
|
59
58
|
*/
|
|
60
59
|
getSelectedTableCells() {
|
|
60
|
+
const tableUtils = this.editor.plugins.get( TableUtils );
|
|
61
61
|
const selection = this.editor.model.document.selection;
|
|
62
62
|
|
|
63
|
-
const selectedCells = getSelectedTableCells( selection );
|
|
63
|
+
const selectedCells = tableUtils.getSelectedTableCells( selection );
|
|
64
64
|
|
|
65
65
|
if ( selectedCells.length == 0 ) {
|
|
66
66
|
return null;
|
|
@@ -81,6 +81,7 @@ export default class TableSelection extends Plugin {
|
|
|
81
81
|
* @returns {module:engine/model/documentfragment~DocumentFragment|null}
|
|
82
82
|
*/
|
|
83
83
|
getSelectionAsFragment() {
|
|
84
|
+
const tableUtils = this.editor.plugins.get( TableUtils );
|
|
84
85
|
const selectedCells = this.getSelectedTableCells();
|
|
85
86
|
|
|
86
87
|
if ( !selectedCells ) {
|
|
@@ -89,10 +90,9 @@ export default class TableSelection extends Plugin {
|
|
|
89
90
|
|
|
90
91
|
return this.editor.model.change( writer => {
|
|
91
92
|
const documentFragment = writer.createDocumentFragment();
|
|
92
|
-
const tableUtils = this.editor.plugins.get( 'TableUtils' );
|
|
93
93
|
|
|
94
|
-
const { first: firstColumn, last: lastColumn } = getColumnIndexes( selectedCells );
|
|
95
|
-
const { first: firstRow, last: lastRow } = getRowIndexes( selectedCells );
|
|
94
|
+
const { first: firstColumn, last: lastColumn } = tableUtils.getColumnIndexes( selectedCells );
|
|
95
|
+
const { first: firstRow, last: lastRow } = tableUtils.getRowIndexes( selectedCells );
|
|
96
96
|
|
|
97
97
|
const sourceTable = selectedCells[ 0 ].findAncestor( 'table' );
|
|
98
98
|
|
|
@@ -101,7 +101,7 @@ export default class TableSelection extends Plugin {
|
|
|
101
101
|
|
|
102
102
|
// If the selection is rectangular there could be a case of all cells in the last row/column spanned over
|
|
103
103
|
// next row/column so the real lastRow/lastColumn should be updated.
|
|
104
|
-
if ( isSelectionRectangular( selectedCells
|
|
104
|
+
if ( tableUtils.isSelectionRectangular( selectedCells ) ) {
|
|
105
105
|
const dimensions = {
|
|
106
106
|
firstColumn,
|
|
107
107
|
lastColumn,
|
|
@@ -269,10 +269,11 @@ export default class TableSelection extends Plugin {
|
|
|
269
269
|
* @param {Array.<*>} args Delete content method arguments.
|
|
270
270
|
*/
|
|
271
271
|
_handleDeleteContent( event, args ) {
|
|
272
|
+
const tableUtils = this.editor.plugins.get( TableUtils );
|
|
272
273
|
const [ selection, options ] = args;
|
|
273
274
|
const model = this.editor.model;
|
|
274
275
|
const isBackward = !options || options.direction == 'backward';
|
|
275
|
-
const selectedTableCells = getSelectedTableCells( selection );
|
|
276
|
+
const selectedTableCells = tableUtils.getSelectedTableCells( selection );
|
|
276
277
|
|
|
277
278
|
if ( !selectedTableCells.length ) {
|
|
278
279
|
return;
|
package/src/tableutils.js
CHANGED
|
@@ -775,6 +775,288 @@ export default class TableUtils extends Plugin {
|
|
|
775
775
|
return Array.from( table.getChildren() )
|
|
776
776
|
.reduce( ( rowCount, child ) => child.is( 'element', 'tableRow' ) ? rowCount + 1 : rowCount, 0 );
|
|
777
777
|
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Creates an instance of the table walker.
|
|
781
|
+
*
|
|
782
|
+
* The table walker iterates internally by traversing the table from row index = 0 and column index = 0.
|
|
783
|
+
* It walks row by row and column by column in order to output values defined in the options.
|
|
784
|
+
* By default it will output only the locations that are occupied by a cell. To include also spanned rows and columns,
|
|
785
|
+
* pass the `includeAllSlots` option.
|
|
786
|
+
*
|
|
787
|
+
* @protected
|
|
788
|
+
* @param {module:engine/model/element~Element} table A table over which the walker iterates.
|
|
789
|
+
* @param {Object} [options={}] An object with configuration.
|
|
790
|
+
* @param {Number} [options.row] A row index for which this iterator will output cells.
|
|
791
|
+
* Can't be used together with `startRow` and `endRow`.
|
|
792
|
+
* @param {Number} [options.startRow=0] A row index from which this iterator should start. Can't be used together with `row`.
|
|
793
|
+
* @param {Number} [options.endRow] A row index at which this iterator should end. Can't be used together with `row`.
|
|
794
|
+
* @param {Number} [options.column] A column index for which this iterator will output cells.
|
|
795
|
+
* Can't be used together with `startColumn` and `endColumn`.
|
|
796
|
+
* @param {Number} [options.startColumn=0] A column index from which this iterator should start. Can't be used together with `column`.
|
|
797
|
+
* @param {Number} [options.endColumn] A column index at which this iterator should end. Can't be used together with `column`.
|
|
798
|
+
* @param {Boolean} [options.includeAllSlots=false] Also return values for spanned cells.
|
|
799
|
+
*/
|
|
800
|
+
createTableWalker( table, options = {} ) {
|
|
801
|
+
return new TableWalker( table, options );
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Returns all model table cells that are fully selected (from the outside)
|
|
806
|
+
* within the provided model selection's ranges.
|
|
807
|
+
*
|
|
808
|
+
* To obtain the cells selected from the inside, use
|
|
809
|
+
* {@link #getTableCellsContainingSelection}.
|
|
810
|
+
*
|
|
811
|
+
* @param {module:engine/model/selection~Selection} selection
|
|
812
|
+
* @returns {Array.<module:engine/model/element~Element>}
|
|
813
|
+
*/
|
|
814
|
+
getSelectedTableCells( selection ) {
|
|
815
|
+
const cells = [];
|
|
816
|
+
|
|
817
|
+
for ( const range of this.sortRanges( selection.getRanges() ) ) {
|
|
818
|
+
const element = range.getContainedElement();
|
|
819
|
+
|
|
820
|
+
if ( element && element.is( 'element', 'tableCell' ) ) {
|
|
821
|
+
cells.push( element );
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
return cells;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Returns all model table cells that the provided model selection's ranges
|
|
830
|
+
* {@link module:engine/model/range~Range#start} inside.
|
|
831
|
+
*
|
|
832
|
+
* To obtain the cells selected from the outside, use
|
|
833
|
+
* {@link #getSelectedTableCells}.
|
|
834
|
+
*
|
|
835
|
+
* @param {module:engine/model/selection~Selection} selection
|
|
836
|
+
* @returns {Array.<module:engine/model/element~Element>}
|
|
837
|
+
*/
|
|
838
|
+
getTableCellsContainingSelection( selection ) {
|
|
839
|
+
const cells = [];
|
|
840
|
+
|
|
841
|
+
for ( const range of selection.getRanges() ) {
|
|
842
|
+
const cellWithSelection = range.start.findAncestor( 'tableCell' );
|
|
843
|
+
|
|
844
|
+
if ( cellWithSelection ) {
|
|
845
|
+
cells.push( cellWithSelection );
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
return cells;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Returns all model table cells that are either completely selected
|
|
854
|
+
* by selection ranges or host selection range
|
|
855
|
+
* {@link module:engine/model/range~Range#start start positions} inside them.
|
|
856
|
+
*
|
|
857
|
+
* Combines {@link #getTableCellsContainingSelection} and
|
|
858
|
+
* {@link #getSelectedTableCells}.
|
|
859
|
+
*
|
|
860
|
+
* @param {module:engine/model/selection~Selection} selection
|
|
861
|
+
* @returns {Array.<module:engine/model/element~Element>}
|
|
862
|
+
*/
|
|
863
|
+
getSelectionAffectedTableCells( selection ) {
|
|
864
|
+
const selectedCells = this.getSelectedTableCells( selection );
|
|
865
|
+
|
|
866
|
+
if ( selectedCells.length ) {
|
|
867
|
+
return selectedCells;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return this.getTableCellsContainingSelection( selection );
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Returns an object with the `first` and `last` row index contained in the given `tableCells`.
|
|
875
|
+
*
|
|
876
|
+
* const selectedTableCells = getSelectedTableCells( editor.model.document.selection );
|
|
877
|
+
*
|
|
878
|
+
* const { first, last } = getRowIndexes( selectedTableCells );
|
|
879
|
+
*
|
|
880
|
+
* console.log( `Selected rows: ${ first } to ${ last }` );
|
|
881
|
+
*
|
|
882
|
+
* @param {Array.<module:engine/model/element~Element>} tableCells
|
|
883
|
+
* @returns {Object} Returns an object with the `first` and `last` table row indexes.
|
|
884
|
+
*/
|
|
885
|
+
getRowIndexes( tableCells ) {
|
|
886
|
+
const indexes = tableCells.map( cell => cell.parent.index );
|
|
887
|
+
|
|
888
|
+
return this._getFirstLastIndexesObject( indexes );
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Returns an object with the `first` and `last` column index contained in the given `tableCells`.
|
|
893
|
+
*
|
|
894
|
+
* const selectedTableCells = getSelectedTableCells( editor.model.document.selection );
|
|
895
|
+
*
|
|
896
|
+
* const { first, last } = getColumnIndexes( selectedTableCells );
|
|
897
|
+
*
|
|
898
|
+
* console.log( `Selected columns: ${ first } to ${ last }` );
|
|
899
|
+
*
|
|
900
|
+
* @param {Array.<module:engine/model/element~Element>} tableCells
|
|
901
|
+
* @returns {Object} Returns an object with the `first` and `last` table column indexes.
|
|
902
|
+
*/
|
|
903
|
+
getColumnIndexes( tableCells ) {
|
|
904
|
+
const table = tableCells[ 0 ].findAncestor( 'table' );
|
|
905
|
+
const tableMap = [ ...new TableWalker( table ) ];
|
|
906
|
+
|
|
907
|
+
const indexes = tableMap
|
|
908
|
+
.filter( entry => tableCells.includes( entry.cell ) )
|
|
909
|
+
.map( entry => entry.column );
|
|
910
|
+
|
|
911
|
+
return this._getFirstLastIndexesObject( indexes );
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Checks if the selection contains cells that do not exceed rectangular selection.
|
|
916
|
+
*
|
|
917
|
+
* In a table below:
|
|
918
|
+
*
|
|
919
|
+
* ┌───┬───┬───┬───┐
|
|
920
|
+
* │ a │ b │ c │ d │
|
|
921
|
+
* ├───┴───┼───┤ │
|
|
922
|
+
* │ e │ f │ │
|
|
923
|
+
* │ ├───┼───┤
|
|
924
|
+
* │ │ g │ h │
|
|
925
|
+
* └───────┴───┴───┘
|
|
926
|
+
*
|
|
927
|
+
* Valid selections are these which create a solid rectangle (without gaps), such as:
|
|
928
|
+
* - a, b (two horizontal cells)
|
|
929
|
+
* - c, f (two vertical cells)
|
|
930
|
+
* - a, b, e (cell "e" spans over four cells)
|
|
931
|
+
* - c, d, f (cell d spans over a cell in the row below)
|
|
932
|
+
*
|
|
933
|
+
* While an invalid selection would be:
|
|
934
|
+
* - a, c (the unselected cell "b" creates a gap)
|
|
935
|
+
* - f, g, h (cell "d" spans over a cell from the row of "f" cell - thus creates a gap)
|
|
936
|
+
*
|
|
937
|
+
* @param {Array.<module:engine/model/element~Element>} selectedTableCells
|
|
938
|
+
* @returns {Boolean}
|
|
939
|
+
*/
|
|
940
|
+
isSelectionRectangular( selectedTableCells ) {
|
|
941
|
+
if ( selectedTableCells.length < 2 || !this._areCellInTheSameTableSection( selectedTableCells ) ) {
|
|
942
|
+
return false;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// A valid selection is a fully occupied rectangle composed of table cells.
|
|
946
|
+
// Below we will calculate the area of a selected table cells and the area of valid selection.
|
|
947
|
+
// The area of a valid selection is defined by top-left and bottom-right cells.
|
|
948
|
+
const rows = new Set();
|
|
949
|
+
const columns = new Set();
|
|
950
|
+
|
|
951
|
+
let areaOfSelectedCells = 0;
|
|
952
|
+
|
|
953
|
+
for ( const tableCell of selectedTableCells ) {
|
|
954
|
+
const { row, column } = this.getCellLocation( tableCell );
|
|
955
|
+
const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 );
|
|
956
|
+
const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 );
|
|
957
|
+
|
|
958
|
+
// Record row & column indexes of current cell.
|
|
959
|
+
rows.add( row );
|
|
960
|
+
columns.add( column );
|
|
961
|
+
|
|
962
|
+
// For cells that spans over multiple rows add also the last row that this cell spans over.
|
|
963
|
+
if ( rowspan > 1 ) {
|
|
964
|
+
rows.add( row + rowspan - 1 );
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// For cells that spans over multiple columns add also the last column that this cell spans over.
|
|
968
|
+
if ( colspan > 1 ) {
|
|
969
|
+
columns.add( column + colspan - 1 );
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
areaOfSelectedCells += ( rowspan * colspan );
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// We can only merge table cells that are in adjacent rows...
|
|
976
|
+
const areaOfValidSelection = getBiggestRectangleArea( rows, columns );
|
|
977
|
+
|
|
978
|
+
return areaOfValidSelection == areaOfSelectedCells;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Returns array of sorted ranges.
|
|
983
|
+
*
|
|
984
|
+
* @param {Iterable.<module:engine/model/range~Range>} ranges
|
|
985
|
+
* @return {Array.<module:engine/model/range~Range>}
|
|
986
|
+
*/
|
|
987
|
+
sortRanges( ranges ) {
|
|
988
|
+
return Array.from( ranges ).sort( compareRangeOrder );
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Helper method to get an object with `first` and `last` indexes from an unsorted array of indexes.
|
|
993
|
+
*
|
|
994
|
+
* @private
|
|
995
|
+
* @param {Number[]} indexes
|
|
996
|
+
* @returns {Object}
|
|
997
|
+
*/
|
|
998
|
+
_getFirstLastIndexesObject( indexes ) {
|
|
999
|
+
const allIndexesSorted = indexes.sort( ( indexA, indexB ) => indexA - indexB );
|
|
1000
|
+
|
|
1001
|
+
const first = allIndexesSorted[ 0 ];
|
|
1002
|
+
const last = allIndexesSorted[ allIndexesSorted.length - 1 ];
|
|
1003
|
+
|
|
1004
|
+
return { first, last };
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Checks if the selection does not mix a header (column or row) with other cells.
|
|
1009
|
+
*
|
|
1010
|
+
* For instance, in the table below valid selections consist of cells with the same letter only.
|
|
1011
|
+
* So, a-a (same heading row and column) or d-d (body cells) are valid while c-d or a-b are not.
|
|
1012
|
+
*
|
|
1013
|
+
* header columns
|
|
1014
|
+
* ↓ ↓
|
|
1015
|
+
* ┌───┬───┬───┬───┐
|
|
1016
|
+
* │ a │ a │ b │ b │ ← header row
|
|
1017
|
+
* ├───┼───┼───┼───┤
|
|
1018
|
+
* │ c │ c │ d │ d │
|
|
1019
|
+
* ├───┼───┼───┼───┤
|
|
1020
|
+
* │ c │ c │ d │ d │
|
|
1021
|
+
* └───┴───┴───┴───┘
|
|
1022
|
+
*
|
|
1023
|
+
* @private
|
|
1024
|
+
* @param {Array.<module:engine/model/element~Element>} tableCells
|
|
1025
|
+
* @returns {Boolean}
|
|
1026
|
+
*/
|
|
1027
|
+
_areCellInTheSameTableSection( tableCells ) {
|
|
1028
|
+
const table = tableCells[ 0 ].findAncestor( 'table' );
|
|
1029
|
+
|
|
1030
|
+
const rowIndexes = this.getRowIndexes( tableCells );
|
|
1031
|
+
const headingRows = parseInt( table.getAttribute( 'headingRows' ) || 0 );
|
|
1032
|
+
|
|
1033
|
+
// Calculating row indexes is a bit cheaper so if this check fails we can't merge.
|
|
1034
|
+
if ( !this._areIndexesInSameSection( rowIndexes, headingRows ) ) {
|
|
1035
|
+
return false;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const headingColumns = parseInt( table.getAttribute( 'headingColumns' ) || 0 );
|
|
1039
|
+
const columnIndexes = this.getColumnIndexes( tableCells );
|
|
1040
|
+
|
|
1041
|
+
// Similarly cells must be in same column section.
|
|
1042
|
+
return this._areIndexesInSameSection( columnIndexes, headingColumns );
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Unified check if table rows/columns indexes are in the same heading/body section.
|
|
1047
|
+
*
|
|
1048
|
+
* @private
|
|
1049
|
+
* @param {Object} params
|
|
1050
|
+
* @param {Number} params.first
|
|
1051
|
+
* @param {Number} params.last
|
|
1052
|
+
* @param {Number} headingSectionSize
|
|
1053
|
+
*/
|
|
1054
|
+
_areIndexesInSameSection( { first, last }, headingSectionSize ) {
|
|
1055
|
+
const firstCellIsInHeading = first < headingSectionSize;
|
|
1056
|
+
const lastCellIsInHeading = last < headingSectionSize;
|
|
1057
|
+
|
|
1058
|
+
return firstCellIsInHeading === lastCellIsInHeading;
|
|
1059
|
+
}
|
|
778
1060
|
}
|
|
779
1061
|
|
|
780
1062
|
// Creates empty rows at the given index in an existing table.
|
|
@@ -943,3 +1225,31 @@ function moveCellsToRow( table, targetRowIndex, cellsToMove, writer ) {
|
|
|
943
1225
|
}
|
|
944
1226
|
}
|
|
945
1227
|
}
|
|
1228
|
+
|
|
1229
|
+
function compareRangeOrder( rangeA, rangeB ) {
|
|
1230
|
+
// Since table cell ranges are disjoint, it's enough to check their start positions.
|
|
1231
|
+
const posA = rangeA.start;
|
|
1232
|
+
const posB = rangeB.start;
|
|
1233
|
+
|
|
1234
|
+
// Checking for equal position (returning 0) is not needed because this would be either:
|
|
1235
|
+
// a. Intersecting range (not allowed by model)
|
|
1236
|
+
// b. Collapsed range on the same position (allowed by model but should not happen).
|
|
1237
|
+
return posA.isBefore( posB ) ? -1 : 1;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// Calculates the area of a maximum rectangle that can span over the provided row & column indexes.
|
|
1241
|
+
//
|
|
1242
|
+
// @param {Array.<Number>} rows
|
|
1243
|
+
// @param {Array.<Number>} columns
|
|
1244
|
+
// @returns {Number}
|
|
1245
|
+
function getBiggestRectangleArea( rows, columns ) {
|
|
1246
|
+
const rowsIndexes = Array.from( rows.values() );
|
|
1247
|
+
const columnIndexes = Array.from( columns.values() );
|
|
1248
|
+
|
|
1249
|
+
const lastRow = Math.max( ...rowsIndexes );
|
|
1250
|
+
const firstRow = Math.min( ...rowsIndexes );
|
|
1251
|
+
const lastColumn = Math.max( ...columnIndexes );
|
|
1252
|
+
const firstColumn = Math.min( ...columnIndexes );
|
|
1253
|
+
|
|
1254
|
+
return ( lastRow - firstRow + 1 ) * ( lastColumn - firstColumn + 1 );
|
|
1255
|
+
}
|
package/theme/table.css
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/* Text alignment of the table header should match the editor settings and override the native browser styling,
|
|
46
|
-
when content is available outside the
|
|
46
|
+
when content is available outside the editor. See https://github.com/ckeditor/ckeditor5/issues/6638 */
|
|
47
47
|
.ck-content[dir="rtl"] .table th {
|
|
48
48
|
text-align: right;
|
|
49
49
|
}
|