@ckeditor/ckeditor5-table
Version:
Table feature for CKEditor 5.
153 lines (152 loc) • 6.69 kB
JavaScript
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module table/converters/downcast
*/
import { toWidget, toWidgetEditable } from 'ckeditor5/src/widget.js';
import TableWalker from './../tablewalker.js';
/**
* Model table element to view table element conversion helper.
*/
export function downcastTable(tableUtils, options) {
return (table, { writer }) => {
const headingRows = table.getAttribute('headingRows') || 0;
const tableElement = writer.createContainerElement('table', null, []);
const figureElement = writer.createContainerElement('figure', { class: 'table' }, tableElement);
// Table head slot.
if (headingRows > 0) {
writer.insert(writer.createPositionAt(tableElement, 'end'), writer.createContainerElement('thead', null, writer.createSlot(element => element.is('element', 'tableRow') && element.index < headingRows)));
}
// Table body slot.
if (headingRows < tableUtils.getRows(table)) {
writer.insert(writer.createPositionAt(tableElement, 'end'), writer.createContainerElement('tbody', null, writer.createSlot(element => element.is('element', 'tableRow') && element.index >= headingRows)));
}
// Dynamic slots.
for (const { positionOffset, filter } of options.additionalSlots) {
writer.insert(writer.createPositionAt(tableElement, positionOffset), writer.createSlot(filter));
}
// Create a slot with items that don't fit into the table.
writer.insert(writer.createPositionAt(tableElement, 'after'), writer.createSlot(element => {
if (element.is('element', 'tableRow')) {
return false;
}
return !options.additionalSlots.some(({ filter }) => filter(element));
}));
return options.asWidget ? toTableWidget(figureElement, writer) : figureElement;
};
}
/**
* Model table row element to view `<tr>` element conversion helper.
*
* @returns Element creator.
*/
export function downcastRow() {
return (tableRow, { writer }) => {
return tableRow.isEmpty ?
writer.createEmptyElement('tr') :
writer.createContainerElement('tr');
};
}
/**
* Model table cell element to view `<td>` or `<th>` element conversion helper.
*
* This conversion helper will create proper `<th>` elements for table cells that are in the heading section (heading row or column)
* and `<td>` otherwise.
*
* @param options.asWidget If set to `true`, the downcast conversion will produce a widget.
* @returns Element creator.
*/
export function downcastCell(options = {}) {
return (tableCell, { writer }) => {
const tableRow = tableCell.parent;
const table = tableRow.parent;
const rowIndex = table.getChildIndex(tableRow);
const tableWalker = new TableWalker(table, { row: rowIndex });
const headingRows = table.getAttribute('headingRows') || 0;
const headingColumns = table.getAttribute('headingColumns') || 0;
let result = null;
// We need to iterate over a table in order to get proper row & column values from a walker.
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) :
writer.createContainerElement(cellElementName);
break;
}
}
return result;
};
}
/**
* Overrides paragraph inside table cell conversion.
*
* This converter:
* * should be used to override default paragraph conversion.
* * 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 options.asWidget If set to `true`, the downcast conversion will produce a widget.
* @returns Element creator.
*/
export function convertParagraphInTableCell(options = {}) {
return (modelElement, { writer }) => {
if (!modelElement.parent.is('element', 'tableCell')) {
return null;
}
if (!isSingleParagraphWithoutAttributes(modelElement)) {
return null;
}
if (options.asWidget) {
return writer.createContainerElement('span', { class: 'ck-table-bogus-paragraph' });
}
else {
// Using `<p>` in case there are some markers on it and transparentRendering will render it anyway.
const viewElement = writer.createContainerElement('p');
writer.setCustomProperty('dataPipeline:transparentRendering', true, viewElement);
return viewElement;
}
};
}
/**
* 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 class="ck-table-bogus-paragraph">`
* * If returned `false` - to a `<p>`
*/
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:
* * Adds a {@link module:engine/view/element~Element#_setCustomProperty custom property} allowing to recognize the table widget element.
* * Calls the {@link module:widget/utils~toWidget} function with the proper element's label creator.
*
* @param writer An instance of the view writer.
* @param label The element's label. It will be concatenated with the table `alt` attribute if one is present.
*/
function toTableWidget(viewElement, writer) {
writer.setCustomProperty('table', true, viewElement);
return toWidget(viewElement, writer, { hasSelectionHandle: true });
}
/**
* Checks if an element has any attributes set.
*/
function hasAnyAttribute(element) {
for (const attributeKey of element.getAttributeKeys()) {
// Ignore selection attributes stored on block elements.
if (attributeKey.startsWith('selection:') || attributeKey == 'htmlEmptyBlock') {
continue;
}
return true;
}
return false;
}