diff --git a/packages/ckeditor5-media-embed/docs/features/media-embed.md b/packages/ckeditor5-media-embed/docs/features/media-embed.md index 931c5442e7b..18cf69b4869 100644 --- a/packages/ckeditor5-media-embed/docs/features/media-embed.md +++ b/packages/ckeditor5-media-embed/docs/features/media-embed.md @@ -101,6 +101,30 @@ By default, the media embed feature outputs semantic `` tags f ``` +Further customization of semantic data output can be done through the {@link module:media-embed/mediaembed~MediaEmbedConfig#preferredElementName `config.mediaEmbed.preferredElementName`} and {@link module:media-embed/mediaembed~MediaEmbedConfig#elementNames `config.mediaEmbed.elementNames`} options. As an example, if `preferredElementName` is set to `o-embed`: + +```html +
+ +
+``` + +And further, to be backward compatible with legacy semantic elements (any element using the `url` attribute, these can be passed via `elementNames`: `['oembed', 'o-embed']`. If there is a semantic tag for ``, you can set `['oembed', 'o-embed', 'mytag']`. To remove support for tags, omit the tag name. To skip handling of `` tags, `['oembed']`: + +```js +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ MediaEmbed, ... ],, + toolbar: [ 'mediaEmbed', ... ] + mediaEmbed: { + elementNames: ['oembed'] + } + } ) + .then( ... ) + .catch( ... ); +``` + + #### Including previews in data Optionally, by setting `mediaEmbed.previewsInData` to `true` you can configure the media embed feature to output media in the same way they look in the editor. So if the media element is "previewable", the media preview (HTML) is saved to the database: diff --git a/packages/ckeditor5-media-embed/src/mediaembed.js b/packages/ckeditor5-media-embed/src/mediaembed.js index 89e3a2854f1..2cdb12b8334 100644 --- a/packages/ckeditor5-media-embed/src/mediaembed.js +++ b/packages/ckeditor5-media-embed/src/mediaembed.js @@ -236,6 +236,44 @@ export default class MediaEmbed extends Plugin { * @member {Array.} module:media-embed/mediaembed~MediaEmbedConfig#removeProviders */ +/** + * Customizing semantic element name. + * + * When `oembed` (default), the feature produces "semantic" data with tag ``: + * + *
+ * + *
+ * + * It can also be set to other element names, for instance, `o-embed` will produce: + * + *
+ * + *
+ * + * @member {String} [module:media-embed/mediaembed~MediaEmbedConfig#preferredElementName] + */ + +/** + * Supporting legacy semantic element names. + * + * When `['oembed', 'o-embed']` (default), the feature renders "semantic" data for content with + * `` an `` tags: + * + *
+ * + *
+ * + *
+ * + *
+ * + * By default, the feature will render new media embeds via the option + * {@link module:media-embed/mediaembed~MediaEmbedConfig#preferredElementName `config.mediaEmbed.preferredElementName`} + * + * @member {Array} [module:media-embed/mediaembed~MediaEmbedConfig#elementNames] + */ + /** * Controls the data format produced by the feature. * diff --git a/packages/ckeditor5-media-embed/src/mediaembedediting.js b/packages/ckeditor5-media-embed/src/mediaembedediting.js index 7cdcd100081..3458f5f7b4a 100644 --- a/packages/ckeditor5-media-embed/src/mediaembedediting.js +++ b/packages/ckeditor5-media-embed/src/mediaembedediting.js @@ -36,6 +36,8 @@ export default class MediaEmbedEditing extends Plugin { super( editor ); editor.config.define( 'mediaEmbed', { + elementNames: [ 'oembed', 'o-embed' ], + preferredElementName: 'oembed', providers: [ { name: 'dailymotion', @@ -162,6 +164,8 @@ export default class MediaEmbedEditing extends Plugin { const t = editor.t; const conversion = editor.conversion; const renderMediaPreview = editor.config.get( 'mediaEmbed.previewsInData' ); + const elementNames = editor.config.get( 'mediaEmbed.elementNames' ); + const preferredElementName = editor.config.get( 'mediaEmbed.preferredElementName' ); const registry = this.registry; editor.commands.add( 'mediaEmbed', new MediaEmbedCommand( editor ) ); @@ -181,6 +185,7 @@ export default class MediaEmbedEditing extends Plugin { const url = modelElement.getAttribute( 'url' ); return createMediaFigureElement( writer, registry, url, { + preferredElementName, renderMediaPreview: url && renderMediaPreview } ); } @@ -189,6 +194,7 @@ export default class MediaEmbedEditing extends Plugin { // Model -> Data (url -> data-oembed-url) conversion.for( 'dataDowncast' ).add( modelToViewUrlAttributeConverter( registry, { + preferredElementName, renderMediaPreview } ) ); @@ -198,6 +204,7 @@ export default class MediaEmbedEditing extends Plugin { view: ( modelElement, { writer } ) => { const url = modelElement.getAttribute( 'url' ); const figure = createMediaFigureElement( writer, registry, url, { + preferredElementName, renderForEditingView: true } ); @@ -208,6 +215,7 @@ export default class MediaEmbedEditing extends Plugin { // Model -> View (url -> data-oembed-url) conversion.for( 'editingDowncast' ).add( modelToViewUrlAttributeConverter( registry, { + preferredElementName, renderForEditingView: true } ) ); @@ -216,7 +224,7 @@ export default class MediaEmbedEditing extends Plugin { // Upcast semantic media. .elementToElement( { view: { - name: 'oembed', + name: new RegExp( `^(${ elementNames.join( '|' ) })$` ), attributes: { url: true } diff --git a/packages/ckeditor5-media-embed/src/mediaregistry.js b/packages/ckeditor5-media-embed/src/mediaregistry.js index 8fd3fbfbbb5..e507910a9b7 100644 --- a/packages/ckeditor5-media-embed/src/mediaregistry.js +++ b/packages/ckeditor5-media-embed/src/mediaregistry.js @@ -67,6 +67,13 @@ export default class MediaRegistry { * @member {Array} */ this.providerDefinitions = providerDefinitions; + + /** + * The preferred element names for newly added media embed. + * + * @member {String} + */ + this.preferredElementName = config.preferredElementName; } /** @@ -206,6 +213,7 @@ class Media { * * @param {module:engine/view/downcastwriter~DowncastWriter} writer The view writer used to produce a view element. * @param {Object} options + * @param {String} [options.preferredElementName] * @param {String} [options.renderMediaPreview] * @param {String} [options.renderForEditingView] * @returns {module:engine/view/element~Element} @@ -233,7 +241,7 @@ class Media { attributes.url = this.url; } - viewElement = writer.createEmptyElement( 'oembed', attributes ); + viewElement = writer.createEmptyElement( options.preferredElementName, attributes ); } writer.setCustomProperty( 'media-content', true, viewElement ); diff --git a/packages/ckeditor5-media-embed/tests/mediaembedediting.js b/packages/ckeditor5-media-embed/tests/mediaembedediting.js index f4c0667ce6f..699e7e8dab4 100644 --- a/packages/ckeditor5-media-embed/tests/mediaembedediting.js +++ b/packages/ckeditor5-media-embed/tests/mediaembedediting.js @@ -477,6 +477,152 @@ describe( 'MediaEmbedEditing', () => { } ); describe( 'conversion in the data pipeline', () => { + describe( 'preferredElementName#o-embed', () => { + beforeEach( () => { + return createTestEditor( { + preferredElementName: 'o-embed', + providers: providerDefinitions + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + doc = model.document; + view = editor.editing.view; + } ); + } ); + + describe( 'model to view', () => { + it( 'should convert', () => { + setModelData( model, '' ); + + expect( editor.getData() ).to.equal( + '
' + + '' + + '
' ); + } ); + + it( 'should convert (no url)', () => { + setModelData( model, '' ); + + expect( editor.getData() ).to.equal( + '
' + + '' + + '
' ); + } ); + + it( 'should convert (preview-less media)', () => { + setModelData( model, '' ); + + expect( editor.getData() ).to.equal( + '
' + + '' + + '
' ); + } ); + } ); + + describe( 'view to model', () => { + it( 'should convert media figure', () => { + editor.setData( '
' ); + + expect( getModelData( model, { withoutSelection: true } ) ) + .to.equal( '' ); + } ); + + it( 'should not convert if there is no media class', () => { + editor.setData( '
My quote
' ); + + expect( getModelData( model, { withoutSelection: true } ) ) + .to.equal( '' ); + } ); + + it( 'should not convert if there is no o-embed wrapper inside #1', () => { + editor.setData( '
' ); + + expect( getModelData( model, { withoutSelection: true } ) ) + .to.equal( '' ); + } ); + + it( 'should not convert if there is no o-embed wrapper inside #2', () => { + editor.setData( '
test
' ); + + expect( getModelData( model, { withoutSelection: true } ) ) + .to.equal( '' ); + } ); + + it( 'should not convert when the wrapper has no data-o-embed-url attribute', () => { + editor.setData( '
' ); + + expect( getModelData( model, { withoutSelection: true } ) ) + .to.equal( '' ); + } ); + + it( 'should not convert in the wrong context', () => { + model.schema.register( 'blockquote', { inheritAllFrom: '$block' } ); + model.schema.addChildCheck( ( ctx, childDef ) => { + if ( ctx.endsWith( '$root' ) && childDef.name == 'media' ) { + return false; + } + } ); + + editor.conversion.elementToElement( { model: 'blockquote', view: 'blockquote' } ); + + editor.setData( + '
' ); + + expect( getModelData( model, { withoutSelection: true } ) ) + .to.equal( '
' ); + } ); + + it( 'should not convert if the o-embed wrapper is already consumed', () => { + editor.data.upcastDispatcher.on( 'element:figure', ( evt, data, conversionApi ) => { + const img = data.viewItem.getChild( 0 ); + conversionApi.consumable.consume( img, { name: true } ); + }, { priority: 'high' } ); + + editor.setData( '
' ); + + expect( getModelData( model, { withoutSelection: true } ) ) + .to.equal( '' ); + } ); + + it( 'should not convert if the figure is already consumed', () => { + editor.data.upcastDispatcher.on( 'element:figure', ( evt, data, conversionApi ) => { + conversionApi.consumable.consume( data.viewItem, { name: true, class: 'image' } ); + }, { priority: 'high' } ); + + editor.setData( '
' ); + + expect( getModelData( model, { withoutSelection: true } ) ) + .to.equal( '' ); + } ); + + it( 'should discard the contents of the media', () => { + editor.setData( '
foo bar
' ); + + expect( getModelData( model, { withoutSelection: true } ) ) + .to.equal( '' ); + } ); + + it( 'should not convert unknown media', () => { + return createTestEditor( { + providers: [ + testProviders.A + ] + } ) + .then( newEditor => { + newEditor.setData( + '
' + + '
' ); + + expect( getModelData( newEditor.model, { withoutSelection: true } ) ) + .to.equal( '' ); + + return newEditor.destroy(); + } ); + } ); + } ); + } ); + describe( 'previewsInData=false', () => { beforeEach( () => { return createTestEditor( {