UNPKG

@ckeditor/ckeditor5-table

Version:

Table feature for CKEditor 5.

198 lines (197 loc) • 8.57 kB
/** * @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/tableediting */ import { Plugin } from 'ckeditor5/src/core.js'; import upcastTable, { ensureParagraphInTableCell, skipEmptyTableRow, upcastTableFigure } from './converters/upcasttable.js'; import { convertParagraphInTableCell, downcastCell, downcastRow, downcastTable } from './converters/downcast.js'; import InsertTableCommand from './commands/inserttablecommand.js'; import InsertRowCommand from './commands/insertrowcommand.js'; import InsertColumnCommand from './commands/insertcolumncommand.js'; import SplitCellCommand from './commands/splitcellcommand.js'; import MergeCellCommand from './commands/mergecellcommand.js'; import RemoveRowCommand from './commands/removerowcommand.js'; import RemoveColumnCommand from './commands/removecolumncommand.js'; import SetHeaderRowCommand from './commands/setheaderrowcommand.js'; import SetHeaderColumnCommand from './commands/setheadercolumncommand.js'; import MergeCellsCommand from './commands/mergecellscommand.js'; import SelectRowCommand from './commands/selectrowcommand.js'; import SelectColumnCommand from './commands/selectcolumncommand.js'; import TableUtils from '../src/tableutils.js'; import injectTableLayoutPostFixer from './converters/table-layout-post-fixer.js'; import injectTableCellParagraphPostFixer from './converters/table-cell-paragraph-post-fixer.js'; import tableHeadingsRefreshHandler from './converters/table-headings-refresh-handler.js'; import tableCellRefreshHandler from './converters/table-cell-refresh-handler.js'; import '../theme/tableediting.css'; /** * The table editing feature. */ export default class TableEditing extends Plugin { /** * @inheritDoc */ static get pluginName() { return 'TableEditing'; } /** * @inheritDoc */ static get isOfficialPlugin() { return true; } /** * @inheritDoc */ static get requires() { return [TableUtils]; } /** * @inheritDoc */ constructor(editor) { super(editor); this._additionalSlots = []; } /** * @inheritDoc */ init() { const editor = this.editor; const model = editor.model; const schema = model.schema; const conversion = editor.conversion; const tableUtils = editor.plugins.get(TableUtils); schema.register('table', { inheritAllFrom: '$blockObject', allowAttributes: ['headingRows', 'headingColumns'] }); schema.register('tableRow', { allowIn: 'table', isLimit: true }); schema.register('tableCell', { allowContentOf: '$container', allowIn: 'tableRow', allowAttributes: ['colspan', 'rowspan'], isLimit: true, isSelectable: true }); // Figure conversion. conversion.for('upcast').add(upcastTableFigure()); // Table conversion. conversion.for('upcast').add(upcastTable()); conversion.for('editingDowncast').elementToStructure({ model: { name: 'table', attributes: ['headingRows'] }, view: downcastTable(tableUtils, { asWidget: true, additionalSlots: this._additionalSlots }) }); conversion.for('dataDowncast').elementToStructure({ model: { name: 'table', attributes: ['headingRows'] }, view: downcastTable(tableUtils, { additionalSlots: this._additionalSlots }) }); // Table row conversion. conversion.for('upcast').elementToElement({ model: 'tableRow', view: 'tr' }); conversion.for('upcast').add(skipEmptyTableRow()); conversion.for('downcast').elementToElement({ model: 'tableRow', view: downcastRow() }); // Table cell conversion. conversion.for('upcast').elementToElement({ model: 'tableCell', view: 'td' }); conversion.for('upcast').elementToElement({ model: 'tableCell', view: 'th' }); conversion.for('upcast').add(ensureParagraphInTableCell('td')); conversion.for('upcast').add(ensureParagraphInTableCell('th')); conversion.for('editingDowncast').elementToElement({ model: 'tableCell', view: downcastCell({ asWidget: true }) }); conversion.for('dataDowncast').elementToElement({ model: 'tableCell', view: downcastCell() }); // Duplicates code - needed to properly refresh paragraph inside a table cell. conversion.for('editingDowncast').elementToElement({ model: 'paragraph', view: convertParagraphInTableCell({ asWidget: true }), converterPriority: 'high' }); conversion.for('dataDowncast').elementToElement({ model: 'paragraph', view: convertParagraphInTableCell(), converterPriority: 'high' }); // Table attributes conversion. conversion.for('downcast').attributeToAttribute({ model: 'colspan', view: 'colspan' }); conversion.for('upcast').attributeToAttribute({ model: { key: 'colspan', value: upcastCellSpan('colspan') }, view: 'colspan' }); conversion.for('downcast').attributeToAttribute({ model: 'rowspan', view: 'rowspan' }); conversion.for('upcast').attributeToAttribute({ model: { key: 'rowspan', value: upcastCellSpan('rowspan') }, view: 'rowspan' }); // Define the config. editor.config.define('table.defaultHeadings.rows', 0); editor.config.define('table.defaultHeadings.columns', 0); // Define all the commands. editor.commands.add('insertTable', new InsertTableCommand(editor)); editor.commands.add('insertTableRowAbove', new InsertRowCommand(editor, { order: 'above' })); editor.commands.add('insertTableRowBelow', new InsertRowCommand(editor, { order: 'below' })); editor.commands.add('insertTableColumnLeft', new InsertColumnCommand(editor, { order: 'left' })); editor.commands.add('insertTableColumnRight', new InsertColumnCommand(editor, { order: 'right' })); editor.commands.add('removeTableRow', new RemoveRowCommand(editor)); editor.commands.add('removeTableColumn', new RemoveColumnCommand(editor)); editor.commands.add('splitTableCellVertically', new SplitCellCommand(editor, { direction: 'vertically' })); editor.commands.add('splitTableCellHorizontally', new SplitCellCommand(editor, { direction: 'horizontally' })); editor.commands.add('mergeTableCells', new MergeCellsCommand(editor)); editor.commands.add('mergeTableCellRight', new MergeCellCommand(editor, { direction: 'right' })); editor.commands.add('mergeTableCellLeft', new MergeCellCommand(editor, { direction: 'left' })); editor.commands.add('mergeTableCellDown', new MergeCellCommand(editor, { direction: 'down' })); editor.commands.add('mergeTableCellUp', new MergeCellCommand(editor, { direction: 'up' })); editor.commands.add('setTableColumnHeader', new SetHeaderColumnCommand(editor)); editor.commands.add('setTableRowHeader', new SetHeaderRowCommand(editor)); editor.commands.add('selectTableRow', new SelectRowCommand(editor)); editor.commands.add('selectTableColumn', new SelectColumnCommand(editor)); injectTableLayoutPostFixer(model); injectTableCellParagraphPostFixer(model); this.listenTo(model.document, 'change:data', () => { tableHeadingsRefreshHandler(model, editor.editing); tableCellRefreshHandler(model, editor.editing); }); } /** * Registers downcast handler for the additional table slot. */ registerAdditionalSlot(slotHandler) { this._additionalSlots.push(slotHandler); } } /** * Returns fixed colspan and rowspan attrbutes values. * * @param type colspan or rowspan. * @returns conversion value function. */ function upcastCellSpan(type) { return (cell) => { const span = parseInt(cell.getAttribute(type)); if (Number.isNaN(span) || span <= 0) { return null; } return span; }; }