Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document list basic integration with styles dropdown. #13878

Merged
merged 19 commits into from
Apr 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 42 additions & 21 deletions packages/ckeditor5-html-support/src/datafilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,27 +216,7 @@ export default class DataFilter extends Plugin {
*/
public allowElement( viewName: string | RegExp ): void {
for ( const definition of this._dataSchema.getDefinitionsForView( viewName, true ) ) {
if ( this._allowedElements.has( definition ) ) {
continue;
}

this._allowedElements.add( definition );

// We need to wait for all features to be initialized before we can register
// element, so we can access existing features model schemas.
// If the data has not been initialized yet, _registerElementsAfterInit() method will take care of
// registering elements.
if ( this._dataInitialized ) {
// Defer registration to the next data pipeline data set so any disallow rules could be applied
// even if added after allow rule (disallowElement).
this.editor.data.once( 'set', () => {
this._fireRegisterEvent( definition );
}, {
// With the highest priority listener we are able to register elements right before
// running data conversion.
priority: priorities.get( 'highest' ) + 1
} );
}
this._addAllowedElement( definition );

// Reset cached map to recalculate it on the next usage.
this._coupledAttributes = null;
Expand Down Expand Up @@ -308,6 +288,42 @@ export default class DataFilter extends Plugin {
return consumeAttributes( viewElement, conversionApi, this._allowedAttributes );
}

/**
* Adds allowed element definition and fires registration event.
*/
private _addAllowedElement( definition: DataSchemaDefinition ): void {
if ( this._allowedElements.has( definition ) ) {
return;
}

this._allowedElements.add( definition );

// For attribute based integrations (table figure, document lists, etc.) register related element definitions.
if ( 'appliesToBlock' in definition && typeof definition.appliesToBlock == 'string' ) {
for ( const relatedDefinition of this._dataSchema.getDefinitionsForModel( definition.appliesToBlock ) ) {
if ( relatedDefinition.isBlock ) {
this._addAllowedElement( relatedDefinition );
}
}
}

// We need to wait for all features to be initialized before we can register
// element, so we can access existing features model schemas.
// If the data has not been initialized yet, _registerElementsAfterInit() method will take care of
// registering elements.
if ( this._dataInitialized ) {
// Defer registration to the next data pipeline data set so any disallow rules could be applied
// even if added after allow rule (disallowElement).
this.editor.data.once( 'set', () => {
this._fireRegisterEvent( definition );
}, {
// With the highest priority listener we are able to register elements right before
// running data conversion.
priority: priorities.get( 'highest' ) + 1
} );
}
}

/**
* Registers elements allowed by {@link module:html-support/datafilter~DataFilter#allowElement} method
* once {@link module:engine/controller/datacontroller~DataController editor's data controller} is initialized.
Expand Down Expand Up @@ -572,6 +588,11 @@ export default class DataFilter extends Plugin {
const conversion = editor.conversion;
const attributeKey = definition.model;

// This element is stored in the model as an attribute on a block element, for example DocumentLists.
if ( definition.appliesToBlock ) {
return;
}

schema.extend( '$text', {
allowAttributes: attributeKey
} );
Expand Down
83 changes: 54 additions & 29 deletions packages/ckeditor5-html-support/src/dataschema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,7 @@ export default class DataSchema extends Plugin {
/**
* A map of registered data schema definitions.
*/
private readonly _definitions: Map<string, DataSchemaDefinition>;

constructor( editor: Editor ) {
super( editor );

this._definitions = new Map();
}
private readonly _definitions: Array<DataSchemaDefinition> = [];

/**
* @inheritDoc
Expand All @@ -81,14 +75,14 @@ export default class DataSchema extends Plugin {
* Add new data schema definition describing block element.
*/
public registerBlockElement( definition: DataSchemaBlockElementDefinition ): void {
this._definitions.set( definition.model, { ...definition, isBlock: true } );
this._definitions.push( { ...definition, isBlock: true } );
}

/**
* Add new data schema definition describing inline element.
*/
public registerInlineElement( definition: DataSchemaInlineElementDefinition ): void {
this._definitions.set( definition.model, { ...definition, isInline: true } );
this._definitions.push( { ...definition, isInline: true } );
}

/**
Expand Down Expand Up @@ -136,12 +130,18 @@ export default class DataSchema extends Plugin {
return definitions;
}

/**
* Returns definitions matching the given model name.
*/
public getDefinitionsForModel( modelName: string ): Array<DataSchemaDefinition> {
return this._definitions.filter( definition => definition.model == modelName );
}

/**
* Returns definitions matching the given view name.
*/
private _getMatchingViewDefinitions( viewName: string | RegExp ): Array<DataSchemaDefinition> {
return Array.from( this._definitions.values() )
.filter( def => def.view && testViewName( viewName, def.view ) );
return this._definitions.filter( def => def.view && testViewName( viewName, def.view ) );
}

/**
Expand All @@ -150,21 +150,31 @@ export default class DataSchema extends Plugin {
* @param modelName Data schema model name.
*/
private* _getReferences( modelName: string ): Iterable<DataSchemaDefinition> {
const { modelSchema } = this._definitions.get( modelName )!;

if ( !modelSchema ) {
return;
}

const inheritProperties = [ 'inheritAllFrom', 'inheritTypesFrom', 'allowWhere', 'allowContentOf', 'allowAttributesOf' ];
const inheritProperties = [
'inheritAllFrom',
'inheritTypesFrom',
'allowWhere',
'allowContentOf',
'allowAttributesOf'
] as const;

const definitions = this._definitions.filter( definition => definition.model == modelName );

for ( const { modelSchema } of definitions ) {
if ( !modelSchema ) {
continue;
}

for ( const property of inheritProperties ) {
for ( const referenceName of toArray( ( modelSchema as any )[ property ] || [] ) ) {
const definition = this._definitions.get( referenceName );
for ( const property of inheritProperties ) {
for ( const referenceName of toArray( modelSchema[ property ] || [] ) ) {
const definitions = this._definitions.filter( definition => definition.model == referenceName );

if ( referenceName !== modelName && definition ) {
yield* this._getReferences( definition.model );
yield definition;
for ( const definition of definitions ) {
if ( referenceName !== modelName ) {
yield* this._getReferences( definition.model );
yield definition;
}
}
}
}
}
Expand All @@ -179,13 +189,20 @@ export default class DataSchema extends Plugin {
* @param definition Definition update.
*/
private _extendDefinition( definition: DataSchemaDefinition ): void {
const currentDefinition = this._definitions.get( definition.model );
const currentDefinitions = Array.from( this._definitions.entries() )
.filter( ( [ , currentDefinition ] ) => currentDefinition.model == definition.model );

if ( currentDefinitions.length == 0 ) {
this._definitions.push( definition );

const mergedDefinition = mergeWith( {}, currentDefinition, definition, ( target, source ) => {
return Array.isArray( target ) ? target.concat( source ) : undefined;
} );
return;
}

this._definitions.set( definition.model, mergedDefinition );
for ( const [ idx, currentDefinition ] of currentDefinitions ) {
this._definitions[ idx ] = mergeWith( {}, currentDefinition, definition, ( target, source ) => {
return Array.isArray( target ) ? target.concat( source ) : undefined;
} );
}
}
}

Expand Down Expand Up @@ -277,4 +294,12 @@ export interface DataSchemaInlineElementDefinition extends DataSchemaDefinition
* {@link module:html-support/datafilter~DataFilter#_registerModelPostFixer GHS post-fixer} for more details.
*/
coupledAttribute?: string;

/**
* Indicates that element should not be converted as a model text attribute.
* It is used to map view elements that do not have a separate model element but their data is stored in a model attribute.
* For example `<tbody>` element does not have a dedicated model element and GHS stores attributes of `<tbody>`
* in the `htmlTbodyAttributes` model attribute of the `table` model element.
*/
appliesToBlock?: boolean | string;
}
16 changes: 8 additions & 8 deletions packages/ckeditor5-html-support/src/generalhtmlsupport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,19 +77,19 @@ export default class GeneralHtmlSupport extends Plugin {
/**
* Returns a GHS model attribute name related to a given view element name.
*
* @internal
* @param viewElementName A view element name.
*/
private getGhsAttributeNameForElement( viewElementName: string ): string {
public getGhsAttributeNameForElement( viewElementName: string ): string {
const dataSchema = this.editor.plugins.get( 'DataSchema' );
const definitions = Array.from( dataSchema.getDefinitionsForView( viewElementName, false ) );

if (
definitions &&
definitions.length &&
( definitions[ 0 ] as DataSchemaInlineElementDefinition ).isInline &&
!definitions[ 0 ].isObject
) {
return definitions[ 0 ].model;
const inlineDefinition = definitions.find( definition => (
( definition as DataSchemaInlineElementDefinition ).isInline && !definitions[ 0 ].isObject
) );

if ( inlineDefinition ) {
return inlineDefinition.model;
}

return 'htmlAttributes';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ function modelToViewMediaAttributeConverter( mediaElementName: string ) {
conversionApi.writer,
attributeOldValue as GHSViewAttributes,
attributeNewValue as GHSViewAttributes,
viewElement! );
viewElement!
);
} );
}
};
Expand Down
9 changes: 7 additions & 2 deletions packages/ckeditor5-html-support/src/integrations/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type {
UpcastElementEvent,
ViewElement } from 'ckeditor5/src/engine';
import { Plugin } from 'ckeditor5/src/core';
import { setViewAttributes, type GHSViewAttributes } from '../utils';
import { updateViewAttributes, type GHSViewAttributes } from '../utils';
import DataFilter, { type DataFilterRegisterEvent } from '../datafilter';
import { getDescendantElement } from './integrationutils';

Expand Down Expand Up @@ -161,7 +161,12 @@ function modelToViewTableAttributeConverter() {
const containerElement = conversionApi.mapper.toViewElement( data.item as Element );
const viewElement = getDescendantElement( conversionApi.writer, containerElement!, elementName );

setViewAttributes( conversionApi.writer, data.attributeNewValue as GHSViewAttributes, viewElement! );
updateViewAttributes(
conversionApi.writer,
data.attributeOldValue as GHSViewAttributes,
data.attributeNewValue as GHSViewAttributes,
viewElement!
);
} );
}
};
Expand Down
45 changes: 42 additions & 3 deletions packages/ckeditor5-html-support/src/schemadefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ import type { DataSchemaBlockElementDefinition, DataSchemaInlineElementDefinitio

export default {
block: [
// Existing features
// Existing features.
{
model: 'codeBlock',
view: 'pre'
Expand Down Expand Up @@ -117,7 +117,7 @@ export default {
view: 'img'
},

// Compatibility features
// Compatibility features.
{
model: 'htmlP',
view: 'p',
Expand Down Expand Up @@ -514,7 +514,46 @@ export default {
}
}
] as Array<DataSchemaBlockElementDefinition>,

inline: [
// Existing features (attribute set on an existing model element).
{
model: 'htmlLiAttributes',
view: 'li',
appliesToBlock: true
},
{
model: 'htmlListAttributes',
view: 'ol',
appliesToBlock: true
},
{
model: 'htmlListAttributes',
view: 'ul',
appliesToBlock: true
},
{
model: 'htmlFigureAttributes',
view: 'figure',
appliesToBlock: 'table'
},
{
model: 'htmlTheadAttributes',
view: 'thead',
appliesToBlock: 'table'
},
{
model: 'htmlTbodyAttributes',
view: 'tbody',
appliesToBlock: 'table'
},
{
model: 'htmlFigureAttributes',
view: 'figure',
appliesToBlock: 'imageBlock'
},

// Compatibility features.
{
model: 'htmlAcronym',
view: 'acronym',
Expand Down Expand Up @@ -756,7 +795,7 @@ export default {
}
},

// Objects
// Objects.
{
model: 'htmlObject',
view: 'object',
Expand Down
16 changes: 16 additions & 0 deletions packages/ckeditor5-html-support/tests/datafilter.js
Original file line number Diff line number Diff line change
Expand Up @@ -1315,6 +1315,22 @@ describe( 'DataFilter', () => {
editor.getData( '<p>foobar</p>' );
} );

it( 'should not register default converters for appliesToBlock', () => {
dataSchema.registerInlineElement( {
view: 'xyz',
model: 'htmlXyz',
appliesToBlock: true
} );

dataFilter.allowElement( 'xyz' );

editor.setData( '<p><xyz>foobar</xyz></p>' );

expect( getModelData( model, { withoutSelection: true } ) ).to.equal( '<paragraph>foobar</paragraph>' );

editor.getData( '<p>foobar</p>' );
} );

it( 'should use correct priority level for existing features', () => {
// 'a' element is registered by data schema with priority 5.
// We are checking if this element will be correctly nested due to different
Expand Down
Loading