Skip to content

Commit

Permalink
Merge pull request #7595 from ckeditor/i/7453
Browse files Browse the repository at this point in the history
Fix (table): Pasting a table into a table with headings should not break table layout. Closes #7453.
  • Loading branch information
jodator committed Jul 13, 2020
2 parents a349af5 + b4eaa69 commit df4485f
Show file tree
Hide file tree
Showing 8 changed files with 393 additions and 18 deletions.
37 changes: 29 additions & 8 deletions packages/ckeditor5-table/src/tableclipboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import TableSelection from './tableselection';
import TableWalker from './tablewalker';
import TableUtils from './tableutils';
import { getColumnIndexes, getRowIndexes, getSelectionAffectedTableCells, isSelectionRectangular } from './utils/selection';
import { getColumnIndexes, getRowIndexes, getSelectionAffectedTableCells, isSelectionRectangular, sortRanges } from './utils/selection';
import {
cropTableToDimensions,
getHorizontallyOverlappingCells,
Expand Down Expand Up @@ -313,7 +313,32 @@ function replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selected
insertPosition = writer.createPositionAfter( cellToInsert );
}

writer.setSelection( cellsToSelect.map( cell => writer.createRangeOn( cell ) ) );
// 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 );
}

// Selection ranges must be sorted because the first and last selection ranges are considered
// as anchor/focus cell ranges for multi-cell selection.
const selectionRanges = sortRanges( cellsToSelect.map( cell => writer.createRangeOn( cell ) ) );

writer.setSelection( selectionRanges );
}

// Expand table (in place) to expected size.
Expand Down Expand Up @@ -484,9 +509,7 @@ function doHorizontalSplit( table, splitRow, limitColumns, writer, startRow = 0
// Filter out cells that are not touching insides of the rectangular selection.
const cellsToSplit = overlappingCells.filter( ( { column, cellWidth } ) => isAffectedBySelection( column, cellWidth, limitColumns ) );

for ( const { cell } of cellsToSplit ) {
splitHorizontally( cell, splitRow, writer );
}
return cellsToSplit.map( ( { cell } ) => splitHorizontally( cell, splitRow, writer ) );
}

