|
|
@@ -56,6 +56,8 @@ export default class TableClipboard extends Plugin {
|
|
|
this.listenTo( viewDocument, 'copy', ( evt, data ) => this._onCopyCut( evt, data ) );
|
|
|
this.listenTo( viewDocument, 'cut', ( evt, data ) => this._onCopyCut( evt, data ) );
|
|
|
this.listenTo( editor.model, 'insertContent', ( evt, args ) => this._onInsertContent( evt, ...args ), { priority: 'high' } );
|
|
|
+
|
|
|
+ this.decorate( '_replaceTableSlotCell' );
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -164,7 +166,7 @@ export default class TableClipboard extends Plugin {
|
|
|
// Content table to which we insert a pasted table.
|
|
|
const selectedTable = selectedTableCells[ 0 ].findAncestor( 'table' );
|
|
|
|
|
|
- const cellsToSelect = replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selectedTable, selection, writer );
|
|
|
+ const cellsToSelect = this._replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selectedTable, selection, writer );
|
|
|
|
|
|
if ( this.editor.plugins.get( 'TableSelection' ).isEnabled ) {
|
|
|
// Selection ranges must be sorted because the first and last selection ranges are considered
|
|
|
@@ -178,6 +180,189 @@ export default class TableClipboard extends Plugin {
|
|
|
}
|
|
|
} );
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Replaces the part of selectedTable with pastedTable.
|
|
|
+ *
|
|
|
+ * @private
|
|
|
+ * @param {module:engine/model/element~Element} pastedTable
|
|
|
+ * @param {Object} pastedDimensions
|
|
|
+ * @param {Number} pastedDimensions.height
|
|
|
+ * @param {Number} pastedDimensions.width
|
|
|
+ * @param {module:engine/model/element~Element} selectedTable
|
|
|
+ * @param {Object} selection
|
|
|
+ * @param {Number} selection.firstColumn
|
|
|
+ * @param {Number} selection.firstRow
|
|
|
+ * @param {Number} selection.lastColumn
|
|
|
+ * @param {Number} selection.lastRow
|
|
|
+ * @param {module:engine/model/writer~Writer} writer
|
|
|
+ * @returns {Array.<module:engine/model/element~Element>}
|
|
|
+ */
|
|
|
+ _replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selectedTable, selection, writer ) {
|
|
|
+ const { width: pastedWidth, height: pastedHeight } = pastedDimensions;
|
|
|
+
|
|
|
+ // Holds two-dimensional array that is addressed by [ row ][ column ] that stores cells anchored at given location.
|
|
|
+ const pastedTableLocationMap = createLocationMap( pastedTable, pastedWidth, pastedHeight );
|
|
|
+
|
|
|
+ const selectedTableMap = [ ...new TableWalker( selectedTable, {
|
|
|
+ startRow: selection.firstRow,
|
|
|
+ endRow: selection.lastRow,
|
|
|
+ startColumn: selection.firstColumn,
|
|
|
+ endColumn: selection.lastColumn,
|
|
|
+ includeAllSlots: true
|
|
|
+ } ) ];
|
|
|
+
|
|
|
+ // Selection must be set to pasted cells (some might be removed or new created).
|
|
|
+ const cellsToSelect = [];
|
|
|
+
|
|
|
+ // Store next cell insert position.
|
|
|
+ let insertPosition;
|
|
|
+
|
|
|
+ // Content table replace cells algorithm iterates over a selected table fragment and:
|
|
|
+ //
|
|
|
+ // - Removes existing table cells at current slot (location).
|
|
|
+ // - Inserts cell from a pasted table for a matched slots.
|
|
|
+ //
|
|
|
+ // This ensures proper table geometry after the paste
|
|
|
+ for ( const tableSlot of selectedTableMap ) {
|
|
|
+ const { row, column } = tableSlot;
|
|
|
+
|
|
|
+ // Save the insert position for current row start.
|
|
|
+ if ( column === selection.firstColumn ) {
|
|
|
+ insertPosition = tableSlot.getPositionBefore();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Map current table slot location to an pasted table slot location.
|
|
|
+ const pastedRow = row - selection.firstRow;
|
|
|
+ const pastedColumn = column - selection.firstColumn;
|
|
|
+ const pastedCell = pastedTableLocationMap[ pastedRow % pastedHeight ][ pastedColumn % pastedWidth ];
|
|
|
+
|
|
|
+ // Clone cell to insert (to duplicate its attributes and children).
|
|
|
+ // Cloning is required to support repeating pasted table content when inserting to a bigger selection.
|
|
|
+ const cellToInsert = pastedCell ? writer.cloneElement( pastedCell ) : null;
|
|
|
+
|
|
|
+ // Replace the cell from the current slot with new table cell.
|
|
|
+ const newTableCell = this._replaceTableSlotCell( tableSlot, cellToInsert, insertPosition, writer );
|
|
|
+
|
|
|
+ // The cell was only removed.
|
|
|
+ if ( !newTableCell ) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Trim the cell if it's row/col-spans would exceed selection area.
|
|
|
+ trimTableCellIfNeeded( newTableCell, row, column, selection.lastRow, selection.lastColumn, writer );
|
|
|
+
|
|
|
+ cellsToSelect.push( newTableCell );
|
|
|
+
|
|
|
+ insertPosition = writer.createPositionAfter( newTableCell );
|
|
|
+ }
|
|
|
+
|
|
|
+ // If there are any headings, all the cells that overlap from heading must be splitted.
|
|
|
+ const headingRows = parseInt( selectedTable.getAttribute( 'headingRows' ) || 0 );
|
|
|
+ const headingColumns = parseInt( selectedTable.getAttribute( 'headingColumns' ) || 0 );
|
|
|
+
|
|
|
+ const areHeadingRowsIntersectingSelection = selection.firstRow < headingRows && headingRows <= selection.lastRow;
|
|
|
+ const areHeadingColumnsIntersectingSelection = selection.firstColumn < headingColumns && headingColumns <= selection.lastColumn;
|
|
|
+
|
|
|
+ if ( areHeadingRowsIntersectingSelection ) {
|
|
|
+ const columnsLimit = { first: selection.firstColumn, last: selection.lastColumn };
|
|
|
+ const newCells = doHorizontalSplit( selectedTable, headingRows, columnsLimit, writer, selection.firstRow );
|
|
|
+
|
|
|
+ cellsToSelect.push( ...newCells );
|
|
|
+ }
|
|
|
+
|
|
|
+ if ( areHeadingColumnsIntersectingSelection ) {
|
|
|
+ const rowsLimit = { first: selection.firstRow, last: selection.lastRow };
|
|
|
+ const newCells = doVerticalSplit( selectedTable, headingColumns, rowsLimit, writer );
|
|
|
+
|
|
|
+ cellsToSelect.push( ...newCells );
|
|
|
+ }
|
|
|
+
|
|
|
+ return cellsToSelect;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Replaces a single table slot.
|
|
|
+ *
|
|
|
+ * @private
|
|
|
+ * @param {module:table/tablewalker~TableSlot} tableSlot
|
|
|
+ * @param {module:engine/model/element~Element} cellToInsert
|
|
|
+ * @param {module:engine/model/position~Position} insertPosition
|
|
|
+ * @param {module:engine/model/writer~Writer} writer
|
|
|
+ * @returns {module:engine/model/element~Element|null} Inserted table cell or null if slot should remain empty.
|
|
|
+ */
|
|
|
+ _replaceTableSlotCell( tableSlot, cellToInsert, insertPosition, writer ) {
|
|
|
+ const { cell, isAnchor } = tableSlot;
|
|
|
+
|
|
|
+ // If the slot is occupied by a cell in a selected table - remove it.
|
|
|
+ // The slot of this cell will be either:
|
|
|
+ // - Replaced by a pasted table cell.
|
|
|
+ // - Spanned by a previously pasted table cell.
|
|
|
+ if ( isAnchor ) {
|
|
|
+ writer.remove( cell );
|
|
|
+ }
|
|
|
+
|
|
|
+ // There is no cell to insert (might be spanned by other cell in a pasted table) - advance to the next content table slot.
|
|
|
+ if ( !cellToInsert ) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ writer.insert( cellToInsert, insertPosition );
|
|
|
+
|
|
|
+ return cellToInsert;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Extract table for pasting into table.
|
|
|
+ *
|
|
|
+ * @private
|
|
|
+ * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
|
|
|
+ * @param {module:engine/model/model~Model} model The editor model.
|
|
|
+ * @returns {module:engine/model/element~Element|null}
|
|
|
+ */
|
|
|
+export function getTableIfOnlyTableInContent( content, model ) {
|
|
|
+ if ( !content.is( 'documentFragment' ) && !content.is( 'element' ) ) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Table passed directly.
|
|
|
+ if ( content.is( 'element', 'table' ) ) {
|
|
|
+ return content;
|
|
|
+ }
|
|
|
+
|
|
|
+ // We do not support mixed content when pasting table into table.
|
|
|
+ // See: https://github.com/ckeditor/ckeditor5/issues/6817.
|
|
|
+ if ( content.childCount == 1 && content.getChild( 0 ).is( 'element', 'table' ) ) {
|
|
|
+ return content.getChild( 0 );
|
|
|
+ }
|
|
|
+
|
|
|
+ // If there are only whitespaces around a table then use that table for pasting.
|
|
|
+
|
|
|
+ const contentRange = model.createRangeIn( content );
|
|
|
+
|
|
|
+ for ( const element of contentRange.getItems() ) {
|
|
|
+ if ( element.is( 'element', 'table' ) ) {
|
|
|
+ // Stop checking if there is some content before table.
|
|
|
+ const rangeBefore = model.createRange( contentRange.start, model.createPositionBefore( element ) );
|
|
|
+
|
|
|
+ if ( model.hasContent( rangeBefore, { ignoreWhitespaces: true } ) ) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Stop checking if there is some content after table.
|
|
|
+ const rangeAfter = model.createRange( model.createPositionAfter( element ), contentRange.end );
|
|
|
+
|
|
|
+ if ( model.hasContent( rangeAfter, { ignoreWhitespaces: true } ) ) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // There wasn't any content neither before nor after.
|
|
|
+ return element;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
}
|
|
|
|
|
|
// Prepares a table for pasting and returns adjusted selection dimensions.
|
|
|
@@ -246,109 +431,6 @@ function prepareTableForPasting( selectedTableCells, pastedDimensions, writer, t
|
|
|
return selection;
|
|
|
}
|
|
|
|
|
|
-// Replaces the part of selectedTable with pastedTable.
|
|
|
-//
|
|
|
-// @param {module:engine/model/element~Element} pastedTable
|
|
|
-// @param {Object} pastedDimensions
|
|
|
-// @param {Number} pastedDimensions.height
|
|
|
-// @param {Number} pastedDimensions.width
|
|
|
-// @param {module:engine/model/element~Element} selectedTable
|
|
|
-// @param {Object} selection
|
|
|
-// @param {Number} selection.firstColumn
|
|
|
-// @param {Number} selection.firstRow
|
|
|
-// @param {Number} selection.lastColumn
|
|
|
-// @param {Number} selection.lastRow
|
|
|
-// @param {module:engine/model/writer~Writer} writer
|
|
|
-// @returns {Array.<module:engine/model/element~Element>}
|
|
|
-function replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selectedTable, selection, writer ) {
|
|
|
- const { width: pastedWidth, height: pastedHeight } = pastedDimensions;
|
|
|
-
|
|
|
- // Holds two-dimensional array that is addressed by [ row ][ column ] that stores cells anchored at given location.
|
|
|
- const pastedTableLocationMap = createLocationMap( pastedTable, pastedWidth, pastedHeight );
|
|
|
-
|
|
|
- const selectedTableMap = [ ...new TableWalker( selectedTable, {
|
|
|
- startRow: selection.firstRow,
|
|
|
- endRow: selection.lastRow,
|
|
|
- startColumn: selection.firstColumn,
|
|
|
- endColumn: selection.lastColumn,
|
|
|
- includeAllSlots: true
|
|
|
- } ) ];
|
|
|
-
|
|
|
- // Selection must be set to pasted cells (some might be removed or new created).
|
|
|
- const cellsToSelect = [];
|
|
|
-
|
|
|
- // Store next cell insert position.
|
|
|
- let insertPosition;
|
|
|
-
|
|
|
- // Content table replace cells algorithm iterates over a selected table fragment and:
|
|
|
- //
|
|
|
- // - Removes existing table cells at current slot (location).
|
|
|
- // - Inserts cell from a pasted table for a matched slots.
|
|
|
- //
|
|
|
- // This ensures proper table geometry after the paste
|
|
|
- for ( const tableSlot of selectedTableMap ) {
|
|
|
- const { row, column, cell, isAnchor } = tableSlot;
|
|
|
-
|
|
|
- // Save the insert position for current row start.
|
|
|
- if ( column === selection.firstColumn ) {
|
|
|
- insertPosition = tableSlot.getPositionBefore();
|
|
|
- }
|
|
|
-
|
|
|
- // If the slot is occupied by a cell in a selected table - remove it.
|
|
|
- // The slot of this cell will be either:
|
|
|
- // - Replaced by a pasted table cell.
|
|
|
- // - Spanned by a previously pasted table cell.
|
|
|
- if ( isAnchor ) {
|
|
|
- writer.remove( cell );
|
|
|
- }
|
|
|
-
|
|
|
- // Map current table slot location to an pasted table slot location.
|
|
|
- const pastedRow = row - selection.firstRow;
|
|
|
- const pastedColumn = column - selection.firstColumn;
|
|
|
- const pastedCell = pastedTableLocationMap[ pastedRow % pastedHeight ][ pastedColumn % pastedWidth ];
|
|
|
-
|
|
|
- // There is no cell to insert (might be spanned by other cell in a pasted table) - advance to the next content table slot.
|
|
|
- if ( !pastedCell ) {
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- // Clone cell to insert (to duplicate its attributes and children).
|
|
|
- // Cloning is required to support repeating pasted table content when inserting to a bigger selection.
|
|
|
- const cellToInsert = writer.cloneElement( pastedCell );
|
|
|
-
|
|
|
- // Trim the cell if it's row/col-spans would exceed selection area.
|
|
|
- trimTableCellIfNeeded( cellToInsert, row, column, selection.lastRow, selection.lastColumn, writer );
|
|
|
-
|
|
|
- writer.insert( cellToInsert, insertPosition );
|
|
|
- cellsToSelect.push( cellToInsert );
|
|
|
-
|
|
|
- insertPosition = writer.createPositionAfter( cellToInsert );
|
|
|
- }
|
|
|
-
|
|
|
- // If there are any headings, all the cells that overlap from heading must be splitted.
|
|
|
- const headingRows = parseInt( selectedTable.getAttribute( 'headingRows' ) || 0 );
|
|
|
- const headingColumns = parseInt( selectedTable.getAttribute( 'headingColumns' ) || 0 );
|
|
|
-
|
|
|
- const areHeadingRowsIntersectingSelection = selection.firstRow < headingRows && headingRows <= selection.lastRow;
|
|
|
- const areHeadingColumnsIntersectingSelection = selection.firstColumn < headingColumns && headingColumns <= selection.lastColumn;
|
|
|
-
|
|
|
- if ( areHeadingRowsIntersectingSelection ) {
|
|
|
- const columnsLimit = { first: selection.firstColumn, last: selection.lastColumn };
|
|
|
- const newCells = doHorizontalSplit( selectedTable, headingRows, columnsLimit, writer, selection.firstRow );
|
|
|
-
|
|
|
- cellsToSelect.push( ...newCells );
|
|
|
- }
|
|
|
-
|
|
|
- if ( areHeadingColumnsIntersectingSelection ) {
|
|
|
- const rowsLimit = { first: selection.firstRow, last: selection.lastRow };
|
|
|
- const newCells = doVerticalSplit( selectedTable, headingColumns, rowsLimit, writer );
|
|
|
-
|
|
|
- cellsToSelect.push( ...newCells );
|
|
|
- }
|
|
|
-
|
|
|
- return cellsToSelect;
|
|
|
-}
|
|
|
-
|
|
|
// Expand table (in place) to expected size.
|
|
|
function expandTableSize( table, expectedHeight, expectedWidth, tableUtils ) {
|
|
|
const tableWidth = tableUtils.getColumns( table );
|
|
|
@@ -369,50 +451,6 @@ function expandTableSize( table, expectedHeight, expectedWidth, tableUtils ) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function getTableIfOnlyTableInContent( content, model ) {
|
|
|
- if ( !content.is( 'documentFragment' ) && !content.is( 'element' ) ) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- // Table passed directly.
|
|
|
- if ( content.is( 'element', 'table' ) ) {
|
|
|
- return content;
|
|
|
- }
|
|
|
-
|
|
|
- // We do not support mixed content when pasting table into table.
|
|
|
- // See: https://github.com/ckeditor/ckeditor5/issues/6817.
|
|
|
- if ( content.childCount == 1 && content.getChild( 0 ).is( 'element', 'table' ) ) {
|
|
|
- return content.getChild( 0 );
|
|
|
- }
|
|
|
-
|
|
|
- // If there are only whitespaces around a table then use that table for pasting.
|
|
|
-
|
|
|
- const contentRange = model.createRangeIn( content );
|
|
|
-
|
|
|
- for ( const element of contentRange.getItems() ) {
|
|
|
- if ( element.is( 'element', 'table' ) ) {
|
|
|
- // Stop checking if there is some content before table.
|
|
|
- const rangeBefore = model.createRange( contentRange.start, model.createPositionBefore( element ) );
|
|
|
-
|
|
|
- if ( model.hasContent( rangeBefore, { ignoreWhitespaces: true } ) ) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- // Stop checking if there is some content after table.
|
|
|
- const rangeAfter = model.createRange( model.createPositionAfter( element ), contentRange.end );
|
|
|
-
|
|
|
- if ( model.hasContent( rangeAfter, { ignoreWhitespaces: true } ) ) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- // There wasn't any content neither before nor after.
|
|
|
- return element;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return null;
|
|
|
-}
|
|
|
-
|
|
|
// Returns two-dimensional array that is addressed by [ row ][ column ] that stores cells anchored at given location.
|
|
|
//
|
|
|
// At given row & column location it might be one of:
|