@ckeditor/ckeditor5-table
Advanced tools
Comparing version 23.0.0 to 23.1.0
{ | ||
"name": "@ckeditor/ckeditor5-table", | ||
"version": "23.0.0", | ||
"version": "23.1.0", | ||
"description": "Table feature for CKEditor 5.", | ||
@@ -13,22 +13,22 @@ "keywords": [ | ||
"dependencies": { | ||
"@ckeditor/ckeditor5-core": "^23.0.0", | ||
"@ckeditor/ckeditor5-ui": "^23.0.0", | ||
"@ckeditor/ckeditor5-widget": "^23.0.0", | ||
"@ckeditor/ckeditor5-core": "^23.1.0", | ||
"@ckeditor/ckeditor5-ui": "^23.1.0", | ||
"@ckeditor/ckeditor5-widget": "^23.1.0", | ||
"lodash-es": "^4.17.15" | ||
}, | ||
"devDependencies": { | ||
"@ckeditor/ckeditor5-alignment": "^23.0.0", | ||
"@ckeditor/ckeditor5-block-quote": "^23.0.0", | ||
"@ckeditor/ckeditor5-clipboard": "^23.0.0", | ||
"@ckeditor/ckeditor5-editor-classic": "^23.0.0", | ||
"@ckeditor/ckeditor5-engine": "^23.0.0", | ||
"@ckeditor/ckeditor5-horizontal-line": "^23.0.0", | ||
"@ckeditor/ckeditor5-image": "^23.0.0", | ||
"@ckeditor/ckeditor5-indent": "^23.0.0", | ||
"@ckeditor/ckeditor5-list": "^23.0.0", | ||
"@ckeditor/ckeditor5-media-embed": "^23.0.0", | ||
"@ckeditor/ckeditor5-paragraph": "^23.0.0", | ||
"@ckeditor/ckeditor5-typing": "^23.0.0", | ||
"@ckeditor/ckeditor5-undo": "^23.0.0", | ||
"@ckeditor/ckeditor5-utils": "^23.0.0", | ||
"@ckeditor/ckeditor5-alignment": "^23.1.0", | ||
"@ckeditor/ckeditor5-block-quote": "^23.1.0", | ||
"@ckeditor/ckeditor5-clipboard": "^23.1.0", | ||
"@ckeditor/ckeditor5-editor-classic": "^23.1.0", | ||
"@ckeditor/ckeditor5-engine": "^23.1.0", | ||
"@ckeditor/ckeditor5-horizontal-line": "^23.1.0", | ||
"@ckeditor/ckeditor5-image": "^23.1.0", | ||
"@ckeditor/ckeditor5-indent": "^23.1.0", | ||
"@ckeditor/ckeditor5-list": "^23.1.0", | ||
"@ckeditor/ckeditor5-media-embed": "^23.1.0", | ||
"@ckeditor/ckeditor5-paragraph": "^23.1.0", | ||
"@ckeditor/ckeditor5-typing": "^23.1.0", | ||
"@ckeditor/ckeditor5-undo": "^23.1.0", | ||
"@ckeditor/ckeditor5-utils": "^23.1.0", | ||
"json-diff": "^0.5.4" | ||
@@ -35,0 +35,0 @@ }, |
@@ -11,3 +11,3 @@ /** | ||
import TableWalker from './../tablewalker'; | ||
import { toWidget, toWidgetEditable, setHighlightHandling } from '@ckeditor/ckeditor5-widget/src/utils'; | ||
import { setHighlightHandling, toWidget, toWidgetEditable } from '@ckeditor/ckeditor5-widget/src/utils'; | ||
@@ -239,2 +239,50 @@ /** | ||
/** | ||
* Overrides paragraph inside table cell conversion. | ||
* | ||
* This converter: | ||
* * should be used to override default paragraph conversion in the editing view. | ||
* * It will only convert <paragraph> placed directly inside <tableCell>. | ||
* * For a single paragraph without attributes it returns `<span>` to simulate data table. | ||
* * For all other cases it returns `<p>` element. | ||
* | ||
* @param {module:engine/model/element~Element} modelElement | ||
* @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi | ||
* @returns {module:engine/view/containerelement~ContainerElement|undefined} | ||
*/ | ||
export function convertParagraphInTableCell( modelElement, conversionApi ) { | ||
const { writer } = conversionApi; | ||
if ( !modelElement.parent.is( 'element', 'tableCell' ) ) { | ||
return; | ||
} | ||
if ( isSingleParagraphWithoutAttributes( modelElement ) ) { | ||
// Use display:inline-block to force Chrome/Safari to limit text mutations to this element. | ||
// See #6062. | ||
return writer.createContainerElement( 'span', { style: 'display:inline-block' } ); | ||
} else { | ||
return writer.createContainerElement( 'p' ); | ||
} | ||
} | ||
/** | ||
* Checks if given model `<paragraph>` is an only child of a parent (`<tableCell>`) and if it has any attribute set. | ||
* | ||
* The paragraph should be converted in the editing view to: | ||
* | ||
* * If returned `true` - to a `<span style="display:inline-block">` | ||
* * If returned `false` - to a `<p>` | ||
* | ||
* @param {module:engine/model/element~Element} modelElement | ||
* @returns {Boolean} | ||
*/ | ||
export function isSingleParagraphWithoutAttributes( modelElement ) { | ||
const tableCell = modelElement.parent; | ||
const isSingleParagraph = tableCell.childCount === 1; | ||
return isSingleParagraph && !hasAnyAttribute( modelElement ); | ||
} | ||
// Converts a given {@link module:engine/view/element~Element} to a table widget: | ||
@@ -332,23 +380,11 @@ // * Adds a {@link module:engine/view/element~Element#_setCustomProperty custom property} allowing to recognize the table widget element. | ||
if ( isSingleParagraph && !hasAnyAttribute( firstChild ) ) { | ||
conversionApi.mapper.bindElements( tableCell, cellElement ); | ||
// Additional requirement for data pipeline to have backward compatible data tables. | ||
if ( !asWidget && !hasAnyAttribute( firstChild ) && isSingleParagraph ) { | ||
const innerParagraph = tableCell.getChild( 0 ); | ||
const paragraphInsertPosition = conversionApi.writer.createPositionAt( cellElement, 'end' ); | ||
conversionApi.consumable.consume( innerParagraph, 'insert' ); | ||
if ( asWidget ) { | ||
// Use display:inline-block to force Chrome/Safari to limit text mutations to this element. | ||
// See #6062. | ||
const fakeParagraph = conversionApi.writer.createContainerElement( 'span', { style: 'display:inline-block' } ); | ||
conversionApi.mapper.bindElements( innerParagraph, fakeParagraph ); | ||
conversionApi.writer.insert( paragraphInsertPosition, fakeParagraph ); | ||
conversionApi.mapper.bindElements( tableCell, cellElement ); | ||
} else { | ||
conversionApi.mapper.bindElements( tableCell, cellElement ); | ||
conversionApi.mapper.bindElements( innerParagraph, cellElement ); | ||
} | ||
} else { | ||
conversionApi.mapper.bindElements( tableCell, cellElement ); | ||
conversionApi.mapper.bindElements( innerParagraph, cellElement ); | ||
} | ||
@@ -355,0 +391,0 @@ } |
@@ -10,2 +10,4 @@ /** | ||
import { isSingleParagraphWithoutAttributes } from './downcast'; | ||
/** | ||
@@ -21,85 +23,55 @@ * Injects a table cell post-fixer into the model which marks the table cell in the differ to have it re-rendered. | ||
* @param {module:engine/model/model~Model} model | ||
* @param {module:engine/conversion/mapper~Mapper} mapper | ||
*/ | ||
export default function injectTableCellRefreshPostFixer( model ) { | ||
model.document.registerPostFixer( () => tableCellRefreshPostFixer( model ) ); | ||
export default function injectTableCellRefreshPostFixer( model, mapper ) { | ||
model.document.registerPostFixer( () => tableCellRefreshPostFixer( model.document.differ, mapper ) ); | ||
} | ||
function tableCellRefreshPostFixer( model ) { | ||
const differ = model.document.differ; | ||
function tableCellRefreshPostFixer( differ, mapper ) { | ||
// Stores cells to be refreshed, so the table cell will be refreshed once for multiple changes. | ||
// Stores cells to be refreshed so the table cell will be refreshed once for multiple changes. | ||
const cellsToRefresh = new Set(); | ||
// 1. Gather all changes inside table cell. | ||
const cellsToCheck = new Set(); | ||
// Counting the paragraph inserts to verify if it increased to more than one paragraph in the current differ. | ||
let insertCount = 0; | ||
for ( const change of differ.getChanges() ) { | ||
const parent = change.type == 'insert' || change.type == 'remove' ? change.position.parent : change.range.start.parent; | ||
const parent = change.type == 'attribute' ? change.range.start.parent : change.position.parent; | ||
if ( !parent.is( 'element', 'tableCell' ) ) { | ||
continue; | ||
if ( parent.is( 'element', 'tableCell' ) ) { | ||
cellsToCheck.add( parent ); | ||
} | ||
if ( change.type == 'insert' ) { | ||
insertCount++; | ||
} | ||
if ( checkRefresh( parent, change.type, insertCount ) ) { | ||
cellsToRefresh.add( parent ); | ||
} | ||
} | ||
if ( cellsToRefresh.size ) { | ||
// @if CK_DEBUG_TABLE // console.log( `Post-fixing table: refreshing cells (${ cellsToRefresh.size }).` ); | ||
// @if CK_DEBUG_TABLE // console.log( `Post-fixing table: Checking table cell to refresh (${ cellsToCheck.size }).` ); | ||
// @if CK_DEBUG_TABLE // let paragraphsRefreshed = 0; | ||
for ( const tableCell of cellsToRefresh.values() ) { | ||
differ.refreshItem( tableCell ); | ||
for ( const tableCell of cellsToCheck.values() ) { | ||
for ( const paragraph of [ ...tableCell.getChildren() ].filter( child => shouldRefresh( child, mapper ) ) ) { | ||
// @if CK_DEBUG_TABLE // console.log( `Post-fixing table: refreshing paragraph in table cell (${++paragraphsRefreshed}).` ); | ||
differ.refreshItem( paragraph ); | ||
} | ||
return true; | ||
} | ||
// Always return false to prevent the refresh post-fixer from re-running on the same set of changes and going into an infinite loop. | ||
// This "post-fixer" does not change the model structure so there shouldn't be need to run other post-fixers again. | ||
// See https://github.com/ckeditor/ckeditor5/issues/1936 & https://github.com/ckeditor/ckeditor5/issues/8200. | ||
return false; | ||
} | ||
// Checks if the model table cell requires refreshing to be re-rendered to a proper state in the view. | ||
// Check if given model element needs refreshing. | ||
// | ||
// This method detects changes that will require renaming `<span>` to `<p>` (or vice versa) in the view. | ||
// | ||
// This method is a simple heuristic that checks only a single change and will sometimes give a false positive result when multiple changes | ||
// will result in a state that does not require renaming in the view (but will be seen as requiring a refresh). | ||
// | ||
// For instance: A `<span>` should be renamed to `<p>` when adding an attribute to a `<paragraph>`. | ||
// But adding one attribute and removing another one will result in a false positive: the check for an added attribute will see one | ||
// attribute on a paragraph and will falsely qualify such change as adding an attribute to a paragraph without any attribute. | ||
// | ||
// @param {module:engine/model/element~Element} tableCell The table cell to check. | ||
// @param {String} type Type of change. | ||
// @param {Number} insertCount The number of inserts in differ. | ||
function checkRefresh( tableCell, type, insertCount ) { | ||
const hasInnerParagraph = Array.from( tableCell.getChildren() ).some( child => child.is( 'element', 'paragraph' ) ); | ||
// If there is no paragraph in table cell then the view doesn't require refreshing. | ||
// | ||
// Why? What we really want to achieve is to make all the old paragraphs (which weren't added in this batch) to be | ||
// converted once again, so that the paragraph-in-table-cell converter can correctly create a `<p>` or a `<span>` element. | ||
// If there are no paragraphs in the table cell, we don't care. | ||
if ( !hasInnerParagraph ) { | ||
// @param {module:engine/model/element~Element} modelElement | ||
// @param {module:engine/conversion/mapper~Mapper} mapper | ||
// @returns {Boolean} | ||
function shouldRefresh( child, mapper ) { | ||
if ( !child.is( 'element', 'paragraph' ) ) { | ||
return false; | ||
} | ||
// For attribute change we only refresh if there is a single paragraph as in this case we may want to change existing `<span>` to `<p>`. | ||
if ( type == 'attribute' ) { | ||
const attributesCount = Array.from( tableCell.getChild( 0 ).getAttributeKeys() ).length; | ||
const viewElement = mapper.toViewElement( child ); | ||
return tableCell.childCount === 1 && attributesCount < 2; | ||
if ( !viewElement ) { | ||
return false; | ||
} | ||
// For other changes (insert, remove) the `<span>` to `<p>` change is needed when: | ||
// | ||
// - another element is added to a single paragraph (childCount becomes >= 2) | ||
// - another element is removed and a single paragraph is left (childCount == 1) | ||
// | ||
// Change is not needed if there were multiple blocks before change. | ||
return tableCell.childCount <= ( type == 'insert' ? insertCount + 1 : 1 ); | ||
return isSingleParagraphWithoutAttributes( child ) !== viewElement.is( 'element', 'span' ); | ||
} |
@@ -47,2 +47,3 @@ /** | ||
for ( const table of tablesToRefresh.values() ) { | ||
// Should be handled by a `triggerBy` configuration. See: https://github.com/ckeditor/ckeditor5/issues/8138. | ||
differ.refreshItem( table ); | ||
@@ -49,0 +50,0 @@ } |
@@ -87,27 +87,2 @@ /** | ||
/** | ||
* A converter that ensures an empty paragraph is inserted in a table cell if no other content was converted. | ||
* | ||
* @returns {Function} Conversion helper. | ||
*/ | ||
export function ensureParagraphInTableCell( elementName ) { | ||
return dispatcher => { | ||
dispatcher.on( `element:${ elementName }`, ( evt, data, conversionApi ) => { | ||
// The default converter will create a model range on converted table cell. | ||
if ( !data.modelRange ) { | ||
return; | ||
} | ||
const tableCell = data.modelRange.start.nodeAfter; | ||
// Ensure a paragraph in the model for empty table cells for converted table cells. | ||
if ( !tableCell.childCount ) { | ||
const modelCursor = conversionApi.writer.createPositionAt( tableCell, 0 ); | ||
conversionApi.writer.insertElement( 'paragraph', modelCursor ); | ||
} | ||
}, { priority: 'low' } ); | ||
}; | ||
} | ||
// Scans table rows and extracts required metadata from the table: | ||
@@ -114,0 +89,0 @@ // |
@@ -59,2 +59,4 @@ /** | ||
this.listenTo( editor.model, 'insertContent', ( evt, args ) => this._onInsertContent( evt, ...args ), { priority: 'high' } ); | ||
this.decorate( '_replaceTableSlotCell' ); | ||
} | ||
@@ -168,3 +170,3 @@ | ||
const cellsToSelect = replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selectedTable, selection, writer ); | ||
const cellsToSelect = this._replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selectedTable, selection, writer ); | ||
@@ -183,4 +185,187 @@ if ( this.editor.plugins.get( 'TableSelection' ).isEnabled ) { | ||
} | ||
/** | ||
* 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. | ||
@@ -252,105 +437,2 @@ // | ||
// 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. | ||
@@ -376,46 +458,2 @@ 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. | ||
@@ -422,0 +460,0 @@ // |
@@ -12,4 +12,5 @@ /** | ||
import upcastTable, { ensureParagraphInTableCell, skipEmptyTableRow } from './converters/upcasttable'; | ||
import upcastTable, { skipEmptyTableRow } from './converters/upcasttable'; | ||
import { | ||
convertParagraphInTableCell, | ||
downcastInsertCell, | ||
@@ -110,7 +111,12 @@ downcastInsertRow, | ||
conversion.for( 'upcast' ).elementToElement( { model: 'tableCell', view: 'th' } ); | ||
conversion.for( 'upcast' ).add( ensureParagraphInTableCell( 'td' ) ); | ||
conversion.for( 'upcast' ).add( ensureParagraphInTableCell( 'th' ) ); | ||
conversion.for( 'editingDowncast' ).add( downcastInsertCell() ); | ||
// Duplicates code - needed to properly refresh paragraph inside table cell. | ||
editor.conversion.for( 'editingDowncast' ).elementToElement( { | ||
model: 'paragraph', | ||
view: convertParagraphInTableCell, | ||
converterPriority: 'high' | ||
} ); | ||
// Table attributes conversion. | ||
@@ -151,3 +157,3 @@ conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } ); | ||
injectTableLayoutPostFixer( model ); | ||
injectTableCellRefreshPostFixer( model ); | ||
injectTableCellRefreshPostFixer( model, editor.editing.mapper ); | ||
injectTableCellParagraphPostFixer( model ); | ||
@@ -154,0 +160,0 @@ } |
@@ -30,2 +30,10 @@ /** | ||
/** | ||
* @inheritDoc | ||
*/ | ||
init() { | ||
this.decorate( 'insertColumns' ); | ||
this.decorate( 'insertRows' ); | ||
} | ||
/** | ||
* Returns the table cell location as an object with table row and table column indexes. | ||
@@ -32,0 +40,0 @@ * |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
867496
134
11033