1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 | import { throttle, isEqual } from 'lodash-es';
|
9 | import { global, DomEmitterMixin } from 'ckeditor5/src/utils';
|
10 | import { Plugin } from 'ckeditor5/src/core';
|
11 | import MouseEventsObserver from '../../src/tablemouse/mouseeventsobserver';
|
12 | import TableEditing from '../tableediting';
|
13 | import TableUtils from '../tableutils';
|
14 | import TableWalker from '../tablewalker';
|
15 | import TableWidthsCommand from './tablewidthscommand';
|
16 | import { downcastTableResizedClass, upcastColgroupElement } from './converters';
|
17 | import { clamp, createFilledArray, sumArray, getColumnEdgesIndexes, getChangedResizedTables, getColumnMinWidthAsPercentage, getElementWidthInPixels, getTableWidthInPixels, normalizeColumnWidths, toPrecision, getDomCellOuterWidth, updateColumnElements, getColumnGroupElement, getTableColumnElements, getTableColumnsWidths } from './utils';
|
18 | import { COLUMN_MIN_WIDTH_IN_PIXELS } from './constants';
|
19 |
|
20 |
|
21 |
|
22 | export default class TableColumnResizeEditing extends Plugin {
|
23 | |
24 |
|
25 |
|
26 | static get requires() {
|
27 | return [TableEditing, TableUtils];
|
28 | }
|
29 | |
30 |
|
31 |
|
32 | static get pluginName() {
|
33 | return 'TableColumnResizeEditing';
|
34 | }
|
35 | |
36 |
|
37 |
|
38 | constructor(editor) {
|
39 | super(editor);
|
40 | this._isResizingActive = false;
|
41 | this.set('_isResizingAllowed', true);
|
42 | this._resizingData = null;
|
43 | this._domEmitter = new (DomEmitterMixin())();
|
44 | this._tableUtilsPlugin = editor.plugins.get('TableUtils');
|
45 | this.on('change:_isResizingAllowed', (evt, name, value) => {
|
46 |
|
47 | const classAction = value ? 'removeClass' : 'addClass';
|
48 | editor.editing.view.change(writer => {
|
49 | for (const root of editor.editing.view.document.roots) {
|
50 | writer[classAction]('ck-column-resize_disabled', editor.editing.view.document.getRoot(root.rootName));
|
51 | }
|
52 | });
|
53 | });
|
54 | }
|
55 | |
56 |
|
57 |
|
58 | init() {
|
59 | this._extendSchema();
|
60 | this._registerPostFixer();
|
61 | this._registerConverters();
|
62 | this._registerResizingListeners();
|
63 | this._registerResizerInserter();
|
64 | const editor = this.editor;
|
65 | const columnResizePlugin = editor.plugins.get('TableColumnResize');
|
66 | const tableEditing = editor.plugins.get('TableEditing');
|
67 | tableEditing.registerAdditionalSlot({
|
68 | filter: element => element.is('element', 'tableColumnGroup'),
|
69 | positionOffset: 0
|
70 | });
|
71 | const tableWidthsCommand = new TableWidthsCommand(editor);
|
72 |
|
73 | editor.commands.add('resizeTableWidth', tableWidthsCommand);
|
74 | editor.commands.add('resizeColumnWidths', tableWidthsCommand);
|
75 |
|
76 |
|
77 |
|
78 |
|
79 | this.bind('_isResizingAllowed').to(editor, 'isReadOnly', columnResizePlugin, 'isEnabled', tableWidthsCommand, 'isEnabled', (isEditorReadOnly, isPluginEnabled, isTableWidthsCommandCommandEnabled) => !isEditorReadOnly && isPluginEnabled && isTableWidthsCommandCommandEnabled);
|
80 | }
|
81 | |
82 |
|
83 |
|
84 | destroy() {
|
85 | this._domEmitter.stopListening();
|
86 | super.destroy();
|
87 | }
|
88 | |
89 |
|
90 |
|
91 |
|
92 |
|
93 |
|
94 | getColumnGroupElement(element) {
|
95 | return getColumnGroupElement(element);
|
96 | }
|
97 | |
98 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 | getTableColumnElements(element) {
|
104 | return getTableColumnElements(element);
|
105 | }
|
106 | |
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 | getTableColumnsWidths(element) {
|
113 | return getTableColumnsWidths(element);
|
114 | }
|
115 | |
116 |
|
117 |
|
118 | _extendSchema() {
|
119 | this.editor.model.schema.extend('table', {
|
120 | allowAttributes: ['tableWidth']
|
121 | });
|
122 | this.editor.model.schema.register('tableColumnGroup', {
|
123 | allowIn: 'table',
|
124 | isLimit: true
|
125 | });
|
126 | this.editor.model.schema.register('tableColumn', {
|
127 | allowIn: 'tableColumnGroup',
|
128 | allowAttributes: ['columnWidth'],
|
129 | isLimit: true
|
130 | });
|
131 | }
|
132 | |
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 | _registerPostFixer() {
|
140 | const editor = this.editor;
|
141 | const model = editor.model;
|
142 | model.document.registerPostFixer(writer => {
|
143 | let changed = false;
|
144 | for (const table of getChangedResizedTables(model)) {
|
145 | const tableColumnGroup = this.getColumnGroupElement(table);
|
146 | const columns = this.getTableColumnElements(tableColumnGroup);
|
147 | const columnWidths = this.getTableColumnsWidths(tableColumnGroup);
|
148 |
|
149 | let normalizedWidths = normalizeColumnWidths(columnWidths);
|
150 |
|
151 | normalizedWidths = adjustColumnWidths(normalizedWidths, table, this);
|
152 | if (isEqual(columnWidths, normalizedWidths)) {
|
153 | continue;
|
154 | }
|
155 | updateColumnElements(columns, tableColumnGroup, normalizedWidths, writer);
|
156 | changed = true;
|
157 | }
|
158 | return changed;
|
159 | });
|
160 | |
161 |
|
162 |
|
163 |
|
164 |
|
165 |
|
166 | function adjustColumnWidths(columnWidths, table, plugin) {
|
167 | const newTableColumnsCount = plugin._tableUtilsPlugin.getColumns(table);
|
168 | const columnsCountDelta = newTableColumnsCount - columnWidths.length;
|
169 | if (columnsCountDelta === 0) {
|
170 | return columnWidths;
|
171 | }
|
172 | const widths = columnWidths.map(width => Number(width.replace('%', '')));
|
173 |
|
174 | const cellSet = getAffectedCells(plugin.editor.model.document.differ, table);
|
175 | for (const cell of cellSet) {
|
176 | const currentColumnsDelta = newTableColumnsCount - widths.length;
|
177 | if (currentColumnsDelta === 0) {
|
178 | continue;
|
179 | }
|
180 |
|
181 | const hasMoreColumns = currentColumnsDelta > 0;
|
182 | const currentColumnIndex = plugin._tableUtilsPlugin.getCellLocation(cell).column;
|
183 | if (hasMoreColumns) {
|
184 | const columnMinWidthAsPercentage = getColumnMinWidthAsPercentage(table, plugin.editor);
|
185 | const columnWidthsToInsert = createFilledArray(currentColumnsDelta, columnMinWidthAsPercentage);
|
186 | widths.splice(currentColumnIndex, 0, ...columnWidthsToInsert);
|
187 | }
|
188 | else {
|
189 |
|
190 |
|
191 |
|
192 | const removedColumnWidths = widths.splice(currentColumnIndex, Math.abs(currentColumnsDelta));
|
193 | widths[currentColumnIndex] += sumArray(removedColumnWidths);
|
194 | }
|
195 | }
|
196 | return widths.map(width => width + '%');
|
197 | }
|
198 | |
199 |
|
200 |
|
201 | function getAffectedCells(differ, table) {
|
202 | const cellSet = new Set();
|
203 | for (const change of differ.getChanges()) {
|
204 | if (change.type == 'insert' &&
|
205 | change.position.nodeAfter &&
|
206 | change.position.nodeAfter.name == 'tableCell' &&
|
207 | change.position.nodeAfter.getAncestors().includes(table)) {
|
208 | cellSet.add(change.position.nodeAfter);
|
209 | }
|
210 | else if (change.type == 'remove') {
|
211 |
|
212 | const referenceNode = (change.position.nodeBefore || change.position.nodeAfter);
|
213 | if (referenceNode.name == 'tableCell' && referenceNode.getAncestors().includes(table)) {
|
214 | cellSet.add(referenceNode);
|
215 | }
|
216 | }
|
217 | }
|
218 | return cellSet;
|
219 | }
|
220 | }
|
221 | |
222 |
|
223 |
|
224 | _registerConverters() {
|
225 | const editor = this.editor;
|
226 | const conversion = editor.conversion;
|
227 |
|
228 | conversion.for('upcast').attributeToAttribute({
|
229 | view: {
|
230 | name: 'figure',
|
231 | key: 'style',
|
232 | value: {
|
233 | width: /[\s\S]+/
|
234 | }
|
235 | },
|
236 | model: {
|
237 | name: 'table',
|
238 | key: 'tableWidth',
|
239 | value: (viewElement) => viewElement.getStyle('width')
|
240 | }
|
241 | });
|
242 | conversion.for('downcast').attributeToAttribute({
|
243 | model: {
|
244 | name: 'table',
|
245 | key: 'tableWidth'
|
246 | },
|
247 | view: (width) => ({
|
248 | name: 'figure',
|
249 | key: 'style',
|
250 | value: {
|
251 | width
|
252 | }
|
253 | })
|
254 | });
|
255 | conversion.elementToElement({ model: 'tableColumnGroup', view: 'colgroup' });
|
256 | conversion.elementToElement({ model: 'tableColumn', view: 'col' });
|
257 | conversion.for('downcast').add(downcastTableResizedClass());
|
258 | conversion.for('upcast').add(upcastColgroupElement(this._tableUtilsPlugin));
|
259 | conversion.for('upcast').attributeToAttribute({
|
260 | view: {
|
261 | name: 'col',
|
262 | styles: {
|
263 | width: /.*/
|
264 | }
|
265 | },
|
266 | model: {
|
267 | key: 'columnWidth',
|
268 | value: (viewElement) => {
|
269 | const viewColWidth = viewElement.getStyle('width');
|
270 | if (!viewColWidth || !viewColWidth.endsWith('%')) {
|
271 | return 'auto';
|
272 | }
|
273 | return viewColWidth;
|
274 | }
|
275 | }
|
276 | });
|
277 | conversion.for('downcast').attributeToAttribute({
|
278 | model: {
|
279 | name: 'tableColumn',
|
280 | key: 'columnWidth'
|
281 | },
|
282 | view: width => ({ key: 'style', value: { width } })
|
283 | });
|
284 | }
|
285 | |
286 |
|
287 |
|
288 | _registerResizingListeners() {
|
289 | const editingView = this.editor.editing.view;
|
290 | editingView.addObserver(MouseEventsObserver);
|
291 | editingView.document.on('mousedown', this._onMouseDownHandler.bind(this), { priority: 'high' });
|
292 | this._domEmitter.listenTo(global.window.document, 'mousemove', throttle(this._onMouseMoveHandler.bind(this), 50));
|
293 | this._domEmitter.listenTo(global.window.document, 'mouseup', this._onMouseUpHandler.bind(this));
|
294 | }
|
295 | |
296 |
|
297 |
|
298 |
|
299 |
|
300 |
|
301 |
|
302 |
|
303 |
|
304 |
|
305 | _onMouseDownHandler(eventInfo, domEventData) {
|
306 | const target = domEventData.target;
|
307 | if (!target.hasClass('ck-table-column-resizer')) {
|
308 | return;
|
309 | }
|
310 | if (!this._isResizingAllowed) {
|
311 | return;
|
312 | }
|
313 | const editor = this.editor;
|
314 | const modelTable = editor.editing.mapper.toModelElement(target.findAncestor('figure'));
|
315 |
|
316 | if (!editor.model.canEditAt(modelTable)) {
|
317 | return;
|
318 | }
|
319 | domEventData.preventDefault();
|
320 | eventInfo.stop();
|
321 |
|
322 | const columnWidthsInPx = _calculateDomColumnWidths(modelTable, this._tableUtilsPlugin, editor);
|
323 | const viewTable = target.findAncestor('table');
|
324 | const editingView = editor.editing.view;
|
325 |
|
326 | if (!Array.from(viewTable.getChildren()).find(viewCol => viewCol.is('element', 'colgroup'))) {
|
327 | editingView.change(viewWriter => {
|
328 | _insertColgroupElement(viewWriter, columnWidthsInPx, viewTable);
|
329 | });
|
330 | }
|
331 | this._isResizingActive = true;
|
332 | this._resizingData = this._getResizingData(domEventData, columnWidthsInPx);
|
333 |
|
334 |
|
335 | editingView.change(writer => _applyResizingAttributesToTable(writer, viewTable, this._resizingData));
|
336 | |
337 |
|
338 |
|
339 |
|
340 |
|
341 |
|
342 |
|
343 |
|
344 |
|
345 |
|
346 | function _calculateDomColumnWidths(modelTable, tableUtilsPlugin, editor) {
|
347 | const columnWidthsInPx = Array(tableUtilsPlugin.getColumns(modelTable));
|
348 | const tableWalker = new TableWalker(modelTable);
|
349 | for (const cellSlot of tableWalker) {
|
350 | const viewCell = editor.editing.mapper.toViewElement(cellSlot.cell);
|
351 | const domCell = editor.editing.view.domConverter.mapViewToDom(viewCell);
|
352 | const domCellWidth = getDomCellOuterWidth(domCell);
|
353 | if (!columnWidthsInPx[cellSlot.column] || domCellWidth < columnWidthsInPx[cellSlot.column]) {
|
354 | columnWidthsInPx[cellSlot.column] = toPrecision(domCellWidth);
|
355 | }
|
356 | }
|
357 | return columnWidthsInPx;
|
358 | }
|
359 | |
360 |
|
361 |
|
362 |
|
363 |
|
364 |
|
365 |
|
366 | function _insertColgroupElement(viewWriter, columnWidthsInPx, viewTable) {
|
367 | const colgroup = viewWriter.createContainerElement('colgroup');
|
368 | for (let i = 0; i < columnWidthsInPx.length; i++) {
|
369 | const viewColElement = viewWriter.createEmptyElement('col');
|
370 | const columnWidthInPc = `${toPrecision(columnWidthsInPx[i] / sumArray(columnWidthsInPx) * 100)}%`;
|
371 | viewWriter.setStyle('width', columnWidthInPc, viewColElement);
|
372 | viewWriter.insert(viewWriter.createPositionAt(colgroup, 'end'), viewColElement);
|
373 | }
|
374 | viewWriter.insert(viewWriter.createPositionAt(viewTable, 0), colgroup);
|
375 | }
|
376 | |
377 |
|
378 |
|
379 |
|
380 |
|
381 |
|
382 |
|
383 | function _applyResizingAttributesToTable(viewWriter, viewTable, resizingData) {
|
384 | const figureInitialPcWidth = resizingData.widths.viewFigureWidth / resizingData.widths.viewFigureParentWidth;
|
385 | viewWriter.addClass('ck-table-resized', viewTable);
|
386 | viewWriter.addClass('ck-table-column-resizer__active', resizingData.elements.viewResizer);
|
387 | viewWriter.setStyle('width', `${toPrecision(figureInitialPcWidth * 100)}%`, viewTable.findAncestor('figure'));
|
388 | }
|
389 | }
|
390 | |
391 |
|
392 |
|
393 |
|
394 |
|
395 |
|
396 |
|
397 |
|
398 |
|
399 | _onMouseMoveHandler(eventInfo, mouseEventData) {
|
400 | if (!this._isResizingActive) {
|
401 | return;
|
402 | }
|
403 | if (!this._isResizingAllowed) {
|
404 | this._onMouseUpHandler();
|
405 | return;
|
406 | }
|
407 | const { columnPosition, flags: { isRightEdge, isTableCentered, isLtrContent }, elements: { viewFigure, viewLeftColumn, viewRightColumn }, widths: { viewFigureParentWidth, tableWidth, leftColumnWidth, rightColumnWidth } } = this._resizingData;
|
408 | const dxLowerBound = -leftColumnWidth + COLUMN_MIN_WIDTH_IN_PIXELS;
|
409 | const dxUpperBound = isRightEdge ?
|
410 | viewFigureParentWidth - tableWidth :
|
411 | rightColumnWidth - COLUMN_MIN_WIDTH_IN_PIXELS;
|
412 |
|
413 |
|
414 |
|
415 | const multiplier = (isLtrContent ? 1 : -1) * (isRightEdge && isTableCentered ? 2 : 1);
|
416 | const dx = clamp((mouseEventData.clientX - columnPosition) * multiplier, Math.min(dxLowerBound, 0), Math.max(dxUpperBound, 0));
|
417 | if (dx === 0) {
|
418 | return;
|
419 | }
|
420 | this.editor.editing.view.change(writer => {
|
421 | const leftColumnWidthAsPercentage = toPrecision((leftColumnWidth + dx) * 100 / tableWidth);
|
422 | writer.setStyle('width', `${leftColumnWidthAsPercentage}%`, viewLeftColumn);
|
423 | if (isRightEdge) {
|
424 | const tableWidthAsPercentage = toPrecision((tableWidth + dx) * 100 / viewFigureParentWidth);
|
425 | writer.setStyle('width', `${tableWidthAsPercentage}%`, viewFigure);
|
426 | }
|
427 | else {
|
428 | const rightColumnWidthAsPercentage = toPrecision((rightColumnWidth - dx) * 100 / tableWidth);
|
429 | writer.setStyle('width', `${rightColumnWidthAsPercentage}%`, viewRightColumn);
|
430 | }
|
431 | });
|
432 | }
|
433 | |
434 |
|
435 |
|
436 |
|
437 |
|
438 |
|
439 | _onMouseUpHandler() {
|
440 | if (!this._isResizingActive) {
|
441 | return;
|
442 | }
|
443 | const { viewResizer, modelTable, viewFigure, viewColgroup } = this._resizingData.elements;
|
444 | const editor = this.editor;
|
445 | const editingView = editor.editing.view;
|
446 | const tableColumnGroup = this.getColumnGroupElement(modelTable);
|
447 | const viewColumns = Array
|
448 | .from(viewColgroup.getChildren())
|
449 | .filter((column) => column.is('view:element'));
|
450 | const columnWidthsAttributeOld = tableColumnGroup ?
|
451 | this.getTableColumnsWidths(tableColumnGroup) :
|
452 | null;
|
453 | const columnWidthsAttributeNew = viewColumns.map(column => column.getStyle('width'));
|
454 | const isColumnWidthsAttributeChanged = !isEqual(columnWidthsAttributeOld, columnWidthsAttributeNew);
|
455 | const tableWidthAttributeOld = modelTable.getAttribute('tableWidth');
|
456 | const tableWidthAttributeNew = viewFigure.getStyle('width');
|
457 | const isTableWidthAttributeChanged = tableWidthAttributeOld !== tableWidthAttributeNew;
|
458 | if (isColumnWidthsAttributeChanged || isTableWidthAttributeChanged) {
|
459 | if (this._isResizingAllowed) {
|
460 | editor.execute('resizeTableWidth', {
|
461 | table: modelTable,
|
462 | tableWidth: `${toPrecision(tableWidthAttributeNew)}%`,
|
463 | columnWidths: columnWidthsAttributeNew
|
464 | });
|
465 | }
|
466 | else {
|
467 |
|
468 |
|
469 | editingView.change(writer => {
|
470 |
|
471 |
|
472 | if (columnWidthsAttributeOld) {
|
473 | for (const viewCol of viewColumns) {
|
474 | writer.setStyle('width', columnWidthsAttributeOld.shift(), viewCol);
|
475 | }
|
476 | }
|
477 | else {
|
478 | writer.remove(viewColgroup);
|
479 | }
|
480 | if (isTableWidthAttributeChanged) {
|
481 |
|
482 |
|
483 | if (tableWidthAttributeOld) {
|
484 | writer.setStyle('width', tableWidthAttributeOld, viewFigure);
|
485 | }
|
486 | else {
|
487 | writer.removeStyle('width', viewFigure);
|
488 | }
|
489 | }
|
490 |
|
491 |
|
492 | if (!columnWidthsAttributeOld && !tableWidthAttributeOld) {
|
493 | writer.removeClass('ck-table-resized', [...viewFigure.getChildren()].find(element => element.name === 'table'));
|
494 | }
|
495 | });
|
496 | }
|
497 | }
|
498 | editingView.change(writer => {
|
499 | writer.removeClass('ck-table-column-resizer__active', viewResizer);
|
500 | });
|
501 | this._isResizingActive = false;
|
502 | this._resizingData = null;
|
503 | }
|
504 | |
505 |
|
506 |
|
507 |
|
508 |
|
509 |
|
510 |
|
511 | _getResizingData(domEventData, columnWidths) {
|
512 | const editor = this.editor;
|
513 | const columnPosition = domEventData.domEvent.clientX;
|
514 | const viewResizer = domEventData.target;
|
515 | const viewLeftCell = viewResizer.findAncestor('td') || viewResizer.findAncestor('th');
|
516 | const modelLeftCell = editor.editing.mapper.toModelElement(viewLeftCell);
|
517 | const modelTable = modelLeftCell.findAncestor('table');
|
518 | const leftColumnIndex = getColumnEdgesIndexes(modelLeftCell, this._tableUtilsPlugin).rightEdge;
|
519 | const lastColumnIndex = this._tableUtilsPlugin.getColumns(modelTable) - 1;
|
520 | const isRightEdge = leftColumnIndex === lastColumnIndex;
|
521 | const isTableCentered = !modelTable.hasAttribute('tableAlignment');
|
522 | const isLtrContent = editor.locale.contentLanguageDirection !== 'rtl';
|
523 | const viewTable = viewLeftCell.findAncestor('table');
|
524 | const viewFigure = viewTable.findAncestor('figure');
|
525 | const viewColgroup = [...viewTable.getChildren()]
|
526 | .find(viewCol => viewCol.is('element', 'colgroup'));
|
527 | const viewLeftColumn = viewColgroup.getChild(leftColumnIndex);
|
528 | const viewRightColumn = isRightEdge ? undefined : viewColgroup.getChild(leftColumnIndex + 1);
|
529 | const viewFigureParentWidth = getElementWidthInPixels(editor.editing.view.domConverter.mapViewToDom(viewFigure.parent));
|
530 | const viewFigureWidth = getElementWidthInPixels(editor.editing.view.domConverter.mapViewToDom(viewFigure));
|
531 | const tableWidth = getTableWidthInPixels(modelTable, editor);
|
532 | const leftColumnWidth = columnWidths[leftColumnIndex];
|
533 | const rightColumnWidth = isRightEdge ? undefined : columnWidths[leftColumnIndex + 1];
|
534 | return {
|
535 | columnPosition,
|
536 | flags: {
|
537 | isRightEdge,
|
538 | isTableCentered,
|
539 | isLtrContent
|
540 | },
|
541 | elements: {
|
542 | viewResizer,
|
543 | modelTable,
|
544 | viewFigure,
|
545 | viewColgroup,
|
546 | viewLeftColumn,
|
547 | viewRightColumn
|
548 | },
|
549 | widths: {
|
550 | viewFigureParentWidth,
|
551 | viewFigureWidth,
|
552 | tableWidth,
|
553 | leftColumnWidth,
|
554 | rightColumnWidth
|
555 | }
|
556 | };
|
557 | }
|
558 | |
559 |
|
560 |
|
561 | _registerResizerInserter() {
|
562 | this.editor.conversion.for('editingDowncast').add(dispatcher => {
|
563 | dispatcher.on('insert:tableCell', (evt, data, conversionApi) => {
|
564 | const modelElement = data.item;
|
565 | const viewElement = conversionApi.mapper.toViewElement(modelElement);
|
566 | const viewWriter = conversionApi.writer;
|
567 | viewWriter.insert(viewWriter.createPositionAt(viewElement, 'end'), viewWriter.createUIElement('div', { class: 'ck-table-column-resizer' }));
|
568 | }, { priority: 'lowest' });
|
569 | });
|
570 | }
|
571 | }
|