@ckeditor/ckeditor5-table 34.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.
@@ -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
+ }