Socket
Socket
Sign inDemoInstall

@ckeditor/ckeditor5-table

Package Overview
Dependencies
Maintainers
1
Versions
607
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ckeditor/ckeditor5-table - npm Package Compare versions

Comparing version 10.1.0 to 11.0.0

lang/translations/ar.po

35

CHANGELOG.md
Changelog
=========
## [11.0.0](https://github.com/ckeditor/ckeditor5-table/compare/v10.1.0...v11.0.0) (2018-10-08)
### Features
* Implemented the table post–fixer which bulletproofs the feature in various complex use–cases (e.g. pasting and real-time collaborative editing). Closes [#13](https://github.com/ckeditor/ckeditor5-table/issues/13). ([01f9a3b](https://github.com/ckeditor/ckeditor5-table/commit/01f9a3b))
* Introduced the toolbar for the table widget (previously it was available only for single cells). Changed the toolbar configuration option from `table.toolbar` to `table.contentToolbar`. Closes [#113](https://github.com/ckeditor/ckeditor5-table/issues/113). Closes [#106](https://github.com/ckeditor/ckeditor5-table/issues/106). ([9f9486d](https://github.com/ckeditor/ckeditor5-table/commit/9f9486d))
Other: `config.table.toolbar` is marked as depracted. Use `config.table.contentToolbar` instead.
* Introduced a support for block content inside tables. Closes [#56](https://github.com/ckeditor/ckeditor5-table/issues/56). ([cdf718e](https://github.com/ckeditor/ckeditor5-table/commit/cdf718e))
### Bug fixes
* A table cell should always have a `<paragraph>` in the model. Closes [#125](https://github.com/ckeditor/ckeditor5-table/issues/125). ([1eb5d6d](https://github.com/ckeditor/ckeditor5-table/commit/1eb5d6d))
* Downcast converter for table attributes should work with not converted child elements. Closes [#92](https://github.com/ckeditor/ckeditor5-table/issues/92). ([a3ea18d](https://github.com/ckeditor/ckeditor5-table/commit/a3ea18d))
* Merging down rowspanned cell from the head with a cell in the body is now disabled. Closes [#86](https://github.com/ckeditor/ckeditor5-table/issues/86). ([cb77e38](https://github.com/ckeditor/ckeditor5-table/commit/cb77e38))
* The upcast conversion will now properly parse inline content in table cell into single paragraph. Closes [ckeditor/ckeditor5#1246](https://github.com/ckeditor/ckeditor5/issues/1246). ([ea1e16d](https://github.com/ckeditor/ckeditor5-table/commit/ea1e16d))
* Toggling headers should always include the column or row the selection is anchored to. Closes [#34](https://github.com/ckeditor/ckeditor5-table/issues/34). ([bce6766](https://github.com/ckeditor/ckeditor5-table/commit/bce6766))
### Other changes
* Aligned `TableToolbar` to the widget toolbar repository. Closes [#107](https://github.com/ckeditor/ckeditor5-table/issues/107). ([e276e66](https://github.com/ckeditor/ckeditor5-table/commit/e276e66))
* Media should not be allowed inside table cells for now. Closes [#124](https://github.com/ckeditor/ckeditor5-table/issues/124). ([2f2fe4a](https://github.com/ckeditor/ckeditor5-table/commit/2f2fe4a))
* Table feature should insert table the same way as other widget features do. Closes [#27](https://github.com/ckeditor/ckeditor5-table/issues/27). ([77d96a4](https://github.com/ckeditor/ckeditor5-table/commit/77d96a4))
* The table cell view post-fixer should use changed elements from the view to make fixes. Closes [#130](https://github.com/ckeditor/ckeditor5-table/issues/130). ([efc53c9](https://github.com/ckeditor/ckeditor5-table/commit/efc53c9))
* Updated the table icon which used to feel bulky with a lighter design. Closes [#117](https://github.com/ckeditor/ckeditor5-table/issues/117). ([cd6f5ff](https://github.com/ckeditor/ckeditor5-table/commit/cd6f5ff))
* Updated translations. ([de47767](https://github.com/ckeditor/ckeditor5-table/commit/de47767))
### BREAKING CHANGES
* The `config.table.toolbar` was renamed to `config.table.contentToolbar`.
* The `injectTablePostFixer()` function from `table/converters/table-post-fixer` is now `injectTableLayoutPostFixer()`and is moved to `table/converters/table-layout-post-fixer` module.
* The `TableUtils#createTable()` method now accepts model `Writer` instance instead of `Position`. The method no longer inserts created table to the model - use returned value instead.
* Removed `table/commands/utils~getParentTable()` method. Use `table/commands/utils~findAncestor()` instead.
## [10.1.0](https://github.com/ckeditor/ckeditor5-table/compare/v10.0.0...v10.1.0) (2018-07-18)

@@ -5,0 +40,0 @@

28

package.json
{
"name": "@ckeditor/ckeditor5-table",
"version": "10.1.0",
"version": "11.0.0",
"description": "Table feature for CKEditor 5.",

@@ -9,15 +9,23 @@ "keywords": [

"ckeditor 5",
"ckeditor5-feature"
"ckeditor5-feature",
"ckeditor5-plugin"
],
"dependencies": {
"@ckeditor/ckeditor5-core": "^11.0.0",
"@ckeditor/ckeditor5-engine": "^10.2.0",
"@ckeditor/ckeditor5-ui": "^11.0.0",
"@ckeditor/ckeditor5-widget": "^10.2.0"
"@ckeditor/ckeditor5-core": "^11.0.1",
"@ckeditor/ckeditor5-engine": "^11.0.0",
"@ckeditor/ckeditor5-ui": "^11.1.0",
"@ckeditor/ckeditor5-widget": "^10.3.0"
},
"devDependencies": {
"@ckeditor/ckeditor5-editor-classic": "^11.0.0",
"@ckeditor/ckeditor5-paragraph": "^10.0.2",
"@ckeditor/ckeditor5-utils": "^10.2.0",
"eslint": "^4.15.0",
"@ckeditor/ckeditor5-alignment": "^10.0.3",
"@ckeditor/ckeditor5-block-quote": "^10.1.0",
"@ckeditor/ckeditor5-clipboard": "^10.0.3",
"@ckeditor/ckeditor5-editor-classic": "^11.0.1",
"@ckeditor/ckeditor5-image": "^11.0.0",
"@ckeditor/ckeditor5-list": "^11.0.2",
"@ckeditor/ckeditor5-media-embed": "^10.0.0",
"@ckeditor/ckeditor5-paragraph": "^10.0.3",
"@ckeditor/ckeditor5-undo": "^10.0.3",
"@ckeditor/ckeditor5-utils": "^11.0.0",
"eslint": "^5.5.0",
"eslint-config-ckeditor5": "^1.0.7",

@@ -24,0 +32,0 @@ "husky": "^0.14.3",

@@ -15,5 +15,9 @@ CKEditor 5 table feature

## Demo
Check out the [demo in the Table feature](https://ckeditor.com/docs/ckeditor5/latest/features/table.html#demo) guide.
## Documentation
See the [`@ckeditor/ckeditor5-table` package](https://docs.ckeditor.com/ckeditor5/latest/api/table.html) page in [CKEditor 5 documentation](https://docs.ckeditor.com/ckeditor5/latest/).
See the [`@ckeditor/ckeditor5-table` package](https://ckeditor.com/docs/ckeditor5/latest/api/table.html) page in [CKEditor 5 documentation](https://ckeditor.com/docs/ckeditor5/latest/).

@@ -20,0 +24,0 @@ ## License

@@ -11,3 +11,3 @@ /**

import Command from '@ckeditor/ckeditor5-core/src/command';
import { getParentTable } from './utils';
import { findAncestor } from './utils';
import TableUtils from '../tableutils';

@@ -58,3 +58,3 @@

const tableParent = getParentTable( selection.getFirstPosition() );
const tableParent = findAncestor( 'table', selection.getFirstPosition() );

@@ -77,5 +77,7 @@ this.isEnabled = !!tableParent;

const table = getParentTable( selection.getFirstPosition() );
const tableCell = selection.getFirstPosition().parent;
const firstPosition = selection.getFirstPosition();
const tableCell = findAncestor( 'tableCell', firstPosition );
const table = tableCell.parent.parent;
const { column } = tableUtils.getCellLocation( tableCell );

@@ -82,0 +84,0 @@ const insertAt = this.order === 'after' ? column + 1 : column;

@@ -11,3 +11,3 @@ /**

import Command from '@ckeditor/ckeditor5-core/src/command';
import { getParentTable } from './utils';
import { findAncestor } from './utils';
import TableUtils from '../tableutils';

@@ -58,3 +58,3 @@

const tableParent = getParentTable( selection.getFirstPosition() );
const tableParent = findAncestor( 'table', selection.getFirstPosition() );

@@ -76,6 +76,7 @@ this.isEnabled = !!tableParent;

const tableCell = selection.getFirstPosition().parent;
const table = getParentTable( selection.getFirstPosition() );
const tableCell = findAncestor( 'tableCell', selection.getFirstPosition() );
const tableRow = tableCell.parent;
const table = tableRow.parent;
const row = table.getChildIndex( tableCell.parent );
const row = table.getChildIndex( tableRow );
const insertAt = this.order === 'below' ? row + 1 : row;

@@ -82,0 +83,0 @@

@@ -12,2 +12,3 @@ /**

import Position from '@ckeditor/ckeditor5-engine/src/model/position';
import { findOptimalInsertionPosition } from '@ckeditor/ckeditor5-widget/src/utils';
import TableUtils from '../tableutils';

@@ -58,11 +59,10 @@

const firstPosition = selection.getFirstPosition();
const insertPosition = findOptimalInsertionPosition( selection );
const isRoot = firstPosition.parent === firstPosition.root;
const insertPosition = isRoot ? Position.createAt( firstPosition ) : Position.createAfter( firstPosition.parent );
model.change( writer => {
const table = tableUtils.createTable( insertPosition, rows, columns );
const table = tableUtils.createTable( writer, rows, columns );
writer.setSelection( Position.createAt( table.getChild( 0 ).getChild( 0 ) ) );
model.insertContent( table, insertPosition );
writer.setSelection( Position.createAt( table.getChild( 0 ).getChild( 0 ).getChild( 0 ) ) );
} );

@@ -69,0 +69,0 @@ }

@@ -14,3 +14,3 @@ /**

import TableWalker from '../tablewalker';
import { updateNumericAttribute } from './utils';
import { findAncestor, updateNumericAttribute } from './utils';
import TableUtils from '../tableutils';

@@ -87,3 +87,3 @@

const doc = model.document;
const tableCell = doc.selection.getFirstPosition().parent;
const tableCell = findAncestor( 'tableCell', doc.selection.getFirstPosition() );
const cellToMerge = this.value;

@@ -102,5 +102,3 @@ const direction = this.direction;

// Remove table cell and merge it contents with merged cell.
writer.move( Range.createIn( cellToRemove ), Position.createAt( cellToExpand, 'end' ) );
writer.remove( cellToRemove );
mergeTableCells( cellToRemove, cellToExpand, writer );

@@ -131,5 +129,5 @@ const spanAttribute = this.isHorizontal ? 'colspan' : 'rowspan';

const doc = model.document;
const element = doc.selection.getFirstPosition().parent;
const tableCell = findAncestor( 'tableCell', doc.selection.getFirstPosition() );
if ( !element.is( 'tableCell' ) ) {
if ( !tableCell ) {
return;

@@ -142,4 +140,4 @@ }

const cellToMerge = this.isHorizontal ?
getHorizontalCell( element, this.direction, tableUtils ) :
getVerticalCell( element, this.direction );
getHorizontalCell( tableCell, this.direction, tableUtils ) :
getVerticalCell( tableCell, this.direction );

@@ -152,3 +150,3 @@ if ( !cellToMerge ) {

const spanAttribute = this.isHorizontal ? 'rowspan' : 'colspan';
const span = parseInt( element.getAttribute( spanAttribute ) || 1 );
const span = parseInt( tableCell.getAttribute( spanAttribute ) || 1 );

@@ -208,6 +206,10 @@ const cellToMergeSpan = parseInt( cellToMerge.getAttribute( spanAttribute ) || 1 );

const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 );
const headingRows = table.getAttribute( 'headingRows' ) || 0;
const isMergeWithBodyCell = direction == 'down' && ( rowIndex + rowspan ) === headingRows;
const isMergeWithHeadCell = direction == 'up' && rowIndex === headingRows;
// Don't search for mergeable cell if direction points out of the current table section.
if ( headingRows && ( ( direction == 'down' && rowIndex === headingRows - 1 ) || ( direction == 'up' && rowIndex === headingRows ) ) ) {
if ( headingRows && ( isMergeWithBodyCell || isMergeWithHeadCell ) ) {
return;

@@ -260,1 +262,29 @@ }

}
// Merges two table cells - will ensure that after merging cells with empty paragraph the result table cell will only have one paragraph.
// If one of the merged table cell is empty the merged table cell will have contents of the non-empty table cell.
// If both are empty the merged table cell will have only one empty paragraph.
//
// @param {module:engine/model/element~Element} cellToRemove
// @param {module:engine/model/element~Element} cellToExpand
// @param {module:engine/model/writer~Writer} writer
function mergeTableCells( cellToRemove, cellToExpand, writer ) {
if ( !isEmpty( cellToRemove ) ) {
if ( isEmpty( cellToExpand ) ) {
writer.remove( Range.createIn( cellToExpand ) );
}
writer.move( Range.createIn( cellToRemove ), Position.createAt( cellToExpand, 'end' ) );
}
// Remove merged table cell.
writer.remove( cellToRemove );
}
// Checks if passed table cell contains empty paragraph.
//
// @param {module:engine/model/element~Element} tableCell
// @returns {Boolean}
function isEmpty( tableCell ) {
return tableCell.childCount == 1 && tableCell.getChild( 0 ).is( 'paragraph' ) && tableCell.getChild( 0 ).isEmpty;
}

@@ -14,3 +14,3 @@ /**

import TableUtils from '../tableutils';
import { updateNumericAttribute } from './utils';
import { findAncestor, updateNumericAttribute } from './utils';

@@ -37,5 +37,5 @@ /**

const selectedElement = selection.getFirstPosition().parent;
const tableCell = findAncestor( 'tableCell', selection.getFirstPosition() );
this.isEnabled = selectedElement.is( 'tableCell' ) && tableUtils.getColumns( selectedElement.parent.parent ) > 1;
this.isEnabled = !!tableCell && tableUtils.getColumns( tableCell.parent.parent ) > 1;
}

@@ -52,3 +52,3 @@

const tableCell = firstPosition.parent;
const tableCell = findAncestor( 'tableCell', firstPosition );
const tableRow = tableCell.parent;

@@ -55,0 +55,0 @@ const table = tableRow.parent;

@@ -15,3 +15,3 @@ /**

import TableWalker from '../tablewalker';
import { updateNumericAttribute } from './utils';
import { findAncestor, updateNumericAttribute } from './utils';

@@ -37,5 +37,5 @@ /**

const element = doc.selection.getFirstPosition().parent;
const tableCell = findAncestor( 'tableCell', doc.selection.getFirstPosition() );
this.isEnabled = element.is( 'tableCell' ) && element.parent.parent.childCount > 1;
this.isEnabled = !!tableCell && tableCell.parent.parent.childCount > 1;
}

@@ -51,3 +51,3 @@

const firstPosition = selection.getFirstPosition();
const tableCell = firstPosition.parent;
const tableCell = findAncestor( 'tableCell', firstPosition );
const tableRow = tableCell.parent;

@@ -54,0 +54,0 @@ const table = tableRow.parent;

@@ -12,3 +12,3 @@ /**

import { getParentTable, updateNumericAttribute } from './utils';
import { findAncestor, updateNumericAttribute } from './utils';

@@ -40,5 +40,5 @@ /**

const position = selection.getFirstPosition();
const tableParent = getParentTable( position );
const tableCell = findAncestor( 'tableCell', position );
const isInTable = !!tableParent;
const isInTable = !!tableCell;

@@ -55,3 +55,3 @@ this.isEnabled = isInTable;

*/
this.value = isInTable && this._isInHeading( position.parent, tableParent );
this.value = isInTable && this._isInHeading( tableCell, tableCell.parent.parent );
}

@@ -75,3 +75,3 @@

const position = selection.getFirstPosition();
const tableCell = position.parent;
const tableCell = findAncestor( 'tableCell', position.parent );
const tableRow = tableCell.parent;

@@ -81,11 +81,8 @@ const table = tableRow.parent;

const currentHeadingColumns = parseInt( table.getAttribute( 'headingColumns' ) || 0 );
const { column: selectionColumn } = tableUtils.getCellLocation( tableCell );
let { column } = tableUtils.getCellLocation( tableCell );
const headingColumnsToSet = currentHeadingColumns > selectionColumn ? selectionColumn : selectionColumn + 1;
if ( column + 1 !== currentHeadingColumns ) {
column++;
}
model.change( writer => {
updateNumericAttribute( 'headingColumns', column, table, writer, 0 );
updateNumericAttribute( 'headingColumns', headingColumnsToSet, table, writer, 0 );
} );

@@ -92,0 +89,0 @@ }

@@ -13,3 +13,3 @@ /**

import { getParentTable, updateNumericAttribute } from './utils';
import { createEmptyTableCell, findAncestor, updateNumericAttribute } from './utils';
import TableWalker from '../tablewalker';

@@ -41,6 +41,5 @@

const position = selection.getFirstPosition();
const tableParent = getParentTable( position );
const tableCell = findAncestor( 'tableCell', position );
const isInTable = !!tableCell;
const isInTable = !!tableParent;
this.isEnabled = isInTable;

@@ -56,3 +55,3 @@

*/
this.value = isInTable && this._isInHeading( position.parent, tableParent );
this.value = isInTable && this._isInHeading( tableCell, tableCell.parent.parent );
}

@@ -75,3 +74,3 @@

const position = selection.getFirstPosition();
const tableCell = position.parent;
const tableCell = findAncestor( 'tableCell', position );
const tableRow = tableCell.parent;

@@ -81,20 +80,18 @@ const table = tableRow.parent;

const currentHeadingRows = table.getAttribute( 'headingRows' ) || 0;
let rowIndex = tableRow.index;
const selectionRow = tableRow.index;
if ( rowIndex + 1 !== currentHeadingRows ) {
rowIndex++;
}
const headingRowsToSet = currentHeadingRows > selectionRow ? selectionRow : selectionRow + 1;
model.change( writer => {
if ( rowIndex ) {
if ( headingRowsToSet ) {
// Changing heading rows requires to check if any of a heading cell is overlapping vertically the table head.
// Any table cell that has a rowspan attribute > 1 will not exceed the table head so we need to fix it in rows below.
const cellsToSplit = getOverlappingCells( table, rowIndex, currentHeadingRows );
const cellsToSplit = getOverlappingCells( table, headingRowsToSet, currentHeadingRows );
for ( const cell of cellsToSplit ) {
splitHorizontally( cell, rowIndex, writer );
splitHorizontally( cell, headingRowsToSet, writer );
}
}
updateNumericAttribute( 'headingRows', rowIndex, table, writer, 0 );
updateNumericAttribute( 'headingRows', headingRowsToSet, table, writer, 0 );
} );

@@ -180,4 +177,5 @@ }

const tableRow = table.getChild( row );
const tableCellPosition = Position.createFromParentAndOffset( tableRow, cellIndex );
writer.insertElement( 'tableCell', attributes, Position.createFromParentAndOffset( tableRow, cellIndex ) );
createEmptyTableCell( writer, tableCellPosition, attributes );
}

@@ -184,0 +182,0 @@ }

@@ -12,2 +12,3 @@ /**

import TableUtils from '../tableutils';
import { findAncestor } from './utils';

@@ -53,5 +54,5 @@ /**

const element = doc.selection.getFirstPosition().parent;
const tableCell = findAncestor( 'tableCell', doc.selection.getFirstPosition() );
this.isEnabled = element.is( 'tableCell' );
this.isEnabled = !!tableCell;
}

@@ -68,3 +69,3 @@

const firstPosition = selection.getFirstPosition();
const tableCell = firstPosition.parent;
const tableCell = findAncestor( 'tableCell', firstPosition );

@@ -71,0 +72,0 @@ const isHorizontally = this.direction === 'horizontally';

@@ -11,12 +11,13 @@ /**

/**
* Returns the parent table.
* Returns the parent element of given name. Returns undefined if position is not inside desired parent.
*
* @param {module:engine/model/position~Position} position
* @param {String} parentName Name of parent element to find.
* @param {module:engine/model/position~Position|module:engine/model/position~Position} position Position to start searching.
* @returns {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment}
*/
export function getParentTable( position ) {
export function findAncestor( parentName, position ) {
let parent = position.parent;
while ( parent ) {
if ( parent.name === 'table' ) {
if ( parent.name === parentName ) {
return parent;

@@ -45,1 +46,14 @@ }

}
/**
* Common method to create empty table cell - it will create proper model structure as table cell must have at least one block inside.
*
* @param {module:engine/model/writer~Writer} writer Model writer.
* @param {module:engine/model/position~Position} insertPosition Position at which table cell should be inserted.
* @param {Object} attributes Element's attributes.
*/
export function createEmptyTableCell( writer, insertPosition, attributes = {} ) {
const tableCell = writer.createElement( 'tableCell', attributes );
writer.insertElement( 'paragraph', tableCell );
writer.insert( tableCell, insertPosition );
}

@@ -77,3 +77,3 @@ /**

conversionApi.writer.insert( viewPosition, asWidget ? tableWidget : figureElement );
}, { priority: 'normal' } );
} );
}

@@ -121,3 +121,3 @@

}
}, { priority: 'normal' } );
} );
}

@@ -164,3 +164,3 @@

}
}, { priority: 'normal' } );
} );
}

@@ -242,3 +242,3 @@

}
}, { priority: 'normal' } );
} );
}

@@ -281,3 +281,3 @@

}
}, { priority: 'normal' } );
} );
}

@@ -315,4 +315,6 @@

// Renames a table cell in the view to a given element name.
// Renames an existing table cell in the view to a given element name.
//
// **Note** This method will not do anything if a view table cell was not yet converted.
//
// @param {module:engine/model/element~Element} tableCell

@@ -325,2 +327,7 @@ // @param {String} desiredCellElementName

// View cell might be not yet converted - skip it as it will be properly created by cell converter later on.
if ( !viewCell ) {
return;
}
let renamedCell;

@@ -336,3 +343,3 @@

} else {
renamedCell = conversionApi.writer.rename( viewCell, desiredCellElementName );
renamedCell = conversionApi.writer.rename( desiredCellElementName, viewCell );
}

@@ -379,4 +386,28 @@

conversionApi.mapper.bindElements( tableCell, cellElement );
const isSingleParagraph = tableCell.childCount === 1 && tableCell.getChild( 0 ).name === 'paragraph';
conversionApi.writer.insert( insertPosition, cellElement );
if ( isSingleParagraph ) {
const innerParagraph = tableCell.getChild( 0 );
const paragraphInsertPosition = ViewPosition.createAt( cellElement, 'end' );
conversionApi.consumable.consume( innerParagraph, 'insert' );
if ( options.asWidget ) {
const containerName = [ ...innerParagraph.getAttributeKeys() ].length ? 'p' : 'span';
const fakeParagraph = conversionApi.writer.createContainerElement( containerName );
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 );
}
}

@@ -498,2 +529,4 @@

//
// **Note** This method will skip not converted table rows.
//
// @param {Array.<module:engine/model/element~Element>} rowsToMove

@@ -507,3 +540,6 @@ // @param {module:engine/view/element~Element} viewTableSection

conversionApi.writer.move( ViewRange.createOn( viewTableRow ), ViewPosition.createAt( viewTableSection, offset ) );
// View table row might be not yet converted - skip it as it will be properly created by cell converter later on.
if ( viewTableRow ) {
conversionApi.writer.move( ViewRange.createOn( viewTableRow ), ViewPosition.createAt( viewTableSection, offset ) );
}
}

@@ -510,0 +546,0 @@ }

@@ -12,2 +12,3 @@ /**

import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';
import { createEmptyTableCell } from '../commands/utils';

@@ -48,2 +49,8 @@ /**

const splitResult = conversionApi.splitToAllowedParent( table, data.modelCursor );
// When there is no split result it means that we can't insert element to model tree, so let's skip it.
if ( !splitResult ) {
return;
}
conversionApi.writer.insert( table, splitResult.position );

@@ -58,5 +65,5 @@ conversionApi.consumable.consume( viewTable, { name: true } );

const row = conversionApi.writer.createElement( 'tableRow' );
conversionApi.writer.insert( row, ModelPosition.createAt( table, 'end' ) );
conversionApi.writer.insert( row, ModelPosition.createAt( table, 'end' ) );
conversionApi.writer.insertElement( 'tableCell', ModelPosition.createAt( row, 'end' ) );
createEmptyTableCell( conversionApi.writer, ModelPosition.createAt( row, 'end' ) );
}

@@ -87,6 +94,54 @@

}
}, { priority: 'normal' } );
} );
};
}
export function upcastTableCell( elementName ) {
return dispatcher => {
dispatcher.on( `element:${ elementName }`, ( evt, data, conversionApi ) => {
const viewTableCell = data.viewItem;
// When element was already consumed then skip it.
if ( !conversionApi.consumable.test( viewTableCell, { name: true } ) ) {
return;
}
const tableCell = conversionApi.writer.createElement( 'tableCell' );
// Insert element on allowed position.
const splitResult = conversionApi.splitToAllowedParent( tableCell, data.modelCursor );
// When there is no split result it means that we can't insert element to model tree, so let's skip it.
if ( !splitResult ) {
return;
}
conversionApi.writer.insert( tableCell, splitResult.position );
conversionApi.consumable.consume( viewTableCell, { name: true } );
const modelCursor = ModelPosition.createAt( tableCell );
conversionApi.convertChildren( viewTableCell, modelCursor );
// Ensure a paragraph in the model for empty table cells.
if ( !tableCell.childCount ) {
conversionApi.writer.insertElement( 'paragraph', modelCursor );
}
// Set conversion result range.
data.modelRange = new ModelRange(
// Range should start before inserted element
ModelPosition.createBefore( tableCell ),
// Should end after but we need to take into consideration that children could split our
// element, so we need to move range after parent of the last converted child.
// before: <allowed>[]</allowed>
// after: <allowed>[<converted><child></child></converted><child></child><converted>]</converted></allowed>
ModelPosition.createAfter( tableCell )
);
// Continue after inserted element.
data.modelCursor = data.modelRange.end;
} );
};
}
// Scans table rows and extracts required metadata from the table:

@@ -93,0 +148,0 @@ //

@@ -21,7 +21,7 @@ /**

*
* It loads the {@link module:table/tableediting~TableEditing table editing feature}
* For a detailed overview, check the {@glink features/table Table feature documentation}.
*
* This is a "glue" plugin which loads the {@link module:table/tableediting~TableEditing table editing feature}
* and {@link module:table/tableui~TableUI table UI feature}.
*
* For a detailed overview, check the {@glink features/table Table feature documentation}.
*
* @extends module:core/plugin~Plugin

@@ -28,0 +28,0 @@ */

@@ -13,5 +13,4 @@ /**

import Range from '@ckeditor/ckeditor5-engine/src/model/range';
import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
import upcastTable from './converters/upcasttable';
import upcastTable, { upcastTableCell } from './converters/upcasttable';
import {

@@ -25,2 +24,3 @@ downcastInsertCell,

} from './converters/downcast';
import InsertTableCommand from './commands/inserttablecommand';

@@ -35,5 +35,9 @@ import InsertRowCommand from './commands/insertrowcommand';

import SetHeaderColumnCommand from './commands/setheadercolumncommand';
import { getParentTable } from './commands/utils';
import TableUtils from './tableutils';
import { findAncestor } from './commands/utils';
import TableUtils from '../src/tableutils';
import injectTableLayoutPostFixer from './converters/table-layout-post-fixer';
import injectTableCellContentsPostFixer from './converters/table-cell-content-post-fixer';
import injectTableCellPostFixer from './converters/tablecell-post-fixer';
import '../theme/tableediting.css';

@@ -52,3 +56,4 @@

const editor = this.editor;
const schema = editor.model.schema;
const model = editor.model;
const schema = model.schema;
const conversion = editor.conversion;

@@ -70,3 +75,2 @@

allowIn: 'tableRow',
allowContentOf: '$block',
allowAttributes: [ 'colspan', 'rowspan' ],

@@ -76,2 +80,23 @@ isLimit: true

// Allow all $block content inside table cell.
schema.extend( '$block', { allowIn: 'tableCell' } );
// Disallow table in table.
schema.addChildCheck( ( context, childDefinition ) => {
if ( childDefinition.name == 'table' && Array.from( context.getNames() ).includes( 'table' ) ) {
return false;
}
} );
// Disallow image and media in table cell.
schema.addChildCheck( ( context, childDefinition ) => {
if ( !Array.from( context.getNames() ).includes( 'table' ) ) {
return;
}
if ( childDefinition.name == 'image' || childDefinition.name == 'media' ) {
return false;
}
} );
// Table conversion.

@@ -91,4 +116,4 @@ conversion.for( 'upcast' ).add( upcastTable() );

// Table cell conversion.
conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) );
conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) );
conversion.for( 'upcast' ).add( upcastTableCell( 'td' ) );
conversion.for( 'upcast' ).add( upcastTableCell( 'th' ) );

@@ -108,2 +133,4 @@ conversion.for( 'editingDowncast' ).add( downcastInsertCell( { asWidget: true } ) );

injectTableCellPostFixer( editor.model, editor.editing );
// Define all the commands.

@@ -130,5 +157,9 @@ editor.commands.add( 'insertTable', new InsertTableCommand( editor ) );

injectTableLayoutPostFixer( model );
injectTableCellContentsPostFixer( model );
// Handle tab key navigation.
this.listenTo( editor.editing.view.document, 'keydown', ( ...args ) => this._handleTabOnSelectedTable( ...args ) );
this.listenTo( editor.editing.view.document, 'keydown', ( ...args ) => this._handleTabInsideTable( ...args ) );
this.editor.keystrokes.set( 'Tab', ( ...args ) => this._handleTabOnSelectedTable( ...args ), { priority: 'low' } );
this.editor.keystrokes.set( 'Tab', this._getTabHandler( true ), { priority: 'low' } );
this.editor.keystrokes.set( 'Shift+Tab', this._getTabHandler( false ), { priority: 'low' } );
}

@@ -151,10 +182,3 @@

*/
_handleTabOnSelectedTable( eventInfo, domEventData ) {
const tabPressed = domEventData.keyCode == keyCodes.tab;
// Act only on TAB & SHIFT-TAB - Do not override native CTRL+TAB handler.
if ( !tabPressed || domEventData.ctrlKey ) {
return;
}
_handleTabOnSelectedTable( domEventData, cancel ) {
const editor = this.editor;

@@ -166,9 +190,7 @@ const selection = editor.model.document.selection;

if ( !selectedElement || selectedElement.name != 'table' ) {
if ( !selectedElement || !selectedElement.is( 'table' ) ) {
return;
}
eventInfo.stop();
domEventData.preventDefault();
domEventData.stopPropagation();
cancel();

@@ -182,73 +204,68 @@ editor.model.change( writer => {

/**
* Handles {@link module:engine/view/document~Document#event:keydown keydown} events for the <kbd>Tab</kbd> key executed inside table
* cell.
* Returns a handler for {@link module:engine/view/document~Document#event:keydown keydown} events for the <kbd>Tab</kbd> key executed
* inside table cell.
*
* @private
* @param {module:utils/eventinfo~EventInfo} eventInfo
* @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
* @param {Boolean} isForward Whether this handler will move selection to the next cell or previous.
*/
_handleTabInsideTable( eventInfo, domEventData ) {
const tabPressed = domEventData.keyCode == keyCodes.tab;
_getTabHandler( isForward ) {
const editor = this.editor;
// Act only on TAB & SHIFT-TAB - Do not override native CTRL+TAB handler.
if ( !tabPressed || domEventData.ctrlKey ) {
return;
}
return ( domEventData, cancel ) => {
const selection = editor.model.document.selection;
const editor = this.editor;
const selection = editor.model.document.selection;
const firstPosition = selection.getFirstPosition();
const table = getParentTable( selection.getFirstPosition() );
const tableCell = findAncestor( 'tableCell', firstPosition );
if ( !table ) {
return;
}
if ( !tableCell ) {
return;
}
domEventData.preventDefault();
domEventData.stopPropagation();
cancel();
const tableCell = selection.focus.parent;
const tableRow = tableCell.parent;
const tableRow = tableCell.parent;
const table = tableRow.parent;
const currentRowIndex = table.getChildIndex( tableRow );
const currentCellIndex = tableRow.getChildIndex( tableCell );
const currentRowIndex = table.getChildIndex( tableRow );
const currentCellIndex = tableRow.getChildIndex( tableCell );
const isForward = !domEventData.shiftKey;
const isFirstCellInRow = currentCellIndex === 0;
const isFirstCellInRow = currentCellIndex === 0;
if ( !isForward && isFirstCellInRow && currentRowIndex === 0 ) {
// It's the first cell of a table - don't do anything (stay in current position).
return;
}
if ( !isForward && isFirstCellInRow && currentRowIndex === 0 ) {
// It's the first cell of a table - don't do anything (stay in current position).
return;
}
const isLastCellInRow = currentCellIndex === tableRow.childCount - 1;
const isLastRow = currentRowIndex === table.childCount - 1;
const isLastCellInRow = currentCellIndex === tableRow.childCount - 1;
const isLastRow = currentRowIndex === table.childCount - 1;
if ( isForward && isLastRow && isLastCellInRow ) {
editor.plugins.get( TableUtils ).insertRows( table, { at: table.childCount } );
}
if ( isForward && isLastRow && isLastCellInRow ) {
editor.plugins.get( TableUtils ).insertRows( table, { at: table.childCount } );
}
let cellToFocus;
let cellToFocus;
// Move to first cell in next row.
if ( isForward && isLastCellInRow ) {
const nextRow = table.getChild( currentRowIndex + 1 );
// Move to first cell in next row.
if ( isForward && isLastCellInRow ) {
const nextRow = table.getChild( currentRowIndex + 1 );
cellToFocus = nextRow.getChild( 0 );
}
// Move to last cell in a previous row.
else if ( !isForward && isFirstCellInRow ) {
const previousRow = table.getChild( currentRowIndex - 1 );
cellToFocus = nextRow.getChild( 0 );
}
// Move to last cell in a previous row.
else if ( !isForward && isFirstCellInRow ) {
const previousRow = table.getChild( currentRowIndex - 1 );
cellToFocus = previousRow.getChild( previousRow.childCount - 1 );
}
// Move to next/previous cell.
else {
cellToFocus = tableRow.getChild( currentCellIndex + ( isForward ? 1 : -1 ) );
}
cellToFocus = previousRow.getChild( previousRow.childCount - 1 );
}
// Move to next/previous cell.
else {
cellToFocus = tableRow.getChild( currentCellIndex + ( isForward ? 1 : -1 ) );
}
editor.model.change( writer => {
writer.setSelection( Range.createIn( cellToFocus ) );
} );
editor.model.change( writer => {
writer.setSelection( Range.createIn( cellToFocus ) );
} );
};
}
}

@@ -11,17 +11,17 @@ /**

import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
import { isTableWidgetSelected, isTableContentSelected } from './utils';
import { repositionContextualBalloon, getBalloonPositionData } from './ui/utils';
import { isTableContentSelected, isTableWidgetSelected } from './utils';
import WidgetToolbarRepository from '@ckeditor/ckeditor5-widget/src/widgettoolbarrepository';
const balloonClassName = 'ck-toolbar-container';
/**
* The table toolbar class. It creates a table toolbar that shows up when the table widget is selected.
* The table toolbar class. It creates toolbars for the table feature and its content (for now only for a table cell content).
*
* Toolbar components are created using the editor {@link module:ui/componentfactory~ComponentFactory ComponentFactory}
* based on the {@link module:core/editor/editor~Editor#config configuration} stored under `table.toolbar`.
* Table toolbar shows up when a table widget is selected. Its components (e.g. buttons) are created based on the
* {@link module:table/table~TableConfig#toolbar `table.tableToolbar` configuration option}.
*
* The toolbar uses the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon}.
* Table content toolbar shows up when the selection is inside the content of a table. It creates its component based on the
* {@link module:table/table~TableConfig#contentToolbar `table.contentToolbar` configuration option}.
*
* Note that the old {@link module:table/table~TableConfig#toolbar `table.toolbar` configuration option} is deprecated
* and will be removed in the next major release.
*
* @extends module:core/plugin~Plugin

@@ -34,3 +34,3 @@ */

static get requires() {
return [ ContextualBalloon ];
return [ WidgetToolbarRepository ];
}

@@ -48,123 +48,50 @@

*/
init() {
const editor = this.editor;
const balloonToolbar = editor.plugins.get( 'BalloonToolbar' );
// If the `BalloonToolbar` plugin is loaded, it should be disabled for tables
// which have their own toolbar to avoid duplication.
// https://github.com/ckeditor/ckeditor5-image/issues/110
if ( balloonToolbar ) {
this.listenTo( balloonToolbar, 'show', evt => {
if ( isTableWidgetSelected( editor.editing.view.document.selection ) ) {
evt.stop();
}
}, { priority: 'high' } );
}
}
/**
* @inheritDoc
*/
afterInit() {
const editor = this.editor;
const toolbarConfig = editor.config.get( 'table.toolbar' );
const widgetToolbarRepository = editor.plugins.get( WidgetToolbarRepository );
// Don't add the toolbar if there is no configuration.
if ( !toolbarConfig || !toolbarConfig.length ) {
return;
}
const tableContentToolbarItems = editor.config.get( 'table.contentToolbar' );
const deprecatedTableContentToolbarItems = editor.config.get( 'table.toolbar' );
/**
* A contextual balloon plugin instance.
*
* @private
* @member {module:ui/panel/balloon/contextualballoon~ContextualBalloon}
*/
this._balloon = this.editor.plugins.get( 'ContextualBalloon' );
const tableToolbarItems = editor.config.get( 'table.tableToolbar' );
/**
* A toolbar view instance used to display the buttons specific for table editing.
*
* @protected
* @type {module:ui/toolbar/toolbarview~ToolbarView}
*/
this._toolbar = new ToolbarView();
// Add buttons to the toolbar.
this._toolbar.fillFromConfig( toolbarConfig, editor.ui.componentFactory );
// Show balloon panel each time table widget is selected.
this.listenTo( editor.ui, 'update', () => {
this._checkIsVisible();
} );
// There is no render method after focus is back in editor, we need to check if balloon panel should be visible.
this.listenTo( editor.ui.focusTracker, 'change:isFocused', () => {
this._checkIsVisible();
}, { priority: 'low' } );
}
/**
* Checks whether the toolbar should show up or hide depending on the current selection.
*
* @private
*/
_checkIsVisible() {
const editor = this.editor;
const viewSelection = editor.editing.view.document.selection;
if ( !editor.ui.focusTracker.isFocused || !isTableContentSelected( viewSelection ) ) {
this._hideToolbar();
} else {
this._showToolbar();
if ( deprecatedTableContentToolbarItems ) {
// eslint-disable-next-line
console.warn(
'`config.table.toolbar` is deprecated and will be removed in the next major release.' +
' Use `config.table.contentToolbar` instead.'
);
}
}
/**
* Shows the {@link #_toolbar} in the {@link #_balloon}.
*
* @private
*/
_showToolbar() {
const editor = this.editor;
if ( this._isVisible ) {
repositionContextualBalloon( editor );
} else if ( !this._balloon.hasView( this._toolbar ) ) {
this._balloon.add( {
view: this._toolbar,
position: getBalloonPositionData( editor ),
balloonClassName
if ( tableContentToolbarItems || deprecatedTableContentToolbarItems ) {
widgetToolbarRepository.register( 'tableContent', {
items: tableContentToolbarItems || deprecatedTableContentToolbarItems,
visibleWhen: isTableContentSelected,
} );
}
}
/**
* Removes the {@link #_toolbar} from the {@link #_balloon}.
*
* @private
*/
_hideToolbar() {
if ( !this._isVisible ) {
return;
if ( tableToolbarItems ) {
widgetToolbarRepository.register( 'table', {
items: tableToolbarItems,
visibleWhen: isTableWidgetSelected,
} );
}
this._balloon.remove( this._toolbar );
}
/**
* Returns `true` when the {@link #_toolbar} is the visible view in the {@link #_balloon}.
*
* @private
* @type {Boolean}
*/
get _isVisible() {
return this._balloon.visibleView == this._toolbar;
}
}
/**
* Items to be placed in the table toolbar.
* This option is used by the {@link module:table/tabletoolbar~TableToolbar} feature.
* Items to be placed in the table content toolbar.
*
* **Note:** This configuration option is deprecated! Use {@link module:table/table~TableConfig#contentToolbar} instead.
*
* Read more about configuring toolbar in {@link module:core/editor/editorconfig~EditorConfig#toolbar}.
*
* @deprecated
* @member {Array.<String>} module:table/table~TableConfig#toolbar
*/
/**
* Items to be placed in the table content toolbar.
* The {@link module:table/tabletoolbar~TableToolbar} plugin is required to make this toolbar working.
*
* Assuming that you use the {@link module:table/tableui~TableUI} feature, the following toolbar items will be available

@@ -180,3 +107,3 @@ * in {@link module:ui/componentfactory~ComponentFactory}:

* const tableConfig = {
* toolbar: [ 'tableRow', 'tableColumn', 'mergeTableCells' ]
* contentToolbar: [ 'tableRow', 'tableColumn', 'mergeTableCells' ]
* };

@@ -189,3 +116,21 @@ *

*
* @member {Array.<String>} module:table/table~TableConfig#toolbar
* @member {Array.<String>} module:table/table~TableConfig#contentToolbar
*/
/**
* Items to be placed in the table toolbar.
* The {@link module:table/tabletoolbar~TableToolbar} plugin is required to make this toolbar working.
*
* You can thus configure the toolbar like this:
*
* const tableConfig = {
* tableToolbar: [ 'blockQuote' ]
* };
*
* Of course, the same buttons can also be used in the
* {@link module:core/editor/editorconfig~EditorConfig#toolbar main editor toolbar}.
*
* Read more about configuring toolbar in {@link module:core/editor/editorconfig~EditorConfig#toolbar}.
*
* @member {Array.<String>} module:table/table~TableConfig#tableToolbar
*/

@@ -14,3 +14,3 @@ /**

import TableWalker from './tablewalker';
import { getParentTable, updateNumericAttribute } from './commands/utils';
import { createEmptyTableCell, updateNumericAttribute } from './commands/utils';

@@ -73,5 +73,14 @@ /**

/**
* Creates an empty table at a given position.
* Creates an empty table with proper structure. The table needs to be inserted into the model,
* ie. using {@link module:engine/model/model~Model#insertContent} function.
*
* @param {module:engine/model/position~Position} position The position where the table will be inserted.
* model.change( ( writer ) => {
* // Create a table of 2 rows and 7 columns:
* const table = tableUtils.createTable( writer, 2, 7);
*
* // Insert table to the model at the best position taking current selection:
* model.insertContent( table );
* }
*
* @param {module:engine/model/writer~Writer} writer The model writer.
* @param {Number} rows The number of rows to create.

@@ -81,14 +90,8 @@ * @param {Number} columns The number of columns to create.

*/
createTable( position, rows, columns ) {
const model = this.editor.model;
createTable( writer, rows, columns ) {
const table = writer.createElement( 'table' );
return model.change( writer => {
const table = writer.createElement( 'table' );
createEmptyRows( writer, table, 0, rows, columns );
writer.insert( table, position );
createEmptyRows( writer, table, 0, rows, columns );
return table;
} );
return table;
}

@@ -300,3 +303,4 @@

const model = this.editor.model;
const table = getParentTable( tableCell );
const tableRow = tableCell.parent;
const table = tableRow.parent;

@@ -437,4 +441,5 @@ const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 );

const table = getParentTable( tableCell );
const splitCellRow = table.getChildIndex( tableCell.parent );
const tableRow = tableCell.parent;
const table = tableRow.parent;
const splitCellRow = table.getChildIndex( tableRow );

@@ -488,3 +493,3 @@ const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 );

writer.insertElement( 'tableCell', newCellsAttributes, position );
createCells( 1, writer, position, newCellsAttributes );
}

@@ -578,3 +583,3 @@ }

for ( let i = 0; i < cells; i++ ) {
writer.insertElement( 'tableCell', attributes, insertPosition );
createEmptyTableCell( writer, insertPosition, attributes );
}

@@ -581,0 +586,0 @@ }

@@ -10,4 +10,4 @@ /**

import { toWidget, isWidget } from '@ckeditor/ckeditor5-widget/src/utils';
import { getParentTable } from './commands/utils';
import { isWidget, toWidget } from '@ckeditor/ckeditor5-widget/src/utils';
import { findAncestor } from './commands/utils';

@@ -22,3 +22,3 @@ const tableSymbol = Symbol( 'isTable' );

* @param {module:engine/view/element~Element} viewElement
* @param {module:engine/view/writer~Writer} writer An instance of the view writer.
* @param {module:engine/view/downcastwriter~DowncastWriter} writer An instance of the view writer.
* @param {String} label The element's label. It will be concatenated with the table `alt` attribute if one is present.

@@ -62,5 +62,5 @@ * @returns {module:engine/view/element~Element}

export function isTableContentSelected( selection ) {
const parentTable = getParentTable( selection.getFirstPosition() );
const parentTable = findAncestor( 'table', selection.getFirstPosition() );
return !!( parentTable && isTableWidget( parentTable.parent ) );
}

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc