diff --git a/packages/ckeditor5-image/src/imageupload/imageuploadediting.ts b/packages/ckeditor5-image/src/imageupload/imageuploadediting.ts index a2350e312c5..18f92ae24aa 100644 --- a/packages/ckeditor5-image/src/imageupload/imageuploadediting.ts +++ b/packages/ckeditor5-image/src/imageupload/imageuploadediting.ts @@ -225,12 +225,14 @@ export default class ImageUploadEditing extends Plugin { } ); // Set the default handler for feeding the image element with `src` and `srcset` attributes. + // Load the image, read and set `width` and `height` attributes (original sizes). this.on( 'uploadComplete', ( evt, { imageElement, data } ) => { const urls = data.urls ? data.urls as Record : data; this.editor.model.change( writer => { writer.setAttribute( 'src', urls.default, imageElement ); this._parseAndSetSrcsetAttributeOnImage( urls, imageElement, writer ); + imageUtils.loadImageAndSetSizeAttributes( imageElement ); } ); }, { priority: 'low' } ); } diff --git a/packages/ckeditor5-image/src/imageutils.ts b/packages/ckeditor5-image/src/imageutils.ts index d8432b33501..73f584e28df 100644 --- a/packages/ckeditor5-image/src/imageutils.ts +++ b/packages/ckeditor5-image/src/imageutils.ts @@ -24,11 +24,17 @@ import type { import { Plugin, type Editor } from 'ckeditor5/src/core'; import { findOptimalInsertionRange, isWidget, toWidget } from 'ckeditor5/src/widget'; import { determineImageTypeForInsertionAtSelection } from './image/utils'; +import { DomEmitterMixin, type DomEmitter, global } from 'ckeditor5/src/utils'; /** * A set of helpers related to images. */ export default class ImageUtils extends Plugin { + /** + * DOM Emitter. + */ + private _domEmitter: DomEmitter = new ( DomEmitterMixin() )(); + /** * @inheritDoc */ @@ -121,6 +127,8 @@ export default class ImageUtils extends Plugin { // Inserting an image might've failed due to schema regulations. if ( imageElement.parent ) { + this.loadImageAndSetSizeAttributes( imageElement ); + return imageElement; } @@ -128,6 +136,43 @@ export default class ImageUtils extends Plugin { } ); } + /** + * Loads image file based on `src`, reads original image sizes and sets them as `width` and `height`. + * + * The `src` attribute may not be available if the user is using an upload adapter. In such a case, + * this method is called again after the upload process is complete and the `src` attribute is available. + */ + public loadImageAndSetSizeAttributes( imageElement: Element ): void { + const src = imageElement.getAttribute( 'src' ) as string; + + if ( !src ) { + return; + } + + if ( imageElement.getAttribute( 'width' ) || imageElement.getAttribute( 'height' ) ) { + return; + } + + const img = new global.window.Image(); + + this._domEmitter.listenTo( img, 'load', ( evt, data ) => { + this._setWidthAndHeight( imageElement, img.naturalWidth, img.naturalHeight ); + this._domEmitter.stopListening( img, 'load' ); + } ); + + img.src = src; + } + + /** + * Sets image `width` and `height` attributes. + */ + private _setWidthAndHeight( imageElement: Element, width: number, height: number ): void { + this.editor.model.enqueueChange( { isUndoable: false }, writer => { + writer.setAttribute( 'width', width, imageElement ); + writer.setAttribute( 'height', height, imageElement ); + } ); + } + /** * Returns an image widget editing view element if one is selected or is among the selection's ancestors. */ @@ -239,6 +284,15 @@ export default class ImageUtils extends Plugin { } } } + + /** + * @inheritDoc + */ + public override destroy(): void { + this._domEmitter.stopListening(); + + return super.destroy(); + } } /** diff --git a/packages/ckeditor5-image/tests/imageupload/imageuploadediting.js b/packages/ckeditor5-image/tests/imageupload/imageuploadediting.js index e05ceb20a0a..fab20c27cc0 100644 --- a/packages/ckeditor5-image/tests/imageupload/imageuploadediting.js +++ b/packages/ckeditor5-image/tests/imageupload/imageuploadediting.js @@ -487,6 +487,32 @@ describe( 'ImageUploadEditing', () => { expect( loader.status ).to.equal( 'idle' ); } ); + it( 'should set image width and height after server response', async () => { + const file = createNativeFileMock(); + setModelData( model, '{}foo bar' ); + editor.execute( 'uploadImage', { file } ); + + await new Promise( res => { + model.document.once( 'change', res ); + loader.file.then( () => nativeReaderMock.mockSuccess( base64Sample ) ); + } ); + + await new Promise( res => { + model.document.once( 'change', res, { priority: 'lowest' } ); + loader.file.then( () => adapterMocks[ 0 ].mockSuccess( { default: '/assets/sample.png' } ) ); + } ); + + await timeout( 100 ); + + expect( getModelData( model ) ).to.equal( + '[]foo bar' + ); + + function timeout( ms ) { + return new Promise( res => setTimeout( res, ms ) ); + } + } ); + it( 'should support adapter response with the normalized `urls` property', async () => { const file = createNativeFileMock(); setModelData( model, '{}foo bar' ); diff --git a/packages/ckeditor5-image/tests/imageutils.js b/packages/ckeditor5-image/tests/imageutils.js index ffb86c9d679..f808daeb7af 100644 --- a/packages/ckeditor5-image/tests/imageutils.js +++ b/packages/ckeditor5-image/tests/imageutils.js @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -/* global console */ +/* global console, setTimeout */ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; import ViewDowncastWriter from '@ckeditor/ckeditor5-engine/src/view/downcastwriter'; @@ -706,6 +706,20 @@ describe( 'ImageUtils plugin', () => { expect( imageElement ).to.be.null; } ); + + it( 'should set image width and height', done => { + setModelData( model, 'f[o]o' ); + + imageUtils.insertImage( { src: '/assets/sample.png' } ); + + setTimeout( () => { + expect( getModelData( model ) ).to.equal( + 'f[]o' + ); + + done(); + }, 100 ); + } ); } ); describe( 'findViewImgElement()', () => {