Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set image width and height on image change #14577

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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