From 4c2508cadee29385775d664cec51076a3a57c23e Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Wed, 12 Jul 2023 15:06:46 +0200 Subject: [PATCH 1/5] Set image width & height on image attribute change. --- .../src/imagesizeattributes.ts | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-image/src/imagesizeattributes.ts b/packages/ckeditor5-image/src/imagesizeattributes.ts index 5331cf081d7..2d4839a9a52 100644 --- a/packages/ckeditor5-image/src/imagesizeattributes.ts +++ b/packages/ckeditor5-image/src/imagesizeattributes.ts @@ -8,8 +8,12 @@ */ import { Plugin } from 'ckeditor5/src/core'; -import type { DowncastDispatcher, DowncastAttributeEvent, ViewElement, Element } from 'ckeditor5/src/engine'; +import type { DowncastDispatcher, DowncastAttributeEvent, ViewElement, Element, ViewContainerElement } from 'ckeditor5/src/engine'; import ImageUtils from './imageutils'; +import ImageLoadObserver, { type ImageLoadedEvent } from './image/imageloadobserver'; +import ImageTypeCommand from './image/imagetypecommand'; + +const IMAGE_WIDGETS_CLASSES_MATCH_REGEXP = /(image|image-inline)/; /** * This plugin enables `width` and `size` attributes in inline and block image elements. @@ -29,6 +33,48 @@ export default class ImageSizeAttributes extends Plugin { return 'ImageSizeAttributes'; } + /** + * @inheritDoc + */ + public init(): void { + const editor = this.editor; + const editing = editor.editing; + + this.listenTo( editing.view.document, 'imageLoaded', ( evt, domEvent ) => { + const image = domEvent.target as HTMLElement; + + if ( !image ) { + return; + } + + const domConverter = editing.view.domConverter; + const imageView = domConverter.domToView( image as HTMLElement ) as ViewElement; + const widgetView = imageView.findAncestor( { classes: IMAGE_WIDGETS_CLASSES_MATCH_REGEXP } ) as ViewContainerElement; + const imageElement = editing.mapper.toModelElement( widgetView )!; + const imageUtils = editor.plugins.get( 'ImageUtils' ); + + if ( imageElement.hasAttribute( 'width' ) || imageElement.hasAttribute( 'height' ) ) { + return; + } + + const setImageSizesOnImageChange = () => { + const changes = Array.from( editor.model.document.differ.getChanges() ); + + for ( const entry of changes ) { + if ( entry.type === 'attribute' ) { + const imageElement = editing.mapper.toModelElement( widgetView )!; + + imageUtils.loadImageAndSetSizeAttributes( imageElement ); + widgetView.off( 'change:attributes', setImageSizesOnImageChange ); + break; + } + } + }; + + widgetView.on( 'change:attributes', setImageSizesOnImageChange ); + } ); + } + /** * @inheritDoc */ From ba9693d5a5197442a97e4b117823279dc93f9b47 Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Wed, 12 Jul 2023 16:43:58 +0200 Subject: [PATCH 2/5] Refactor: extract getImageWidgetFromImageView function to ImageUtils. --- .../src/imageresize/imageresizehandles.ts | 6 ++---- .../ckeditor5-image/src/imagesizeattributes.ts | 16 ++++------------ packages/ckeditor5-image/src/imageutils.ts | 12 +++++++++++- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/ckeditor5-image/src/imageresize/imageresizehandles.ts b/packages/ckeditor5-image/src/imageresize/imageresizehandles.ts index 3c611ceef46..61ed31120da 100644 --- a/packages/ckeditor5-image/src/imageresize/imageresizehandles.ts +++ b/packages/ckeditor5-image/src/imageresize/imageresizehandles.ts @@ -7,7 +7,7 @@ * @module image/imageresize/imageresizehandles */ -import type { Element, ViewContainerElement, ViewElement } from 'ckeditor5/src/engine'; +import type { Element, ViewElement } from 'ckeditor5/src/engine'; import { Plugin } from 'ckeditor5/src/core'; import { WidgetResize } from 'ckeditor5/src/widget'; import ImageUtils from '../imageutils'; @@ -23,8 +23,6 @@ const RESIZABLE_IMAGES_CSS_SELECTOR = 'span.image-inline.ck-widget > img,' + 'span.image-inline.ck-widget > picture > img'; -const IMAGE_WIDGETS_CLASSES_MATCH_REGEXP = /(image|image-inline)/; - const RESIZED_IMAGE_CLASS = 'image_resized'; /** @@ -76,7 +74,7 @@ export default class ImageResizeHandles extends Plugin { const domConverter = editor.editing.view.domConverter; const imageView = domConverter.domToView( domEvent.target as HTMLElement ) as ViewElement; - const widgetView = imageView.findAncestor( { classes: IMAGE_WIDGETS_CLASSES_MATCH_REGEXP } ) as ViewContainerElement; + const widgetView = imageUtils.getImageWidgetFromImageView( imageView )!; let resizer = this.editor.plugins.get( WidgetResize ).getResizerByViewElement( widgetView ); if ( resizer ) { diff --git a/packages/ckeditor5-image/src/imagesizeattributes.ts b/packages/ckeditor5-image/src/imagesizeattributes.ts index 2d4839a9a52..550315b1852 100644 --- a/packages/ckeditor5-image/src/imagesizeattributes.ts +++ b/packages/ckeditor5-image/src/imagesizeattributes.ts @@ -8,12 +8,9 @@ */ import { Plugin } from 'ckeditor5/src/core'; -import type { DowncastDispatcher, DowncastAttributeEvent, ViewElement, Element, ViewContainerElement } from 'ckeditor5/src/engine'; +import type { DowncastDispatcher, DowncastAttributeEvent, ViewElement, Element } from 'ckeditor5/src/engine'; import ImageUtils from './imageutils'; -import ImageLoadObserver, { type ImageLoadedEvent } from './image/imageloadobserver'; -import ImageTypeCommand from './image/imagetypecommand'; - -const IMAGE_WIDGETS_CLASSES_MATCH_REGEXP = /(image|image-inline)/; +import { type ImageLoadedEvent } from './image/imageloadobserver'; /** * This plugin enables `width` and `size` attributes in inline and block image elements. @@ -42,16 +39,11 @@ export default class ImageSizeAttributes extends Plugin { this.listenTo( editing.view.document, 'imageLoaded', ( evt, domEvent ) => { const image = domEvent.target as HTMLElement; - - if ( !image ) { - return; - } - + const imageUtils = editor.plugins.get( 'ImageUtils' ); const domConverter = editing.view.domConverter; const imageView = domConverter.domToView( image as HTMLElement ) as ViewElement; - const widgetView = imageView.findAncestor( { classes: IMAGE_WIDGETS_CLASSES_MATCH_REGEXP } ) as ViewContainerElement; + const widgetView = imageUtils.getImageWidgetFromImageView( imageView )!; const imageElement = editing.mapper.toModelElement( widgetView )!; - const imageUtils = editor.plugins.get( 'ImageUtils' ); if ( imageElement.hasAttribute( 'width' ) || imageElement.hasAttribute( 'height' ) ) { return; diff --git a/packages/ckeditor5-image/src/imageutils.ts b/packages/ckeditor5-image/src/imageutils.ts index c1f57694ec0..92b7a7c2715 100644 --- a/packages/ckeditor5-image/src/imageutils.ts +++ b/packages/ckeditor5-image/src/imageutils.ts @@ -19,13 +19,16 @@ import type { ViewDocumentFragment, DowncastWriter, Model, - Position + Position, + ViewContainerElement } from 'ckeditor5/src/engine'; 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'; +const IMAGE_WIDGETS_CLASSES_MATCH_REGEXP = /(image|image-inline)/; + /** * A set of helpers related to images. */ @@ -211,6 +214,13 @@ export default class ImageUtils extends Plugin { return this.isImage( selectedElement ) ? selectedElement : selection.getFirstPosition()!.findAncestor( 'imageBlock' ); } + /** + * Returns an image widget editing view based on the passed image view. + */ + public getImageWidgetFromImageView( imageView: ViewElement ): ViewContainerElement | null { + return imageView.findAncestor( { classes: IMAGE_WIDGETS_CLASSES_MATCH_REGEXP } ) as ViewContainerElement; + } + /** * Checks if image can be inserted at current model selection. * From 635a03140d850bdb3c40b978779f2d31d469a060 Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Thu, 13 Jul 2023 13:04:14 +0200 Subject: [PATCH 3/5] Tests: set image width and height on image change. --- .../tests/imagesizeattributes.js | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/packages/ckeditor5-image/tests/imagesizeattributes.js b/packages/ckeditor5-image/tests/imagesizeattributes.js index 903ecb4715a..61fb67e6bac 100644 --- a/packages/ckeditor5-image/tests/imagesizeattributes.js +++ b/packages/ckeditor5-image/tests/imagesizeattributes.js @@ -4,6 +4,8 @@ */ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; +import global from '@ckeditor/ckeditor5-utils/src/dom/global'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; @@ -17,6 +19,8 @@ import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; +/* global Event */ + describe( 'ImageSizeAttributes', () => { let editor, model, view; @@ -47,6 +51,166 @@ describe( 'ImageSizeAttributes', () => { expect( ImageSizeAttributes.requires ).to.have.members( [ ImageUtils ] ); } ); + describe( 'init()', () => { + let editor, model, modelRoot, element, domRoot, imageUtils; + + beforeEach( async () => { + element = global.document.createElement( 'div' ); + global.document.body.appendChild( element ); + + await createEditor(); + } ); + + afterEach( async () => { + element.remove(); + + await editor.destroy(); + } ); + + describe( 'inline image: set width and height on image change', () => { + it( 'should set image width and height on image attribute change', () => { + editor.setData( + '

' + ); + + const spy = sinon.spy( imageUtils, 'loadImageAndSetSizeAttributes' ); + const imageElement = modelRoot.getChild( 0 ).getChild( 0 ); + + domRoot.querySelector( 'img' ).dispatchEvent( new Event( 'load' ) ); + + expect( spy.notCalled ).to.be.true; + + model.change( writer => { + writer.setAttribute( 'resizedWidth', '50%', imageElement ); + } ); + + expect( spy.callCount ).to.equal( 1 ); + } ); + + it( 'should set image width and height only on first image attribute change', () => { + editor.setData( + '

' + ); + + const spy = sinon.spy( imageUtils, 'loadImageAndSetSizeAttributes' ); + const imageElement = modelRoot.getChild( 0 ).getChild( 0 ); + + domRoot.querySelector( 'img' ).dispatchEvent( new Event( 'load' ) ); + + expect( spy.notCalled ).to.be.true; + + model.change( writer => { + writer.setAttribute( 'resizedWidth', '50%', imageElement ); + } ); + + expect( spy.callCount ).to.equal( 1 ); + + model.change( writer => { + writer.setAttribute( 'resizedWidth', '20%', imageElement ); + } ); + + expect( spy.callCount ).to.equal( 1 ); + } ); + + it( 'should not try to set image width and height on image attribute change, if image already has width set', () => { + editor.setData( + '

' + ); + + const spy = sinon.spy( imageUtils, 'loadImageAndSetSizeAttributes' ); + const imageElement = modelRoot.getChild( 0 ).getChild( 0 ); + + domRoot.querySelector( 'img' ).dispatchEvent( new Event( 'load' ) ); + + expect( spy.notCalled ).to.be.true; + + model.change( writer => { + writer.setAttribute( 'resizedWidth', '50%', imageElement ); + } ); + + expect( spy.notCalled ).to.be.true; + } ); + } ); + + describe( 'block image: set width and height on image change', () => { + it( 'should set image width and height on image attribute change', () => { + editor.setData( + '
' + ); + + const spy = sinon.spy( imageUtils, 'loadImageAndSetSizeAttributes' ); + const imageElement = modelRoot.getChild( 0 ).getChild( 0 ); + + domRoot.querySelector( 'img' ).dispatchEvent( new Event( 'load' ) ); + + expect( spy.notCalled ).to.be.true; + + model.change( writer => { + writer.setAttribute( 'resizedWidth', '50%', imageElement ); + } ); + + expect( spy.callCount ).to.equal( 1 ); + } ); + + it( 'should set image width and height only on first image attribute change', () => { + editor.setData( + '
' + ); + + const spy = sinon.spy( imageUtils, 'loadImageAndSetSizeAttributes' ); + const imageElement = modelRoot.getChild( 0 ).getChild( 0 ); + + domRoot.querySelector( 'img' ).dispatchEvent( new Event( 'load' ) ); + + expect( spy.notCalled ).to.be.true; + + model.change( writer => { + writer.setAttribute( 'resizedWidth', '50%', imageElement ); + } ); + + expect( spy.callCount ).to.equal( 1 ); + + model.change( writer => { + writer.setAttribute( 'resizedWidth', '20%', imageElement ); + } ); + + expect( spy.callCount ).to.equal( 1 ); + } ); + + it( 'should not try to set image width and height on image attribute change, if image already has width set', () => { + editor.setData( + '
' + ); + + const spy = sinon.spy( imageUtils, 'loadImageAndSetSizeAttributes' ); + const imageElement = modelRoot.getChild( 0 ).getChild( 0 ); + + domRoot.querySelector( 'img' ).dispatchEvent( new Event( 'load' ) ); + + expect( spy.notCalled ).to.be.true; + + model.change( writer => { + writer.setAttribute( 'resizedWidth', '50%', imageElement ); + } ); + + expect( spy.notCalled ).to.be.true; + } ); + } ); + + async function createEditor() { + editor = await ClassicEditor.create( element, { + plugins: [ + Paragraph, ImageInlineEditing, ImageSizeAttributes, ImageResizeEditing + ] + } ); + + model = editor.model; + modelRoot = editor.model.document.getRoot(); + domRoot = editor.editing.view.getDomRoot(); + imageUtils = editor.plugins.get( 'ImageUtils' ); + } + } ); + describe( 'schema', () => { it( 'should allow the "width" and "height" attributes on the imageBlock element', () => { expect( model.schema.checkAttribute( [ '$root', 'imageBlock' ], 'width' ) ).to.be.true; From 6e5a1c667d5df68d085a16074e32d0fbedf582dd Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Thu, 13 Jul 2023 14:26:45 +0200 Subject: [PATCH 4/5] Make sure image widget view exists. --- packages/ckeditor5-image/src/imagesizeattributes.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-image/src/imagesizeattributes.ts b/packages/ckeditor5-image/src/imagesizeattributes.ts index 550315b1852..d799523040d 100644 --- a/packages/ckeditor5-image/src/imagesizeattributes.ts +++ b/packages/ckeditor5-image/src/imagesizeattributes.ts @@ -42,7 +42,12 @@ export default class ImageSizeAttributes extends Plugin { const imageUtils = editor.plugins.get( 'ImageUtils' ); const domConverter = editing.view.domConverter; const imageView = domConverter.domToView( image as HTMLElement ) as ViewElement; - const widgetView = imageUtils.getImageWidgetFromImageView( imageView )!; + const widgetView = imageUtils.getImageWidgetFromImageView( imageView ); + + if ( !widgetView ) { + return; + } + const imageElement = editing.mapper.toModelElement( widgetView )!; if ( imageElement.hasAttribute( 'width' ) || imageElement.hasAttribute( 'height' ) ) { From a6a1c345ff0f79e98acc56e97416cee8efc15f85 Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Fri, 14 Jul 2023 12:03:52 +0200 Subject: [PATCH 5/5] Update return type. Co-authored-by: Arkadiusz Filipczak --- packages/ckeditor5-image/src/imageutils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-image/src/imageutils.ts b/packages/ckeditor5-image/src/imageutils.ts index 92b7a7c2715..39a0170c799 100644 --- a/packages/ckeditor5-image/src/imageutils.ts +++ b/packages/ckeditor5-image/src/imageutils.ts @@ -218,7 +218,7 @@ export default class ImageUtils extends Plugin { * Returns an image widget editing view based on the passed image view. */ public getImageWidgetFromImageView( imageView: ViewElement ): ViewContainerElement | null { - return imageView.findAncestor( { classes: IMAGE_WIDGETS_CLASSES_MATCH_REGEXP } ) as ViewContainerElement; + return imageView.findAncestor( { classes: IMAGE_WIDGETS_CLASSES_MATCH_REGEXP } ) as ( ViewContainerElement | null ); } /**