@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
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @module table/tablecolumnresize/tablecolumnresizeediting
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/* istanbul ignore file */
|
|
11
|
+
|
|
12
|
+
import { throttle } from 'lodash-es';
|
|
13
|
+
import { global, DomEmitterMixin } from 'ckeditor5/src/utils';
|
|
14
|
+
import { Plugin } from 'ckeditor5/src/core';
|
|
15
|
+
|
|
16
|
+
import MouseEventsObserver from '../../src/tablemouse/mouseeventsobserver';
|
|
17
|
+
import TableEditing from '../tableediting';
|
|
18
|
+
import TableWalker from '../tablewalker';
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
upcastColgroupElement,
|
|
22
|
+
downcastTableColumnWidthsAttribute
|
|
23
|
+
} from './converters';
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
clamp,
|
|
27
|
+
fillArray,
|
|
28
|
+
sumArray,
|
|
29
|
+
getAffectedTables,
|
|
30
|
+
getColumnIndex,
|
|
31
|
+
getColumnWidthsInPixels,
|
|
32
|
+
getColumnMinWidthAsPercentage,
|
|
33
|
+
getElementWidthInPixels,
|
|
34
|
+
getTableWidthInPixels,
|
|
35
|
+
getNumberOfColumn,
|
|
36
|
+
isTableRendered,
|
|
37
|
+
normalizeColumnWidthsAttribute,
|
|
38
|
+
toPrecision,
|
|
39
|
+
insertColumnResizerElements,
|
|
40
|
+
removeColumnResizerElements
|
|
41
|
+
} from './utils';
|
|
42
|
+
|
|
43
|
+
import { COLUMN_MIN_WIDTH_IN_PIXELS } from './constants';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The table column resize editing plugin.
|
|
47
|
+
*
|
|
48
|
+
* @extends module:core/plugin~Plugin
|
|
49
|
+
*/
|
|
50
|
+
export default class TableColumnResizeEditing extends Plugin {
|
|
51
|
+
/**
|
|
52
|
+
* @inheritDoc
|
|
53
|
+
*/
|
|
54
|
+
static get requires() {
|
|
55
|
+
return [ TableEditing ];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @inheritDoc
|
|
60
|
+
*/
|
|
61
|
+
static get pluginName() {
|
|
62
|
+
return 'TableColumnResizeEditing';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @inheritDoc
|
|
67
|
+
*/
|
|
68
|
+
constructor( editor ) {
|
|
69
|
+
super( editor );
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A flag indicating if the column resizing is in progress.
|
|
73
|
+
*
|
|
74
|
+
* @private
|
|
75
|
+
* @member {Boolean}
|
|
76
|
+
*/
|
|
77
|
+
this._isResizingActive = false;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A flag indicating if the column resizing is allowed. It is not allowed if the editor is in read-only mode or the
|
|
81
|
+
* `TableColumnResize` plugin is disabled.
|
|
82
|
+
*
|
|
83
|
+
* @private
|
|
84
|
+
* @member {Boolean}
|
|
85
|
+
*/
|
|
86
|
+
this._isResizingAllowed = true;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A temporary storage for the required data needed to correctly calculate the widths of the resized columns. This storage is
|
|
90
|
+
* initialized when column resizing begins, and is purged upon completion.
|
|
91
|
+
*
|
|
92
|
+
* @private
|
|
93
|
+
* @member {Object|null}
|
|
94
|
+
*/
|
|
95
|
+
this._resizingData = null;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Internal map to store reference between a cell and its columnIndex. This information is required in postfixer to properly
|
|
99
|
+
* recognize if the cell was inserted or deleted.
|
|
100
|
+
*
|
|
101
|
+
* @private
|
|
102
|
+
* @member {Map}
|
|
103
|
+
*/
|
|
104
|
+
this._columnIndexMap = new Map();
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Internal map to store reference between a cell and operation that was performed on it (insert/remove). This is required
|
|
108
|
+
* in order to add/remove resizers based on operation performed (which is done on 'render').
|
|
109
|
+
*
|
|
110
|
+
* @private
|
|
111
|
+
* @member {Map}
|
|
112
|
+
*/
|
|
113
|
+
this._cellsModified = new Map();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @inheritDoc
|
|
118
|
+
*/
|
|
119
|
+
init() {
|
|
120
|
+
this._extendSchema();
|
|
121
|
+
this._setupConversion();
|
|
122
|
+
this._setupPostFixer();
|
|
123
|
+
this._setupColumnResizers();
|
|
124
|
+
this._registerColgroupFixer();
|
|
125
|
+
this._registerResizerInserter();
|
|
126
|
+
|
|
127
|
+
const editor = this.editor;
|
|
128
|
+
const columnResizePlugin = editor.plugins.get( 'TableColumnResize' );
|
|
129
|
+
|
|
130
|
+
this.bind( '_isResizingAllowed' ).to(
|
|
131
|
+
editor, 'isReadOnly',
|
|
132
|
+
columnResizePlugin, 'isEnabled',
|
|
133
|
+
( isEditorReadOnly, isPluginEnabled ) => !isEditorReadOnly && isPluginEnabled
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Registers new attributes for a table and a table cell model elements.
|
|
139
|
+
*
|
|
140
|
+
* @private
|
|
141
|
+
*/
|
|
142
|
+
_extendSchema() {
|
|
143
|
+
const editor = this.editor;
|
|
144
|
+
const schema = editor.model.schema;
|
|
145
|
+
|
|
146
|
+
schema.extend( 'table', {
|
|
147
|
+
allowAttributes: [ 'tableWidth', 'columnWidths' ]
|
|
148
|
+
} );
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Registers table column resizer converters.
|
|
153
|
+
*
|
|
154
|
+
* @private
|
|
155
|
+
*/
|
|
156
|
+
_setupConversion() {
|
|
157
|
+
const editor = this.editor;
|
|
158
|
+
const conversion = editor.conversion;
|
|
159
|
+
|
|
160
|
+
conversion.for( 'upcast' ).attributeToAttribute( {
|
|
161
|
+
view: {
|
|
162
|
+
name: 'figure',
|
|
163
|
+
key: 'style',
|
|
164
|
+
value: {
|
|
165
|
+
width: /[\s\S]+/
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
model: {
|
|
169
|
+
name: 'table',
|
|
170
|
+
key: 'tableWidth',
|
|
171
|
+
value: viewElement => viewElement.getStyle( 'width' )
|
|
172
|
+
}
|
|
173
|
+
} );
|
|
174
|
+
|
|
175
|
+
conversion.for( 'downcast' ).attributeToAttribute( {
|
|
176
|
+
model: {
|
|
177
|
+
name: 'table',
|
|
178
|
+
key: 'tableWidth'
|
|
179
|
+
},
|
|
180
|
+
view: width => ( {
|
|
181
|
+
name: 'figure',
|
|
182
|
+
key: 'style',
|
|
183
|
+
value: {
|
|
184
|
+
width
|
|
185
|
+
}
|
|
186
|
+
} )
|
|
187
|
+
} );
|
|
188
|
+
|
|
189
|
+
conversion.for( 'upcast' ).add( upcastColgroupElement( editor ) );
|
|
190
|
+
conversion.for( 'downcast' ).add( downcastTableColumnWidthsAttribute() );
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Registers table column resizer post-fixer.
|
|
195
|
+
*
|
|
196
|
+
* It checks if the change from the differ concerns a table-related element or an attribute. If yes, then it is responsible for the
|
|
197
|
+
* following:
|
|
198
|
+
* (1) Depending on whether the `enableResize` event is not prevented...
|
|
199
|
+
* (1.1) ...removing the `columnWidths` attribute from the table and all the cells from column index map, or
|
|
200
|
+
* (1.2) ...adding the `columnWidths` attribute to the table.
|
|
201
|
+
* (2) Adjusting the `columnWidths` attribute to guarantee that the sum of the widths from all columns is 100%.
|
|
202
|
+
* (2.1) Add all cells to column index map with its column index (to properly handle column insertion and deletion).
|
|
203
|
+
* (3) Checking if columns have been added or removed...
|
|
204
|
+
* (3.1) ... in the middle of the table, or
|
|
205
|
+
* (3.2) ... at the table end.
|
|
206
|
+
* (4) Checking if the inline cell width has been configured and transferring its value to the appropriate column, but currently only
|
|
207
|
+
* for a cell that is not spanned horizontally.
|
|
208
|
+
*
|
|
209
|
+
* @private
|
|
210
|
+
*/
|
|
211
|
+
_setupPostFixer() {
|
|
212
|
+
const editor = this.editor;
|
|
213
|
+
const columnIndexMap = this._columnIndexMap;
|
|
214
|
+
const cellsModified = this._cellsModified;
|
|
215
|
+
|
|
216
|
+
editor.model.document.registerPostFixer( writer => {
|
|
217
|
+
const changes = editor.model.document.differ.getChanges();
|
|
218
|
+
|
|
219
|
+
let changed = false;
|
|
220
|
+
|
|
221
|
+
for ( const table of getAffectedTables( changes, editor.model ) ) {
|
|
222
|
+
// (1.1) Remove the `columnWidths` attribute from the table and all the cells from column index map if the
|
|
223
|
+
// manual width is not allowed for a given cell. There is no need to process the given table anymore.
|
|
224
|
+
if ( this.fire( 'disableResize', table ) ) {
|
|
225
|
+
if ( table.hasAttribute( 'columnWidths' ) ) {
|
|
226
|
+
writer.removeAttribute( 'columnWidths', table );
|
|
227
|
+
|
|
228
|
+
for ( const { cell } of new TableWalker( table ) ) {
|
|
229
|
+
columnIndexMap.delete( cell );
|
|
230
|
+
cellsModified.set( cell, 'remove' );
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
changed = true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// (1.2) Add the `columnWidths` attribute to the table with the 'auto' special value for each column, what means that it is
|
|
240
|
+
// calculated proportionally to the whole table width.
|
|
241
|
+
const numberOfColumns = getNumberOfColumn( table, editor );
|
|
242
|
+
|
|
243
|
+
if ( !table.hasAttribute( 'columnWidths' ) ) {
|
|
244
|
+
const columnWidthsAttribute = fillArray( numberOfColumns, 'auto' ).join( ',' );
|
|
245
|
+
|
|
246
|
+
writer.setAttribute( 'columnWidths', columnWidthsAttribute, table );
|
|
247
|
+
|
|
248
|
+
changed = true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// (2) Adjust the `columnWidths` attribute to guarantee that the sum of the widths from all columns is 100%.
|
|
252
|
+
const columnWidths = normalizeColumnWidthsAttribute( table.getAttribute( 'columnWidths' ) );
|
|
253
|
+
|
|
254
|
+
let removedColumnWidths = null;
|
|
255
|
+
let isColumnInsertionHandled = false;
|
|
256
|
+
let isColumnDeletionHandled = false;
|
|
257
|
+
|
|
258
|
+
for ( const { cell, cellWidth: cellColumnWidth, column } of new TableWalker( table ) ) {
|
|
259
|
+
// (2.1) Add all cells to column index map with its column index. Do not process the given cell anymore, because the
|
|
260
|
+
// `columnIndex` reference in the map is required to properly handle column insertion and deletion.
|
|
261
|
+
if ( !columnIndexMap.has( cell ) ) {
|
|
262
|
+
columnIndexMap.set( cell, column );
|
|
263
|
+
cellsModified.set( cell, 'insert' );
|
|
264
|
+
|
|
265
|
+
changed = true;
|
|
266
|
+
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const previousColumn = columnIndexMap.get( cell );
|
|
271
|
+
|
|
272
|
+
const isColumnInsertion = previousColumn < column;
|
|
273
|
+
const isColumnDeletion = previousColumn > column;
|
|
274
|
+
|
|
275
|
+
// (3.1) Handle column insertion and update the `columnIndex` references in column index map for affected cells.
|
|
276
|
+
if ( isColumnInsertion ) {
|
|
277
|
+
if ( !isColumnInsertionHandled ) {
|
|
278
|
+
const columnMinWidthAsPercentage = getColumnMinWidthAsPercentage( table, editor );
|
|
279
|
+
const isColumnSwapped = columnIndexMap.get( cell.previousSibling ) === column;
|
|
280
|
+
const columnWidthsToInsert = isColumnSwapped ?
|
|
281
|
+
removedColumnWidths :
|
|
282
|
+
fillArray( column - previousColumn, columnMinWidthAsPercentage );
|
|
283
|
+
|
|
284
|
+
columnWidths.splice( previousColumn, 0, ...columnWidthsToInsert );
|
|
285
|
+
|
|
286
|
+
isColumnInsertionHandled = true;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
columnIndexMap.set( cell, column );
|
|
290
|
+
cellsModified.set( cell, 'insert' );
|
|
291
|
+
|
|
292
|
+
changed = true;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// (3.1) Handle column deletion and update the `columnIndex` references in column index map for affected cells.
|
|
296
|
+
if ( isColumnDeletion ) {
|
|
297
|
+
if ( !isColumnDeletionHandled ) {
|
|
298
|
+
removedColumnWidths = columnWidths.splice( column, previousColumn - column );
|
|
299
|
+
|
|
300
|
+
const isColumnSwapped = cell.nextSibling && columnIndexMap.get( cell.nextSibling ) === column;
|
|
301
|
+
|
|
302
|
+
if ( !isColumnSwapped ) {
|
|
303
|
+
const columnToExpand = column > 0 ? column - 1 : column;
|
|
304
|
+
|
|
305
|
+
columnWidths[ columnToExpand ] += sumArray( removedColumnWidths );
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
isColumnDeletionHandled = true;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
columnIndexMap.set( cell, column );
|
|
312
|
+
cellsModified.set( cell, 'insert' );
|
|
313
|
+
|
|
314
|
+
changed = true;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// (4) Check if the inline cell width has been configured and transfer its value to the appropriate column.
|
|
318
|
+
if ( cell.hasAttribute( 'width' ) ) {
|
|
319
|
+
// Currently, only the inline width from the cells that are not horizontally spanned are supported.
|
|
320
|
+
if ( cellColumnWidth !== 1 ) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// It may happen that the table is not yet fully rendered in the editing view (i.e. it does not contain the
|
|
325
|
+
// `<colgroup>` yet), but the cell has an inline width set. In that case it is not possible to properly convert the
|
|
326
|
+
// inline cell width as a percentage value to the whole table width. Currently, we just ignore this case and
|
|
327
|
+
// initialize the table with all the default (equal) column widths.
|
|
328
|
+
if ( !isTableRendered( table, editor ) ) {
|
|
329
|
+
writer.removeAttribute( 'width', cell );
|
|
330
|
+
|
|
331
|
+
changed = true;
|
|
332
|
+
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const tableWidthInPixels = getTableWidthInPixels( table, editor );
|
|
337
|
+
const columnWidthsInPixels = getColumnWidthsInPixels( table, editor );
|
|
338
|
+
const columnMinWidthAsPercentage = getColumnMinWidthAsPercentage( table, editor );
|
|
339
|
+
|
|
340
|
+
const cellWidth = parseFloat( cell.getAttribute( 'width' ) );
|
|
341
|
+
|
|
342
|
+
const isWidthInPixels = cell.getAttribute( 'width' ).endsWith( 'px' );
|
|
343
|
+
const isWidthAsPercentage = cell.getAttribute( 'width' ).endsWith( '%' );
|
|
344
|
+
|
|
345
|
+
// Currently, only inline width in pixels or as percentage is supported.
|
|
346
|
+
if ( !isWidthInPixels && !isWidthAsPercentage ) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const isRightEdge = !cell.nextSibling;
|
|
351
|
+
|
|
352
|
+
if ( isRightEdge ) {
|
|
353
|
+
const rootWidthInPixels = getElementWidthInPixels( editor.editing.view.getDomRoot() );
|
|
354
|
+
const lastColumnIndex = numberOfColumns - 1;
|
|
355
|
+
const lastColumnWidthInPixels = columnWidthsInPixels[ lastColumnIndex ];
|
|
356
|
+
|
|
357
|
+
let tableWidthNew;
|
|
358
|
+
|
|
359
|
+
if ( isWidthInPixels ) {
|
|
360
|
+
const cellWidthLowerBound = COLUMN_MIN_WIDTH_IN_PIXELS;
|
|
361
|
+
const cellWidthUpperBound = rootWidthInPixels - ( tableWidthInPixels - lastColumnWidthInPixels );
|
|
362
|
+
|
|
363
|
+
columnWidthsInPixels[ lastColumnIndex ] = clamp( cellWidth, cellWidthLowerBound, cellWidthUpperBound );
|
|
364
|
+
|
|
365
|
+
tableWidthNew = sumArray( columnWidthsInPixels );
|
|
366
|
+
|
|
367
|
+
// Update all the column widths.
|
|
368
|
+
for ( let columnIndex = 0; columnIndex <= lastColumnIndex; columnIndex++ ) {
|
|
369
|
+
columnWidths[ columnIndex ] = toPrecision( columnWidthsInPixels[ columnIndex ] * 100 / tableWidthNew );
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
const cellWidthLowerBound = columnMinWidthAsPercentage;
|
|
373
|
+
const cellWidthUpperBound = 100 - ( tableWidthInPixels - lastColumnWidthInPixels ) * 100 /
|
|
374
|
+
rootWidthInPixels;
|
|
375
|
+
|
|
376
|
+
columnWidths[ lastColumnIndex ] = clamp( cellWidth, cellWidthLowerBound, cellWidthUpperBound );
|
|
377
|
+
|
|
378
|
+
tableWidthNew = ( tableWidthInPixels - lastColumnWidthInPixels ) * 100 /
|
|
379
|
+
( 100 - columnWidths[ lastColumnIndex ] );
|
|
380
|
+
|
|
381
|
+
// Update all the column widths, except the last one, which has been already adjusted.
|
|
382
|
+
for ( let columnIndex = 0; columnIndex <= lastColumnIndex - 1; columnIndex++ ) {
|
|
383
|
+
columnWidths[ columnIndex ] = toPrecision( columnWidthsInPixels[ columnIndex ] * 100 / tableWidthNew );
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
writer.setAttribute( 'width', `${ toPrecision( tableWidthNew * 100 / rootWidthInPixels ) }%`, table );
|
|
388
|
+
} else {
|
|
389
|
+
const currentColumnWidth = columnWidthsInPixels[ column ];
|
|
390
|
+
const nextColumnWidth = columnWidthsInPixels[ column + 1 ];
|
|
391
|
+
const bothColumnWidth = currentColumnWidth + nextColumnWidth;
|
|
392
|
+
|
|
393
|
+
const cellMaxWidthAsPercentage = ( bothColumnWidth - COLUMN_MIN_WIDTH_IN_PIXELS ) * 100 / tableWidthInPixels;
|
|
394
|
+
|
|
395
|
+
let cellWidthAsPercentage = isWidthInPixels ?
|
|
396
|
+
cellWidth * 100 / tableWidthInPixels :
|
|
397
|
+
cellWidth;
|
|
398
|
+
|
|
399
|
+
cellWidthAsPercentage = clamp( cellWidthAsPercentage, columnMinWidthAsPercentage, cellMaxWidthAsPercentage );
|
|
400
|
+
|
|
401
|
+
const dxAsPercentage = cellWidthAsPercentage - columnWidths[ column ];
|
|
402
|
+
|
|
403
|
+
columnWidths[ column ] += dxAsPercentage;
|
|
404
|
+
columnWidths[ column + 1 ] -= dxAsPercentage;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
writer.removeAttribute( 'width', cell );
|
|
408
|
+
|
|
409
|
+
changed = true;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const isColumnInsertionAtEnd = numberOfColumns > columnWidths.length;
|
|
414
|
+
const isColumnDeletionAtEnd = numberOfColumns < columnWidths.length;
|
|
415
|
+
|
|
416
|
+
// (3.2) Handle column insertion at table end.
|
|
417
|
+
if ( isColumnInsertionAtEnd ) {
|
|
418
|
+
const columnMinWidthAsPercentage = getColumnMinWidthAsPercentage( table, editor );
|
|
419
|
+
const numberOfInsertedColumns = numberOfColumns - columnWidths.length;
|
|
420
|
+
const insertedColumnWidths = fillArray( numberOfInsertedColumns, columnMinWidthAsPercentage );
|
|
421
|
+
|
|
422
|
+
columnWidths.splice( columnWidths.length, 0, ...insertedColumnWidths );
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// (3.2) Handle column deletion at table end.
|
|
426
|
+
if ( isColumnDeletionAtEnd ) {
|
|
427
|
+
const removedColumnWidths = columnWidths.splice( numberOfColumns );
|
|
428
|
+
|
|
429
|
+
columnWidths[ numberOfColumns - 1 ] += sumArray( removedColumnWidths );
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const columnWidthsAttribute = columnWidths.map( width => `${ width }%` ).join( ',' );
|
|
433
|
+
|
|
434
|
+
if ( table.getAttribute( 'columnWidths' ) === columnWidthsAttribute ) {
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
writer.setAttribute( 'columnWidths', columnWidthsAttribute, table );
|
|
439
|
+
|
|
440
|
+
changed = true;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return changed;
|
|
444
|
+
} );
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Initializes column resizing feature by registering mouse event handlers for `mousedown`, `mouseup` and `mousemove` events.
|
|
449
|
+
*
|
|
450
|
+
* @private
|
|
451
|
+
*/
|
|
452
|
+
_setupColumnResizers() {
|
|
453
|
+
const editor = this.editor;
|
|
454
|
+
const editingView = editor.editing.view;
|
|
455
|
+
|
|
456
|
+
editingView.addObserver( MouseEventsObserver );
|
|
457
|
+
editingView.document.on( 'mousedown', this._onMouseDownHandler.bind( this ), { priority: 'high' } );
|
|
458
|
+
|
|
459
|
+
const domEmitter = Object.create( DomEmitterMixin );
|
|
460
|
+
|
|
461
|
+
domEmitter.listenTo( global.window.document, 'mouseup', this._onMouseUpHandler.bind( this ) );
|
|
462
|
+
domEmitter.listenTo( global.window.document, 'mousemove', throttle( this._onMouseMoveHandler.bind( this ), 50 ) );
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Handles the `mousedown` event on column resizer element.
|
|
467
|
+
*
|
|
468
|
+
* @private
|
|
469
|
+
* @param {module:utils/eventinfo~EventInfo} eventInfo
|
|
470
|
+
* @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
|
|
471
|
+
*/
|
|
472
|
+
_onMouseDownHandler( eventInfo, domEventData ) {
|
|
473
|
+
const editor = this.editor;
|
|
474
|
+
const editingView = editor.editing.view;
|
|
475
|
+
|
|
476
|
+
if ( !domEventData.target.hasClass( 'table-column-resizer' ) ) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if ( !this._isResizingAllowed ) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
domEventData.preventDefault();
|
|
485
|
+
eventInfo.stop();
|
|
486
|
+
|
|
487
|
+
this._isResizingActive = true;
|
|
488
|
+
this._resizingData = this._getResizingData( domEventData );
|
|
489
|
+
|
|
490
|
+
editingView.change( writer => {
|
|
491
|
+
writer.addClass( 'table-column-resizer__active', this._resizingData.elements.viewResizer );
|
|
492
|
+
} );
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Handles the `mouseup` event if previously the `mousedown` event was triggered from the column resizer element.
|
|
497
|
+
*
|
|
498
|
+
* @private
|
|
499
|
+
* @param {module:utils/eventinfo~EventInfo} eventInfo
|
|
500
|
+
* @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
|
|
501
|
+
*/
|
|
502
|
+
_onMouseUpHandler() {
|
|
503
|
+
const editor = this.editor;
|
|
504
|
+
const editingView = editor.editing.view;
|
|
505
|
+
|
|
506
|
+
if ( !this._isResizingActive ) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const {
|
|
511
|
+
modelTable,
|
|
512
|
+
viewColgroup,
|
|
513
|
+
viewFigure,
|
|
514
|
+
viewResizer
|
|
515
|
+
} = this._resizingData.elements;
|
|
516
|
+
|
|
517
|
+
const columnWidthsAttributeOld = modelTable.getAttribute( 'columnWidths' );
|
|
518
|
+
const columnWidthsAttributeNew = [ ...viewColgroup.getChildren() ]
|
|
519
|
+
.map( viewCol => viewCol.getStyle( 'width' ) )
|
|
520
|
+
.join( ',' );
|
|
521
|
+
|
|
522
|
+
const isColumnWidthsAttributeChanged = columnWidthsAttributeOld !== columnWidthsAttributeNew;
|
|
523
|
+
|
|
524
|
+
const tableWidthAttributeOld = modelTable.getAttribute( 'tableWidth' );
|
|
525
|
+
const tableWidthAttributeNew = viewFigure.getStyle( 'width' );
|
|
526
|
+
|
|
527
|
+
const isTableWidthAttributeChanged = tableWidthAttributeOld !== tableWidthAttributeNew;
|
|
528
|
+
|
|
529
|
+
if ( isColumnWidthsAttributeChanged || isTableWidthAttributeChanged ) {
|
|
530
|
+
if ( this._isResizingAllowed ) {
|
|
531
|
+
// Commit all changes to the model.
|
|
532
|
+
editor.model.change( writer => {
|
|
533
|
+
if ( isColumnWidthsAttributeChanged ) {
|
|
534
|
+
writer.setAttribute( 'columnWidths', columnWidthsAttributeNew, modelTable );
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if ( isTableWidthAttributeChanged ) {
|
|
538
|
+
writer.setAttribute( 'tableWidth', `${ toPrecision( tableWidthAttributeNew ) }%`, modelTable );
|
|
539
|
+
}
|
|
540
|
+
} );
|
|
541
|
+
} else {
|
|
542
|
+
// In read-only mode revert all changes in the editing view. The model is not touched so it does not need to be restored.
|
|
543
|
+
editingView.change( writer => {
|
|
544
|
+
if ( isColumnWidthsAttributeChanged ) {
|
|
545
|
+
const columnWidths = columnWidthsAttributeOld.split( ',' );
|
|
546
|
+
|
|
547
|
+
for ( const viewCol of viewColgroup.getChildren() ) {
|
|
548
|
+
writer.setStyle( 'width', columnWidths.shift(), viewCol );
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if ( isTableWidthAttributeChanged ) {
|
|
553
|
+
if ( tableWidthAttributeOld ) {
|
|
554
|
+
writer.setStyle( 'width', tableWidthAttributeOld, viewFigure );
|
|
555
|
+
} else {
|
|
556
|
+
writer.removeStyle( 'width', viewFigure );
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
} );
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
editingView.change( writer => {
|
|
564
|
+
writer.removeClass( 'table-column-resizer__active', viewResizer );
|
|
565
|
+
} );
|
|
566
|
+
|
|
567
|
+
this._isResizingActive = false;
|
|
568
|
+
this._resizingData = null;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Handles the `mousemove` event if previously the `mousedown` event was triggered from the column resizer element.
|
|
573
|
+
*
|
|
574
|
+
* @private
|
|
575
|
+
* @param {module:utils/eventinfo~EventInfo} eventInfo
|
|
576
|
+
* @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
|
|
577
|
+
*/
|
|
578
|
+
_onMouseMoveHandler( eventInfo, domEventData ) {
|
|
579
|
+
const editor = this.editor;
|
|
580
|
+
const editingView = editor.editing.view;
|
|
581
|
+
|
|
582
|
+
if ( !this._isResizingActive ) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if ( !this._isResizingAllowed ) {
|
|
587
|
+
this._onMouseUpHandler();
|
|
588
|
+
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const {
|
|
593
|
+
columnPosition,
|
|
594
|
+
flags: {
|
|
595
|
+
isRightEdge,
|
|
596
|
+
isLtrContent,
|
|
597
|
+
isTableCentered
|
|
598
|
+
},
|
|
599
|
+
widths: {
|
|
600
|
+
viewFigureParentWidth,
|
|
601
|
+
tableWidth,
|
|
602
|
+
leftColumnWidth,
|
|
603
|
+
rightColumnWidth
|
|
604
|
+
},
|
|
605
|
+
elements: {
|
|
606
|
+
viewFigure,
|
|
607
|
+
viewLeftColumn,
|
|
608
|
+
viewRightColumn
|
|
609
|
+
}
|
|
610
|
+
} = this._resizingData;
|
|
611
|
+
|
|
612
|
+
const dxLowerBound = -leftColumnWidth + COLUMN_MIN_WIDTH_IN_PIXELS;
|
|
613
|
+
|
|
614
|
+
const dxUpperBound = isRightEdge ?
|
|
615
|
+
viewFigureParentWidth - tableWidth :
|
|
616
|
+
rightColumnWidth - COLUMN_MIN_WIDTH_IN_PIXELS;
|
|
617
|
+
|
|
618
|
+
// The multiplier is needed for calculating the proper movement offset:
|
|
619
|
+
// - it should negate the sign if content language direction is right-to-left,
|
|
620
|
+
// - it should double the offset if the table edge is resized and table is centered.
|
|
621
|
+
const multiplier = ( isLtrContent ? 1 : -1 ) * ( isRightEdge && isTableCentered ? 2 : 1 );
|
|
622
|
+
|
|
623
|
+
const dx = clamp(
|
|
624
|
+
( domEventData.clientX - columnPosition ) * multiplier,
|
|
625
|
+
Math.min( dxLowerBound, 0 ),
|
|
626
|
+
Math.max( dxUpperBound, 0 )
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
if ( dx === 0 ) {
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
editingView.change( writer => {
|
|
634
|
+
const leftColumnWidthAsPercentage = toPrecision( ( leftColumnWidth + dx ) * 100 / tableWidth );
|
|
635
|
+
|
|
636
|
+
writer.setStyle( 'width', `${ leftColumnWidthAsPercentage }%`, viewLeftColumn );
|
|
637
|
+
|
|
638
|
+
if ( isRightEdge ) {
|
|
639
|
+
const tableWidthAsPercentage = toPrecision( ( tableWidth + dx ) * 100 / viewFigureParentWidth );
|
|
640
|
+
|
|
641
|
+
writer.setStyle( 'width', `${ tableWidthAsPercentage }%`, viewFigure );
|
|
642
|
+
} else {
|
|
643
|
+
const rightColumnWidthAsPercentage = toPrecision( ( rightColumnWidth - dx ) * 100 / tableWidth );
|
|
644
|
+
|
|
645
|
+
writer.setStyle( 'width', `${ rightColumnWidthAsPercentage }%`, viewRightColumn );
|
|
646
|
+
}
|
|
647
|
+
} );
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Retrieves and returns required data needed to correctly calculate the widths of the resized columns.
|
|
652
|
+
*
|
|
653
|
+
* @private
|
|
654
|
+
* @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
|
|
655
|
+
* @returns {Object}
|
|
656
|
+
*/
|
|
657
|
+
_getResizingData( domEventData ) {
|
|
658
|
+
const editor = this.editor;
|
|
659
|
+
|
|
660
|
+
const columnPosition = domEventData.domEvent.clientX;
|
|
661
|
+
|
|
662
|
+
const viewResizer = domEventData.target;
|
|
663
|
+
const viewLeftCell = viewResizer.findAncestor( 'td' ) || viewResizer.findAncestor( 'th' );
|
|
664
|
+
const modelLeftCell = editor.editing.mapper.toModelElement( viewLeftCell );
|
|
665
|
+
const modelTable = modelLeftCell.findAncestor( 'table' );
|
|
666
|
+
|
|
667
|
+
const leftColumnIndex = getColumnIndex( modelLeftCell, this._columnIndexMap ).rightEdge;
|
|
668
|
+
const lastColumnIndex = getNumberOfColumn( modelTable, editor ) - 1;
|
|
669
|
+
|
|
670
|
+
const isRightEdge = leftColumnIndex === lastColumnIndex;
|
|
671
|
+
const isTableCentered = !modelTable.hasAttribute( 'tableAlignment' );
|
|
672
|
+
const isLtrContent = editor.locale.contentLanguageDirection !== 'rtl';
|
|
673
|
+
|
|
674
|
+
const viewTable = viewLeftCell.findAncestor( 'table' );
|
|
675
|
+
const viewFigure = viewTable.findAncestor( 'figure' );
|
|
676
|
+
const viewColgroup = [ ...viewTable.getChildren() ].find( viewCol => viewCol.is( 'element', 'colgroup' ) );
|
|
677
|
+
const viewLeftColumn = viewColgroup.getChild( leftColumnIndex );
|
|
678
|
+
const viewRightColumn = isRightEdge ? undefined : viewColgroup.getChild( leftColumnIndex + 1 );
|
|
679
|
+
|
|
680
|
+
const viewFigureParentWidth = getElementWidthInPixels( editor.editing.view.domConverter.mapViewToDom( viewFigure.parent ) );
|
|
681
|
+
const tableWidth = getTableWidthInPixels( modelTable, editor );
|
|
682
|
+
const columnWidths = getColumnWidthsInPixels( modelTable, editor );
|
|
683
|
+
const leftColumnWidth = columnWidths[ leftColumnIndex ];
|
|
684
|
+
const rightColumnWidth = isRightEdge ? undefined : columnWidths[ leftColumnIndex + 1 ];
|
|
685
|
+
|
|
686
|
+
return {
|
|
687
|
+
columnPosition,
|
|
688
|
+
elements: {
|
|
689
|
+
modelTable,
|
|
690
|
+
viewFigure,
|
|
691
|
+
viewColgroup,
|
|
692
|
+
viewLeftColumn,
|
|
693
|
+
viewRightColumn,
|
|
694
|
+
viewResizer
|
|
695
|
+
},
|
|
696
|
+
widths: {
|
|
697
|
+
viewFigureParentWidth,
|
|
698
|
+
tableWidth,
|
|
699
|
+
leftColumnWidth,
|
|
700
|
+
rightColumnWidth
|
|
701
|
+
},
|
|
702
|
+
flags: {
|
|
703
|
+
isRightEdge,
|
|
704
|
+
isTableCentered,
|
|
705
|
+
isLtrContent
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Inserts colgroup if it is missing from table (e.g. after table insertion into table).
|
|
712
|
+
*
|
|
713
|
+
* @private
|
|
714
|
+
*/
|
|
715
|
+
_registerColgroupFixer() {
|
|
716
|
+
const editor = this.editor;
|
|
717
|
+
|
|
718
|
+
this.listenTo( editor.editing.view.document, 'layoutChanged', () => {
|
|
719
|
+
const table = editor.model.document.selection.getFirstPosition().findAncestor( 'table' );
|
|
720
|
+
const tableView = editor.editing.view.document.selection.getFirstPosition().getAncestors().reverse().find(
|
|
721
|
+
element => element.name === 'table'
|
|
722
|
+
);
|
|
723
|
+
const tableViewContainsColgroup = tableView && [ ...tableView.getChildren() ].find(
|
|
724
|
+
viewElement => viewElement.is( 'element', 'colgroup' )
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
if ( table && table.hasAttribute( 'columnWidths' ) && tableView && !tableViewContainsColgroup ) {
|
|
728
|
+
editor.editing.reconvertItem( table );
|
|
729
|
+
}
|
|
730
|
+
}, { priority: 'low' } );
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Registers a handler on 'render' to properly insert/remove resizers after all postfixers finished their job.
|
|
735
|
+
*
|
|
736
|
+
* @private
|
|
737
|
+
*/
|
|
738
|
+
_registerResizerInserter() {
|
|
739
|
+
const editor = this.editor;
|
|
740
|
+
const view = editor.editing.view;
|
|
741
|
+
const cellsModified = this._cellsModified;
|
|
742
|
+
|
|
743
|
+
view.on( 'render', () => {
|
|
744
|
+
for ( const [ cell, operation ] of cellsModified.entries() ) {
|
|
745
|
+
const viewCell = editor.editing.mapper.toViewElement( cell );
|
|
746
|
+
|
|
747
|
+
view.change( viewWriter => {
|
|
748
|
+
if ( operation === 'insert' ) {
|
|
749
|
+
insertColumnResizerElements( viewWriter, viewCell );
|
|
750
|
+
} else if ( operation === 'remove' ) {
|
|
751
|
+
removeColumnResizerElements( viewWriter, viewCell );
|
|
752
|
+
}
|
|
753
|
+
} );
|
|
754
|
+
}
|
|
755
|
+
cellsModified.clear();
|
|
756
|
+
}, { priority: 'lowest' } );
|
|
757
|
+
}
|
|
758
|
+
}
|