diff --git a/packages/ckeditor5-image/package.json b/packages/ckeditor5-image/package.json index 61589e3dac9..04c7057610d 100644 --- a/packages/ckeditor5-image/package.json +++ b/packages/ckeditor5-image/package.json @@ -33,11 +33,13 @@ "@ckeditor/ckeditor5-essentials": "^38.0.1", "@ckeditor/ckeditor5-heading": "^38.0.1", "@ckeditor/ckeditor5-html-embed": "^38.0.1", + "@ckeditor/ckeditor5-html-support": "^38.0.1", "@ckeditor/ckeditor5-indent": "^38.0.1", "@ckeditor/ckeditor5-link": "^38.0.1", "@ckeditor/ckeditor5-list": "^38.0.1", "@ckeditor/ckeditor5-media-embed": "^38.0.1", "@ckeditor/ckeditor5-paragraph": "^38.0.1", + "@ckeditor/ckeditor5-paste-from-office": "^38.0.1", "@ckeditor/ckeditor5-table": "^38.0.1", "@ckeditor/ckeditor5-theme-lark": "^38.0.1", "@ckeditor/ckeditor5-typing": "^38.0.1", diff --git a/packages/ckeditor5-image/src/imageresize/imageresizeediting.ts b/packages/ckeditor5-image/src/imageresize/imageresizeediting.ts index 4e0adbfa3cd..80d1fca5bce 100644 --- a/packages/ckeditor5-image/src/imageresize/imageresizeediting.ts +++ b/packages/ckeditor5-image/src/imageresize/imageresizeediting.ts @@ -82,11 +82,11 @@ export default class ImageResizeEditing extends Plugin { private _registerSchema(): void { if ( this.editor.plugins.has( 'ImageBlockEditing' ) ) { - this.editor.model.schema.extend( 'imageBlock', { allowAttributes: 'resizedWidth' } ); + this.editor.model.schema.extend( 'imageBlock', { allowAttributes: [ 'resizedWidth', 'resizedHeight' ] } ); } if ( this.editor.plugins.has( 'ImageInlineEditing' ) ) { - this.editor.model.schema.extend( 'imageInline', { allowAttributes: 'resizedWidth' } ); + this.editor.model.schema.extend( 'imageInline', { allowAttributes: [ 'resizedWidth', 'resizedHeight' ] } ); } } @@ -97,6 +97,7 @@ export default class ImageResizeEditing extends Plugin { */ private _registerConverters( imageType: 'imageBlock' | 'imageInline' ) { const editor = this.editor; + const imageUtils: ImageUtils = editor.plugins.get( 'ImageUtils' ); // Dedicated converter to propagate image's attribute to the img tag. editor.conversion.for( 'downcast' ).add( dispatcher => @@ -118,6 +119,35 @@ export default class ImageResizeEditing extends Plugin { } ) ); + editor.conversion.for( 'dataDowncast' ).add( dispatcher => + dispatcher.on( `attribute:resizedHeight:${ imageType }`, ( evt, data, conversionApi ) => { + if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { + return; + } + + const viewWriter = conversionApi.writer; + const viewElement = conversionApi.mapper.toViewElement( data.item ); + + viewWriter.setStyle( 'height', data.attributeNewValue, viewElement ); + } ) + ); + + editor.conversion.for( 'editingDowncast' ).add( dispatcher => + dispatcher.on( `attribute:resizedHeight:${ imageType }`, ( evt, data, conversionApi ) => { + if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { + return; + } + + if ( data.attributeNewValue !== null ) { + const viewWriter = conversionApi.writer; + const figure = conversionApi.mapper.toViewElement( data.item ); + const target = imageType === 'imageInline' ? imageUtils.findViewImgElement( figure ) : figure; + + viewWriter.setStyle( 'height', data.attributeNewValue, target ); + } + } ) + ); + editor.conversion.for( 'upcast' ) .attributeToAttribute( { view: { @@ -128,7 +158,45 @@ export default class ImageResizeEditing extends Plugin { }, model: { key: 'resizedWidth', - value: ( viewElement: ViewElement ) => viewElement.getStyle( 'width' ) + value: ( viewElement: ViewElement ) => { + const widthStyle = imageUtils.getSizeInPx( viewElement.getStyle( 'width' ) ); + const heightStyle = imageUtils.getSizeInPx( viewElement.getStyle( 'height' ) ); + + // If both image styles: width & height are set, they will override the image width & height attributes in the + // browser. In this case, the image looks the same as if these styles were applied to attributes instead of styles. + // That's why we can upcast these styles to width & height attributes instead of resizedWidth and resizedHeight. + if ( widthStyle && heightStyle ) { + return null; + } + + return viewElement.getStyle( 'width' ); + } + } + } ); + + editor.conversion.for( 'upcast' ) + .attributeToAttribute( { + view: { + name: imageType === 'imageBlock' ? 'figure' : 'img', + styles: { + height: /.+/ + } + }, + model: { + key: 'resizedHeight', + value: ( viewElement: ViewElement ) => { + const widthStyle = imageUtils.getSizeInPx( viewElement.getStyle( 'width' ) ); + const heightStyle = imageUtils.getSizeInPx( viewElement.getStyle( 'height' ) ); + + // If both image styles: width & height are set, they will override the image width & height attributes in the + // browser. In this case, the image looks the same as if these styles were applied to attributes instead of styles. + // That's why we can upcast these styles to width & height attributes instead of resizedWidth and resizedHeight. + if ( widthStyle && heightStyle ) { + return null; + } + + return viewElement.getStyle( 'height' ); + } } } ); } diff --git a/packages/ckeditor5-image/src/imageresize/imageresizehandles.ts b/packages/ckeditor5-image/src/imageresize/imageresizehandles.ts index 04d3880b600..eab0555cc22 100644 --- a/packages/ckeditor5-image/src/imageresize/imageresizehandles.ts +++ b/packages/ckeditor5-image/src/imageresize/imageresizehandles.ts @@ -10,6 +10,7 @@ import type { Element, ViewContainerElement, ViewElement } from 'ckeditor5/src/engine'; import { Plugin } from 'ckeditor5/src/core'; import { WidgetResize } from 'ckeditor5/src/widget'; +import ImageUtils from '../imageutils'; import ImageLoadObserver, { type ImageLoadedEvent } from '../image/imageloadobserver'; import type ResizeImageCommand from './resizeimagecommand'; @@ -37,7 +38,7 @@ export default class ImageResizeHandles extends Plugin { * @inheritDoc */ public static get requires() { - return [ WidgetResize ] as const; + return [ WidgetResize, ImageUtils ] as const; } /** @@ -63,6 +64,7 @@ export default class ImageResizeHandles extends Plugin { private _setupResizerCreator(): void { const editor = this.editor; const editingView = editor.editing.view; + const imageUtils: ImageUtils = editor.plugins.get( 'ImageUtils' ); editingView.addObserver( ImageLoadObserver ); @@ -130,6 +132,27 @@ export default class ImageResizeHandles extends Plugin { writer.addClass( RESIZED_IMAGE_CLASS, widgetView ); } ); } + + const target = imageModel.name === 'imageInline' ? imageView : widgetView; + + if ( target.getStyle( 'height' ) ) { + editingView.change( writer => { + writer.removeStyle( 'height', target ); + } ); + } + } ); + + resizer.on( 'begin', () => { + const img = imageUtils.findViewImgElement( imageView )!; + const aspectRatio = img.getStyle( 'aspect-ratio' ); + const widthAttr = imageModel.getAttribute( 'width' ); + const heightAttr = imageModel.getAttribute( 'height' ); + + if ( widthAttr && heightAttr && !aspectRatio ) { + editingView.change( writer => { + writer.setStyle( 'aspect-ratio', `${ widthAttr }/${ heightAttr }`, img ); + } ); + } } ); resizer.bind( 'isEnabled' ).to( this ); diff --git a/packages/ckeditor5-image/src/imageresize/resizeimagecommand.ts b/packages/ckeditor5-image/src/imageresize/resizeimagecommand.ts index cb424e2af27..df14fef73f0 100644 --- a/packages/ckeditor5-image/src/imageresize/resizeimagecommand.ts +++ b/packages/ckeditor5-image/src/imageresize/resizeimagecommand.ts @@ -72,6 +72,7 @@ export default class ResizeImageCommand extends Command { if ( imageElement ) { model.change( writer => { writer.setAttribute( 'resizedWidth', options.width, imageElement ); + writer.removeAttribute( 'resizedHeight', imageElement ); } ); } } diff --git a/packages/ckeditor5-image/src/imagesizeattributes.ts b/packages/ckeditor5-image/src/imagesizeattributes.ts index e1c67109a78..c438e2eb72a 100644 --- a/packages/ckeditor5-image/src/imagesizeattributes.ts +++ b/packages/ckeditor5-image/src/imagesizeattributes.ts @@ -60,6 +60,27 @@ export default class ImageSizeAttributes extends Plugin { const viewElementName = imageType === 'imageBlock' ? 'figure' : 'img'; editor.conversion.for( 'upcast' ) + .attributeToAttribute( { + view: { + name: imageType === 'imageBlock' ? 'figure' : 'img', + styles: { + width: /.+/ + } + }, + model: { + key: 'width', + value: ( viewElement: ViewElement ) => { + const widthStyle = imageUtils.getSizeInPx( viewElement.getStyle( 'width' ) ); + const heightStyle = imageUtils.getSizeInPx( viewElement.getStyle( 'height' ) ); + + if ( widthStyle && heightStyle ) { + return widthStyle; + } + + return null; + } + } + } ) .attributeToAttribute( { view: { name: viewElementName, @@ -72,6 +93,27 @@ export default class ImageSizeAttributes extends Plugin { value: ( viewElement: ViewElement ) => viewElement.getAttribute( 'width' ) } } ) + .attributeToAttribute( { + view: { + name: imageType === 'imageBlock' ? 'figure' : 'img', + styles: { + height: /.+/ + } + }, + model: { + key: 'height', + value: ( viewElement: ViewElement ) => { + const widthStyle = imageUtils.getSizeInPx( viewElement.getStyle( 'width' ) ); + const heightStyle = imageUtils.getSizeInPx( viewElement.getStyle( 'height' ) ); + + if ( widthStyle && heightStyle ) { + return heightStyle; + } + + return null; + } + } + } ) .attributeToAttribute( { view: { name: viewElementName, @@ -91,7 +133,9 @@ export default class ImageSizeAttributes extends Plugin { attachDowncastConverter( dispatcher, 'height', 'height' ); } ); - function attachDowncastConverter( dispatcher: DowncastDispatcher, modelAttributeName: string, viewAttributeName: string ) { + function attachDowncastConverter( + dispatcher: DowncastDispatcher, modelAttributeName: string, viewAttributeName: string + ) { dispatcher.on( `attribute:${ modelAttributeName }:${ imageType }`, ( evt, data, conversionApi ) => { if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { return; @@ -106,7 +150,17 @@ export default class ImageSizeAttributes extends Plugin { } else { viewWriter.removeAttribute( viewAttributeName, img ); } + + const width = data.item.getAttribute( 'width' ); + const height = data.item.getAttribute( 'height' ); + const isResized = data.item.hasAttribute( 'resizedWidth' ); + const aspectRatio = img.getStyle( 'aspect-ratio' ); + + if ( width && height && !aspectRatio && isResized ) { + viewWriter.setStyle( 'aspect-ratio', `${ width }/${ height }`, img ); + } } ); } } } + diff --git a/packages/ckeditor5-image/src/imageutils.ts b/packages/ckeditor5-image/src/imageutils.ts index 73f584e28df..c1f57694ec0 100644 --- a/packages/ckeditor5-image/src/imageutils.ts +++ b/packages/ckeditor5-image/src/imageutils.ts @@ -293,6 +293,17 @@ export default class ImageUtils extends Plugin { return super.destroy(); } + + /** + * Returns parsed value of the size, but only if it contains unit: px. + */ + public getSizeInPx( size: string | undefined ): number | null { + if ( size && size.endsWith( 'px' ) ) { + return parseInt( size ); + } + + return null; + } } /** diff --git a/packages/ckeditor5-image/tests/imageresize/imageresizeediting.js b/packages/ckeditor5-image/tests/imageresize/imageresizeediting.js index ad10f15334b..4ba2c179c67 100644 --- a/packages/ckeditor5-image/tests/imageresize/imageresizeediting.js +++ b/packages/ckeditor5-image/tests/imageresize/imageresizeediting.js @@ -18,6 +18,7 @@ import ImageBlockEditing from '../../src/image/imageblockediting'; import ImageInlineEditing from '../../src/image/imageinlineediting'; import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; import { focusEditor } from '@ckeditor/ckeditor5-widget/tests/widgetresize/_utils/utils'; import { IMAGE_SRC_FIXTURE } from './_utils/utils'; @@ -84,55 +85,153 @@ describe( 'ImageResizeEditing', () => { editor = await createEditor(); } ); - it( 'upcasts 100px width correctly', () => { - editor.setData( `
` ); + describe( 'width', () => { + it( 'upcasts 100px width correctly', () => { + editor.setData( `
` ); - expect( editor.model.document.getRoot().getChild( 0 ).getAttribute( 'resizedWidth' ) ).to.equal( '100px' ); - } ); + expect( editor.model.document.getRoot().getChild( 0 ).getAttribute( 'resizedWidth' ) ).to.equal( '100px' ); + } ); - it( 'upcasts 50% width correctly', () => { - editor.setData( `
` ); + it( 'upcasts 50% width correctly', () => { + editor.setData( `
` ); - expect( editor.model.document.getRoot().getChild( 0 ).getAttribute( 'resizedWidth' ) ).to.equal( '50%' ); - } ); + expect( editor.model.document.getRoot().getChild( 0 ).getAttribute( 'resizedWidth' ) ).to.equal( '50%' ); + } ); - it( 'downcasts 100px width correctly', () => { - setData( editor.model, `` ); + it( 'does not upcast width if height is set too', () => { + editor.setData( `
` ); - expect( editor.getData() ) - .to.equal( `
` ); - } ); + expect( editor.model.document.getRoot().getChild( 0 ).getAttribute( 'resizedWidth' ) ).to.be.undefined; + } ); - it( 'downcasts 50% width correctly', () => { - setData( editor.model, `` ); + it( 'downcasts 100px width correctly', () => { + setData( editor.model, `` ); - expect( editor.getData() ) - .to.equal( `
` ); - } ); + expect( editor.getData() ) + .to.equal( `
` ); + } ); - it( 'removes style and extra class when no longer resized', () => { - setData( editor.model, `` ); + it( 'downcasts 50% width correctly', () => { + setData( editor.model, `` ); - const imageModel = editor.model.document.getRoot().getChild( 0 ); + expect( editor.getData() ) + .to.equal( `
` ); + } ); + + it( 'removes style and extra class when no longer resized', () => { + setData( editor.model, `` ); + + const imageModel = editor.model.document.getRoot().getChild( 0 ); + + editor.model.change( writer => { + writer.removeAttribute( 'resizedWidth', imageModel ); + } ); - editor.model.change( writer => { - writer.removeAttribute( 'resizedWidth', imageModel ); + expect( editor.getData() ) + .to.equal( `
` ); } ); - expect( editor.getData() ) - .to.equal( `
` ); + it( 'doesn\'t downcast consumed tokens', () => { + editor.conversion.for( 'downcast' ).add( dispatcher => + dispatcher.on( 'attribute:resizedWidth:imageBlock', ( evt, data, conversionApi ) => { + conversionApi.consumable.consume( data.item, 'attribute:resizedWidth:imageBlock' ); + }, { priority: 'high' } ) + ); + setData( editor.model, `` ); + + expect( editor.getData() ) + .to.equal( `
` ); + } ); } ); - it( 'doesn\'t downcast consumed tokens', () => { - editor.conversion.for( 'downcast' ).add( dispatcher => - dispatcher.on( 'attribute:resizedWidth:imageBlock', ( evt, data, conversionApi ) => { - conversionApi.consumable.consume( data.item, 'attribute:resizedWidth:imageBlock' ); - }, { priority: 'high' } ) - ); - setData( editor.model, `` ); + describe( 'height', () => { + describe( 'upcast', () => { + it( 'upcasts 100px height correctly', () => { + editor.setData( `
` ); + + expect( editor.model.document.getRoot().getChild( 0 ).getAttribute( 'resizedHeight' ) ).to.equal( '100px' ); + } ); - expect( editor.getData() ) - .to.equal( `
` ); + it( 'upcasts 50% height correctly', () => { + editor.setData( `
` ); + + expect( editor.model.document.getRoot().getChild( 0 ).getAttribute( 'resizedHeight' ) ).to.equal( '50%' ); + } ); + + it( 'does not upcast height if width is set too', () => { + editor.setData( `
` ); + + expect( editor.model.document.getRoot().getChild( 0 ).getAttribute( 'resizedHeight' ) ).to.be.undefined; + } ); + } ); + + describe( 'data downcast', () => { + it( 'downcasts 100px height correctly', () => { + setData( editor.model, `` ); + + expect( editor.getData() ) + .to.equal( `
` ); + } ); + + it( 'downcasts 50% height correctly', () => { + setData( editor.model, `` ); + + expect( editor.getData() ) + .to.equal( `
` ); + } ); + + it( 'doesn\'t downcast consumed tokens', () => { + editor.conversion.for( 'dataDowncast' ).add( dispatcher => + dispatcher.on( 'attribute:resizedHeight:imageBlock', ( evt, data, conversionApi ) => { + conversionApi.consumable.consume( data.item, 'attribute:resizedHeight:imageBlock' ); + }, { priority: 'high' } ) + ); + setData( editor.model, `` ); + + expect( editor.getData() ) + .to.equal( `
` ); + } ); + } ); + + describe( 'editing downcast', () => { + it( 'downcasts 100px height correctly', () => { + setData( editor.model, `` ); + + expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( + '
' + + `` + + '
' + + '
' + ); + } ); + + it( 'downcasts 50% height correctly', () => { + setData( editor.model, `` ); + + expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( + '
' + + `` + + '
' + + '
' + ); + } ); + + it( 'doesn\'t downcast consumed tokens', () => { + editor.conversion.for( 'editingDowncast' ).add( dispatcher => + dispatcher.on( 'attribute:resizedHeight:imageBlock', ( evt, data, conversionApi ) => { + conversionApi.consumable.consume( data.item, 'attribute:resizedHeight:imageBlock' ); + }, { priority: 'high' } ) + ); + setData( editor.model, `` ); + + expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( + '
' + + `` + + '
' + + '
' + ); + } ); + } ); } ); } ); @@ -141,59 +240,191 @@ describe( 'ImageResizeEditing', () => { editor = await createEditor(); } ); - it( 'upcasts 100px width correctly', () => { - editor.setData( - `

Lorem ipsum

` - ); + describe( 'width', () => { + it( 'upcasts 100px width correctly', () => { + editor.setData( + `

Lorem ipsum

` + ); - expect( editor.model.document.getRoot().getChild( 0 ).getChild( 1 ).getAttribute( 'resizedWidth' ) ).to.equal( '100px' ); - } ); + expect( editor.model.document.getRoot().getChild( 0 ).getChild( 1 ).getAttribute( 'resizedWidth' ) ).to.equal( '100px' ); + } ); - it( 'upcasts 50% width correctly', () => { - editor.setData( `

Lorem ipsum

` ); + it( 'upcasts 50% width correctly', () => { + editor.setData( + `

Lorem ipsum

` + ); - expect( editor.model.document.getRoot().getChild( 0 ).getChild( 1 ).getAttribute( 'resizedWidth' ) ).to.equal( '50%' ); - } ); + expect( editor.model.document.getRoot().getChild( 0 ).getChild( 1 ).getAttribute( 'resizedWidth' ) ).to.equal( '50%' ); + } ); - it( 'downcasts 100px resizedWidth correctly', () => { - setData( editor.model, `` ); + it( 'does not upcast width if height is set too', () => { + editor.setData( + '

Lorem ' + + `` + + ' ipsum

' + ); + + expect( editor.model.document.getRoot().getChild( 0 ).getChild( 1 ).getAttribute( 'resizedWidth' ) ).to.be.undefined; + } ); - expect( editor.getData() ) - .to.equal( - `

` + it( 'downcasts 100px resizedWidth correctly', () => { + setData( editor.model, + `` ); - } ); - it( 'downcasts 50% resizedWidth correctly', () => { - setData( editor.model, `` ); + expect( editor.getData() ) + .to.equal( + `

` + ); + } ); - expect( editor.getData() ) - .to.equal( `

` ); - } ); + it( 'downcasts 50% resizedWidth correctly', () => { + setData( editor.model, + `` + ); + + expect( editor.getData() ) + .to.equal( `

` ); + } ); + + it( 'removes style and extra class when no longer resized', () => { + setData( editor.model, + `` + ); - it( 'removes style and extra class when no longer resized', () => { - setData( editor.model, `` ); + const imageModel = editor.model.document.getRoot().getChild( 0 ).getChild( 0 ); - const imageModel = editor.model.document.getRoot().getChild( 0 ).getChild( 0 ); + editor.model.change( writer => { + writer.removeAttribute( 'resizedWidth', imageModel ); + } ); - editor.model.change( writer => { - writer.removeAttribute( 'resizedWidth', imageModel ); + expect( editor.getData() ) + .to.equal( `

` ); } ); - expect( editor.getData() ) - .to.equal( `

` ); + it( 'doesn\'t downcast consumed tokens', () => { + editor.conversion.for( 'downcast' ).add( dispatcher => + dispatcher.on( 'attribute:resizedWidth:imageInline', ( evt, data, conversionApi ) => { + conversionApi.consumable.consume( data.item, 'attribute:resizedWidth:imageInline' ); + }, { priority: 'high' } ) + ); + setData( editor.model, + `` + ); + + expect( editor.getData() ) + .to.equal( `

` ); + } ); } ); - it( 'doesn\'t downcast consumed tokens', () => { - editor.conversion.for( 'downcast' ).add( dispatcher => - dispatcher.on( 'attribute:resizedWidth:imageInline', ( evt, data, conversionApi ) => { - conversionApi.consumable.consume( data.item, 'attribute:resizedWidth:imageInline' ); - }, { priority: 'high' } ) - ); - setData( editor.model, `` ); + describe( 'height', () => { + describe( 'upcast', () => { + it( 'upcasts 100px height correctly', () => { + editor.setData( + `

Lorem ipsum

` + ); + + expect( editor.model.document.getRoot().getChild( 0 ).getChild( 1 ).getAttribute( 'resizedHeight' ) ) + .to.equal( '100px' ); + } ); + + it( 'upcasts 50% height correctly', () => { + editor.setData( + `

Lorem ipsum

` + ); + + expect( editor.model.document.getRoot().getChild( 0 ).getChild( 1 ).getAttribute( 'resizedHeight' ) ).to.equal( '50%' ); + } ); + + it( 'does not upcast height if width is set too', () => { + editor.setData( + '

Lorem ' + + `` + + ' ipsum

' + ); + + expect( editor.model.document.getRoot().getChild( 0 ).getChild( 1 ).getAttribute( 'resizedHeight' ) ).to.be.undefined; + } ); + } ); + + describe( 'data downcast', () => { + it( 'downcasts 100px resizedHeight correctly', () => { + setData( editor.model, + `` + ); + + expect( editor.getData() ) + .to.equal( + `

` + ); + } ); + + it( 'downcasts 50% resizedHeight correctly', () => { + setData( editor.model, + `` + ); + + expect( editor.getData() ) + .to.equal( `

` ); + } ); + + it( 'doesn\'t downcast consumed tokens', () => { + editor.conversion.for( 'dataDowncast' ).add( dispatcher => + dispatcher.on( 'attribute:resizedHeight:imageInline', ( evt, data, conversionApi ) => { + conversionApi.consumable.consume( data.item, 'attribute:resizedHeight:imageInline' ); + }, { priority: 'high' } ) + ); + setData( + editor.model, `` + ); + + expect( editor.getData() ) + .to.equal( `

` ); + } ); + } ); - expect( editor.getData() ) - .to.equal( `

` ); + describe( 'editing downcast', () => { + it( 'downcasts 100px resizedHeight correctly', () => { + setData( editor.model, + `` + ); + + expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( + '

' + + `` + + '

' + ); + } ); + + it( 'downcasts 50% resizedHeight correctly', () => { + setData( editor.model, + `` + ); + + expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( + '

' + + `` + + '

' + ); + } ); + + it( 'doesn\'t downcast consumed tokens', () => { + editor.conversion.for( 'editingDowncast' ).add( dispatcher => + dispatcher.on( 'attribute:resizedHeight:imageInline', ( evt, data, conversionApi ) => { + conversionApi.consumable.consume( data.item, 'attribute:resizedHeight:imageInline' ); + }, { priority: 'high' } ) + ); + setData( editor.model, + `` + ); + + expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( + '

' + + `` + + '

' + ); + } ); + } ); } ); } ); @@ -208,11 +439,23 @@ describe( 'ImageResizeEditing', () => { await newEditor.destroy(); } ); + it( 'allows the resizedHeight attribute when ImageBlock plugin is enabled', async () => { + const newEditor = await ClassicEditor.create( editorElement, { plugins: [ ImageBlockEditing, ImageResizeEditing ] } ); + expect( newEditor.model.schema.checkAttribute( [ '$root', 'imageBlock' ], 'resizedHeight' ) ).to.be.true; + await newEditor.destroy(); + } ); + it( 'allows the resizedWidth attribute when ImageInline plugin is enabled', async () => { const newEditor = await ClassicEditor.create( editorElement, { plugins: [ ImageInlineEditing, ImageResizeEditing ] } ); expect( newEditor.model.schema.checkAttribute( [ '$root', 'imageInline' ], 'resizedWidth' ) ).to.be.true; await newEditor.destroy(); } ); + + it( 'allows the resizedHeight attribute when ImageInline plugin is enabled', async () => { + const newEditor = await ClassicEditor.create( editorElement, { plugins: [ ImageInlineEditing, ImageResizeEditing ] } ); + expect( newEditor.model.schema.checkAttribute( [ '$root', 'imageInline' ], 'resizedHeight' ) ).to.be.true; + await newEditor.destroy(); + } ); } ); describe( 'command', () => { diff --git a/packages/ckeditor5-image/tests/imageresize/imageresizehandles.js b/packages/ckeditor5-image/tests/imageresize/imageresizehandles.js index 8c3233a3c81..89de2711284 100644 --- a/packages/ckeditor5-image/tests/imageresize/imageresizehandles.js +++ b/packages/ckeditor5-image/tests/imageresize/imageresizehandles.js @@ -656,6 +656,68 @@ describe( 'ImageResizeHandles', () => { } ); } ); + describe( 'height style', () => { + beforeEach( async () => { + editor = await createEditor(); + + await setModelAndWaitForImages( editor, + '[' + + `` + + ']' + ); + + widget = viewDocument.getRoot().getChild( 0 ).getChild( 0 ); + } ); + + it( 'is removed after starting resizing', () => { + const resizerPosition = 'bottom-left'; + const domParts = getWidgetDomParts( editor, widget, resizerPosition ); + const initialPointerPosition = getHandleCenterPoint( domParts.widget, resizerPosition ); + const viewImage = widget.getChild( 0 ); + + expect( viewImage.getStyle( 'height' ) ).to.equal( '50px' ); + + resizerMouseSimulator.down( editor, domParts.resizeHandle ); + + resizerMouseSimulator.move( editor, domParts.resizeHandle, null, initialPointerPosition ); + + expect( viewImage.getStyle( 'height' ) ).to.be.undefined; + + resizerMouseSimulator.up( editor ); + } ); + } ); + + describe( 'aspect-ratio style', () => { + beforeEach( async () => { + editor = await createEditor(); + + await setModelAndWaitForImages( editor, + '[' + + `` + + ']' + ); + + widget = viewDocument.getRoot().getChild( 0 ).getChild( 0 ); + } ); + + it( 'is added after starting resizing, if width & height attributes are set', () => { + const resizerPosition = 'bottom-left'; + const domParts = getWidgetDomParts( editor, widget, resizerPosition ); + const initialPointerPosition = getHandleCenterPoint( domParts.widget, resizerPosition ); + const viewImage = widget.getChild( 0 ); + + expect( viewImage.getStyle( 'aspec-ratio' ) ).to.be.undefined; + + resizerMouseSimulator.down( editor, domParts.resizeHandle ); + + resizerMouseSimulator.move( editor, domParts.resizeHandle, null, initialPointerPosition ); + + expect( viewImage.getStyle( 'aspect-ratio' ) ).to.equal( '100/50' ); + + resizerMouseSimulator.up( editor ); + } ); + } ); + describe( 'undo integration', () => { beforeEach( async () => { editor = await createEditor(); diff --git a/packages/ckeditor5-image/tests/imageresize/resizeimagecommand.js b/packages/ckeditor5-image/tests/imageresize/resizeimagecommand.js index b127a7cdf66..c7a0bba1f5a 100644 --- a/packages/ckeditor5-image/tests/imageresize/resizeimagecommand.js +++ b/packages/ckeditor5-image/tests/imageresize/resizeimagecommand.js @@ -115,5 +115,14 @@ describe( 'ResizeImageCommand', () => { expect( getData( model ) ).to.equal( '[]' ); expect( model.document.getRoot().getChild( 0 ).hasAttribute( 'resizedWidth' ) ).to.be.false; } ); + + it( 'removes image resizedHeight', () => { + setData( model, '[]' ); + + command.execute( { width: '100%' } ); + + expect( getData( model ) ).to.equal( '[]' ); + expect( model.document.getRoot().getChild( 0 ).hasAttribute( 'resizedHeight' ) ).to.be.false; + } ); } ); } ); diff --git a/packages/ckeditor5-image/tests/imagesizeattributes.js b/packages/ckeditor5-image/tests/imagesizeattributes.js index e46acfaf402..adf13c5d061 100644 --- a/packages/ckeditor5-image/tests/imagesizeattributes.js +++ b/packages/ckeditor5-image/tests/imagesizeattributes.js @@ -10,6 +10,7 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import ImageBlockEditing from '../src/image/imageblockediting'; import ImageInlineEditing from '../src/image/imageinlineediting'; import ImageSizeAttributes from '../src/imagesizeattributes'; +import ImageResizeEditing from '../src/imageresize/imageresizeediting'; import ImageUtils from '../src/imageutils'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; @@ -63,13 +64,13 @@ describe( 'ImageSizeAttributes', () => { describe( 'inline images', () => { it( 'should upcast width attribute correctly', () => { editor.setData( - '

Lorem ipsum

' + '

Lorem ipsum

' ); expect( getData( model, { withoutSelection: true } ) ).to.equal( '' + 'Lorem ' + - '' + + '' + ' ipsum' + '' ); @@ -77,39 +78,95 @@ describe( 'ImageSizeAttributes', () => { it( 'should upcast height attribute correctly', () => { editor.setData( - '

Lorem ipsum

' + '

Lorem ipsum

' ); expect( getData( model, { withoutSelection: true } ) ).to.equal( '' + 'Lorem ' + - '' + + '' + ' ipsum' + '' ); } ); + + it( 'should upcast width & height styles if they both are set', () => { + editor.setData( + '

Lorem ipsum

' + ); + + expect( getData( model, { withoutSelection: true } ) ).to.equal( + '' + + 'Lorem ' + + '' + + ' ipsum' + + '' + ); + } ); + + it( 'should not upcast width style if height style is missing', () => { + editor.setData( + '

Lorem ipsum

' + ); + + expect( editor.model.document.getRoot().getChild( 0 ).getChild( 1 ).getAttribute( 'width' ) ).to.be.undefined; + } ); + + it( 'should not upcast height style if width style is missing', () => { + editor.setData( + '

Lorem ipsum

' + ); + + expect( editor.model.document.getRoot().getChild( 0 ).getChild( 1 ).getAttribute( 'height' ) ).to.be.undefined; + } ); } ); describe( 'block images', () => { it( 'should upcast width attribute correctly', () => { editor.setData( - '
' + '
' ); expect( getData( model, { withoutSelection: true } ) ).to.equal( - '' + '' ); } ); it( 'should upcast height attribute correctly', () => { editor.setData( - '
' + '
' + ); + + expect( getData( model, { withoutSelection: true } ) ).to.equal( + '' + ); + } ); + + it( 'should upcast width & height styles if they both are set', () => { + editor.setData( + '
' ); expect( getData( model, { withoutSelection: true } ) ).to.equal( - '' + '' ); } ); + + it( 'should not upcast width style if height style is missing', () => { + editor.setData( + '
' + ); + + expect( editor.model.document.getRoot().getChild( 0 ).getAttribute( 'width' ) ).to.be.undefined; + } ); + + it( 'should not upcast height style if width style is missing', () => { + editor.setData( + '
' + ); + + expect( editor.model.document.getRoot().getChild( 0 ).getAttribute( 'height' ) ).to.be.undefined; + } ); } ); } ); @@ -117,33 +174,33 @@ describe( 'ImageSizeAttributes', () => { describe( 'inline images', () => { it( 'should downcast width attribute correctly', () => { editor.setData( - '

Lorem ipsum

' + '

Lorem ipsum

' ); expect( getViewData( view, { withoutSelection: true } ) ).to.equal( '

Lorem ' + - '' + + '' + ' ipsum

' ); expect( editor.getData() ).to.equal( - '

Lorem ipsum

' + '

Lorem ipsum

' ); } ); it( 'should downcast height attribute correctly', () => { editor.setData( - '

Lorem ipsum

' + '

Lorem ipsum

' ); expect( getViewData( view, { withoutSelection: true } ) ).to.equal( '

Lorem ' + - '' + + '' + ' ipsum

' ); expect( editor.getData() ).to.equal( - '

Lorem ipsum

' + '

Lorem ipsum

' ); } ); @@ -153,7 +210,7 @@ describe( 'ImageSizeAttributes', () => { conversionApi.consumable.consume( data.item, 'attribute:width:imageInline' ); }, { priority: 'high' } ) ); - setData( model, '' ); + setData( model, '' ); expect( editor.getData() ).to.equal( '

' @@ -166,7 +223,7 @@ describe( 'ImageSizeAttributes', () => { conversionApi.consumable.consume( data.item, 'attribute:height:imageInline' ); }, { priority: 'high' } ) ); - setData( model, '' ); + setData( model, '' ); expect( editor.getData() ).to.equal( '

' @@ -174,7 +231,7 @@ describe( 'ImageSizeAttributes', () => { } ); it( 'should remove width attribute properly', () => { - setData( model, '' ); + setData( model, '' ); const imageModel = editor.model.document.getRoot().getChild( 0 ).getChild( 0 ); @@ -187,7 +244,7 @@ describe( 'ImageSizeAttributes', () => { } ); it( 'should remove height attribute properly', () => { - setData( model, '' ); + setData( model, '' ); const imageModel = editor.model.document.getRoot().getChild( 0 ).getChild( 0 ); @@ -198,38 +255,87 @@ describe( 'ImageSizeAttributes', () => { expect( editor.getData() ) .to.equal( '

' ); } ); + + describe( 'with image resize plugin', () => { + let editor, view; + + beforeEach( async () => { + editor = await VirtualTestEditor.create( { + plugins: [ Paragraph, ImageInlineEditing, ImageSizeAttributes, ImageResizeEditing ] + } ); + + view = editor.editing.view; + } ); + + afterEach( async () => { + await editor.destroy(); + } ); + + it( 'should add aspect-ratio if attributes are set and image is resized', () => { + editor.setData( + '

' + ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equal( + '

' + + '' + + '

' + ); + + expect( editor.getData() ).to.equal( + '

' + ); + } ); + + it( 'should not add aspect-ratio if attributes are set but image is not resized', () => { + editor.setData( + '

' + ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equal( + '

' + + '' + + '

' + ); + + expect( editor.getData() ).to.equal( + '

' + ); + } ); + } ); } ); describe( 'block images', () => { it( 'should downcast width attribute correctly', () => { editor.setData( - '
' + '
' ); expect( getViewData( view, { withoutSelection: true } ) ).to.equal( '
' + - '' + + '' + '
' ); expect( editor.getData() ).to.equal( - '
' + '
' ); } ); it( 'should downcast height attribute correctly', () => { editor.setData( - '
' + '
' ); expect( getViewData( view, { withoutSelection: true } ) ).to.equal( '
' + - '' + + '' + '
' ); expect( editor.getData() ).to.equal( - '
' + '
' ); } ); @@ -239,7 +345,7 @@ describe( 'ImageSizeAttributes', () => { conversionApi.consumable.consume( data.item, 'attribute:width:imageBlock' ); }, { priority: 'high' } ) ); - setData( model, '' ); + setData( model, '' ); expect( editor.getData() ).to.equal( '
' @@ -260,7 +366,7 @@ describe( 'ImageSizeAttributes', () => { } ); it( 'should remove width attribute properly', () => { - setData( model, '' ); + setData( model, '' ); const imageModel = editor.model.document.getRoot().getChild( 0 ); @@ -273,7 +379,7 @@ describe( 'ImageSizeAttributes', () => { } ); it( 'should remove height attribute properly', () => { - setData( model, '' ); + setData( model, '' ); const imageModel = editor.model.document.getRoot().getChild( 0 ); @@ -284,6 +390,58 @@ describe( 'ImageSizeAttributes', () => { expect( editor.getData() ) .to.equal( '
' ); } ); + + describe( 'with image resize plugin', () => { + let editor, view; + + beforeEach( async () => { + editor = await VirtualTestEditor.create( { + plugins: [ Paragraph, ImageBlockEditing, ImageSizeAttributes, ImageResizeEditing ] + } ); + + view = editor.editing.view; + } ); + + afterEach( async () => { + await editor.destroy(); + } ); + + it( 'should add aspect-ratio if attributes are set and image is resized', () => { + editor.setData( + '
' + ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equal( + '
' + + '' + + '
' + ); + + expect( editor.getData() ).to.equal( + '
' + + '' + + '
' + ); + } ); + + it( 'should not add aspect-ratio if attributes are set but image is not resized', () => { + editor.setData( + '
' + ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equal( + '
' + + '' + + '
' + ); + + expect( editor.getData() ).to.equal( + '
' + + '' + + '
' + ); + } ); + } ); } ); } ); } ); diff --git a/packages/ckeditor5-image/tests/manual/imagesizeattributes.js b/packages/ckeditor5-image/tests/manual/imagesizeattributes.js index 999f0365676..8c0e86fc074 100644 --- a/packages/ckeditor5-image/tests/manual/imagesizeattributes.js +++ b/packages/ckeditor5-image/tests/manual/imagesizeattributes.js @@ -15,6 +15,7 @@ import CloudServices from '@ckeditor/ckeditor5-cloud-services/src/cloudservices' import ImageResize from '../../src/imageresize'; import ImageSizeAttributes from '../../src/imagesizeattributes'; import ImageUpload from '../../src/imageupload'; +import PasteFromOffice from '@ckeditor/ckeditor5-paste-from-office/src/pastefromoffice'; import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config'; @@ -28,7 +29,8 @@ const commonConfig = { Indent, IndentBlock, CloudServices, - EasyImage + EasyImage, + PasteFromOffice ], toolbar: [ 'heading', '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'insertTable', 'undo', 'redo', 'outdent', 'indent' ], diff --git a/packages/ckeditor5-image/tests/manual/imagesizeattributesallcases.html b/packages/ckeditor5-image/tests/manual/imagesizeattributesallcases.html new file mode 100644 index 00000000000..0e06131a879 --- /dev/null +++ b/packages/ckeditor5-image/tests/manual/imagesizeattributesallcases.html @@ -0,0 +1,50 @@ + + diff --git a/packages/ckeditor5-image/tests/manual/imagesizeattributesallcases.js b/packages/ckeditor5-image/tests/manual/imagesizeattributesallcases.js new file mode 100644 index 00000000000..d1be3ef7eb7 --- /dev/null +++ b/packages/ckeditor5-image/tests/manual/imagesizeattributesallcases.js @@ -0,0 +1,385 @@ +/** + * @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 + */ + +/* global document, console, window, CKEditorInspector */ + +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; +import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; +import Indent from '@ckeditor/ckeditor5-indent/src/indent'; +import Code from '@ckeditor/ckeditor5-basic-styles/src/code'; +import IndentBlock from '@ckeditor/ckeditor5-indent/src/indentblock'; +import EasyImage from '@ckeditor/ckeditor5-easy-image/src/easyimage'; +import CloudServices from '@ckeditor/ckeditor5-cloud-services/src/cloudservices'; +import ImageResize from '../../src/imageresize'; +import ImageSizeAttributes from '../../src/imagesizeattributes'; +import ImageUpload from '../../src/imageupload'; +import PasteFromOffice from '@ckeditor/ckeditor5-paste-from-office/src/pastefromoffice'; +import { getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + +import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config'; + +const commonConfig = { + plugins: [ + ArticlePluginSet, + ImageResize, + Code, + ImageSizeAttributes, + ImageUpload, + Indent, + IndentBlock, + CloudServices, + EasyImage, + PasteFromOffice + ], + toolbar: [ 'heading', '|', 'bold', 'italic', 'link', + 'bulletedList', 'numberedList', 'blockQuote', 'insertTable', 'undo', 'redo', 'outdent', 'indent' ], + image: { + toolbar: [ 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText', '|', 'toggleImageCaption', 'resizeImage' ] + }, + table: { + contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ], + tableToolbar: [ 'bold', 'italic' ] + }, + cloudServices: CS_CONFIG +}; + +const configPx = { + plugins: [ + ArticlePluginSet, + ImageResize, + Code, + ImageSizeAttributes, + ImageUpload, + Indent, + IndentBlock, + CloudServices, + EasyImage, + PasteFromOffice + ], + toolbar: [ 'heading', '|', 'bold', 'italic', 'link', + 'bulletedList', 'numberedList', 'blockQuote', 'insertTable', 'undo', 'redo', 'outdent', 'indent' ], + image: { + resizeUnit: 'px', + toolbar: [ 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText', '|', 'toggleImageCaption', 'resizeImage' ] + }, + table: { + contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ], + tableToolbar: [ 'bold', 'italic' ] + }, + cloudServices: CS_CONFIG +}; + +const editors = [ + { + id: 'inline1', + title: '[Inline] plain (no attributes, no styles)', + config: commonConfig, + data: '

' + }, + { + id: 'inline2', + title: '[Inline] natural size | width + height attributes: Resize in %', + config: commonConfig, + data: '

' + }, + { + id: 'inline3', + config: commonConfig, + data: '

', + title: '[Inline] natural size | width + height attributes: Resized (width % style)' + }, + { + id: 'inline4', + config: commonConfig, + data: '

', + title: '[Inline] natural size | width + height attributes: Resized (width % style)' + }, + { + id: 'inline5', + title: '[Inline] natural size | width + height attributes: Resize in px', + config: configPx, + data: '

' + }, + { + id: 'inline6', + title: '[Inline] natural size | width + height attributes: Resized (width px style only)', + config: configPx, + data: '

' + }, + { + id: 'inline7', + title: '[Inline] natural size | width + height attributes: Resized (width and height px style)', + config: configPx, + data: '

' + }, + { + id: 'inline8', + title: '[Inline] natural size | styles only (w/o width & height attributes): Resize in %', + config: commonConfig, + data: '

' + }, + { + id: 'inline9', + title: '[Inline] natural size | only resize in % (only width style)', + config: commonConfig, + data: '

' + }, + { + id: 'inline10', + title: '[Inline] natural size | styles only (w/o width & height attributes): Resize in px', + config: configPx, + data: '

' + }, + { + id: 'inline11', + title: '[Inline] broken aspect ratio | width + height attributes', + config: commonConfig, + data: '

' + }, + { + id: 'inline12', + title: '[Inline] broken aspect ratio | styles only (w/o width & height attributes)', + config: commonConfig, + data: '

' + }, + { + id: 'block1', + title: '[Block] plain (no attributes, no styles)', + config: commonConfig, + data: '
' + }, + { + id: 'block2', + title: '[Block] natural size | width + height attributes: Resize in %', + config: commonConfig, + data: '
' + }, + { + id: 'block3', + title: '[Block] natural size | width + height attributes: Resized (width % style)', + config: commonConfig, + data: '
' + + '
' + }, + { + id: 'block4', + title: '[Block] natural size | width + height attributes: Resized (width % style)', + config: commonConfig, + data: '
' + + '
' + }, + { + id: 'block5', + title: '[Block] natural size | width + height attributes: Resize in px', + config: configPx, + data: '
' + }, + { + id: 'block6', + title: '[Block] natural size | width + height attributes: Resized (width px style only)', + config: configPx, + data: '
' + + '
' + }, + { + id: 'block7', + title: '[Block] natural size | width + height attributes: Resized (width and height px style)', + config: configPx, + data: '
' + + '
' + }, + { + id: 'block8', + title: '[Block] natural size | styles only (w/o width & height attributes): Resize in %', + config: commonConfig, + data: '
' + + '
' + }, + { + id: 'block9', + title: '[Block] natural size | only resize in % (only width style)', + config: commonConfig, + data: '
' + + '
' + }, + { + id: 'block10', + title: '[Block] natural size | styles only (w/o width & height attributes): Resize in px', + config: configPx, + data: '
' + + '
' + }, + { + id: 'block11', + title: '[Block] broken aspect ratio | width + height attributes', + config: commonConfig, + data: '
' + + '
' + }, + { + id: 'block12', + title: '[Block] broken aspect ratio | styles only (w/o width & height attributes)', + config: commonConfig, + data: '
' + + '
' + }, + { + id: 'inline101', + title: '[Inline] natural size | width + height attributes: Resized (height % style)', + config: commonConfig, + data: '

' + }, + { + id: 'inline102', + title: '[Inline] natural size | width + height attributes: Resized (height px style)', + config: configPx, + data: '

' + }, + { + id: 'inline103', + title: '[Inline] natural size | only resize in % (only height style)', + config: commonConfig, + data: '

' + }, + { + id: 'inline104', + title: '[Inline] natural size | only resize in px (only height style)', + config: configPx, + data: '

' + }, + { + id: 'inline105', + title: '[Inline] width + height attributes: Resized (height & width % style)', + config: commonConfig, + data: '

' + }, + { + id: 'inline106', + title: '[Inline] only resize in % (height & width % style)', + config: commonConfig, + data: '

' + }, + { + id: 'block101', + title: '[Block] natural size | width + height attributes: Resized (height % style)', + config: commonConfig, + data: '
' + + '
' + }, + { + id: 'block102', + title: '[Block] natural size | width + height attributes: Resized (height px style)', + config: configPx, + data: '
' + + '
' + }, + { + id: 'block103', + title: '[Block] natural size | only resize in % (only height style)', + config: commonConfig, + data: '
' + + '
' + }, + { + id: 'block104', + title: '[Block] natural size | only resize in px (only height style)', + config: configPx, + data: '
' + + '
' + }, + { + id: 'block105', + title: '[Block] width + height attributes: Resized (height & width % style)', + config: commonConfig, + data: '
' + + '
' + }, + { + id: 'block106', + title: '[Block] only resize in % (height & width % style)', + config: commonConfig, + data: '
' + + '
' + } +]; + +for ( const editorObj of editors ) { + insertEditorStructure( editorObj ); + + ( async function initTest() { + const domElement = document.querySelector( `#${ editorObj.id }` ); + + await ClassicEditor + .create( domElement, { ...editorObj.config, initialData: editorObj.data } ) + .then( editor => { + window[ editorObj.id ] = editor; + + editor.model.document.on( 'change:data', () => { + updateLogsAndData( domElement, editor ); + } ); + + logInitialData( domElement, editorObj ); + updateLogsAndData( domElement, editor ); + + CKEditorInspector.attach( { [ editorObj.id ]: editor } ); + } ) + .catch( err => { + console.error( err.stack ); + } ); + }() ); +} + +function insertEditorStructure( editorObj ) { + const colorClass = editorObj.id.startsWith( 'inline' ) ? 'inlineColor' : 'blockColor'; + + document.body.insertAdjacentHTML( 'beforeend', + `

${ editorObj.title }

` + + `Editor id: ${ editorObj.id }` + + '
' + + `
` + + '
' + + `

Initial data:

` + + `

Model:

` + + '
' + ); +} + +function logInitialData( domElement, editorObj ) { + const editorDataText = domElement.parentElement.querySelector( '.editor-data-text' ); + + editorDataText.insertAdjacentText( 'beforeend', editorObj.data ); + editorDataText.insertAdjacentHTML( 'beforeend', '

Output data:

' ); +} + +function updateLogsAndData( domElement, editor ) { + const editorModel = domElement.parentElement.querySelector( '.editor-model' ); + const editorDataHtml = domElement.parentElement.querySelector( '.editor-data' ); + const editorDataText = domElement.parentElement.querySelector( '.editor-data-text' ); + + // model + editorModel.insertAdjacentText( 'beforeend', getData( editor.model, { withoutSelection: true } ) ); + editorModel.insertAdjacentHTML( 'beforeend', '

---

' ); + + // data (html) + editorDataHtml.innerHTML = editor.getData(); + + // data (output data) + editorDataText.insertAdjacentText( 'beforeend', editor.getData() ); + editorDataText.insertAdjacentHTML( 'beforeend', '

---

' ); +} diff --git a/packages/ckeditor5-image/tests/manual/imagesizeattributesallcases.md b/packages/ckeditor5-image/tests/manual/imagesizeattributesallcases.md new file mode 100644 index 00000000000..2156997d8bf --- /dev/null +++ b/packages/ckeditor5-image/tests/manual/imagesizeattributesallcases.md @@ -0,0 +1,14 @@ +## Image size attributes + +This manual tests consists of many use cases for different combinations of image attributes and styles: +* `width` (image width attribute) +* `height` (image height attribute) +* `resizedWidth` (image width style) +* `resizedHeight` (image height style) + +Image in the editor should look like the image next to the editor (created from editor's output data): +* after initial editor load +* after resizing image in the editor + +**Note**: Every time an image is resized, the code blocks below the editor are updated with refreshed output data and model. +It's then easier to compare what has changed after resizing. diff --git a/packages/ckeditor5-image/tests/manual/imagesizeattributesghs.html b/packages/ckeditor5-image/tests/manual/imagesizeattributesghs.html new file mode 100644 index 00000000000..9904fd1b1a2 --- /dev/null +++ b/packages/ckeditor5-image/tests/manual/imagesizeattributesghs.html @@ -0,0 +1,15 @@ +
+

[Inline] width + height attributes:

+

+

[Inline] width style:

+

+

[Inline] height style:

+

+ +

[Block] width + height attributes:

+
+

[Block] width style:

+
+

[Block] height style:

+
+
diff --git a/packages/ckeditor5-image/tests/manual/imagesizeattributesghs.js b/packages/ckeditor5-image/tests/manual/imagesizeattributesghs.js new file mode 100644 index 00000000000..a776fb35cfd --- /dev/null +++ b/packages/ckeditor5-image/tests/manual/imagesizeattributesghs.js @@ -0,0 +1,74 @@ +/** + * @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 + */ + +/* global document, console, window */ + +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; +import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; +import Indent from '@ckeditor/ckeditor5-indent/src/indent'; +import Code from '@ckeditor/ckeditor5-basic-styles/src/code'; +import IndentBlock from '@ckeditor/ckeditor5-indent/src/indentblock'; +import CloudServices from '@ckeditor/ckeditor5-cloud-services/src/cloudservices'; +import ImageResize from '../../src/imageresize'; +import ImageSizeAttributes from '../../src/imagesizeattributes'; +import ImageUpload from '../../src/imageupload'; +import PasteFromOffice from '@ckeditor/ckeditor5-paste-from-office/src/pastefromoffice'; +import GeneralHtmlSupport from '@ckeditor/ckeditor5-html-support/src/generalhtmlsupport'; + +import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config'; + +const commonConfig = { + plugins: [ + ArticlePluginSet, + ImageResize, + Code, + ImageSizeAttributes, + ImageUpload, + Indent, + IndentBlock, + CloudServices, + PasteFromOffice, + GeneralHtmlSupport + ], + toolbar: [ 'heading', '|', 'bold', 'italic', 'link', + 'bulletedList', 'numberedList', 'blockQuote', 'insertTable', 'undo', 'redo', 'outdent', 'indent' ], + image: { + toolbar: [ 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText', '|', 'toggleImageCaption', 'resizeImage' ] + }, + table: { + contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ], + tableToolbar: [ 'bold', 'italic' ] + }, + htmlSupport: { + allow: [ + // Enables all HTML features. + { + name: /.*/, + attributes: true, + classes: true, + styles: true + } + ], + disallow: [ + { + attributes: [ + { key: /^on(.*)/i, value: true }, + { key: /.*/, value: /(\b)(on\S+)(\s*)=|javascript:|(<\s*)(\/*)script/i }, + { key: /.*/, value: /data:(?!image\/(png|jpeg|gif|webp))/i } + ] + }, + { name: 'script' } + ] + }, + cloudServices: CS_CONFIG +}; + +( async function initTest() { + window.editor = await ClassicEditor + .create( document.querySelector( '#editor-ghs-with-width-height-attributes' ), commonConfig ) + .catch( err => { + console.error( err.stack ); + } ); +}() ); diff --git a/packages/ckeditor5-image/tests/manual/imagesizeattributesghs.md b/packages/ckeditor5-image/tests/manual/imagesizeattributesghs.md new file mode 100644 index 00000000000..b5f7eb3716c --- /dev/null +++ b/packages/ckeditor5-image/tests/manual/imagesizeattributesghs.md @@ -0,0 +1,7 @@ +## Image size attributes with full GHS + +A manual test to check image attributes and styles with GHS integration: +* `width` (image width attribute) +* `height` (image height attribute) +* `resizedWidth` (image width style) +* `resizedHeight` (image height style) diff --git a/packages/ckeditor5-image/theme/image.css b/packages/ckeditor5-image/theme/image.css index 4fa08564a0c..0d857998e19 100644 --- a/packages/ckeditor5-image/theme/image.css +++ b/packages/ckeditor5-image/theme/image.css @@ -29,9 +29,6 @@ /* Make sure the image is never smaller than the parent container (See: https://github.com/ckeditor/ckeditor5/issues/9300). */ min-width: 100%; - - /* Preserve aspect ratio after introducing width and height attributes for image element. */ - height: auto; } } @@ -63,9 +60,6 @@ /* Prevents overflowing the editing root boundaries when an inline image is very wide. */ max-width: 100%; - - /* Preserve aspect ratio after introducing width and height attributes for image element. */ - height: auto; } } } diff --git a/packages/ckeditor5-image/theme/imageresize.css b/packages/ckeditor5-image/theme/imageresize.css index 1bb7afac7f5..15351410a77 100644 --- a/packages/ckeditor5-image/theme/imageresize.css +++ b/packages/ckeditor5-image/theme/imageresize.css @@ -3,6 +3,12 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ +/* Preserve aspect ratio of the resized image after introducing image height attribute. */ +.ck-content figure.image_resized img, +.ck-content img.image_resized { + height: auto; +} + .ck-content .image.image_resized { max-width: 100%; /* @@ -25,6 +31,12 @@ } .ck.ck-editor__editable { + /* Preserve aspect ratio of the resized image after introducing image height attribute. */ + & .image.image_resized img, + & .image-inline.image_resized img { + height: auto; + } + /* The resized inline image nested in the table should respect its parent size. See https://github.com/ckeditor/ckeditor5/issues/9117. */ & td,