Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
05f7580
Add table cell type experimental flag.
Mati365 Nov 26, 2025
89e33cd
Rename experimental flag.
Mati365 Nov 26, 2025
8034fb0
Fix incorrect move of table row containing only `th` elements to `the…
Mati365 Nov 26, 2025
188d5b4
Merge pull request #19448 from ckeditor/ck/19431
Mati365 Nov 26, 2025
46bfcc8
Add basic converters and UI.
Mati365 Nov 27, 2025
a3ffbfa
Fix crashing table alignment.
Mati365 Nov 27, 2025
16dc1d1
Adjust editing plugin architecture.
Mati365 Nov 28, 2025
bce73cd
Adjust command structure.
Mati365 Nov 28, 2025
5f4d8d6
Adjust import
Mati365 Nov 28, 2025
b02c901
Adjust export
Mati365 Nov 28, 2025
0f62599
Adjust export
Mati365 Nov 28, 2025
27d7e5a
Fix compiler errors.
Mati365 Nov 28, 2025
0b492b4
Improve support for inserting new rows / columns.
Mati365 Nov 28, 2025
95b9dff
Simplify few places.
Mati365 Nov 28, 2025
46c4cea
Speedup postifxer.
Mati365 Nov 28, 2025
7014a93
Init basic testing.
Mati365 Nov 28, 2025
10a5dd9
Add reconversion tests.
Mati365 Nov 28, 2025
e0f28d6
Add tests for inserting table columns and rows
Mati365 Nov 28, 2025
9d808c0
Add more testing for reconversion.
Mati365 Nov 28, 2025
13729fb
Add testing for experimental flag.
Mati365 Nov 28, 2025
8fd3b8e
Add more testing, optimize postfixers.
Mati365 Nov 28, 2025
a82350d
Add more syncing tests.
Mati365 Nov 28, 2025
e29b6a4
Use commands in tests instead of raw attributes to make them more reu…
Mati365 Nov 28, 2025
6fa6bd6
Add command testing.
Mati365 Nov 28, 2025
5e9ff15
Add missing test.
Mati365 Nov 28, 2025
d2faf46
Adjust tests.
Mati365 Nov 28, 2025
7b84213
Unskip test.
Mati365 Nov 28, 2025
1924720
Adjust docs.
Mati365 Nov 28, 2025
c2c52fa
Fix too greedy column scanning algorithm
Mati365 Nov 28, 2025
f0d7fd2
Table cell properties form UI updated.
pszczesniak Nov 28, 2025
6898313
Added stylesheet import in theme-lark.
pszczesniak Nov 28, 2025
701b3c2
Tests and small code refactor.
pszczesniak Dec 1, 2025
6a280c5
Merge branch 'ck/feat/table-cell-type-v47' into ck/19249
pszczesniak Dec 1, 2025
c3b0981
Merge pull request #19456 from ckeditor/ck/19249
pszczesniak Dec 1, 2025
4472f59
Add default layout table value for table cell property command.
Mati365 Dec 1, 2025
21f3e64
Introduce new table utils methods.
Mati365 Dec 1, 2025
b8c0de2
Set cell type during insert and remove in table utils.
Mati365 Dec 2, 2025
3d44cfa
Restore some logic related to incrementing heading rows and heading cols
Mati365 Dec 2, 2025
6f08770
Use different API to set heading rows count.
Mati365 Dec 2, 2025
904a8bd
Extract table cell type refresh handler to different file.
Mati365 Dec 2, 2025
c2bec78
Unify plugins.
Mati365 Dec 2, 2025
9fbbb34
Fix mod name.
Mati365 Dec 2, 2025
4a4944f
Minor realign functions.
Mati365 Dec 2, 2025
9e24b44
Extend API methods.
Mati365 Dec 2, 2025
933d3a5
Prevent possible crashes.
Mati365 Dec 2, 2025
d67e2d6
Fix more edge cases.
Mati365 Dec 2, 2025
8ed83d1
Fix more crashes.
Mati365 Dec 2, 2025
f5d80e9
Rename attribute.
Mati365 Dec 2, 2025
a455da1
Adjust testing.
Mati365 Dec 2, 2025
1630d59
Fix failing tests.
Mati365 Dec 2, 2025
3202f2d
Add more editing tests.
Mati365 Dec 2, 2025
086faa7
Add more tests for inserting rows / columns.
Mati365 Dec 2, 2025
661014d
Add more tests.
Mati365 Dec 2, 2025
c4dfc2a
Init testing for table utils cell type support.
Mati365 Dec 2, 2025
969fb85
Add more testing.
Mati365 Dec 2, 2025
9646637
Merge pull request #19463 from ckeditor/ck/extend-tableutils-to-set-c…
Mati365 Dec 2, 2025
f9505e2
Add test for edge case.
Mati365 Dec 2, 2025
0ef92f0
Disable `tableCellType` command on layout tables.
Mati365 Dec 3, 2025
6a239fe
Disable cell type UI dropdown if command is disabled.
Mati365 Dec 3, 2025
f4e5b13
Merge branch 'master' into ck/feat/table-cell-type-v47
Mati365 Dec 3, 2025
8ebf89d
Add UI package coverage.
Mati365 Dec 3, 2025
255f43c
Add RTC conflicts resolution to table cell type. (#19475)
Mati365 Dec 4, 2025
62cd55a
Restore few things.
Mati365 Dec 4, 2025
ba3efe9
Better reuse of code.
Mati365 Dec 4, 2025
100fcb3
Add few more tests.
Mati365 Dec 4, 2025
49e67f2
Fix incorrect behavior of UI when selected multiple cells of differen…
Mati365 Dec 4, 2025
2265bc1
Minor postfixer fix.
Mati365 Dec 4, 2025
ad7fa5d
Disable redundant refresh handler.
Mati365 Dec 4, 2025
5768fa6
Fix options.
Mati365 Dec 4, 2025
97d31cf
Strict table cell type support checks.
Mati365 Dec 5, 2025
5eb03f1
Disable table cell type attribute upcasting for layout tables.
Mati365 Dec 5, 2025
571d6d1
Adjust checks.
Mati365 Dec 5, 2025
0a3d9ab
Adjust upcasting of layout tables.
Mati365 Dec 5, 2025
7e69d63
Move cell refresh handlers.
Mati365 Dec 5, 2025
1af2b2d
Adjust manual test.
Mati365 Dec 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changelog/20251126125431_ck_19431.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
type: Fix
scope:
- ckeditor5-table
closes:
- https://github.com/ckeditor/ckeditor5/issues/19431
---

Add experimental fix for incorrect table rows marking and moving as header row when preceding rows are not header rows.

This change needs to be enabled by setting `experimentalFlags.tableCellTypeSupport` to `true`. In the next major release, this flag will be removed and the fix will be enabled by default.
21 changes: 15 additions & 6 deletions packages/ckeditor5-engine/tests/model/operation/transform/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class Client {
this.name = name;
}

init() {
init( editorConfig = {} ) {
return ModelTestEditor.create( {
// Typing is needed for delete command.
// UndoEditing is needed for undo command.
Expand All @@ -42,7 +42,8 @@ export class Client {
plugins: [
Typing, Paragraph, LegacyListEditing, UndoEditing, BlockQuoteEditing, HeadingEditing, BoldEditing, TableEditing,
ImageBlockEditing
]
],
...editorConfig
} ).then( editor => {
this.editor = editor;
this.document = editor.model.document;
Expand Down Expand Up @@ -87,15 +88,19 @@ export class Client {
this.syncedVersion = this.document.version;
}

setSelection( start, end ) {
setSelection( start, end, { bufferOperations } = {} ) {
if ( !end ) {
end = start.slice();
}

const startPos = this._getPosition( start );
const endPos = this._getPosition( end );

this.editor.model.document.selection._setTo( new ModelRange( startPos, endPos ) );
if ( bufferOperations ) {
this._processAction( 'setSelection', new ModelRange( startPos, endPos ) );
} else {
this.editor.model.document.selection._setTo( new ModelRange( startPos, endPos ) );
}
}

insert( itemString, path ) {
Expand Down Expand Up @@ -243,6 +248,10 @@ export class Client {
return this._getPositionFromSelection( type );
}

if ( path instanceof ModelPosition ) {
return path.clone();
}

return new ModelPosition( this.document.getRoot(), path );
}

Expand Down Expand Up @@ -272,13 +281,13 @@ export class Client {
bufferOperations( operations, this );
}

static get( clientName ) {
static get( clientName, { editorConfig } = {} ) {
const client = new Client( clientName );
client.orderNumber = clients.size;

clients.add( client );

return client.init();
return client.init( editorConfig ?? {} );
}
}

Expand Down
3 changes: 3 additions & 0 deletions packages/ckeditor5-table/lang/contexts.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
"Table toolbar": "The label used by assistive technologies describing a table toolbar attached to a table widget.",
"Table properties": "The label describing the form allowing to specify the properties of a selected table.",
"Cell properties": "The label describing the form allowing to specify the properties of a selected table cell.",
"Cell type": "The label for the dropdown that allows configuring the type of a table cell (data or header).",
"Data cell": "The label for the dropdown option for a data table cell.",
"Header cell": "The label for the dropdown option for a header table cell.",
"Border": "The label describing a group of border–related form fields (border style, color, etc.).",
"Style": "The label for the dropdown that allows configuring the border style of a table or a table cell.",
"Width": "The label for the input that allows configuring the width of a table or a table cell or the width of a border.",
Expand Down
13 changes: 12 additions & 1 deletion packages/ckeditor5-table/src/augmentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ import type {
TableBorderStyleCommand,
TableBorderWidthCommand,
TableHeightCommand,
TableWidthCommand
TableWidthCommand,
TableCellTypeCommand
} from './index.js';

declare module '@ckeditor/ckeditor5-engine' {
Expand All @@ -88,6 +89,15 @@ declare module '@ckeditor/ckeditor5-engine' {
* This will be enabled by default in the future CKEditor 5 releases.
*/
useExtendedTableBlockAlignment?: boolean;

/**
* When enabled, the
* {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing table cell properties feature}
* will introduce a special selector for table cell types (i.e. header and data cells) in the cell properties dropdown.
*
* This will be enabled by default in the future CKEditor 5 releases.
*/
tableCellTypeSupport?: boolean;
}
}

Expand Down Expand Up @@ -159,6 +169,7 @@ declare module '@ckeditor/ckeditor5-core' {
tableCellPadding: TableCellPaddingCommand;
tableCellVerticalAlignment: TableCellVerticalAlignmentCommand;
tableCellWidth: TableCellWidthCommand;
tableCellType: TableCellTypeCommand;
tableAlignment: TableAlignmentCommand;
tableBackgroundColor: TableBackgroundColorCommand;
tableBorderColor: TableBorderColorCommand;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@
import { Command } from 'ckeditor5/src/core.js';
import { type TableUtils } from '../tableutils.js';

import {
isHeadingColumnCell,
updateNumericAttribute
} from '../utils/common.js';
import { isHeadingColumnCell } from '../utils/common.js';
import { getHorizontallyOverlappingCells, splitVertically } from '../utils/structure.js';

/**
Expand Down Expand Up @@ -97,7 +94,7 @@ export class SetHeaderColumnCommand extends Command {
}
}

updateNumericAttribute( 'headingColumns', headingColumnsToSet, table, writer, 0 );
tableUtils.setHeadingColumnsCount( writer, table, headingColumnsToSet );
} );
}
}
3 changes: 1 addition & 2 deletions packages/ckeditor5-table/src/commands/setheaderrowcommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { Command } from 'ckeditor5/src/core.js';
import type { ModelElement } from 'ckeditor5/src/engine.js';
import { type TableUtils } from '../tableutils.js';

import { updateNumericAttribute } from '../utils/common.js';
import { getVerticallyOverlappingCells, splitHorizontally } from '../utils/structure.js';

/**
Expand Down Expand Up @@ -97,7 +96,7 @@ export class SetHeaderRowCommand extends Command {
}
}

updateNumericAttribute( 'headingRows', headingRowsToSet, table, writer, 0 );
tableUtils.setHeadingRowsCount( writer, table, headingRowsToSet );
} );
}

Expand Down
36 changes: 30 additions & 6 deletions packages/ckeditor5-table/src/converters/downcast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ import type {
DowncastConversionApi
} from 'ckeditor5/src/engine.js';

import { TableWalker } from './../tablewalker.js';
import { type TableUtils } from '../tableutils.js';
import type { TableConversionAdditionalSlot } from '../tableediting.js';
import { downcastTableAlignmentConfig, type TableAlignmentValues } from './tableproperties.js';
import { getNormalizedDefaultTableProperties } from '../utils/table-properties.js';
import { isTableCellTypeEnabled } from '../utils/common.js';
import { TableWalker } from '../tablewalker.js';

/**
* Model table element to view table element conversion helper.
Expand Down Expand Up @@ -106,11 +107,29 @@ export function downcastRow(): DowncastElementCreatorFunction {
* and `<td>` otherwise.
*
* @internal
* @param editor The editor instance.
* @param options.asWidget If set to `true`, the downcast conversion will produce a widget.
* @returns Element creator.
*/
export function downcastCell( options: { asWidget?: boolean } = {} ): DowncastElementCreatorFunction {
export function downcastCell( editor: Editor, options: { asWidget?: boolean } = {} ): DowncastElementCreatorFunction {
let cellTypeEnabled: boolean | null = null;

return ( tableCell, { writer } ) => {
cellTypeEnabled ??= isTableCellTypeEnabled( editor );

// If the table cell type feature is enabled, then we can simply check the cell type attribute.
if ( cellTypeEnabled ) {
const cellElementName: 'td' | 'th' = (
tableCell.getAttribute( 'tableCellType' ) === 'header' ?
'th' :
'td'
);

return createCellElement( writer, cellElementName );
}

// If the the table cell type feature is not enabled, we should iterate through the table structure
// to determine whether the cell is in the heading section.
const tableRow = tableCell.parent as ModelElement;
const table = tableRow.parent as ModelElement;
const rowIndex = table.getChildIndex( tableRow )!;
Expand All @@ -125,17 +144,22 @@ export function downcastCell( options: { asWidget?: boolean } = {} ): DowncastEl
for ( const tableSlot of tableWalker ) {
if ( tableSlot.cell == tableCell ) {
const isHeading = tableSlot.row < headingRows || tableSlot.column < headingColumns;
const cellElementName = isHeading ? 'th' : 'td';

result = options.asWidget ?
toWidgetEditable( writer.createEditableElement( cellElementName ), writer, { withAriaRole: false } ) :
writer.createContainerElement( cellElementName );
result = createCellElement( writer, isHeading ? 'th' : 'td' );
break;
}
}

return result;
};

function createCellElement( writer: ViewDowncastWriter, name: string ) {
return (
options.asWidget ?
toWidgetEditable( writer.createEditableElement( name ), writer, { withAriaRole: false } ) :
writer.createContainerElement( name )
);
}
}

/**
Expand Down
34 changes: 24 additions & 10 deletions packages/ckeditor5-table/src/converters/upcasttable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import type { ModelElement, UpcastDispatcher, UpcastElementEvent, ViewElement, ViewNode } from 'ckeditor5/src/engine.js';
import type { Editor } from 'ckeditor5/src/core.js';

import { createEmptyTableCell } from '../utils/common.js';
import { getViewTableFromWrapper } from '../utils/structure.js';
Expand Down Expand Up @@ -81,7 +82,9 @@ export function upcastTableFigure() {
* @returns Conversion helper.
* @internal
*/
export function upcastTable() {
export function upcastTable( editor: Editor ) {
const ignoreHeaderRowMoveIfNormalRowsBefore = !!editor.config.get( 'experimentalFlags.tableCellTypeSupport' );

return ( dispatcher: UpcastDispatcher ): void => {
dispatcher.on<UpcastElementEvent>( 'element:table', ( evt, data, conversionApi ) => {
const viewTable = data.viewItem;
Expand All @@ -91,7 +94,7 @@ export function upcastTable() {
return;
}

const { rows, headingRows, headingColumns } = scanTable( viewTable );
const { rows, headingRows, headingColumns } = scanTable( viewTable, ignoreHeaderRowMoveIfNormalRowsBefore );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd consider this a fix, not a breaking change, so the experimental flag is not needed here.


// Only set attributes if values is greater then 0.
const attributes: { headingColumns?: number; headingRows?: number } = {};
Expand Down Expand Up @@ -200,10 +203,16 @@ export function ensureParagraphInTableCell( elementName: string ) {
* headingRows - The number of rows that go as table headers.
* headingColumns - The maximum number of row headings.
* rows - Sorted `<tr>` elements as they should go into the model - ie. if `<thead>` is inserted after `<tbody>` in the view.
*
* @param viewTable The view table element.
* @param ignoreHeaderRowMoveIfNormalRowsBefore If set to `true`, then heading rows detection will stop
* once a normal row is found between heading rows. If set to `false`, then heading rows will be detected
* even if normal rows are between them.
* @returns The table metadata.
*/
function scanTable( viewTable: ViewElement ) {
let headingRows = 0;
function scanTable( viewTable: ViewElement, ignoreHeaderRowMoveIfNormalRowsBefore: boolean ) {
let headingColumns: number | undefined = undefined;
let shouldAccumulateHeadingRows: boolean | null = null;

// The `<tbody>` and `<thead>` sections in the DOM do not have to be in order `<thead>` -> `<tbody>` and there might be more than one
// of them.
Expand Down Expand Up @@ -232,8 +241,9 @@ function scanTable( viewTable: ViewElement ) {
}

// Save the first `<thead>` in the table as table header - all other ones will be converted to table body rows.
if ( tableChild.name === 'thead' && !firstTheadElement ) {
firstTheadElement = tableChild;
if ( tableChild.name === 'thead' ) {
shouldAccumulateHeadingRows = null;
firstTheadElement ||= tableChild;
}

// There might be some extra empty text nodes between the `<tr>`s.
Expand Down Expand Up @@ -261,13 +271,17 @@ function scanTable( viewTable: ViewElement ) {
// of the cell span from the previous row.
// Issue: https://github.com/ckeditor/ckeditor5/issues/17556
( maxPrevColumns === null || trColumns.length === maxPrevColumns ) &&
trColumns.every( e => e.is( 'element', 'th' ) )
trColumns.every( e => e.is( 'element', 'th' ) ) &&
// If there is at least one "normal" table row between heading rows, then stop accumulating heading rows.
// However, if flag `ignoreHeaderRowMoveIfNormalRowsBefore` is set to `false`, then ignore this rule.
( !ignoreHeaderRowMoveIfNormalRowsBefore || shouldAccumulateHeadingRows === null || shouldAccumulateHeadingRows )
)
) {
headingRows++;
headRows.push( tr );
shouldAccumulateHeadingRows = true;
} else {
bodyRows.push( tr );
shouldAccumulateHeadingRows = false;
}

// We use the maximum number of columns to avoid false positives when detecting
Expand All @@ -293,13 +307,13 @@ function scanTable( viewTable: ViewElement ) {
}

// Update headingColumns.
if ( !headingColumns || index < headingColumns ) {
if ( headingColumns === undefined || index < headingColumns ) {
headingColumns = index;
}
}

return {
headingRows,
headingRows: headRows.length,
headingColumns: headingColumns || 0,
rows: [ ...headRows, ...bodyRows ]
};
Expand Down
6 changes: 5 additions & 1 deletion packages/ckeditor5-table/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ export { TableCellHeightCommand } from './tablecellproperties/commands/tablecell
export { TableCellHorizontalAlignmentCommand } from './tablecellproperties/commands/tablecellhorizontalalignmentcommand.js';
export { TableCellPaddingCommand } from './tablecellproperties/commands/tablecellpaddingcommand.js';
export { TableCellVerticalAlignmentCommand } from './tablecellproperties/commands/tablecellverticalalignmentcommand.js';
export { TableCellPropertyCommand } from './tablecellproperties/commands/tablecellpropertycommand.js';
export { TableCellWidthCommand } from './tablecellwidth/commands/tablecellwidthcommand.js';
export { TableAlignmentCommand } from './tableproperties/commands/tablealignmentcommand.js';
export { TableBackgroundColorCommand } from './tableproperties/commands/tablebackgroundcolorcommand.js';
Expand All @@ -82,6 +81,11 @@ export { TableBorderWidthCommand } from './tableproperties/commands/tableborderw
export { TableHeightCommand } from './tableproperties/commands/tableheightcommand.js';
export { TableWidthCommand } from './tableproperties/commands/tablewidthcommand.js';
export { TablePropertyCommand, type TablePropertyCommandExecuteOptions } from './tableproperties/commands/tablepropertycommand.js';
export { TableCellTypeCommand, type TableCellType } from './tablecellproperties/commands/tablecelltypecommand.js';
export {
TableCellPropertyCommand,
type TableCellPropertyCommandAfterExecuteEvent
} from './tablecellproperties/commands/tablecellpropertycommand.js';

export type { ViewDocumentTableMouseMoveEvent, ViewDocumentTableMouseLeaveEvent } from './tablemouse/mouseeventsobserver.js';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/

import { Command, type Editor } from 'ckeditor5/src/core.js';
import type { ModelElement, Batch } from 'ckeditor5/src/engine.js';
import type { ModelElement, Batch, ModelWriter } from 'ckeditor5/src/engine.js';
import { type TableUtils } from '../../tableutils.js';
import { getSelectionAffectedTable } from '../../utils/common.js';

Expand Down Expand Up @@ -55,6 +55,10 @@ export class TableCellPropertyCommand extends Command {

// Hardcoded defaults for layout table.
switch ( attributeName ) {
case 'tableCellType':
this._defaultLayoutTableValue = 'data';
break;

case 'tableCellBorderStyle':
this._defaultLayoutTableValue = 'none';
break;
Expand Down Expand Up @@ -113,6 +117,12 @@ export class TableCellPropertyCommand extends Command {
} else {
tableCells.forEach( tableCell => writer.removeAttribute( this.attributeName, tableCell ) );
}

this.fire<TableCellPropertyCommandAfterExecuteEvent>( 'afterExecute', {
writer,
tableCells,
valueToSet
} );
} );
}

Expand Down Expand Up @@ -156,3 +166,17 @@ export class TableCellPropertyCommand extends Command {
return everyCellHasAttribute ? firstCellValue : undefined;
}
}

/**
* Fired after the the {@link module:table/tablecellproperties/commands/tablecellpropertycommand~TableCellPropertyCommand}
*
* @eventName ~TableCellPropertyCommand#afterExecute
*/
export type TableCellPropertyCommandAfterExecuteEvent = {
name: 'afterExecute';
args: [ {
writer: ModelWriter;
tableCells: Array<ModelElement>;
valueToSet: unknown;
} ];
};
Loading