UNPKG

@ckeditor/ckeditor5-html-support

Version:

HTML Support feature for CKEditor 5.

127 lines (126 loc) 5.59 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 */ import { Plugin } from 'ckeditor5/src/core.js'; import { priorities } from 'ckeditor5/src/utils.js'; import { modelToViewBlockAttributeConverter, viewToModelBlockAttributeConverter } from '../converters.js'; import { DataFilter } from '../datafilter.js'; import { getHtmlAttributeName } from '../utils.js'; /** * Provides the General HTML Support integration for elements which can behave like sectioning element (e.g. article) or * element accepting only inline content (e.g. paragraph). * * The distinction between this two content models is important for choosing correct schema model and proper content conversion. * As an example, it ensures that: * * * children elements paragraphing is enabled for sectioning elements only, * * element and its content can be correctly handled by editing view (splitting and merging elements), * * model element HTML is semantically correct and easier to work with. * * If element contains any block element, it will be treated as a sectioning element and registered using * {@link module:html-support/dataschema~HtmlSupportDataSchemaDefinition#model} and * {@link module:html-support/dataschema~HtmlSupportDataSchemaDefinition#modelSchema} in editor schema. * Otherwise, it will be registered under * {@link module:html-support/dataschema~HtmlSupportDataSchemaBlockElementDefinition#paragraphLikeModel} model * name with model schema accepting only inline content (inheriting from `$block`). */ export class DualContentModelElementSupport extends Plugin { /** * @inheritDoc */ static get requires() { return [DataFilter]; } /** * @inheritDoc */ static get pluginName() { return 'DualContentModelElementSupport'; } /** * @inheritDoc */ static get isOfficialPlugin() { return true; } /** * @inheritDoc */ init() { const dataFilter = this.editor.plugins.get(DataFilter); dataFilter.on('register', (evt, definition) => { const blockDefinition = definition; const editor = this.editor; const schema = editor.model.schema; const conversion = editor.conversion; if (!blockDefinition.paragraphLikeModel) { return; } // Can only apply to newly registered features. if (schema.isRegistered(blockDefinition.model) || schema.isRegistered(blockDefinition.paragraphLikeModel)) { return; } const paragraphLikeModelDefinition = { model: blockDefinition.paragraphLikeModel, view: blockDefinition.view }; schema.register(blockDefinition.model, blockDefinition.modelSchema); schema.register(paragraphLikeModelDefinition.model, { inheritAllFrom: '$block' }); conversion.for('upcast').elementToElement({ view: blockDefinition.view, model: (viewElement, { writer }) => { if (this._hasBlockContent(viewElement)) { return writer.createElement(blockDefinition.model); } return writer.createElement(paragraphLikeModelDefinition.model); }, // With a `low` priority, `paragraph` plugin auto-paragraphing mechanism is executed. Make sure // this listener is called before it. If not, some elements will be transformed into a paragraph. converterPriority: priorities.low + 0.5 }); conversion.for('downcast').elementToElement({ view: blockDefinition.view, model: blockDefinition.model }); this._addAttributeConversion(blockDefinition); conversion.for('downcast').elementToElement({ view: paragraphLikeModelDefinition.view, model: paragraphLikeModelDefinition.model }); this._addAttributeConversion(paragraphLikeModelDefinition); evt.stop(); }); } /** * Checks whether the given view element includes any other block element. */ _hasBlockContent(viewElement) { const view = this.editor.editing.view; const blockElements = view.domConverter.blockElements; // Traversing the viewElement subtree looking for block elements. // Especially for the cases like <div><a href="#"><p>foo</p></a></div>. // https://github.com/ckeditor/ckeditor5/issues/11513 for (const viewItem of view.createRangeIn(viewElement).getItems()) { if (viewItem.is('element') && blockElements.includes(viewItem.name)) { return true; } } return false; } /** * Adds attribute filtering conversion for the given data schema. */ _addAttributeConversion(definition) { const editor = this.editor; const conversion = editor.conversion; const dataFilter = editor.plugins.get(DataFilter); editor.model.schema.extend(definition.model, { allowAttributes: getHtmlAttributeName(definition.view) }); conversion.for('upcast').add(viewToModelBlockAttributeConverter(definition, dataFilter)); conversion.for('downcast').add(modelToViewBlockAttributeConverter(definition)); } }