@ckeditor/ckeditor5-html-support
Version:
HTML Support feature for CKEditor 5.
164 lines (163 loc) • 6.81 kB
JavaScript
/**
* @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
import { Plugin } from 'ckeditor5/src/core';
import { updateViewAttributes } from '../utils';
import DataFilter from '../datafilter';
import { getDescendantElement } from './integrationutils';
/**
* Provides the General HTML Support integration with {@link module:table/table~Table Table} feature.
*/
export default class TableElementSupport extends Plugin {
/**
* @inheritDoc
*/
static get requires() {
return [DataFilter];
}
/**
* @inheritDoc
*/
static get pluginName() {
return 'TableElementSupport';
}
/**
* @inheritDoc
*/
init() {
const editor = this.editor;
if (!editor.plugins.has('TableEditing')) {
return;
}
const schema = editor.model.schema;
const conversion = editor.conversion;
const dataFilter = editor.plugins.get(DataFilter);
const tableUtils = editor.plugins.get('TableUtils');
dataFilter.on('register:figure', () => {
conversion.for('upcast').add(viewToModelFigureAttributeConverter(dataFilter));
});
dataFilter.on('register:table', (evt, definition) => {
if (definition.model !== 'table') {
return;
}
schema.extend('table', {
allowAttributes: [
'htmlTableAttributes',
// Figure, thead and tbody elements don't have model counterparts.
// We will be preserving attributes on table element using these attribute keys.
'htmlFigureAttributes', 'htmlTheadAttributes', 'htmlTbodyAttributes'
]
});
conversion.for('upcast').add(viewToModelTableAttributeConverter(dataFilter));
conversion.for('downcast').add(modelToViewTableAttributeConverter());
editor.model.document.registerPostFixer(createHeadingRowsPostFixer(editor.model, tableUtils));
evt.stop();
});
}
}
/**
* Creates a model post-fixer for thead and tbody GHS related attributes.
*/
function createHeadingRowsPostFixer(model, tableUtils) {
return writer => {
const changes = model.document.differ.getChanges();
let wasFixed = false;
for (const change of changes) {
if (change.type != 'attribute' || change.attributeKey != 'headingRows') {
continue;
}
const table = change.range.start.nodeAfter;
const hasTHeadAttributes = table.getAttribute('htmlTheadAttributes');
const hasTBodyAttributes = table.getAttribute('htmlTbodyAttributes');
if (hasTHeadAttributes && !change.attributeNewValue) {
writer.removeAttribute('htmlTheadAttributes', table);
wasFixed = true;
}
else if (hasTBodyAttributes && change.attributeNewValue == tableUtils.getRows(table)) {
writer.removeAttribute('htmlTbodyAttributes', table);
wasFixed = true;
}
}
return wasFixed;
};
}
/**
* View-to-model conversion helper preserving allowed attributes on {@link module:table/table~Table Table}
* feature model element.
*
* @returns Returns a conversion callback.
*/
function viewToModelTableAttributeConverter(dataFilter) {
return (dispatcher) => {
dispatcher.on('element:table', (evt, data, conversionApi) => {
if (!data.modelRange) {
return;
}
const viewTableElement = data.viewItem;
preserveElementAttributes(viewTableElement, 'htmlTableAttributes');
for (const childNode of viewTableElement.getChildren()) {
if (childNode.is('element', 'thead')) {
preserveElementAttributes(childNode, 'htmlTheadAttributes');
}
if (childNode.is('element', 'tbody')) {
preserveElementAttributes(childNode, 'htmlTbodyAttributes');
}
}
function preserveElementAttributes(viewElement, attributeName) {
const viewAttributes = dataFilter.processViewAttributes(viewElement, conversionApi);
if (viewAttributes) {
conversionApi.writer.setAttribute(attributeName, viewAttributes, data.modelRange);
}
}
}, { priority: 'low' });
};
}
/**
* View-to-model conversion helper preserving allowed attributes on {@link module:table/table~Table Table}
* feature model element from figure view element.
*
* @returns Returns a conversion callback.
*/
function viewToModelFigureAttributeConverter(dataFilter) {
return (dispatcher) => {
dispatcher.on('element:figure', (evt, data, conversionApi) => {
const viewFigureElement = data.viewItem;
if (!data.modelRange || !viewFigureElement.hasClass('table')) {
return;
}
const viewAttributes = dataFilter.processViewAttributes(viewFigureElement, conversionApi);
if (viewAttributes) {
conversionApi.writer.setAttribute('htmlFigureAttributes', viewAttributes, data.modelRange);
}
}, { priority: 'low' });
};
}
/**
* Model-to-view conversion helper applying attributes from {@link module:table/table~Table Table}
* feature.
*
* @returns Returns a conversion callback.
*/
function modelToViewTableAttributeConverter() {
return (dispatcher) => {
addAttributeConversionDispatcherHandler('table', 'htmlTableAttributes');
addAttributeConversionDispatcherHandler('figure', 'htmlFigureAttributes');
addAttributeConversionDispatcherHandler('thead', 'htmlTheadAttributes');
addAttributeConversionDispatcherHandler('tbody', 'htmlTbodyAttributes');
function addAttributeConversionDispatcherHandler(elementName, attributeName) {
dispatcher.on(`attribute:${attributeName}:table`, (evt, data, conversionApi) => {
if (!conversionApi.consumable.test(data.item, evt.name)) {
return;
}
const containerElement = conversionApi.mapper.toViewElement(data.item);
const viewElement = getDescendantElement(conversionApi.writer, containerElement, elementName);
if (!viewElement) {
return;
}
conversionApi.consumable.consume(data.item, evt.name);
updateViewAttributes(conversionApi.writer, data.attributeOldValue, data.attributeNewValue, viewElement);
});
}
};
}