From 372c275e594f35344a26d0b534088b09ef6e30d8 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Fri, 16 Jun 2023 18:50:02 +0200 Subject: [PATCH 01/20] PoC of preserving empty inline elements. --- .../ckeditor5-html-support/src/converters.ts | 48 +++++++++++++++---- .../ckeditor5-html-support/src/datafilter.ts | 36 ++++++++++++-- .../tests/manual/ghs-all-features.html | 14 +++++- 3 files changed, 83 insertions(+), 15 deletions(-) diff --git a/packages/ckeditor5-html-support/src/converters.ts b/packages/ckeditor5-html-support/src/converters.ts index a77c14fd6dd..bf150b036bf 100644 --- a/packages/ckeditor5-html-support/src/converters.ts +++ b/packages/ckeditor5-html-support/src/converters.ts @@ -19,7 +19,8 @@ import type { UpcastConversionApi, UpcastDispatcher, UpcastElementEvent, - ViewElement + ViewElement, + Item } from 'ckeditor5/src/engine'; import { toWidget } from 'ckeditor5/src/widget'; import { @@ -102,7 +103,7 @@ export function createObjectView( viewName: string, modelElement: Element, write export function viewToAttributeInlineConverter( { view: viewName, model: attributeKey }: DataSchemaInlineElementDefinition, dataFilter: DataFilter -) { +): ( dispatcher: UpcastDispatcher ) => void { return ( dispatcher: UpcastDispatcher ): void => { dispatcher.on( `element:${ viewName }`, ( evt, data, conversionApi ) => { let viewAttributes = dataFilter.processViewAttributes( data.viewItem, conversionApi ); @@ -125,19 +126,46 @@ export function viewToAttributeInlineConverter( 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 || {} ); + // Convert empty inline element if it has any attributes. + if ( data.modelRange!.isCollapsed && Object.keys( viewAttributes ).length ) { + const modelElement = conversionApi.writer.createElement( 'htmlEmptyElement' ); - conversionApi.writer.setAttribute( attributeKey, attributesToAdd, node ); + if ( !conversionApi.safeInsert( modelElement, data.modelCursor ) ) { + return; } + + // TODO How to preserve spaces around? They are trimmed by DOM converter while parsing HTML string. + + const parts = conversionApi.getSplitParts( modelElement ); + + data.modelRange = conversionApi.writer.createRange( + data.modelRange!.start, + conversionApi.writer.createPositionAfter( parts[ parts.length - 1 ] ) + ); + + conversionApi.updateConversionResult( modelElement, data ); + setAttributeOnItem( modelElement, viewAttributes, conversionApi ); + + return; + } + + // Set attribute on each item in range according to the schema. + for ( const node of data.modelRange!.getItems() ) { + setAttributeOnItem( node, viewAttributes, conversionApi ); } }, { priority: 'low' } ); }; + + function setAttributeOnItem( node: Item, viewAttributes: GHSViewAttributes, conversionApi: UpcastConversionApi ): void { + 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 ); + } + } } /** diff --git a/packages/ckeditor5-html-support/src/datafilter.ts b/packages/ckeditor5-html-support/src/datafilter.ts index 8c6006178d0..f01620abcaf 100644 --- a/packages/ckeditor5-html-support/src/datafilter.ts +++ b/packages/ckeditor5-html-support/src/datafilter.ts @@ -47,6 +47,7 @@ import { import { getHtmlAttributeName, + setViewAttributes, type GHSViewAttributes } from './utils'; @@ -662,12 +663,39 @@ export default class DataFilter extends Plugin { schema.setAttributeProperties( attributeKey, definition.attributeProperties ); } + if ( !schema.isRegistered( 'htmlEmptyElement' ) ) { + schema.register( 'htmlEmptyElement', { + allowWhere: '$text', + allowAttributesOf: '$text', + isInline: true + } ); + } + conversion.for( 'upcast' ).add( viewToAttributeInlineConverter( definition, this ) ); - conversion.for( 'downcast' ).attributeToElement( { - model: attributeKey, - view: attributeToViewInlineConverter( definition ) - } ); + conversion.for( 'downcast' ) + .attributeToElement( { + model: attributeKey, + view: attributeToViewInlineConverter( definition ) + } ) + .elementToElement( { + model: 'htmlEmptyElement', + view: ( item, { writer, consumable } ) => { + if ( !item.hasAttribute( attributeKey ) ) { + return; + } + + // TODO Handle keyboard arrows navigation for elements styled as display: inline-block + + const viewElement = writer.createEmptyElement( definition.view! ); + const attributeValue = item.getAttribute( attributeKey ) as GHSViewAttributes; + + consumable.consume( item, `attribute:${ attributeKey }` ); + setViewAttributes( writer, attributeValue, viewElement ); + + return viewElement; + } + } ); } } diff --git a/packages/ckeditor5-html-support/tests/manual/ghs-all-features.html b/packages/ckeditor5-html-support/tests/manual/ghs-all-features.html index 1816b66eeca..1e973e21eed 100644 --- a/packages/ckeditor5-html-support/tests/manual/ghs-all-features.html +++ b/packages/ckeditor5-html-support/tests/manual/ghs-all-features.html @@ -15,12 +15,24 @@ .text-italic { font-style: italic; } + i.inline-icon { + display: inline-block; + width: 1em; + height: 1em; + background: red; + }
-

Feature paragraph

+

empty inline at start

+

Text with empty inline inside

+

Text with empty inline a the end

+

ignores empty inline without any attributes

+
+