function doVerticalSplit( table, splitColumn, limitRows, writer ) {
Expand All @@ -500,9 +523,7 @@ function doVerticalSplit( table, splitColumn, limitRows, writer ) {
// Filter out cells that are not touching insides of the rectangular selection.
const cellsToSplit = overlappingCells.filter( ( { row, cellHeight } ) => isAffectedBySelection( row, cellHeight, limitRows ) );

for ( const { cell, column } of cellsToSplit ) {
splitVertically( cell, column, splitColumn, writer );
}
return cellsToSplit.map( ( { cell, column } ) => splitVertically( cell, column, splitColumn, writer ) );
}

// Checks if cell at given row (column) is affected by a rectangular selection defined by first/last column (row).
Expand Down
4 changes: 4 additions & 0 deletions packages/ckeditor5-table/src/utils/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,15 @@ export function updateNumericAttribute( key, value, item, writer, defaultValue =
* @param {module:engine/model/writer~Writer} writer The model writer.
* @param {module:engine/model/position~Position} insertPosition The position at which the table cell should be inserted.
* @param {Object} attributes The element attributes.
* @returns {module:engine/model/element~Element} Created table cell.
*/
export function createEmptyTableCell( writer, insertPosition, attributes = {} ) {
const tableCell = writer.createElement( 'tableCell', attributes );

writer.insertElement( 'paragraph', tableCell );
writer.insert( tableCell, insertPosition );

return tableCell;
}

/**
Expand Down
14 changes: 10 additions & 4 deletions packages/ckeditor5-table/src/utils/selection.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,16 @@ export function isSelectionRectangular( selectedTableCells, tableUtils ) {
return areaOfValidSelection == areaOfSelectedCells;
}

/**
* Returns array of sorted ranges.
*
* @param {Iterable.<module:engine/model/range~Range>} ranges
* @return {Array.<module:engine/model/range~Range>}
*/
export function sortRanges( ranges ) {
return Array.from( ranges ).sort( compareRangeOrder );
}

// Helper method to get an object with `first` and `last` indexes from an unsorted array of indexes.
function getFirstLastIndexesObject( indexes ) {
const allIndexesSorted = indexes.sort( ( indexA, indexB ) => indexA - indexB );
Expand All @@ -197,10 +207,6 @@ function getFirstLastIndexesObject( indexes ) {
return { first, last };
}

function sortRanges( rangesIterator ) {
return Array.from( rangesIterator ).sort( compareRangeOrder );
}

function compareRangeOrder( rangeA, rangeB ) {
// Since table cell ranges are disjoint, it's enough to check their start positions.
const posA = rangeA.start;
Expand Down
12 changes: 10 additions & 2 deletions packages/ckeditor5-table/src/utils/structure.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export function getVerticallyOverlappingCells( table, overlapRow, startRow = 0 )
* @param {module:engine/model/element~Element} tableCell
* @param {Number} splitRow
* @param {module:engine/model/writer~Writer} writer
* @returns {module:engine/model/element~Element} Created table cell.
*/
export function splitHorizontally( tableCell, splitRow, writer ) {
const tableRow = tableCell.parent;
Expand All @@ -164,6 +165,7 @@ export function splitHorizontally( tableCell, splitRow, writer ) {
const endRow = startRow + newRowspan;
const tableMap = [ ...new TableWalker( table, { startRow, endRow, includeAllSlots: true } ) ];

let newCell = null;
let columnIndex;

for ( const tableSlot of tableMap ) {
Expand All @@ -174,12 +176,14 @@ export function splitHorizontally( tableCell, splitRow, writer ) {
}

if ( columnIndex !== undefined && columnIndex === column && row === endRow ) {
createEmptyTableCell( writer, tableSlot.getPositionBefore(), newCellAttributes );
newCell = createEmptyTableCell( writer, tableSlot.getPositionBefore(), newCellAttributes );
}
}

// Update the rowspan attribute after updating table.
updateNumericAttribute( 'rowspan', newRowspan, tableCell, writer );

return newCell;
}

/**
Expand Down Expand Up @@ -232,6 +236,7 @@ export function getHorizontallyOverlappingCells( table, overlapColumn ) {
* @param {Number} columnIndex The table cell column index.
* @param {Number} splitColumn The index of column to split cell on.
* @param {module:engine/model/writer~Writer} writer
* @returns {module:engine/model/element~Element} Created table cell.
*/
export function splitVertically( tableCell, columnIndex, splitColumn, writer ) {
const colspan = parseInt( tableCell.getAttribute( 'colspan' ) );
Expand All @@ -250,9 +255,12 @@ export function splitVertically( tableCell, columnIndex, splitColumn, writer ) {
newCellAttributes.rowspan = rowspan;
}

createEmptyTableCell( writer, writer.createPositionAfter( tableCell ), newCellAttributes );
const newCell = createEmptyTableCell( writer, writer.createPositionAfter( tableCell ), newCellAttributes );

// Update the colspan attribute after updating table.
updateNumericAttribute( 'colspan', newColspan, tableCell, writer );

return newCell;
}

/**
Expand Down
15 changes: 13 additions & 2 deletions packages/ckeditor5-table/tests/manual/tablemocking.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,16 @@
box-sizing: border-box;
margin: 10px 0;
}
pre,code {
pre, code {
font-size: 11px;
font-family: Menlo, Consolas, Lucida Console, Courier New, dejavu sans mono, monospace;
}
#table-tools pre {
background: hsl( 0, 0%, 95% );
max-height:300px;
overflow: auto;
padding: 10px;
}
.diff-add {
color: hsl( 120, 70%, 35% );
}
Expand Down Expand Up @@ -61,4 +67,9 @@
<div id="editor">
</div>

<pre id="ascii-art"></pre>
<div id="table-tools">
<pre id="ascii-art"></pre>

<h3>Clipboard preview:</h3>
<pre id="clipboard"></pre>
</div>
4 changes: 4 additions & 0 deletions packages/ckeditor5-table/tests/manual/tablemocking.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ ClassicEditor
const asciiOut = document.getElementById( 'ascii-art' );
const modelData = document.getElementById( 'model-data' );

editor.editing.view.document.on( 'paste', ( evt, data ) => {
document.getElementById( 'clipboard' ).innerText = data.dataTransfer.getData( 'text/html' ).replace( />(?=<)/g, '>\n' );
} );

document.getElementById( 'clear-content' ).addEventListener( 'click', () => {
editor.setData( '' );
} );
Expand Down
2 changes: 0 additions & 2 deletions packages/ckeditor5-table/tests/manual/tablemocking.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,3 @@ setModelData( model, modelTable( [
[ '40', '41', '42', '43', '44' ]
] ) );
```

**Note:** Cell content is ignored while generating ASCII-art and `modelTableData`.
Loading

0 comments on commit df4485f

Please sign in to comment.