UNPKG

@ckeditor/ckeditor5-html-support

Version:

HTML Support feature for CKEditor 5.

150 lines (149 loc) 7.15 kB
/** * @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 { toWidget } from 'ckeditor5/src/widget'; import { setViewAttributes, mergeViewElementAttributes, updateViewAttributes, getHtmlAttributeName } from './utils'; /** * View-to-model conversion helper for object elements. * * Preserves object element content in `htmlContent` attribute. * * @returns Returns a conversion callback. */ export function viewToModelObjectConverter({ model: modelName }) { return (viewElement, conversionApi) => { // Let's keep element HTML and its attributes, so we can rebuild element in downcast conversions. return conversionApi.writer.createElement(modelName, { htmlContent: viewElement.getCustomProperty('$rawContent') }); }; } /** * Conversion helper converting an object element to an HTML object widget. * * @returns Returns a conversion callback. */ export function toObjectWidgetConverter(editor, { view: viewName, isInline }) { const t = editor.t; return (modelElement, { writer }) => { const widgetLabel = t('HTML object'); const viewElement = createObjectView(viewName, modelElement, writer); const viewAttributes = modelElement.getAttribute(getHtmlAttributeName(viewName)); writer.addClass('html-object-embed__content', viewElement); if (viewAttributes) { setViewAttributes(writer, viewAttributes, viewElement); } // Widget cannot be a raw element because the widget system would not be able // to add its UI to it. Thus, we need separate view container. const viewContainer = writer.createContainerElement(isInline ? 'span' : 'div', { class: 'html-object-embed', 'data-html-object-embed-label': widgetLabel }, viewElement); return toWidget(viewContainer, writer, { label: widgetLabel }); }; } /** * Creates object view element from the given model element. */ export function createObjectView(viewName, modelElement, writer) { return writer.createRawElement(viewName, null, (domElement, domConverter) => { domConverter.setContentOf(domElement, modelElement.getAttribute('htmlContent')); }); } /** * View-to-attribute conversion helper preserving inline element attributes on `$text`. * * @returns Returns a conversion callback. */ export function viewToAttributeInlineConverter({ view: viewName, model: attributeKey }, dataFilter) { return (dispatcher) => { dispatcher.on(`element:${viewName}`, (evt, data, conversionApi) => { let viewAttributes = dataFilter.processViewAttributes(data.viewItem, conversionApi); // Do not apply the attribute if the element itself is already consumed and there are no view attributes to store. if (!viewAttributes && !conversionApi.consumable.test(data.viewItem, { name: true })) { return; } // Otherwise, we might need to convert it to an empty object just to preserve element itself, // for example `<cite>` => <$text htmlCite="{}">. viewAttributes = viewAttributes || {}; // Consume the element itself if it wasn't consumed by any other converter. conversionApi.consumable.consume(data.viewItem, { name: true }); // Since we are converting to attribute we need a range on which we will set the attribute. // If the range is not created yet, we will create it. if (!data.modelRange) { data = Object.assign(data, conversionApi.convertChildren(data.viewItem, data.modelCursor)); } // Set attribute on each item in range according to the schema. for (const node of data.modelRange.getItems()) { if (conversionApi.schema.checkAttribute(node, attributeKey)) { // Node's children are converted recursively, so node can already include model attribute. // We want to extend it, not replace. const nodeAttributes = node.getAttribute(attributeKey); const attributesToAdd = mergeViewElementAttributes(viewAttributes, nodeAttributes || {}); conversionApi.writer.setAttribute(attributeKey, attributesToAdd, node); } } }, { priority: 'low' }); }; } /** * Attribute-to-view conversion helper applying attributes to view element preserved on `$text`. * * @returns Returns a conversion callback. */ export function attributeToViewInlineConverter({ priority, view: viewName }) { return (attributeValue, conversionApi) => { if (!attributeValue) { return; } const { writer } = conversionApi; const viewElement = writer.createAttributeElement(viewName, null, { priority }); setViewAttributes(writer, attributeValue, viewElement); return viewElement; }; } /** * View-to-model conversion helper preserving allowed attributes on block element. * * All matched attributes will be preserved on `html*Attributes` attribute. * * @returns Returns a conversion callback. */ export function viewToModelBlockAttributeConverter({ view: viewName }, dataFilter) { return (dispatcher) => { dispatcher.on(`element:${viewName}`, (evt, data, conversionApi) => { // Converting an attribute of an element that has not been converted to anything does not make sense // because there will be nowhere to set that attribute on. At this stage, the element should've already // been converted. A collapsed range can show up in to-do lists (<input>) or complex widgets (e.g. table). // (https://github.com/ckeditor/ckeditor5/issues/11000). if (!data.modelRange || data.modelRange.isCollapsed) { return; } const viewAttributes = dataFilter.processViewAttributes(data.viewItem, conversionApi); if (!viewAttributes) { return; } conversionApi.writer.setAttribute(getHtmlAttributeName(data.viewItem.name), viewAttributes, data.modelRange); }, { priority: 'low' }); }; } /** * Model-to-view conversion helper applying attributes preserved in `html*Attributes` attribute * for block elements. * * @returns Returns a conversion callback. */ export function modelToViewBlockAttributeConverter({ view: viewName, model: modelName }) { return (dispatcher) => { dispatcher.on(`attribute:${getHtmlAttributeName(viewName)}:${modelName}`, (evt, data, conversionApi) => { if (!conversionApi.consumable.consume(data.item, evt.name)) { return; } const { attributeOldValue, attributeNewValue } = data; const viewWriter = conversionApi.writer; const viewElement = conversionApi.mapper.toViewElement(data.item); updateViewAttributes(viewWriter, attributeOldValue, attributeNewValue, viewElement); }); }; }