ckeditor5-image-upload-base64
Version:
The development environment of CKEditor 5 – the best browser-based rich text editor.
902 lines (766 loc) • 34 kB
JavaScript
/**
* @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @module table/tableutils
*/
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import TableWalker from './tablewalker';
import { createEmptyTableCell, updateNumericAttribute } from './utils/common';
import { removeEmptyColumns, removeEmptyRows } from './utils/structure';
/**
* The table utilities plugin.
*
* @extends module:core/plugin~Plugin
*/
export default class TableUtils extends Plugin {
/**
* @inheritDoc
*/
static get pluginName() {
return 'TableUtils';
}
/**
* Returns the table cell location as an object with table row and table column indexes.
*
* For instance, in the table below:
*
* 0 1 2 3
* +---+---+---+---+
* 0 | a | b | c |
* + + +---+
* 1 | | | d |
* +---+---+ +---+
* 2 | e | | f |
* +---+---+---+---+
*
* the method will return:
*
* const cellA = table.getNodeByPath( [ 0, 0 ] );
* editor.plugins.get( 'TableUtils' ).getCellLocation( cellA );
* // will return { row: 0, column: 0 }
*
* const cellD = table.getNodeByPath( [ 1, 0 ] );
* editor.plugins.get( 'TableUtils' ).getCellLocation( cellD );
* // will return { row: 1, column: 3 }
*
* @param {module:engine/model/element~Element} tableCell
* @returns {Object} Returns a `{row, column}` object.
*/
getCellLocation( tableCell ) {
const tableRow = tableCell.parent;
const table = tableRow.parent;
const rowIndex = table.getChildIndex( tableRow );
const tableWalker = new TableWalker( table, { row: rowIndex } );
for ( const { cell, row, column } of tableWalker ) {
if ( cell === tableCell ) {
return { row, column };
}
}
}
/**
* Creates an empty table with a proper structure. The table needs to be inserted into the model,
* for example, by using the {@link module:engine/model/model~Model#insertContent} function.
*
* model.change( ( writer ) => {
* // Create a table of 2 rows and 7 columns:
* const table = tableUtils.createTable( writer, { rows: 2, columns: 7 } );
*
* // Insert a table to the model at the best position taking the current selection:
* model.insertContent( table );
* }
*
* @param {module:engine/model/writer~Writer} writer The model writer.
* @param {Object} options
* @param {Number} [options.rows=2] The number of rows to create.
* @param {Number} [options.columns=2] The number of columns to create.
* @param {Number} [options.headingRows=0] The number of heading rows.
* @param {Number} [options.headingColumns=0] The number of heading columns.
* @returns {module:engine/model/element~Element} The created table element.
*/
createTable( writer, options ) {
const table = writer.createElement( 'table' );
const rows = parseInt( options.rows ) || 2;
const columns = parseInt( options.columns ) || 2;
createEmptyRows( writer, table, 0, rows, columns );
if ( options.headingRows ) {
updateNumericAttribute( 'headingRows', options.headingRows, table, writer, 0 );
}
if ( options.headingColumns ) {
updateNumericAttribute( 'headingColumns', options.headingColumns, table, writer, 0 );
}
return table;
}
/**
* Inserts rows into a table.
*
* editor.plugins.get( 'TableUtils' ).insertRows( table, { at: 1, rows: 2 } );
*
* Assuming the table on the left, the above code will transform it to the table on the right:
*
* row index
* 0 +---+---+---+ `at` = 1, +---+---+---+ 0
* | a | b | c | `rows` = 2, | a | b | c |
* 1 + +---+---+ <-- insert here + +---+---+ 1
* | | d | e | | | | |
* 2 + +---+---+ will give: + +---+---+ 2
* | | f | g | | | | |
* 3 +---+---+---+ + +---+---+ 3
* | | d | e |
* + +---+---+ 4
* + + f | g |
* +---+---+---+ 5
*
* @param {module:engine/model/element~Element} table The table model element where the rows will be inserted.
* @param {Object} options
* @param {Number} [options.at=0] The row index at which the rows will be inserted.
* @param {Number} [options.rows=1] The number of rows to insert.
* @param {Boolean|undefined} [options.copyStructureFromAbove] The flag for copying row structure. Note that
* the row structure will not be copied if this option is not provided.
*/
insertRows( table, options = {} ) {
const model = this.editor.model;
const insertAt = options.at || 0;
const rowsToInsert = options.rows || 1;
const isCopyStructure = options.copyStructureFromAbove !== undefined;
const copyStructureFrom = options.copyStructureFromAbove ? insertAt - 1 : insertAt;
const rows = this.getRows( table );
const columns = this.getColumns( table );
model.change( writer => {
const headingRows = table.getAttribute( 'headingRows' ) || 0;
// Inserting rows inside heading section requires to update `headingRows` attribute as the heading section will grow.
if ( headingRows > insertAt ) {
updateNumericAttribute( 'headingRows', headingRows + rowsToInsert, table, writer, 0 );
}
// Inserting at the end or at the beginning of a table doesn't require to calculate anything special.
if ( !isCopyStructure && ( insertAt === 0 || insertAt === rows ) ) {
createEmptyRows( writer, table, insertAt, rowsToInsert, columns );
return;
}
// Iterate over all the rows above the inserted rows in order to check for the row-spanned cells.
const walkerEndRow = isCopyStructure ? Math.max( insertAt, copyStructureFrom ) : insertAt;
const tableIterator = new TableWalker( table, { endRow: walkerEndRow } );
// Store spans of the reference row to reproduce it's structure. This array is column number indexed.
const rowColSpansMap = new Array( columns ).fill( 1 );
for ( const { row, column, cellHeight, cellWidth, cell } of tableIterator ) {
const lastCellRow = row + cellHeight - 1;
const isOverlappingInsertedRow = row < insertAt && insertAt <= lastCellRow;
const isReferenceRow = row <= copyStructureFrom && copyStructureFrom <= lastCellRow;
// If the cell is row-spanned and overlaps the inserted row, then reserve space for it in the row map.
if ( isOverlappingInsertedRow ) {
// This cell overlaps the inserted rows so we need to expand it further.
writer.setAttribute( 'rowspan', cellHeight + rowsToInsert, cell );
// Mark this cell with negative number to indicate how many cells should be skipped when adding the new cells.
rowColSpansMap[ column ] = -cellWidth;
}
// Store the colspan from reference row.
else if ( isCopyStructure && isReferenceRow ) {
rowColSpansMap[ column ] = cellWidth;
}
}
for ( let rowIndex = 0; rowIndex < rowsToInsert; rowIndex++ ) {
const tableRow = writer.createElement( 'tableRow' );
writer.insert( tableRow, table, insertAt );
for ( let cellIndex = 0; cellIndex < rowColSpansMap.length; cellIndex++ ) {
const colspan = rowColSpansMap[ cellIndex ];
const insertPosition = writer.createPositionAt( tableRow, 'end' );
// Insert the empty cell only if this slot is not row-spanned from any other cell.
if ( colspan > 0 ) {
createEmptyTableCell( writer, insertPosition, colspan > 1 ? { colspan } : null );
}
// Skip the col-spanned slots, there won't be any cells.
cellIndex += Math.abs( colspan ) - 1;
}
}
} );
}
/**
* Inserts columns into a table.
*
* editor.plugins.get( 'TableUtils' ).insertColumns( table, { at: 1, columns: 2 } );
*
* Assuming the table on the left, the above code will transform it to the table on the right:
*
* 0 1 2 3 0 1 2 3 4 5
* +---+---+---+ +---+---+---+---+---+
* | a | b | | a | b |
* + +---+ + +---+
* | | c | | | c |
* +---+---+---+ will give: +---+---+---+---+---+
* | d | e | f | | d | | | e | f |
* +---+ +---+ +---+---+---+ +---+
* | g | | h | | g | | | | h |
* +---+---+---+ +---+---+---+---+---+
* | i | | i |
* +---+---+---+ +---+---+---+---+---+
* ^---- insert here, `at` = 1, `columns` = 2
*
* @param {module:engine/model/element~Element} table The table model element where the columns will be inserted.
* @param {Object} options
* @param {Number} [options.at=0] The column index at which the columns will be inserted.
* @param {Number} [options.columns=1] The number of columns to insert.
*/
insertColumns( table, options = {} ) {
const model = this.editor.model;
const insertAt = options.at || 0;
const columnsToInsert = options.columns || 1;
model.change( writer => {
const headingColumns = table.getAttribute( 'headingColumns' );
// Inserting columns inside heading section requires to update `headingColumns` attribute as the heading section will grow.
if ( insertAt < headingColumns ) {
writer.setAttribute( 'headingColumns', headingColumns + columnsToInsert, table );
}
const tableColumns = this.getColumns( table );
// Inserting at the end and at the beginning of a table doesn't require to calculate anything special.
if ( insertAt === 0 || tableColumns === insertAt ) {
for ( const tableRow of table.getChildren() ) {
createCells( columnsToInsert, writer, writer.createPositionAt( tableRow, insertAt ? 'end' : 0 ) );
}
return;
}
const tableWalker = new TableWalker( table, { column: insertAt, includeAllSlots: true } );
for ( const tableSlot of tableWalker ) {
const { row, cell, cellAnchorColumn, cellAnchorRow, cellWidth, cellHeight } = tableSlot;
// When iterating over column the table walker outputs either:
// - cells at given column index (cell "e" from method docs),
// - spanned columns (spanned cell from row between cells "g" and "h" - spanned by "e", only if `includeAllSlots: true`),
// - or a cell from the same row which spans over this column (cell "a").
if ( cellAnchorColumn < insertAt ) {
// If cell is anchored in previous column, it is a cell that spans over an inserted column (cell "a" & "i").
// For such cells expand them by a number of columns inserted.
writer.setAttribute( 'colspan', cellWidth + columnsToInsert, cell );
// This cell will overlap cells in rows below so skip them (because of `includeAllSlots` option) - (cell "a")
const lastCellRow = cellAnchorRow + cellHeight - 1;
for ( let i = row; i <= lastCellRow; i++ ) {
tableWalker.skipRow( i );
}
} else {
// It's either cell at this column index or spanned cell by a row-spanned cell from row above.
// In table above it's cell "e" and a spanned position from row below (empty cell between cells "g" and "h")
createCells( columnsToInsert, writer, tableSlot.getPositionBefore() );
}
}
} );
}
/**
* Removes rows from the given `table`.
*
* This method re-calculates the table geometry including `rowspan` attribute of table cells overlapping removed rows
* and table headings values.
*
* editor.plugins.get( 'TableUtils' ).removeRows( table, { at: 1, rows: 2 } );
*
* Executing the above code in the context of the table on the left will transform its structure as presented on the right:
*
* row index
* ┌───┬───┬───┐ `at` = 1 ┌───┬───┬───┐
* 0 │ a │ b │ c │ `rows` = 2 │ a │ b │ c │ 0
* │ ├───┼───┤ │ ├───┼───┤
* 1 │ │ d │ e │ <-- remove from here │ │ d │ g │ 1
* │ │ ├───┤ will give: ├───┼───┼───┤
* 2 │ │ │ f │ │ h │ i │ j │ 2
* │ │ ├───┤ └───┴───┴───┘
* 3 │ │ │ g │
* ├───┼───┼───┤
* 4 │ h │ i │ j │
* └───┴───┴───┘
*
* @param {module:engine/model/element~Element} table
* @param {Object} options
* @param {Number} options.at The row index at which the removing rows will start.
* @param {Number} [options.rows=1] The number of rows to remove.
*/
removeRows( table, options ) {
const model = this.editor.model;
const rowsToRemove = options.rows || 1;
const first = options.at;
const last = first + rowsToRemove - 1;
model.change( writer => {
// Removing rows from the table require that most calculations to be done prior to changing table structure.
// Preparations must be done in the same enqueueChange callback to use the current table structure.
// 1. Preparation - get row-spanned cells that have to be modified after removing rows.
const { cellsToMove, cellsToTrim } = getCellsToMoveAndTrimOnRemoveRow( table, first, last );
// 2. Execution
// 2a. Move cells from removed rows that extends over a removed section - must be done before removing rows.
// This will fill any gaps in a rows below that previously were empty because of row-spanned cells.
if ( cellsToMove.size ) {
const rowAfterRemovedSection = last + 1;
moveCellsToRow( table, rowAfterRemovedSection, cellsToMove, writer );
}
// 2b. Remove all required rows.
for ( let i = last; i >= first; i-- ) {
writer.remove( table.getChild( i ) );
}
// 2c. Update cells from rows above that overlap removed section. Similar to step 2 but does not involve moving cells.
for ( const { rowspan, cell } of cellsToTrim ) {
updateNumericAttribute( 'rowspan', rowspan, cell, writer );
}
// 2d. Adjust heading rows if removed rows were in a heading section.
updateHeadingRows( table, first, last, writer );
// 2e. Remove empty columns (without anchored cells) if there are any.
if ( !removeEmptyColumns( table, this ) ) {
// If there wasn't any empty columns then we still need to check if this wasn't called
// because of cleaning empty rows and we only removed one of them.
removeEmptyRows( table, this );
}
} );
}
/**
* Removes columns from the given `table`.
*
* This method re-calculates the table geometry including the `colspan` attribute of table cells overlapping removed columns
* and table headings values.
*
* editor.plugins.get( 'TableUtils' ).removeColumns( table, { at: 1, columns: 2 } );
*
* Executing the above code in the context of the table on the left will transform its structure as presented on the right:
*
* 0 1 2 3 4 0 1 2
* ┌───────────────┬───┐ ┌───────┬───┐
* │ a │ b │ │ a │ b │
* │ ├───┤ │ ├───┤
* │ │ c │ │ │ c │
* ├───┬───┬───┬───┼───┤ will give: ├───┬───┼───┤
* │ d │ e │ f │ g │ h │ │ d │ g │ h │
* ├───┼───┼───┤ ├───┤ ├───┤ ├───┤
* │ i │ j │ k │ │ l │ │ i │ │ l │
* ├───┴───┴───┴───┴───┤ ├───┴───┴───┤
* │ m │ │ m │
* └───────────────────┘ └───────────┘
* ^---- remove from here, `at` = 1, `columns` = 2
*
* @param {module:engine/model/element~Element} table
* @param {Object} options
* @param {Number} options.at The row index at which the removing columns will start.
* @param {Number} [options.columns=1] The number of columns to remove.
*/
removeColumns( table, options ) {
const model = this.editor.model;
const first = options.at;
const columnsToRemove = options.columns || 1;
const last = options.at + columnsToRemove - 1;
model.change( writer => {
adjustHeadingColumns( table, { first, last }, writer );
for ( let removedColumnIndex = last; removedColumnIndex >= first; removedColumnIndex-- ) {
for ( const { cell, column, cellWidth } of [ ...new TableWalker( table ) ] ) {
// If colspaned cell overlaps removed column decrease its span.
if ( column <= removedColumnIndex && cellWidth > 1 && column + cellWidth > removedColumnIndex ) {
updateNumericAttribute( 'colspan', cellWidth - 1, cell, writer );
} else if ( column === removedColumnIndex ) {
// The cell in removed column has colspan of 1.
writer.remove( cell );
}
}
}
// Remove empty rows that could appear after removing columns.
if ( !removeEmptyRows( table, this ) ) {
// If there wasn't any empty rows then we still need to check if this wasn't called
// because of cleaning empty columns and we only removed one of them.
removeEmptyColumns( table, this );
}
} );
}
/**
* Divides a table cell vertically into several ones.
*
* The cell will be visually split into more cells by updating colspans of other cells in a column
* and inserting cells (columns) after that cell.
*
* In the table below, if cell "a" is split into 3 cells:
*
* +---+---+---+
* | a | b | c |
* +---+---+---+
* | d | e | f |
* +---+---+---+
*
* it will result in the table below:
*
* +---+---+---+---+---+
* | a | | | b | c |
* +---+---+---+---+---+
* | d | e | f |
* +---+---+---+---+---+
*
* So cell "d" will get its `colspan` updated to `3` and 2 cells will be added (2 columns will be created).
*
* Splitting a cell that already has a `colspan` attribute set will distribute the cell `colspan` evenly and the remainder
* will be left to the original cell:
*
* +---+---+---+
* | a |
* +---+---+---+
* | b | c | d |
* +---+---+---+
*
* Splitting cell "a" with `colspan=3` into 2 cells will create 1 cell with a `colspan=a` and cell "a" that will have `colspan=2`:
*
* +---+---+---+
* | a | |
* +---+---+---+
* | b | c | d |
* +---+---+---+
*
* @param {module:engine/model/element~Element} tableCell
* @param {Number} numberOfCells
*/
splitCellVertically( tableCell, numberOfCells = 2 ) {
const model = this.editor.model;
const tableRow = tableCell.parent;
const table = tableRow.parent;
const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 );
const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 );
model.change( writer => {
// First check - the cell spans over multiple rows so before doing anything else just split this cell.
if ( colspan > 1 ) {
// Get spans of new (inserted) cells and span to update of split cell.
const { newCellsSpan, updatedSpan } = breakSpanEvenly( colspan, numberOfCells );
updateNumericAttribute( 'colspan', updatedSpan, tableCell, writer );
// Each inserted cell will have the same attributes:
const newCellsAttributes = {};
// Do not store default value in the model.
if ( newCellsSpan > 1 ) {
newCellsAttributes.colspan = newCellsSpan;
}
// Copy rowspan of split cell.
if ( rowspan > 1 ) {
newCellsAttributes.rowspan = rowspan;
}
const cellsToInsert = colspan > numberOfCells ? numberOfCells - 1 : colspan - 1;
createCells( cellsToInsert, writer, writer.createPositionAfter( tableCell ), newCellsAttributes );
}
// Second check - the cell has colspan of 1 or we need to create more cells then the currently one spans over.
if ( colspan < numberOfCells ) {
const cellsToInsert = numberOfCells - colspan;
// First step: expand cells on the same column as split cell.
const tableMap = [ ...new TableWalker( table ) ];
// Get the column index of split cell.
const { column: splitCellColumn } = tableMap.find( ( { cell } ) => cell === tableCell );
// Find cells which needs to be expanded vertically - those on the same column or those that spans over split cell's column.
const cellsToUpdate = tableMap.filter( ( { cell, cellWidth, column } ) => {
const isOnSameColumn = cell !== tableCell && column === splitCellColumn;
const spansOverColumn = ( column < splitCellColumn && column + cellWidth > splitCellColumn );
return isOnSameColumn || spansOverColumn;
} );
// Expand cells vertically.
for ( const { cell, cellWidth } of cellsToUpdate ) {
writer.setAttribute( 'colspan', cellWidth + cellsToInsert, cell );
}
// Second step: create columns after split cell.
// Each inserted cell will have the same attributes:
const newCellsAttributes = {};
// Do not store default value in the model.
// Copy rowspan of split cell.
if ( rowspan > 1 ) {
newCellsAttributes.rowspan = rowspan;
}
createCells( cellsToInsert, writer, writer.createPositionAfter( tableCell ), newCellsAttributes );
const headingColumns = table.getAttribute( 'headingColumns' ) || 0;
// Update heading section if split cell is in heading section.
if ( headingColumns > splitCellColumn ) {
updateNumericAttribute( 'headingColumns', headingColumns + cellsToInsert, table, writer );
}
}
} );
}
/**
* Divides a table cell horizontally into several ones.
*
* The cell will be visually split into more cells by updating rowspans of other cells in the row and inserting rows with a single cell
* below.
*
* If in the table below cell "b" is split into 3 cells:
*
* +---+---+---+
* | a | b | c |
* +---+---+---+
* | d | e | f |
* +---+---+---+
*
* It will result in the table below:
*
* +---+---+---+
* | a | b | c |
* + +---+ +
* | | | |
* + +---+ +
* | | | |
* +---+---+---+
* | d | e | f |
* +---+---+---+
*
* So cells "a" and "b" will get their `rowspan` updated to `3` and 2 rows with a single cell will be added.
*
* Splitting a cell that already has a `rowspan` attribute set will distribute the cell `rowspan` evenly and the remainder
* will be left to the original cell:
*
* +---+---+---+
* | a | b | c |
* + +---+---+
* | | d | e |
* + +---+---+
* | | f | g |
* + +---+---+
* | | h | i |
* +---+---+---+
*
* Splitting cell "a" with `rowspan=4` into 3 cells will create 2 cells with a `rowspan=1` and cell "a" will have `rowspan=2`:
*
* +---+---+---+
* | a | b | c |
* + +---+---+
* | | d | e |
* +---+---+---+
* | | f | g |
* +---+---+---+
* | | h | i |
* +---+---+---+
*
* @param {module:engine/model/element~Element} tableCell
* @param {Number} numberOfCells
*/
splitCellHorizontally( tableCell, numberOfCells = 2 ) {
const model = this.editor.model;
const tableRow = tableCell.parent;
const table = tableRow.parent;
const splitCellRow = table.getChildIndex( tableRow );
const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 );
const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 );
model.change( writer => {
// First check - the cell spans over multiple rows so before doing anything else just split this cell.
if ( rowspan > 1 ) {
// Cache table map before updating table.
const tableMap = [ ...new TableWalker( table, {
startRow: splitCellRow,
endRow: splitCellRow + rowspan - 1,
includeAllSlots: true
} ) ];
// Get spans of new (inserted) cells and span to update of split cell.
const { newCellsSpan, updatedSpan } = breakSpanEvenly( rowspan, numberOfCells );
updateNumericAttribute( 'rowspan', updatedSpan, tableCell, writer );
const { column: cellColumn } = tableMap.find( ( { cell } ) => cell === tableCell );
// Each inserted cell will have the same attributes:
const newCellsAttributes = {};
// Do not store default value in the model.
if ( newCellsSpan > 1 ) {
newCellsAttributes.rowspan = newCellsSpan;
}
// Copy colspan of split cell.
if ( colspan > 1 ) {
newCellsAttributes.colspan = colspan;
}
for ( const tableSlot of tableMap ) {
const { column, row } = tableSlot;
// As both newly created cells and the split cell might have rowspan,
// the insertion of new cells must go to appropriate rows:
//
// 1. It's a row after split cell + it's height.
const isAfterSplitCell = row >= splitCellRow + updatedSpan;
// 2. Is on the same column.
const isOnSameColumn = column === cellColumn;
// 3. And it's row index is after previous cell height.
const isInEvenlySplitRow = ( row + splitCellRow + updatedSpan ) % newCellsSpan === 0;
if ( isAfterSplitCell && isOnSameColumn && isInEvenlySplitRow ) {
createCells( 1, writer, tableSlot.getPositionBefore(), newCellsAttributes );
}
}
}
// Second check - the cell has rowspan of 1 or we need to create more cells than the current cell spans over.
if ( rowspan < numberOfCells ) {
// We already split the cell in check one so here we split to the remaining number of cells only.
const cellsToInsert = numberOfCells - rowspan;
// This check is needed since we need to check if there are any cells from previous rows than spans over this cell's row.
const tableMap = [ ...new TableWalker( table, { startRow: 0, endRow: splitCellRow } ) ];
// First step: expand cells.
for ( const { cell, cellHeight, row } of tableMap ) {
// Expand rowspan of cells that are either:
// - on the same row as current cell,
// - or are below split cell row and overlaps that row.
if ( cell !== tableCell && row + cellHeight > splitCellRow ) {
const rowspanToSet = cellHeight + cellsToInsert;
writer.setAttribute( 'rowspan', rowspanToSet, cell );
}
}
// Second step: create rows with single cell below split cell.
const newCellsAttributes = {};
// Copy colspan of split cell.
if ( colspan > 1 ) {
newCellsAttributes.colspan = colspan;
}
createEmptyRows( writer, table, splitCellRow + 1, cellsToInsert, 1, newCellsAttributes );
// Update heading section if split cell is in heading section.
const headingRows = table.getAttribute( 'headingRows' ) || 0;
if ( headingRows > splitCellRow ) {
updateNumericAttribute( 'headingRows', headingRows + cellsToInsert, table, writer );
}
}
} );
}
/**
* Returns the number of columns for a given table.
*
* editor.plugins.get( 'TableUtils' ).getColumns( table );
*
* @param {module:engine/model/element~Element} table The table to analyze.
* @returns {Number}
*/
getColumns( table ) {
// Analyze first row only as all the rows should have the same width.
const row = table.getChild( 0 );
return [ ...row.getChildren() ].reduce( ( columns, row ) => {
const columnWidth = parseInt( row.getAttribute( 'colspan' ) || 1 );
return columns + columnWidth;
}, 0 );
}
/**
* Returns the number of rows for a given table.
*
* editor.plugins.get( 'TableUtils' ).getRows( table );
*
* @param {module:engine/model/element~Element} table The table to analyze.
* @returns {Number}
*/
getRows( table ) {
// Simple row counting, not including rowspan due to #6427.
return table.childCount;
}
}
// Creates empty rows at the given index in an existing table.
//
// @param {module:engine/model/writer~Writer} writer
// @param {module:engine/model/element~Element} table
// @param {Number} insertAt The row index of row insertion.
// @param {Number} rows The number of rows to create.
// @param {Number} tableCellToInsert The number of cells to insert in each row.
function createEmptyRows( writer, table, insertAt, rows, tableCellToInsert, attributes = {} ) {
for ( let i = 0; i < rows; i++ ) {
const tableRow = writer.createElement( 'tableRow' );
writer.insert( tableRow, table, insertAt );
createCells( tableCellToInsert, writer, writer.createPositionAt( tableRow, 'end' ), attributes );
}
}
// Creates cells at a given position.
//
// @param {Number} columns The number of columns to create
// @param {module:engine/model/writer~Writer} writer
// @param {module:engine/model/position~Position} insertPosition
function createCells( cells, writer, insertPosition, attributes = {} ) {
for ( let i = 0; i < cells; i++ ) {
createEmptyTableCell( writer, insertPosition, attributes );
}
}
// Evenly distributes the span of a cell to a number of provided cells.
// The resulting spans will always be integer values.
//
// For instance breaking a span of 7 into 3 cells will return:
//
// { newCellsSpan: 2, updatedSpan: 3 }
//
// as two cells will have a span of 2 and the remainder will go the first cell so its span will change to 3.
//
// @param {Number} span The span value do break.
// @param {Number} numberOfCells The number of resulting spans.
// @returns {{newCellsSpan: Number, updatedSpan: Number}}
function breakSpanEvenly( span, numberOfCells ) {
if ( span < numberOfCells ) {
return { newCellsSpan: 1, updatedSpan: 1 };
}
const newCellsSpan = Math.floor( span / numberOfCells );
const updatedSpan = ( span - newCellsSpan * numberOfCells ) + newCellsSpan;
return { newCellsSpan, updatedSpan };
}
// Updates heading columns attribute if removing a row from head section.
function adjustHeadingColumns( table, removedColumnIndexes, writer ) {
const headingColumns = table.getAttribute( 'headingColumns' ) || 0;
if ( headingColumns && removedColumnIndexes.first < headingColumns ) {
const headingsRemoved = Math.min( headingColumns - 1 /* Other numbers are 0-based */, removedColumnIndexes.last ) -
removedColumnIndexes.first + 1;
writer.setAttribute( 'headingColumns', headingColumns - headingsRemoved, table );
}
}
// Calculates a new heading rows value for removing rows from heading section.
function updateHeadingRows( table, first, last, writer ) {
const headingRows = table.getAttribute( 'headingRows' ) || 0;
if ( first < headingRows ) {
const newRows = last < headingRows ? headingRows - ( last - first + 1 ) : first;
updateNumericAttribute( 'headingRows', newRows, table, writer, 0 );
}
}
// Finds cells that will be:
// - trimmed - Cells that are "above" removed rows sections and overlap the removed section - their rowspan must be trimmed.
// - moved - Cells from removed rows section might stick out of. These cells are moved to the next row after a removed section.
//
// Sample table with overlapping & sticking out cells:
//
// +----+----+----+----+----+
// | 00 | 01 | 02 | 03 | 04 |
// +----+ + + + +
// | 10 | | | | |
// +----+----+ + + +
// | 20 | 21 | | | | <-- removed row
// + + +----+ + +
// | | | 32 | | | <-- removed row
// +----+ + +----+ +
// | 40 | | | 43 | |
// +----+----+----+----+----+
//
// In a table above:
// - cells to trim: '02', '03' & '04'.
// - cells to move: '21' & '32'.
function getCellsToMoveAndTrimOnRemoveRow( table, first, last ) {
const cellsToMove = new Map();
const cellsToTrim = [];
for ( const { row, column, cellHeight, cell } of new TableWalker( table, { endRow: last } ) ) {
const lastRowOfCell = row + cellHeight - 1;
const isCellStickingOutFromRemovedRows = row >= first && row <= last && lastRowOfCell > last;
if ( isCellStickingOutFromRemovedRows ) {
const rowspanInRemovedSection = last - row + 1;
const rowSpanToSet = cellHeight - rowspanInRemovedSection;
cellsToMove.set( column, {
cell,
rowspan: rowSpanToSet
} );
}
const isCellOverlappingRemovedRows = row < first && lastRowOfCell >= first;
if ( isCellOverlappingRemovedRows ) {
let rowspanAdjustment;
// Cell fully covers removed section - trim it by removed rows count.
if ( lastRowOfCell >= last ) {
rowspanAdjustment = last - first + 1;
}
// Cell partially overlaps removed section - calculate cell's span that is in removed section.
else {
rowspanAdjustment = lastRowOfCell - first + 1;
}
cellsToTrim.push( {
cell,
rowspan: cellHeight - rowspanAdjustment
} );
}
}
return { cellsToMove, cellsToTrim };
}
function moveCellsToRow( table, targetRowIndex, cellsToMove, writer ) {
const tableWalker = new TableWalker( table, {
includeAllSlots: true,
row: targetRowIndex
} );
const tableRowMap = [ ...tableWalker ];
const row = table.getChild( targetRowIndex );
let previousCell;
for ( const { column, cell, isAnchor } of tableRowMap ) {
if ( cellsToMove.has( column ) ) {
const { cell: cellToMove, rowspan } = cellsToMove.get( column );
const targetPosition = previousCell ?
writer.createPositionAfter( previousCell ) :
writer.createPositionAt( row, 0 );
writer.move( writer.createRangeOn( cellToMove ), targetPosition );
updateNumericAttribute( 'rowspan', rowspan, cellToMove, writer );
previousCell = cellToMove;
} else if ( isAnchor ) {
// If cell is spanned then `cell` holds reference to overlapping cell. See ckeditor/ckeditor5#6502.
previousCell = cell;
}
}
}