Skip to content

Commit

Permalink
Merge pull request #14577 from ckeditor/ck/14557-set-image-width-and-…
Browse files Browse the repository at this point in the history
…height-on-image-change

Task (image): Image width and height should be set on image change (resize, alignment, etc.). Closes #14557.
  • Loading branch information
mmotyczynska authored Jul 14, 2023
2 parents 5f60808 + a6a1c34 commit 96be2e4
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';

/**
Expand Down Expand Up @@ -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 ) {
Expand Down
43 changes: 43 additions & 0 deletions packages/ckeditor5-image/src/imagesizeattributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import { Plugin } from 'ckeditor5/src/core';
import type { DowncastDispatcher, DowncastAttributeEvent, ViewElement, Element } from 'ckeditor5/src/engine';
import ImageUtils from './imageutils';
import { type ImageLoadedEvent } from './image/imageloadobserver';

/**
* This plugin enables `width` and `size` attributes in inline and block image elements.
Expand All @@ -29,6 +30,48 @@ export default class ImageSizeAttributes extends Plugin {
return 'ImageSizeAttributes';
}

/**
* @inheritDoc
*/
public init(): void {
const editor = this.editor;
const editing = editor.editing;

this.listenTo<ImageLoadedEvent>( editing.view.document, 'imageLoaded', ( evt, domEvent ) => {
const image = domEvent.target as HTMLElement;
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 );

if ( !widgetView ) {
return;
}

const imageElement = editing.mapper.toModelElement( widgetView )!;

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
*/
Expand Down
12 changes: 11 additions & 1 deletion packages/ckeditor5-image/src/imageutils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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 | null );
}

/**
* Checks if image can be inserted at current model selection.
*
Expand Down
164 changes: 164 additions & 0 deletions packages/ckeditor5-image/tests/imagesizeattributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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;

Expand Down Expand Up @@ -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(
'<p><img src="/assets/sample.png" "></p>'
);

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(
'<p><img src="/assets/sample.png" "></p>'
);

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(
'<p><img width="100" src="/assets/sample.png" "></p>'
);

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(
'<figure class="image"><img src="/assets/sample.png"></figure>'
);

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(
'<figure class="image"><img src="/assets/sample.png"></figure>'
);

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(
'<figure class="image"><img width="100" src="/assets/sample.png"></figure>'
);

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;
Expand Down

0 comments on commit 96be2e4

Please sign in to comment.