diff --git a/packages/ckeditor5-engine/src/index.ts b/packages/ckeditor5-engine/src/index.ts index 7a6ba06b27b..f86baf4d29a 100644 --- a/packages/ckeditor5-engine/src/index.ts +++ b/packages/ckeditor5-engine/src/index.ts @@ -92,7 +92,7 @@ export type { Marker } from './model/markercollection'; export type { default as Batch } from './model/batch'; export type { default as Differ, DiffItem, DiffItemAttribute, DiffItemInsert, DiffItemRemove } from './model/differ'; export type { default as Item } from './model/item'; -export type { default as Node } from './model/node'; +export type { default as Node, NodeAttributes } from './model/node'; export type { default as RootElement } from './model/rootelement'; export type { default as Schema, diff --git a/packages/ckeditor5-html-support/tests/integrations/image.js b/packages/ckeditor5-html-support/tests/integrations/image.js index afc19cf2ec1..2fe011d3cea 100644 --- a/packages/ckeditor5-html-support/tests/integrations/image.js +++ b/packages/ckeditor5-html-support/tests/integrations/image.js @@ -2475,6 +2475,8 @@ describe( 'ImageElementSupport', () => { 'src', 'srcset', 'linkHref', + 'width', + 'height', 'htmlImgAttributes', 'htmlFigureAttributes', 'htmlLinkAttributes' @@ -2509,6 +2511,8 @@ describe( 'ImageElementSupport', () => { 'alt', 'src', 'srcset', + 'width', + 'height', 'htmlA', 'htmlImgAttributes' ] ); diff --git a/packages/ckeditor5-image/package.json b/packages/ckeditor5-image/package.json index d040dd1555b..fa00c096047 100644 --- a/packages/ckeditor5-image/package.json +++ b/packages/ckeditor5-image/package.json @@ -26,6 +26,8 @@ "@ckeditor/ckeditor5-cloud-services": "39.0.2", "@ckeditor/ckeditor5-core": "39.0.2", "@ckeditor/ckeditor5-dev-utils": "^38.0.0", + "@ckeditor/ckeditor5-html-support": "39.0.2", + "@ckeditor/ckeditor5-paste-from-office": "39.0.2", "@ckeditor/ckeditor5-easy-image": "39.0.2", "@ckeditor/ckeditor5-editor-classic": "39.0.2", "@ckeditor/ckeditor5-engine": "39.0.2", diff --git a/packages/ckeditor5-image/src/augmentation.ts b/packages/ckeditor5-image/src/augmentation.ts index 2962b03ac01..202f63f0a93 100644 --- a/packages/ckeditor5-image/src/augmentation.ts +++ b/packages/ckeditor5-image/src/augmentation.ts @@ -26,6 +26,7 @@ import type { ImageCaptionUtils, ImageInsertUI, ImageResizeEditing, + ImageSizeAttributes, ImageStyleEditing, ImageStyleUI, ImageTextAlternativeEditing, @@ -76,6 +77,7 @@ declare module '@ckeditor/ckeditor5-core' { [ ImageCaptionUtils.pluginName ]: ImageCaptionUtils; [ ImageInsertUI.pluginName ]: ImageInsertUI; [ ImageResizeEditing.pluginName ]: ImageResizeEditing; + [ ImageSizeAttributes.pluginName ]: ImageSizeAttributes; [ ImageStyleEditing.pluginName ]: ImageStyleEditing; [ ImageStyleUI.pluginName ]: ImageStyleUI; [ ImageTextAlternativeEditing.pluginName ]: ImageTextAlternativeEditing; diff --git a/packages/ckeditor5-image/src/image/converters.ts b/packages/ckeditor5-image/src/image/converters.ts index 1ea58c6f498..5a11f85bd8b 100644 --- a/packages/ckeditor5-image/src/image/converters.ts +++ b/packages/ckeditor5-image/src/image/converters.ts @@ -19,8 +19,6 @@ import type { import { first, type GetCallback } from 'ckeditor5/src/utils'; import type ImageUtils from '../imageutils'; -type SrcsetAttributeType = null | { data: unknown; width: unknown }; - /** * Returns a function that converts the image view representation: * @@ -178,7 +176,7 @@ export function upcastPicture( imageUtils: ImageUtils ): ( dispatcher: UpcastDis } /** - * Converter used to convert the `srcset` model image attribute to the `srcset`, `sizes` and `width` attributes in the view. + * Converter used to convert the `srcset` model image attribute to the `srcset` and `sizes` attributes in the view. * * @internal * @param imageType The type of the image. @@ -197,27 +195,13 @@ export function downcastSrcsetAttribute( const img = imageUtils.findViewImgElement( element )!; if ( data.attributeNewValue === null ) { - const srcset = data.attributeOldValue as SrcsetAttributeType; - - if ( srcset && srcset.data ) { - writer.removeAttribute( 'srcset', img ); - writer.removeAttribute( 'sizes', img ); - - if ( srcset.width ) { - writer.removeAttribute( 'width', img ); - } - } + writer.removeAttribute( 'srcset', img ); + writer.removeAttribute( 'sizes', img ); } else { - const srcset = data.attributeNewValue as SrcsetAttributeType; - - if ( srcset && srcset.data ) { - writer.setAttribute( 'srcset', srcset.data, img ); + if ( data.attributeNewValue ) { + writer.setAttribute( 'srcset', data.attributeNewValue, img ); // Always outputting `100vw`. See https://github.com/ckeditor/ckeditor5-image/issues/2. writer.setAttribute( 'sizes', '100vw', img ); - - if ( srcset.width ) { - writer.setAttribute( 'width', srcset.width, img ); - } } } }; diff --git a/packages/ckeditor5-image/src/image/imageblockediting.ts b/packages/ckeditor5-image/src/image/imageblockediting.ts index 92c2ee1895e..c851e8f5360 100644 --- a/packages/ckeditor5-image/src/image/imageblockediting.ts +++ b/packages/ckeditor5-image/src/image/imageblockediting.ts @@ -18,6 +18,7 @@ import { } from './converters'; import ImageEditing from './imageediting'; +import ImageSizeAttributes from '../imagesizeattributes'; import ImageTypeCommand from './imagetypecommand'; import ImageUtils from '../imageutils'; import { @@ -41,7 +42,7 @@ export default class ImageBlockEditing extends Plugin { * @inheritDoc */ public static get requires() { - return [ ImageEditing, ImageUtils, ClipboardPipeline ] as const; + return [ ImageEditing, ImageSizeAttributes, ImageUtils, ClipboardPipeline ] as const; } /** diff --git a/packages/ckeditor5-image/src/image/imageediting.ts b/packages/ckeditor5-image/src/image/imageediting.ts index a1e43bc981d..71dbb7af6cb 100644 --- a/packages/ckeditor5-image/src/image/imageediting.ts +++ b/packages/ckeditor5-image/src/image/imageediting.ts @@ -59,20 +59,7 @@ export default class ImageEditing extends Plugin { name: 'img', key: 'srcset' }, - model: { - key: 'srcset', - value: ( viewImage: ViewElement ) => { - const value: Record = { - data: viewImage.getAttribute( 'srcset' )! - }; - - if ( viewImage.hasAttribute( 'width' ) ) { - value.width = viewImage.getAttribute( 'width' )!; - } - - return value; - } - } + model: 'srcset' } ); const insertImageCommand = new InsertImageCommand( editor ); diff --git a/packages/ckeditor5-image/src/image/imageinlineediting.ts b/packages/ckeditor5-image/src/image/imageinlineediting.ts index 948e75dcaba..12b1c3a6423 100644 --- a/packages/ckeditor5-image/src/image/imageinlineediting.ts +++ b/packages/ckeditor5-image/src/image/imageinlineediting.ts @@ -17,6 +17,7 @@ import { } from './converters'; import ImageEditing from './imageediting'; +import ImageSizeAttributes from '../imagesizeattributes'; import ImageTypeCommand from './imagetypecommand'; import ImageUtils from '../imageutils'; import { @@ -40,7 +41,7 @@ export default class ImageInlineEditing extends Plugin { * @inheritDoc */ public static get requires() { - return [ ImageEditing, ImageUtils, ClipboardPipeline ] as const; + return [ ImageEditing, ImageSizeAttributes, ImageUtils, ClipboardPipeline ] as const; } /** diff --git a/packages/ckeditor5-image/src/image/utils.ts b/packages/ckeditor5-image/src/image/utils.ts index a9b83d982bc..2c0f1707bee 100644 --- a/packages/ckeditor5-image/src/image/utils.ts +++ b/packages/ckeditor5-image/src/image/utils.ts @@ -135,3 +135,28 @@ export function determineImageTypeForInsertionAtSelection( // Otherwise insert an inline image. return 'imageInline'; } + +/** + * Returns parsed value of the size, but only if it contains unit: px. + */ +export function getSizeValueIfInPx( size: string | undefined ): number | null { + if ( size && size.endsWith( 'px' ) ) { + return parseInt( size ); + } + + return null; +} + +/** + * Returns true if both styles (width and height) are set. + * + * 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. + */ +export function widthAndHeightStylesAreBothSet( viewElement: ViewElement ): boolean { + const widthStyle = getSizeValueIfInPx( viewElement.getStyle( 'width' ) ); + const heightStyle = getSizeValueIfInPx( viewElement.getStyle( 'height' ) ); + + return !!( widthStyle && heightStyle ); +} diff --git a/packages/ckeditor5-image/src/imageresize/imageresizeediting.ts b/packages/ckeditor5-image/src/imageresize/imageresizeediting.ts index 2d9356afa17..006c41fc909 100644 --- a/packages/ckeditor5-image/src/imageresize/imageresizeediting.ts +++ b/packages/ckeditor5-image/src/imageresize/imageresizeediting.ts @@ -11,6 +11,7 @@ import type { ViewElement } from 'ckeditor5/src/engine'; import { type Editor, Plugin } from 'ckeditor5/src/core'; import ImageUtils from '../imageutils'; import ResizeImageCommand from './resizeimagecommand'; +import { widthAndHeightStylesAreBothSet } from '../image/utils'; /** * The image resize editing feature. @@ -82,11 +83,11 @@ export default class ImageResizeEditing extends Plugin { private _registerSchema(): void { if ( this.editor.plugins.has( 'ImageBlockEditing' ) ) { - this.editor.model.schema.extend( 'imageBlock', { allowAttributes: 'width' } ); + this.editor.model.schema.extend( 'imageBlock', { allowAttributes: [ 'resizedWidth', 'resizedHeight' ] } ); } if ( this.editor.plugins.has( 'ImageInlineEditing' ) ) { - this.editor.model.schema.extend( 'imageInline', { allowAttributes: 'width' } ); + this.editor.model.schema.extend( 'imageInline', { allowAttributes: [ 'resizedWidth', 'resizedHeight' ] } ); } } @@ -97,23 +98,55 @@ 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 => - dispatcher.on( `attribute:width:${ imageType }`, ( evt, data, conversionApi ) => { + dispatcher.on( `attribute:resizedWidth:${ imageType }`, ( evt, data, conversionApi ) => { if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { return; } const viewWriter = conversionApi.writer; - const figure = conversionApi.mapper.toViewElement( data.item ); + const viewImg = conversionApi.mapper.toViewElement( data.item ); if ( data.attributeNewValue !== null ) { - viewWriter.setStyle( 'width', data.attributeNewValue, figure ); - viewWriter.addClass( 'image_resized', figure ); + viewWriter.setStyle( 'width', data.attributeNewValue, viewImg ); + viewWriter.addClass( 'image_resized', viewImg ); } else { - viewWriter.removeStyle( 'width', figure ); - viewWriter.removeClass( 'image_resized', figure ); + viewWriter.removeStyle( 'width', viewImg ); + viewWriter.removeClass( 'image_resized', viewImg ); + } + } ) + ); + + editor.conversion.for( 'dataDowncast' ).attributeToAttribute( { + model: { + name: imageType, + key: 'resizedHeight' + }, + view: modelAttributeValue => ( { + key: 'style', + value: { + 'height': modelAttributeValue + } + } ) + } ); + + editor.conversion.for( 'editingDowncast' ).add( dispatcher => + dispatcher.on( `attribute:resizedHeight:${ imageType }`, ( evt, data, conversionApi ) => { + if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { + return; + } + + const viewWriter = conversionApi.writer; + const viewImg = conversionApi.mapper.toViewElement( data.item ); + const target = imageType === 'imageInline' ? imageUtils.findViewImgElement( viewImg ) : viewImg; + + if ( data.attributeNewValue !== null ) { + viewWriter.setStyle( 'height', data.attributeNewValue, target ); + } else { + viewWriter.removeStyle( 'height', target ); } } ) ); @@ -127,8 +160,34 @@ export default class ImageResizeEditing extends Plugin { } }, model: { - key: 'width', - value: ( viewElement: ViewElement ) => viewElement.getStyle( 'width' ) + key: 'resizedWidth', + value: ( viewElement: ViewElement ) => { + if ( widthAndHeightStylesAreBothSet( viewElement ) ) { + 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 ) => { + if ( widthAndHeightStylesAreBothSet( viewElement ) ) { + 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 9f152364309..0c182e637e5 100644 --- a/packages/ckeditor5-image/src/imageresize/imageresizehandles.ts +++ b/packages/ckeditor5-image/src/imageresize/imageresizehandles.ts @@ -7,9 +7,10 @@ * @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'; import ImageLoadObserver, { type ImageLoadedEvent } from '../image/imageloadobserver'; import type ResizeImageCommand from './resizeimagecommand'; @@ -22,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'; /** @@ -37,7 +36,7 @@ export default class ImageResizeHandles extends Plugin { * @inheritDoc */ public static get requires() { - return [ WidgetResize ] as const; + return [ WidgetResize, ImageUtils ] as const; } /** @@ -63,6 +62,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 ); @@ -74,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 ) { @@ -130,6 +130,14 @@ 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.bind( 'isEnabled' ).to( this ); diff --git a/packages/ckeditor5-image/src/imageresize/resizeimagecommand.ts b/packages/ckeditor5-image/src/imageresize/resizeimagecommand.ts index 5b2184e76fb..0bf83c4b1ec 100644 --- a/packages/ckeditor5-image/src/imageresize/resizeimagecommand.ts +++ b/packages/ckeditor5-image/src/imageresize/resizeimagecommand.ts @@ -33,11 +33,11 @@ export default class ResizeImageCommand extends Command { this.isEnabled = !!element; - if ( !element || !element.hasAttribute( 'width' ) ) { + if ( !element || !element.hasAttribute( 'resizedWidth' ) ) { this.value = null; } else { this.value = { - width: element.getAttribute( 'width' ) as string, + width: element.getAttribute( 'resizedWidth' ) as string, height: null }; } @@ -71,7 +71,9 @@ export default class ResizeImageCommand extends Command { if ( imageElement ) { model.change( writer => { - writer.setAttribute( 'width', options.width, imageElement ); + writer.setAttribute( 'resizedWidth', options.width, imageElement ); + writer.removeAttribute( 'resizedHeight', imageElement ); + imageUtils.setImageNaturalSizeAttributes( imageElement ); } ); } } diff --git a/packages/ckeditor5-image/src/imagesizeattributes.ts b/packages/ckeditor5-image/src/imagesizeattributes.ts new file mode 100644 index 00000000000..eec5a0e1cf1 --- /dev/null +++ b/packages/ckeditor5-image/src/imagesizeattributes.ts @@ -0,0 +1,170 @@ +/** + * @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 + */ + +/** + * @module image/imagesizeattributes + */ + +import { Plugin } from 'ckeditor5/src/core'; +import type { DowncastDispatcher, DowncastAttributeEvent, ViewElement, Element } from 'ckeditor5/src/engine'; +import ImageUtils from './imageutils'; +import { widthAndHeightStylesAreBothSet, getSizeValueIfInPx } from './image/utils'; + +/** + * This plugin enables `width` and `height` attributes in inline and block image elements. + */ +export default class ImageSizeAttributes extends Plugin { + /** + * @inheritDoc + */ + public static get requires() { + return [ ImageUtils ] as const; + } + + /** + * @inheritDoc + */ + public static get pluginName() { + return 'ImageSizeAttributes' as const; + } + + /** + * @inheritDoc + */ + public afterInit(): void { + this._registerSchema(); + this._registerConverters( 'imageBlock' ); + this._registerConverters( 'imageInline' ); + } + + /** + * Registers the `width` and `height` attributes for inline and block images. + */ + private _registerSchema(): void { + if ( this.editor.plugins.has( 'ImageBlockEditing' ) ) { + this.editor.model.schema.extend( 'imageBlock', { allowAttributes: [ 'width', 'height' ] } ); + } + + if ( this.editor.plugins.has( 'ImageInlineEditing' ) ) { + this.editor.model.schema.extend( 'imageInline', { allowAttributes: [ 'width', 'height' ] } ); + } + } + + /** + * Registers converters for `width` and `height` attributes. + */ + private _registerConverters( imageType: 'imageBlock' | 'imageInline' ): void { + const editor = this.editor; + const imageUtils = editor.plugins.get( 'ImageUtils' ); + const viewElementName = imageType === 'imageBlock' ? 'figure' : 'img'; + + editor.conversion.for( 'upcast' ) + .attributeToAttribute( { + view: { + name: viewElementName, + styles: { + width: /.+/ + } + }, + model: { + key: 'width', + value: ( viewElement: ViewElement ) => { + if ( widthAndHeightStylesAreBothSet( viewElement ) ) { + return getSizeValueIfInPx( viewElement.getStyle( 'width' ) ); + } + + return null; + } + } + } ) + .attributeToAttribute( { + view: { + name: viewElementName, + key: 'width' + }, + model: 'width' + } ) + .attributeToAttribute( { + view: { + name: viewElementName, + styles: { + height: /.+/ + } + }, + model: { + key: 'height', + value: ( viewElement: ViewElement ) => { + if ( widthAndHeightStylesAreBothSet( viewElement ) ) { + return getSizeValueIfInPx( viewElement.getStyle( 'height' ) ); + } + + return null; + } + } + } ) + .attributeToAttribute( { + view: { + name: viewElementName, + key: 'height' + }, + model: 'height' + } ); + + // Dedicated converters to propagate attributes to the element. + editor.conversion.for( 'editingDowncast' ).add( dispatcher => { + attachDowncastConverter( dispatcher, 'width', 'width', true ); + attachDowncastConverter( dispatcher, 'height', 'height', true ); + } ); + + editor.conversion.for( 'dataDowncast' ).add( dispatcher => { + attachDowncastConverter( dispatcher, 'width', 'width', false ); + attachDowncastConverter( dispatcher, 'height', 'height', false ); + } ); + + function attachDowncastConverter( + dispatcher: DowncastDispatcher, + modelAttributeName: string, + viewAttributeName: string, + setRatioForInlineImage: boolean + ) { + dispatcher.on( `attribute:${ modelAttributeName }:${ imageType }`, ( evt, data, conversionApi ) => { + if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { + return; + } + + const viewWriter = conversionApi.writer; + const viewElement = conversionApi.mapper.toViewElement( data.item as Element )!; + const img = imageUtils.findViewImgElement( viewElement )!; + + if ( data.attributeNewValue !== null ) { + viewWriter.setAttribute( viewAttributeName, data.attributeNewValue, img ); + } else { + viewWriter.removeAttribute( viewAttributeName, img ); + } + + // Do not set aspect-ratio for pictures. See https://github.com/ckeditor/ckeditor5/issues/14579. + if ( data.item.hasAttribute( 'sources' ) ) { + return; + } + + const isResized = data.item.hasAttribute( 'resizedWidth' ); + + // Do not set aspect ratio for inline images which are not resized (data pipeline). + if ( imageType === 'imageInline' && !isResized && !setRatioForInlineImage ) { + return; + } + + const width = data.item.getAttribute( 'width' ); + const height = data.item.getAttribute( 'height' ); + const aspectRatio = img.getStyle( 'aspect-ratio' ); + + if ( width && height && !aspectRatio ) { + viewWriter.setStyle( 'aspect-ratio', `${ width }/${ height }`, img ); + } + } ); + } + } +} + diff --git a/packages/ckeditor5-image/src/imagestyle/imagestylecommand.ts b/packages/ckeditor5-image/src/imagestyle/imagestylecommand.ts index 812c7bd707b..561bfb6b251 100644 --- a/packages/ckeditor5-image/src/imagestyle/imagestylecommand.ts +++ b/packages/ckeditor5-image/src/imagestyle/imagestylecommand.ts @@ -116,6 +116,8 @@ export default class ImageStyleCommand extends Command { } else { writer.setAttribute( 'imageStyle', requestedStyle, imageElement ); } + + imageUtils.setImageNaturalSizeAttributes( imageElement ); } ); } diff --git a/packages/ckeditor5-image/src/imageupload/imageuploadediting.ts b/packages/ckeditor5-image/src/imageupload/imageuploadediting.ts index cd62c7df1c8..f93e374c484 100644 --- a/packages/ckeditor5-image/src/imageupload/imageuploadediting.ts +++ b/packages/ckeditor5-image/src/imageupload/imageuploadediting.ts @@ -9,7 +9,15 @@ import { Plugin, type Editor } from 'ckeditor5/src/core'; -import { UpcastWriter, type Element, type Item, type Writer, type DataTransfer, type ViewElement } from 'ckeditor5/src/engine'; +import { + UpcastWriter, + type Element, + type Item, + type Writer, + type DataTransfer, + type ViewElement, + type NodeAttributes +} from 'ckeditor5/src/engine'; import { Notification } from 'ckeditor5/src/ui'; import { ClipboardPipeline, type ViewDocumentClipboardInputEvent } from 'ckeditor5/src/clipboard'; @@ -225,12 +233,14 @@ export default class ImageUploadEditing extends Plugin { } ); // Set the default handler for feeding the image element with `src` and `srcset` attributes. + // Also set the natural `width` and `height` attributes (if not already set). 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.setImageNaturalSizeAttributes( imageElement ); } ); }, { priority: 'low' } ); } @@ -396,10 +406,15 @@ export default class ImageUploadEditing extends Plugin { .join( ', ' ); if ( srcsetAttribute != '' ) { - writer.setAttribute( 'srcset', { - data: srcsetAttribute, - width: maxWidth - }, image ); + const attributes: NodeAttributes = { + srcset: srcsetAttribute + }; + + if ( !image.hasAttribute( 'width' ) && !image.hasAttribute( 'height' ) ) { + attributes.width = maxWidth; + } + + writer.setAttributes( attributes, image ); } } } diff --git a/packages/ckeditor5-image/src/imageutils.ts b/packages/ckeditor5-image/src/imageutils.ts index 3a305cf7426..eebab43f061 100644 --- a/packages/ckeditor5-image/src/imageutils.ts +++ b/packages/ckeditor5-image/src/imageutils.ts @@ -19,16 +19,25 @@ 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. */ export default class ImageUtils extends Plugin { + /** + * DOM Emitter. + */ + private _domEmitter: DomEmitter = new ( DomEmitterMixin() )(); + /** * @inheritDoc */ @@ -121,6 +130,8 @@ export default class ImageUtils extends Plugin { // Inserting an image might've failed due to schema regulations. if ( imageElement.parent ) { + this.setImageNaturalSizeAttributes( imageElement ); + return imageElement; } @@ -128,6 +139,43 @@ export default class ImageUtils extends Plugin { } ); } + /** + * 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 setImageNaturalSizeAttributes( imageElement: Element ): void { + const src = imageElement.getAttribute( 'src' ) as string; + + if ( !src ) { + return; + } + + if ( imageElement.getAttribute( 'width' ) || imageElement.getAttribute( 'height' ) ) { + return; + } + + this.editor.model.change( writer => { + const img = new global.window.Image(); + + this._domEmitter.listenTo( img, 'load', () => { + if ( !imageElement.getAttribute( 'width' ) && !imageElement.getAttribute( 'height' ) ) { + // We use writer.batch to be able to undo (in a single step) width and height setting + // along with any change that triggered this action (e.g. image resize or image style change). + this.editor.model.enqueueChange( writer.batch, writer => { + writer.setAttribute( 'width', img.naturalWidth, imageElement ); + writer.setAttribute( 'height', img.naturalHeight, imageElement ); + } ); + } + + this._domEmitter.stopListening( img, 'load' ); + } ); + + img.src = src; + } ); + } + /** * Returns an image widget editing view element if one is selected or is among the selection's ancestors. */ @@ -166,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. * @@ -239,6 +294,15 @@ export default class ImageUtils extends Plugin { } } } + + /** + * @inheritDoc + */ + public override destroy(): void { + this._domEmitter.stopListening(); + + return super.destroy(); + } } /** diff --git a/packages/ckeditor5-image/src/index.ts b/packages/ckeditor5-image/src/index.ts index 9b23864448f..b8c45971529 100644 --- a/packages/ckeditor5-image/src/index.ts +++ b/packages/ckeditor5-image/src/index.ts @@ -19,6 +19,7 @@ export { default as ImageResize } from './imageresize'; export { default as ImageResizeButtons } from './imageresize/imageresizebuttons'; export { default as ImageResizeEditing } from './imageresize/imageresizeediting'; export { default as ImageResizeHandles } from './imageresize/imageresizehandles'; +export { default as ImageSizeAttributes } from './imagesizeattributes'; export { default as ImageStyle } from './imagestyle'; export { default as ImageStyleEditing } from './imagestyle/imagestyleediting'; export { default as ImageStyleUI } from './imagestyle/imagestyleui'; diff --git a/packages/ckeditor5-image/tests/image/imageblockediting.js b/packages/ckeditor5-image/tests/image/imageblockediting.js index 31d5b66b4db..ddb0d4e66e1 100644 --- a/packages/ckeditor5-image/tests/image/imageblockediting.js +++ b/packages/ckeditor5-image/tests/image/imageblockediting.js @@ -140,7 +140,7 @@ describe( 'ImageBlockEditing', () => { it( 'should convert srcset attribute to srcset and sizes attribute', () => { setModelData( model, - '' + + '' + '' ); @@ -151,28 +151,6 @@ describe( 'ImageBlockEditing', () => { ); } ); - it( 'should convert srcset attribute to width, srcset and add sizes attribute', () => { - setModelData( model, - '' + - '' - ); - - expect( normalizeHtml( editor.getData() ) ).to.equal( - '
' + - '' + - '' + - '
' - ); - } ); - it( 'should not convert srcset attribute if is already consumed', () => { editor.data.downcastDispatcher.on( 'attribute:srcset:imageBlock', ( evt, data, conversionApi ) => { const modelImage = data.item; @@ -184,7 +162,7 @@ describe( 'ImageBlockEditing', () => { '' + + 'srcset="small.png 148w, big.png 1024w">' + '' ); @@ -195,12 +173,12 @@ describe( 'ImageBlockEditing', () => { ); } ); - it( 'should not convert srcset attribute if has wrong data', () => { + it( 'should not convert srcset attribute if has no data', () => { setModelData( model, '' + + 'srcset="">' + '' ); const image = doc.getRoot().getChild( 0 ); @@ -344,27 +322,11 @@ describe( 'ImageBlockEditing', () => { expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( - '' + + '' + '' ); } ); - it( 'should convert image with srcset and width attributes', () => { - editor.setData( - '
' + - 'alt text' + - '
' - ); - - expect( getModelData( model, { withoutSelection: true } ) ).to.equal( - '' + - '' - ); - } ); - it( 'should ignore sizes attribute', () => { editor.setData( '
' + @@ -374,7 +336,7 @@ describe( 'ImageBlockEditing', () => { expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( - '' + + '' + '' ); } ); @@ -552,7 +514,7 @@ describe( 'ImageBlockEditing', () => { '' + + 'srcset="small.png 148w, big.png 1024w">' + '' ); expect( getViewData( view, { withoutSelection: true } ) ).to.equal( @@ -562,12 +524,12 @@ describe( 'ImageBlockEditing', () => { ); } ); - it( 'should not convert srcset attribute if has wrong data', () => { + it( 'should not convert srcset attribute if has no data', () => { setModelData( model, '' + + 'srcset="">' + '' ); const image = doc.getRoot().getChild( 0 ); @@ -582,30 +544,9 @@ describe( 'ImageBlockEditing', () => { ); } ); - it( 'should convert srcset attribute to srcset, width and sizes', () => { - setModelData( model, - '' + - '' ); - - expect( getViewData( view, { withoutSelection: true } ) ).to.equal( - '
' + - '' + - '' + - '
' - ); - } ); - it( 'should remove sizes and srcsset attribute when srcset attribute is removed from model', () => { setModelData( model, - '' + '' ); const image = doc.getRoot().getChild( 0 ); @@ -620,26 +561,6 @@ describe( 'ImageBlockEditing', () => { ); } ); - it( 'should remove width, sizes and srcsset attribute when srcset attribute is removed from model', () => { - setModelData( model, - '' + - '' - ); - const image = doc.getRoot().getChild( 0 ); - - model.change( writer => { - writer.removeAttribute( 'srcset', image ); - } ); - - expect( getViewData( view, { withoutSelection: true } ) ).to.equal( - '
' + - '' + - '
' - ); - } ); - it( 'should not convert srcset attribute if is already consumed', () => { editor.editing.downcastDispatcher.on( 'attribute:srcset:imageBlock', ( evt, data, conversionApi ) => { const modelImage = data.item; @@ -651,7 +572,7 @@ describe( 'ImageBlockEditing', () => { '' + + 'srcset="small.png 148w, big.png 1024w">' + '' ); diff --git a/packages/ckeditor5-image/tests/image/imageediting.js b/packages/ckeditor5-image/tests/image/imageediting.js index 6ad93d45d79..5119bbee9f9 100644 --- a/packages/ckeditor5-image/tests/image/imageediting.js +++ b/packages/ckeditor5-image/tests/image/imageediting.js @@ -161,7 +161,7 @@ describe( 'ImageEditing', () => { it( 'should convert srcset attribute to srcset and sizes attribute', () => { setModelData( model, - '' + + '' + '' ); @@ -173,7 +173,7 @@ describe( 'ImageEditing', () => { setModelData( model, '' + - '' + + '' + '' ); @@ -182,48 +182,6 @@ describe( 'ImageEditing', () => { ); } ); - it( 'should convert srcset attribute to width, srcset and add sizes attribute', () => { - setModelData( model, - '' + - '' - ); - - expect( normalizeHtml( editor.getData() ) ).to.equal( - '
' + - '' + - '' + - '
' - ); - - setModelData( model, - '' + - '' - ); - - expect( normalizeHtml( editor.getData() ) ).to.equal( - '

' + - '' + - '' + - '

' - ); - } ); - it( 'should not convert srcset attribute if is already consumed', () => { editor.data.downcastDispatcher.on( 'attribute:srcset:imageBlock', ( evt, data, conversionApi ) => { const modelImage = data.item; @@ -235,7 +193,7 @@ describe( 'ImageEditing', () => { '' + + 'srcset="small.png 148w, big.png 1024w">' + '' ); @@ -255,19 +213,19 @@ describe( 'ImageEditing', () => { '' + + 'srcset="small.png 148w, big.png 1024w">' + '' ); expect( editor.getData() ).to.equal( '

alt text

' ); } ); - it( 'should not convert srcset attribute if has wrong data', () => { + it( 'should not convert srcset attribute if has no data', () => { setModelData( model, '' + + 'srcset="">' + '' ); const image = doc.getRoot().getChild( 0 ); @@ -285,7 +243,7 @@ describe( 'ImageEditing', () => { '' + + 'srcset="">' + '' ); const imageInline = doc.getRoot().getChild( 0 ); @@ -468,7 +426,7 @@ describe( 'ImageEditing', () => { expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( - '' + + '' + '' ); @@ -479,40 +437,12 @@ describe( 'ImageEditing', () => { expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '' + - '' + + '' + '' + '' ); } ); - it( 'should convert image with srcset and width attributes', () => { - editor.setData( - '
' + - 'alt text' + - '
' - ); - - expect( getModelData( model, { withoutSelection: true } ) ).to.equal( - '' + - '' - ); - - editor.setData( - '

alt text

' - ); - - expect( getModelData( model, { withoutSelection: true } ) ).to.equal( - '' + - '' - ); - } ); - it( 'should ignore sizes attribute', () => { editor.setData( '
' + @@ -522,7 +452,7 @@ describe( 'ImageEditing', () => { expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( - '' + + '' + '' ); @@ -533,7 +463,7 @@ describe( 'ImageEditing', () => { expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '' + - '' + + '' + '' + '' ); @@ -842,7 +772,7 @@ describe( 'ImageEditing', () => { '' + + 'srcset="small.png 148w, big.png 1024w">' + '' ); expect( getViewData( view, { withoutSelection: true } ) ).to.equal( @@ -855,7 +785,7 @@ describe( 'ImageEditing', () => { '' + + 'srcset="small.png 148w, big.png 1024w">' + '' ); expect( getViewData( view, { withoutSelection: true } ) ).to.equal( @@ -865,12 +795,12 @@ describe( 'ImageEditing', () => { ); } ); - it( 'should not convert srcset attribute if has wrong data', () => { + it( 'should not convert srcset attribute if has no data', () => { setModelData( model, '' + + 'srcset="">' + '' ); let image = doc.getRoot().getChild( 0 ); @@ -903,49 +833,9 @@ describe( 'ImageEditing', () => { ); } ); - it( 'should convert srcset attribute to srcset, width and sizes', () => { - setModelData( model, - '' + - '' ); - - expect( getViewData( view, { withoutSelection: true } ) ).to.equal( - '
' + - '' + - '' + - '
' - ); - - setModelData( model, - '' + - '' ); - - expect( getViewData( view, { withoutSelection: true } ) ).to.equal( - '

' + - '' + - '' + - '

' - ); - } ); - it( 'should remove sizes and srcsset attribute when srcset attribute is removed from model', () => { setModelData( model, - '' + + '' + '' ); let image = doc.getRoot().getChild( 0 ); @@ -962,7 +852,7 @@ describe( 'ImageEditing', () => { setModelData( model, '' + - '' + + '' + '' ); image = doc.getRoot().getChild( 0 ).getChild( 0 ); @@ -978,44 +868,6 @@ describe( 'ImageEditing', () => { ); } ); - it( 'should remove width, sizes and srcsset attribute when srcset attribute is removed from model', () => { - setModelData( model, - '' + - '' - ); - let image = doc.getRoot().getChild( 0 ); - - model.change( writer => { - writer.removeAttribute( 'srcset', image ); - } ); - - expect( getViewData( view, { withoutSelection: true } ) ).to.equal( - '
' + - '' + - '
' - ); - - setModelData( model, - '' + - '' - ); - image = doc.getRoot().getChild( 0 ).getChild( 0 ); - - model.change( writer => { - writer.removeAttribute( 'srcset', image ); - } ); - - expect( getViewData( view, { withoutSelection: true } ) ).to.equal( - '

' + - '' + - '

' - ); - } ); - it( 'should not convert srcset attribute if is already consumed', () => { editor.editing.downcastDispatcher.on( 'attribute:srcset:imageBlock', ( evt, data, conversionApi ) => { const modelImage = data.item; @@ -1027,7 +879,7 @@ describe( 'ImageEditing', () => { '' + + 'srcset="small.png 148w, big.png 1024w">' + '' ); @@ -1047,7 +899,7 @@ describe( 'ImageEditing', () => { '' + + 'srcset="small.png 148w, big.png 1024w">' + '' ); diff --git a/packages/ckeditor5-image/tests/image/imageinlineediting.js b/packages/ckeditor5-image/tests/image/imageinlineediting.js index 228b5993e85..9fdc7cb4c57 100644 --- a/packages/ckeditor5-image/tests/image/imageinlineediting.js +++ b/packages/ckeditor5-image/tests/image/imageinlineediting.js @@ -148,7 +148,7 @@ describe( 'ImageInlineEditing', () => { it( 'should convert srcset attribute to srcset and sizes attribute', () => { setModelData( model, '' + - '' + + '' + '' ); @@ -157,28 +157,6 @@ describe( 'ImageInlineEditing', () => { ); } ); - it( 'should convert srcset attribute to width, srcset and add sizes attribute', () => { - setModelData( model, - '' + - '' - ); - - expect( normalizeHtml( editor.getData() ) ).to.equal( - '

' + - '' + - '' + - '

' - ); - } ); - it( 'should not convert srcset attribute if is already consumed', () => { editor.data.downcastDispatcher.on( 'attribute:srcset:imageInline', ( evt, data, conversionApi ) => { const modelImage = data.item; @@ -190,19 +168,19 @@ describe( 'ImageInlineEditing', () => { '' + + 'srcset="small.png 148w, big.png 1024w">' + '' ); expect( editor.getData() ).to.equal( '

alt text

' ); } ); - it( 'should not convert srcset attribute if has wrong data', () => { + it( 'should not convert srcset attribute if has no data', () => { setModelData( model, '' + + 'srcset="">' + '' ); const imageInline = doc.getRoot().getChild( 0 ).getChild( 0 ); @@ -323,26 +301,12 @@ describe( 'ImageInlineEditing', () => { expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '' + - '' + + '' + '' + '' ); } ); - it( 'should convert image with srcset and width attributes', () => { - editor.setData( - '

alt text

' - ); - - expect( getModelData( model, { withoutSelection: true } ) ).to.equal( - '' + - '' - ); - } ); - it( 'should ignore sizes attribute', () => { editor.setData( '

alt text

' @@ -351,7 +315,7 @@ describe( 'ImageInlineEditing', () => { expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( '' + - '' + + '' + '' + '' ); @@ -579,7 +543,7 @@ describe( 'ImageInlineEditing', () => { '' + + 'srcset="small.png 148w, big.png 1024w">' + '' ); expect( getViewData( view, { withoutSelection: true } ) ).to.equal( @@ -589,12 +553,12 @@ describe( 'ImageInlineEditing', () => { ); } ); - it( 'should not convert srcset attribute if has wrong data', () => { + it( 'should not convert srcset attribute if has no data', () => { setModelData( model, '' + + 'srcset="">' + '' ); const image = doc.getRoot().getChild( 0 ).getChild( 0 ); @@ -609,31 +573,10 @@ describe( 'ImageInlineEditing', () => { ); } ); - it( 'should convert srcset attribute to srcset, width and sizes', () => { - setModelData( model, - '' + - '' ); - - expect( getViewData( view, { withoutSelection: true } ) ).to.equal( - '

' + - '' + - '' + - '

' - ); - } ); - it( 'should remove sizes and srcsset attribute when srcset attribute is removed from model', () => { setModelData( model, '' + - '' + + '' + '' ); const image = doc.getRoot().getChild( 0 ).getChild( 0 ); @@ -649,26 +592,6 @@ describe( 'ImageInlineEditing', () => { ); } ); - it( 'should remove width, sizes and srcsset attribute when srcset attribute is removed from model', () => { - setModelData( model, - '' + - '' - ); - const image = doc.getRoot().getChild( 0 ).getChild( 0 ); - - model.change( writer => { - writer.removeAttribute( 'srcset', image ); - } ); - - expect( getViewData( view, { withoutSelection: true } ) ).to.equal( - '

' + - '' + - '

' - ); - } ); - it( 'should not convert srcset attribute if is already consumed', () => { editor.editing.downcastDispatcher.on( 'attribute:srcset:imageInline', ( evt, data, conversionApi ) => { const modelImage = data.item; @@ -680,7 +603,7 @@ describe( 'ImageInlineEditing', () => { '' + + 'srcset="small.png 148w, big.png 1024w">' + '' ); @@ -911,7 +834,7 @@ describe( 'ImageInlineEditing', () => { viewDocument.fire( 'clipboardInput', { dataTransfer } ); expect( getModelData( model ) ).to.equal( - 'f[]oo' + 'f[]oo' ); } ); } ); diff --git a/packages/ckeditor5-image/tests/image/imagetypecommand.js b/packages/ckeditor5-image/tests/image/imagetypecommand.js index 77c6ec68bc6..5a2df222604 100644 --- a/packages/ckeditor5-image/tests/image/imagetypecommand.js +++ b/packages/ckeditor5-image/tests/image/imagetypecommand.js @@ -186,14 +186,14 @@ describe( 'ImageTypeCommand', () => { it( 'should convert inline image with srcset attribute to block image', () => { setModelData( model, ` - [] + [] ` ); blockCommand.execute(); expect( getModelData( model ) ).to.equal( - `[]` + `[]` ); } ); @@ -423,14 +423,14 @@ describe( 'ImageTypeCommand', () => { it( 'should convert block image with srcset attribute to inline image', () => { setModelData( model, - `[]` + `[]` ); inlineCommand.execute(); expect( getModelData( model ) ).to.equal( '' + - `[]` + + `[]` + '' ); } ); diff --git a/packages/ckeditor5-image/tests/imageresize/imageresizeediting.js b/packages/ckeditor5-image/tests/imageresize/imageresizeediting.js index 1f47d221bdd..4782da140d8 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,170 @@ 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( 'width' ) ).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( 'width' ) ).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( 'width', 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:width:imageBlock', ( evt, data, conversionApi ) => { - conversionApi.consumable.consume( data.item, 'attribute:width: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( 'removes `height` style in view if `resizedHeight` is removed from model', () => { + setData( editor.model, `` ); + + const imageModel = editor.model.document.getRoot().getChild( 0 ); + + editor.model.change( writer => { + writer.removeAttribute( 'resizedHeight', imageModel ); + } ); + + 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 +257,209 @@ 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( 'width' ) ).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( 'width' ) ).to.equal( '50%' ); - } ); + expect( editor.model.document.getRoot().getChild( 0 ).getChild( 1 ).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( + '

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% width 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( 'width', 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:width:imageInline', ( evt, data, conversionApi ) => { - conversionApi.consumable.consume( data.item, 'attribute:width: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( 'removes `height` style in view if `resizedHeight` is removed from model', () => { + setData( editor.model, + `` + ); + + const imageModel = editor.model.document.getRoot().getChild( 0 ).getChild( 0 ); + + editor.model.change( writer => { + writer.removeAttribute( 'resizedHeight', imageModel ); + } ); + + 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( + '

' + + `` + + '

' + ); + } ); + } ); } ); } ); @@ -202,15 +468,27 @@ describe( 'ImageResizeEditing', () => { editor = await createEditor(); } ); - it( 'allows the width attribute when ImageBlock plugin is enabled', async () => { + it( 'allows the resizedWidth attribute when ImageBlock plugin is enabled', async () => { const newEditor = await ClassicEditor.create( editorElement, { plugins: [ ImageBlockEditing, ImageResizeEditing ] } ); - expect( newEditor.model.schema.checkAttribute( [ '$root', 'imageBlock' ], 'width' ) ).to.be.true; + expect( newEditor.model.schema.checkAttribute( [ '$root', 'imageBlock' ], 'resizedWidth' ) ).to.be.true; + 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 width attribute when ImageInline plugin is enabled', async () => { + 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' ], 'width' ) ).to.be.true; + expect( newEditor.model.schema.checkAttribute( [ '$root', 'imageInline' ], 'resizedHeight' ) ).to.be.true; await newEditor.destroy(); } ); } ); diff --git a/packages/ckeditor5-image/tests/imageresize/imageresizehandles.js b/packages/ckeditor5-image/tests/imageresize/imageresizehandles.js index f5d08ce0546..8c596c15ba4 100644 --- a/packages/ckeditor5-image/tests/imageresize/imageresizehandles.js +++ b/packages/ckeditor5-image/tests/imageresize/imageresizehandles.js @@ -192,7 +192,7 @@ describe( 'ImageResizeHandles', () => { const modelItem = editor.model.document.getRoot().getChild( 1 ); - expect( modelItem.getAttribute( 'width' ), 'model width attribute' ).to.be.undefined; + expect( modelItem.getAttribute( 'resizedWidth' ), 'model width attribute' ).to.be.undefined; } ); } ); @@ -283,7 +283,7 @@ describe( 'ImageResizeHandles', () => { to: finalPointerPosition } ); - expect( model.getAttribute( 'width' ) ).to.equal( '60px' ); + expect( model.getAttribute( 'resizedWidth' ) ).to.equal( '60px' ); } ); } ); @@ -367,7 +367,7 @@ describe( 'ImageResizeHandles', () => { to: finalPointerPosition } ); - expect( model.getAttribute( 'width' ) ).to.equal( '76px' ); + expect( model.getAttribute( 'resizedWidth' ) ).to.equal( '76px' ); } ); it( 'retains width after removing srcset', async () => { @@ -384,7 +384,9 @@ describe( 'ImageResizeHandles', () => { writer.removeAttribute( 'srcset', model ); } ); - const expectedHtml = '
'; + const expectedHtml = '
' + + '' + + '
'; expect( editor.getData() ).to.equal( expectedHtml ); } ); @@ -650,7 +652,38 @@ describe( 'ImageResizeHandles', () => { const modelItem = editor.model.document.getRoot().getChild( 1 ).getChild( 0 ); - expect( modelItem.getAttribute( 'width' ), 'model width attribute' ).to.be.undefined; + expect( modelItem.getAttribute( 'resizedWidth' ), 'model width attribute' ).to.be.undefined; + } ); + } ); + + 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 ); } ); } ); @@ -805,9 +838,12 @@ describe( 'ImageResizeHandles', () => { await setModelAndWaitForImages( editor, '' + - '[]' + + '[' + + ']' + '' ); @@ -825,7 +861,7 @@ describe( 'ImageResizeHandles', () => { to: finalPointerPosition } ); - expect( model.getAttribute( 'width' ) ).to.equal( '76px' ); + expect( model.getAttribute( 'resizedWidth' ) ).to.equal( '76px' ); } ); it( 'retains width after removing srcset', async () => { @@ -842,7 +878,7 @@ describe( 'ImageResizeHandles', () => { writer.removeAttribute( 'srcset', model ); } ); - const expectedHtml = '

'; + const expectedHtml = '

'; expect( editor.getData() ).to.equal( expectedHtml ); } ); diff --git a/packages/ckeditor5-image/tests/imageresize/resizeimagecommand.js b/packages/ckeditor5-image/tests/imageresize/resizeimagecommand.js index e87f610869e..56c8c9e2623 100644 --- a/packages/ckeditor5-image/tests/imageresize/resizeimagecommand.js +++ b/packages/ckeditor5-image/tests/imageresize/resizeimagecommand.js @@ -4,9 +4,13 @@ */ import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; +import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import ResizeImageCommand from '../../src/imageresize/resizeimagecommand'; import ImageResizeEditing from '../../src/imageresize/imageresizeediting'; -import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + +/* eslint-disable no-undef */ describe( 'ResizeImageCommand', () => { let editor, model, command; @@ -24,7 +28,7 @@ describe( 'ResizeImageCommand', () => { isObject: true, isBlock: true, allowWhere: '$block', - allowAttributes: 'width' + allowAttributes: 'resizedWidth' } ); model.schema.register( 'caption', { @@ -40,25 +44,25 @@ describe( 'ResizeImageCommand', () => { describe( '#isEnabled', () => { it( 'is true when image is selected', () => { - setData( model, '

x

[]

x

' ); + setData( model, '

x

[]

x

' ); expect( command ).to.have.property( 'isEnabled', true ); } ); it( 'is true when the selection is inside a block image caption', () => { - setData( model, '[F]oo' ); + setData( model, '[F]oo' ); expect( command ).to.have.property( 'isEnabled', true ); } ); it( 'is false when image is not selected', () => { - setData( model, '

x[]

' ); + setData( model, '

x[]

' ); expect( command ).to.have.property( 'isEnabled', false ); } ); it( 'is false when more than one image is selected', () => { - setData( model, '

x

[]' ); + setData( model, '

x

[]' ); expect( command ).to.have.property( 'isEnabled', false ); } ); @@ -66,24 +70,24 @@ describe( 'ResizeImageCommand', () => { describe( '#value', () => { it( 'is null when image is not selected', () => { - setData( model, '

x[]

' ); + setData( model, '

x[]

' ); expect( command ).to.have.property( 'value', null ); } ); it( 'is set to an object with a width property (and height set to null) when a block image is selected', () => { - setData( model, '

x

[]

x

' ); + setData( model, '

x

[]

x

' ); expect( command ).to.have.deep.property( 'value', { width: '50px', height: null } ); } ); it( 'is set to an object with a width property (and height set to null) when the selection is in a block image caption', () => { - setData( model, '[]Foo' ); + setData( model, '[]Foo' ); expect( command ).to.have.deep.property( 'value', { width: '50px', height: null } ); } ); - it( 'is set to null if image does not have the width set', () => { + it( 'is set to null if image does not have the resizedWidth set', () => { setData( model, '

x

[]

x

' ); expect( command ).to.have.property( 'value', null ); @@ -91,29 +95,99 @@ describe( 'ResizeImageCommand', () => { } ); describe( 'execute()', () => { - it( 'sets image width', () => { - setData( model, '[]' ); + it( 'sets image resizedWidth', () => { + setData( model, '[]' ); command.execute( { width: '100%' } ); - expect( getData( model ) ).to.equal( '[]' ); + expect( getData( model ) ).to.equal( '[]' ); } ); - it( 'sets image width when selection is in a block image caption', () => { - setData( model, 'F[o]o' ); + it( 'sets image resizedWidth when selection is in a block image caption', () => { + setData( model, 'F[o]o' ); command.execute( { width: '100%' } ); - expect( getData( model ) ).to.equal( 'F[o]o' ); + expect( getData( model ) ).to.equal( 'F[o]o' ); } ); - it( 'removes image width when null passed', () => { - setData( model, '[]' ); + it( 'removes image resizedWidth when null passed', () => { + setData( model, '[]' ); command.execute( { width: null } ); expect( getData( model ) ).to.equal( '[]' ); - expect( model.document.getRoot().getChild( 0 ).hasAttribute( 'width' ) ).to.be.false; + 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; + } ); + + describe( 'image width and height attributes', () => { + let editor, model, command, editorElement; + + beforeEach( async () => { + editorElement = document.createElement( 'div' ); + + document.body.appendChild( editorElement ); + + editor = await ClassicTestEditor.create( editorElement, { + plugins: [ ArticlePluginSet, ImageResizeEditing ], + image: { + toolbar: [ 'imageStyle:block' ] + } + } ); + + model = editor.model; + command = new ResizeImageCommand( editor ); + } ); + + afterEach( async () => { + editorElement.remove(); + return editor.destroy(); + } ); + + it( 'should set width and height when resizedWidth is set (and be undoable in single step)', async () => { + const initialData = '[]'; + + setData( model, initialData ); + + command.execute( { width: '100%' } ); + await timeout( 100 ); + + expect( getData( model ) ).to.equal( + '[]' + ); + + editor.execute( 'undo' ); + + expect( getData( model ) ).to.equal( initialData ); + } ); + + it( 'should set width and height when resizedWidth is removed (and be undoable in single step)', async () => { + const initialData = '[]'; + + setData( model, initialData ); + + command.execute( { width: null } ); + await timeout( 100 ); + + expect( getData( model ) ).to.equal( '[]' ); + + editor.execute( 'undo' ); + + expect( getData( model ) ).to.equal( initialData ); + } ); } ); } ); + + function timeout( ms ) { + return new Promise( res => setTimeout( res, ms ) ); + } } ); diff --git a/packages/ckeditor5-image/tests/imagesizeattributes.js b/packages/ckeditor5-image/tests/imagesizeattributes.js new file mode 100644 index 00000000000..d5ecdf575e3 --- /dev/null +++ b/packages/ckeditor5-image/tests/imagesizeattributes.js @@ -0,0 +1,571 @@ +/** + * @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 + */ + +import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; + +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 PictureEditing from '../src/pictureediting'; +import ImageUtils from '../src/imageutils'; +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'; + +describe( 'ImageSizeAttributes', () => { + let editor, model, view; + + testUtils.createSinonSandbox(); + + beforeEach( async () => { + editor = await VirtualTestEditor.create( { + plugins: [ Paragraph, ImageBlockEditing, ImageInlineEditing, ImageSizeAttributes ] + } ); + + model = editor.model; + view = editor.editing.view; + } ); + + afterEach( async () => { + await editor.destroy(); + } ); + + it( 'should be named', () => { + expect( ImageSizeAttributes.pluginName ).to.equal( 'ImageSizeAttributes' ); + } ); + + it( 'should be loaded', () => { + expect( editor.plugins.get( ImageSizeAttributes ) ).to.be.instanceOf( ImageSizeAttributes ); + } ); + + it( 'should require ImageUtils', () => { + expect( ImageSizeAttributes.requires ).to.have.members( [ 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; + expect( model.schema.checkAttribute( [ '$root', 'imageBlock' ], 'height' ) ).to.be.true; + } ); + + it( 'should allow the "width" and "height" attributes on the imageInline element', () => { + expect( model.schema.checkAttribute( [ '$root', 'imageInline' ], 'width' ) ).to.be.true; + expect( model.schema.checkAttribute( [ '$root', 'imageInline' ], 'height' ) ).to.be.true; + } ); + } ); + + describe( 'conversion', () => { + describe( 'upcast', () => { + describe( 'inline images', () => { + it( 'should upcast width attribute correctly', () => { + editor.setData( + '

Lorem ipsum

' + ); + + expect( getData( model, { withoutSelection: true } ) ).to.equal( + '' + + 'Lorem ' + + '' + + ' ipsum' + + '' + ); + } ); + + it( 'should upcast height attribute correctly', () => { + editor.setData( + '

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; + } ); + } ); + } ); + + describe( 'downcast', () => { + describe( 'inline images', () => { + it( 'should downcast width attribute correctly', () => { + editor.setData( + '

Lorem ipsum

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

Lorem ' + + '' + + ' ipsum

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

Lorem ipsum

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

Lorem ipsum

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

Lorem ' + + '' + + ' ipsum

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

Lorem ipsum

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

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

' + ); + } ); + + it( 'should remove width attribute properly', () => { + setData( model, '' ); + + const imageModel = editor.model.document.getRoot().getChild( 0 ).getChild( 0 ); + + editor.model.change( writer => { + writer.removeAttribute( 'width', imageModel ); + } ); + + expect( editor.getData() ) + .to.equal( '

' ); + } ); + + it( 'should remove height attribute properly', () => { + setData( model, '' ); + + const imageModel = editor.model.document.getRoot().getChild( 0 ).getChild( 0 ); + + editor.model.change( writer => { + writer.removeAttribute( 'height', imageModel ); + } ); + + expect( editor.getData() ) + .to.equal( '

' ); + } ); + + describe( 'with image resize plugin', () => { + let editor, view; + + beforeEach( async () => { + editor = await VirtualTestEditor.create( { + plugins: [ Paragraph, ImageInlineEditing, PictureEditing, 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 add aspect-ratio if attributes are set but image is not resized (but to editing view only)', () => { + editor.setData( + '

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

' + + '' + + '

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

' + ); + } ); + + it( 'should not add aspect-ratio if it is a picture', () => { + editor.setData( + '

' + + '' + + '' + + '' + + '' + + '

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

' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '

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

' + + '' + + '' + + '' + + '' + + '

' + ); + } ); + + it( 'should not add aspect-ratio if it is a picture and image has width and height set', () => { + 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( + '
' + ); + } ); + + it( 'should not downcast consumed tokens for width attribute', () => { + editor.conversion.for( 'downcast' ).add( dispatcher => + dispatcher.on( 'attribute:width:imageBlock', ( evt, data, conversionApi ) => { + conversionApi.consumable.consume( data.item, 'attribute:width:imageBlock' ); + }, { priority: 'high' } ) + ); + setData( model, '' ); + + expect( editor.getData() ).to.equal( + '
' + ); + } ); + + it( 'should not downcast consumed tokens for height attribute', () => { + editor.conversion.for( 'downcast' ).add( dispatcher => + dispatcher.on( 'attribute:height:imageBlock', ( evt, data, conversionApi ) => { + conversionApi.consumable.consume( data.item, 'attribute:height:imageBlock' ); + }, { priority: 'high' } ) + ); + setData( model, '' ); + + expect( editor.getData() ).to.equal( + '
' + ); + } ); + + it( 'should remove width attribute properly', () => { + setData( model, '' ); + + const imageModel = editor.model.document.getRoot().getChild( 0 ); + + editor.model.change( writer => { + writer.removeAttribute( 'width', imageModel ); + } ); + + expect( editor.getData() ) + .to.equal( '
' ); + } ); + + it( 'should remove height attribute properly', () => { + setData( model, '' ); + + const imageModel = editor.model.document.getRoot().getChild( 0 ); + + editor.model.change( writer => { + writer.removeAttribute( 'height', imageModel ); + } ); + + expect( editor.getData() ) + .to.equal( '
' ); + } ); + + describe( 'with image resize plugin', () => { + let editor, view; + + beforeEach( async () => { + editor = await VirtualTestEditor.create( { + plugins: [ Paragraph, ImageBlockEditing, PictureEditing, 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 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( + '
' + + '' + + '
' + ); + } ); + + it( 'should not add aspect-ratio if it is a picture', () => { + editor.setData( + '
' + + '' + + '' + + '' + + '' + + '
' + ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equal( + '
' + + '' + + '' + + '' + + '' + + '
' + ); + + expect( editor.getData() ).to.equal( + '
' + + '' + + '' + + '' + + '' + + '
' + ); + } ); + + it( 'should not add aspect-ratio if it is a picture and image has width and height set', () => { + editor.setData( + '
' + + '' + + '' + + '' + + '' + + '
' + ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equal( + '
' + + '' + + '' + + '' + + '' + + '
' + ); + + expect( editor.getData() ).to.equal( + '
' + + '' + + '' + + '' + + '' + + '
' + ); + } ); + } ); + } ); + } ); + } ); +} ); diff --git a/packages/ckeditor5-image/tests/imagestyle/imagestylecommand.js b/packages/ckeditor5-image/tests/imagestyle/imagestylecommand.js index 51540875df9..4bc84cfbe16 100644 --- a/packages/ckeditor5-image/tests/imagestyle/imagestylecommand.js +++ b/packages/ckeditor5-image/tests/imagestyle/imagestylecommand.js @@ -367,6 +367,41 @@ describe( 'ImageStyleCommand', () => { expect( getData( model ) ).to.equal( '[]' ); expect( command.value ).to.equal( defaultInline.name ); } ); + + it( 'should set width and height when imageStyle is set (and be undoable in single step)', async () => { + const initialData = '[]'; + + setData( model, initialData ); + command.execute( { value: anyImage.name } ); + await timeout( 100 ); + + expect( getData( model ) ).to.equal( + `[]' + ); + + editor.execute( 'undo' ); + + expect( getData( model ) ) + .to.equal( initialData ); + } ); + + it( 'should set width and height when imageStyle is removed (and be undoable in single step)', async () => { + const initialData = + `[]`; + + setData( model, initialData ); + command.execute( { value: defaultInline.name } ); + await timeout( 100 ); + + expect( getData( model ) ) + .to.equal( '[]' ); + + editor.execute( 'undo' ); + + expect( getData( model ) ) + .to.equal( initialData ); + } ); } ); describe( 'when a block image is selected', () => { @@ -435,6 +470,10 @@ describe( 'ImageStyleCommand', () => { .to.equal( `Fo[o]` ); } ); } ); + + function timeout( ms ) { + return new Promise( res => setTimeout( res, ms ) ); + } } ); describe( 'shouldConvertImageType()', () => { diff --git a/packages/ckeditor5-image/tests/imageupload/imageuploadediting.js b/packages/ckeditor5-image/tests/imageupload/imageuploadediting.js index e05ceb20a0a..ebb1343d6b7 100644 --- a/packages/ckeditor5-image/tests/imageupload/imageuploadediting.js +++ b/packages/ckeditor5-image/tests/imageupload/imageuploadediting.js @@ -487,6 +487,84 @@ 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' + ); + } ); + + it( 'should not modify image width if width was set before server response', async () => { + setModelData( model, '[]foo' ); + + const clipboardHtml = ``; + const dataTransfer = mockDataTransfer( clipboardHtml ); + + const targetRange = model.createRange( model.createPositionAt( doc.getRoot(), 1 ), model.createPositionAt( doc.getRoot(), 1 ) ); + const targetViewRange = editor.editing.mapper.toViewRange( targetRange ); + + viewDocument.fire( 'clipboardInput', { dataTransfer, targetRanges: [ targetViewRange ] } ); + + 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', 800: 'image-800.png' } ) ); + } ); + + await timeout( 100 ); + + expect( getModelData( model ) ).to.equal( + '[]foo' + ); + } ); + + it( 'should not modify image width if height was set before server response', async () => { + setModelData( model, '[]foo' ); + + const clipboardHtml = ``; + const dataTransfer = mockDataTransfer( clipboardHtml ); + + const targetRange = model.createRange( model.createPositionAt( doc.getRoot(), 1 ), model.createPositionAt( doc.getRoot(), 1 ) ); + const targetViewRange = editor.editing.mapper.toViewRange( targetRange ); + + viewDocument.fire( 'clipboardInput', { dataTransfer, targetRanges: [ targetViewRange ] } ); + + 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', 800: 'image-800.png' } ) ); + } ); + + await timeout( 100 ); + + expect( getModelData( model ) ).to.equal( + '[]foo' + ); + } ); + it( 'should support adapter response with the normalized `urls` property', async () => { const file = createNativeFileMock(); setModelData( model, '{}foo bar' ); @@ -1483,3 +1561,7 @@ function base64ToBlob( base64Data ) { return new Blob( byteArrays, { type } ); } + +function timeout( ms ) { + return new Promise( res => setTimeout( res, ms ) ); +} 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()', () => { diff --git a/packages/ckeditor5-image/tests/manual/game_boy.jpg b/packages/ckeditor5-image/tests/manual/game_boy.jpg new file mode 100644 index 00000000000..6d089a61687 Binary files /dev/null and b/packages/ckeditor5-image/tests/manual/game_boy.jpg differ diff --git a/packages/ckeditor5-image/tests/manual/imagesizeattributes.html b/packages/ckeditor5-image/tests/manual/imagesizeattributes.html new file mode 100644 index 00000000000..15392c753d6 --- /dev/null +++ b/packages/ckeditor5-image/tests/manual/imagesizeattributes.html @@ -0,0 +1,60 @@ + + +
+

Just width and height attributes

+ +
+ Foo +
+ +

+ Lorem ipsum + Bar + dolor sit amet. +

+ +

width and height + sizes

+ +
+ Foo +
+ +

+ Lorem ipsum + Bar + dolor sit amet. +

+ +

width and height attributes + ImageResize (px)

+ +
+ Foo +
+ +

+ Lorem ipsum + Bar + dolor sit amet. +

+ +

width and height attributes + ImageResize (%)

+ +
+ Foo +
+ +

+ Lorem ipsum + Bar + dolor sit amet. +

+
diff --git a/packages/ckeditor5-image/tests/manual/imagesizeattributes.js b/packages/ckeditor5-image/tests/manual/imagesizeattributes.js new file mode 100644 index 00000000000..77d42ce2c89 --- /dev/null +++ b/packages/ckeditor5-image/tests/manual/imagesizeattributes.js @@ -0,0 +1,54 @@ +/** + * @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 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 { 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 +}; + +ClassicEditor + .create( document.querySelector( '#editor-width-height-attributes' ), commonConfig ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-image/tests/manual/imagesizeattributes.md b/packages/ckeditor5-image/tests/manual/imagesizeattributes.md new file mode 100644 index 00000000000..7b5a3700001 --- /dev/null +++ b/packages/ckeditor5-image/tests/manual/imagesizeattributes.md @@ -0,0 +1,4 @@ +## Image size attributes + +* Images (inline and block) should have `width` and `heigth` attributes set. +* Resized images should preserve aspect ratio. 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..9fd913591ad --- /dev/null +++ b/packages/ckeditor5-image/tests/manual/imagesizeattributesallcases.js @@ -0,0 +1,374 @@ +/** + * @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 ImageResize from '../../src/imageresize'; +import ImageSizeAttributes from '../../src/imagesizeattributes'; +import PictureEditing from '../../src/pictureediting'; +import PasteFromOffice from '@ckeditor/ckeditor5-paste-from-office/src/pastefromoffice'; +import { getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + +const commonConfig = getConfig(); +const configPx = getConfig( true ); + +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', + title: '[Inline] natural size | width + height attributes: Resized (width % style)', + config: commonConfig, + data: '

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

' + }, + { + 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: '
' + }, + { + id: 'inline201', + title: '[Picture: Inline] plain (no styles)', + config: commonConfig, + data: '

' + + '' + + '' + + '' + + '

' + }, + { + id: 'inline202', + title: '[Picture: Inline] resized (width style %)', + config: commonConfig, + data: '

' + + '' + + '' + + '' + + '

' + }, + { + id: 'block201', + title: '[Picture: Block] plain (no styles)', + config: commonConfig, + data: '
' + + '' + + '' + + '' + + '
' + }, + { + id: 'block202', + title: '[Picture: Block] resized (width style %)', + config: commonConfig, + data: '
' + + '' + + '' + + '' + + '
' + } +]; + +function getConfig( resizeUnitInPx = false ) { + const config = { + plugins: [ + ArticlePluginSet, + ImageResize, + Code, + ImageSizeAttributes, + Indent, + IndentBlock, + PictureEditing, + 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' ] + } + }; + + if ( resizeUnitInPx ) { + config.image.resizeUnit = 'px'; + } + + return config; +} + +async function initEditors() { + await Promise.all( editors.map( async editorObj => { + insertEditorStructure( editorObj ); + + const domElement = document.querySelector( `#${ editorObj.id }` ); + + const editor = await ClassicEditor.create( domElement, { ...editorObj.config, initialData: editorObj.data } ); + + window[ editorObj.id ] = editor; + + editor.model.document.on( 'change:data', () => { + updateLogsAndData( domElement, editor ); + } ); + + logInitialData( domElement, editorObj ); + updateLogsAndData( domElement, editor ); + } ) ); + + CKEditorInspector.attach( Object.fromEntries( editors.map( editorObj => [ editorObj.id, window[ editorObj.id ] ] ) ) ); +} + +initEditors().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..0fdd068a07f --- /dev/null +++ b/packages/ckeditor5-image/tests/manual/imagesizeattributesallcases.md @@ -0,0 +1,15 @@ +## 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 + * the exception to this are inline images that have not been resized (because in the editor they have `max-width: 100%`) +* 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..3202f35df72 --- /dev/null +++ b/packages/ckeditor5-image/tests/manual/imagesizeattributesghs.js @@ -0,0 +1,75 @@ +/** + * @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 +}; + +ClassicEditor + .create( document.querySelector( '#editor-ghs-with-width-height-attributes' ), commonConfig ) + .then( editor => { + window.editor = editor; + } ) + .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/tests/manual/parrot_2.jpg b/packages/ckeditor5-image/tests/manual/parrot_2.jpg new file mode 100644 index 00000000000..b2ec03739ed Binary files /dev/null and b/packages/ckeditor5-image/tests/manual/parrot_2.jpg differ diff --git a/packages/ckeditor5-image/tests/pictureediting.js b/packages/ckeditor5-image/tests/pictureediting.js index 65bf88ed1c7..8f07e3a460b 100644 --- a/packages/ckeditor5-image/tests/pictureediting.js +++ b/packages/ckeditor5-image/tests/pictureediting.js @@ -230,7 +230,7 @@ describe( 'PictureEditing', () => { expect( getModelData( model ) ).to.equal( '[]' + 'foo' + - '' + + '' + 'bar' + '' ); @@ -449,9 +449,9 @@ describe( 'PictureEditing', () => { expect( getModelData( model ) ).to.equal( '[' + 'Text of the caption' + ']' @@ -492,9 +492,9 @@ describe( 'PictureEditing', () => { expect( getModelData( model ) ).to.equal( '[' + 'Text of the caption' + ']' diff --git a/packages/ckeditor5-image/theme/image.css b/packages/ckeditor5-image/theme/image.css index 09b5f9b82d4..8051a285114 100644 --- a/packages/ckeditor5-image/theme/image.css +++ b/packages/ckeditor5-image/theme/image.css @@ -28,7 +28,11 @@ max-width: 100%; /* Make sure the image is never smaller than the parent container (See: https://github.com/ckeditor/ckeditor5/issues/9300). */ - min-width: 100% + min-width: 100%; + + /* Keep proportions of the block image if the height is set and the image is wider than the editor width. + See https://github.com/ckeditor/ckeditor5/issues/14542. */ + height: auto; } } @@ -105,6 +109,12 @@ } } + /* Keep proportions of the inline image if the height is set and the image is wider than the editor width. + See https://github.com/ckeditor/ckeditor5/issues/14542. */ + & .image-inline img { + height: auto; + } + /* The inline image nested in the table should have its original size if not resized. See https://github.com/ckeditor/ckeditor5/issues/9117. */ & td, diff --git a/packages/ckeditor5-image/theme/imageresize.css b/packages/ckeditor5-image/theme/imageresize.css index 1bb7afac7f5..ad86a3b6b16 100644 --- a/packages/ckeditor5-image/theme/imageresize.css +++ b/packages/ckeditor5-image/theme/imageresize.css @@ -3,6 +3,11 @@ * 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 img.image_resized { + height: auto; +} + .ck-content .image.image_resized { max-width: 100%; /* diff --git a/packages/ckeditor5-link/tests/linkimageediting.js b/packages/ckeditor5-link/tests/linkimageediting.js index cebd74f3d21..aae72ac98b2 100644 --- a/packages/ckeditor5-link/tests/linkimageediting.js +++ b/packages/ckeditor5-link/tests/linkimageediting.js @@ -100,7 +100,7 @@ describe( 'LinkImageEditing', () => { setModelData( model, '' + + 'srcset="small.png 148w, big.png 1024w">' + '' ); diff --git a/packages/ckeditor5-paste-from-office/tests/_data/image/adjacent-groups/model.edge.word2016.html b/packages/ckeditor5-paste-from-office/tests/_data/image/adjacent-groups/model.edge.word2016.html index 75ae5e72d24..f3da2c79d4d 100644 --- a/packages/ckeditor5-paste-from-office/tests/_data/image/adjacent-groups/model.edge.word2016.html +++ b/packages/ckeditor5-paste-from-office/tests/_data/image/adjacent-groups/model.edge.word2016.html @@ -37,4 +37,4 @@ - + diff --git a/packages/ckeditor5-paste-from-office/tests/_data/image/adjacent-groups/model.safari.word2016.html b/packages/ckeditor5-paste-from-office/tests/_data/image/adjacent-groups/model.safari.word2016.html index 98450ab1c8e..3deb4f0e931 100644 --- a/packages/ckeditor5-paste-from-office/tests/_data/image/adjacent-groups/model.safari.word2016.html +++ b/packages/ckeditor5-paste-from-office/tests/_data/image/adjacent-groups/model.safari.word2016.html @@ -26,4 +26,4 @@ - + diff --git a/packages/ckeditor5-paste-from-office/tests/_data/image/adjacent-groups/model.word2016.html b/packages/ckeditor5-paste-from-office/tests/_data/image/adjacent-groups/model.word2016.html index 98b1777db06..1792d387f7c 100644 --- a/packages/ckeditor5-paste-from-office/tests/_data/image/adjacent-groups/model.word2016.html +++ b/packages/ckeditor5-paste-from-office/tests/_data/image/adjacent-groups/model.word2016.html @@ -34,4 +34,4 @@ - + diff --git a/packages/ckeditor5-paste-from-office/tests/_data/image/alternative-text/model.safari.word2016.html b/packages/ckeditor5-paste-from-office/tests/_data/image/alternative-text/model.safari.word2016.html index 84a291b292b..1c0e4fd2892 100644 --- a/packages/ckeditor5-paste-from-office/tests/_data/image/alternative-text/model.safari.word2016.html +++ b/packages/ckeditor5-paste-from-office/tests/_data/image/alternative-text/model.safari.word2016.html @@ -1 +1 @@ - + diff --git a/packages/ckeditor5-paste-from-office/tests/_data/image/alternative-text/model.word2016.html b/packages/ckeditor5-paste-from-office/tests/_data/image/alternative-text/model.word2016.html index 51c5d3dd9fd..b221ed55acb 100644 --- a/packages/ckeditor5-paste-from-office/tests/_data/image/alternative-text/model.word2016.html +++ b/packages/ckeditor5-paste-from-office/tests/_data/image/alternative-text/model.word2016.html @@ -1 +1 @@ - + diff --git a/packages/ckeditor5-paste-from-office/tests/_data/image/linked/model.safari.word2016.html b/packages/ckeditor5-paste-from-office/tests/_data/image/linked/model.safari.word2016.html index bb8d531a9e9..a0fd88da596 100644 --- a/packages/ckeditor5-paste-from-office/tests/_data/image/linked/model.safari.word2016.html +++ b/packages/ckeditor5-paste-from-office/tests/_data/image/linked/model.safari.word2016.html @@ -1,2 +1,2 @@ - + diff --git a/packages/ckeditor5-paste-from-office/tests/_data/image/linked/model.word2016.html b/packages/ckeditor5-paste-from-office/tests/_data/image/linked/model.word2016.html index 27b855fd5b5..054ee09c9a2 100644 --- a/packages/ckeditor5-paste-from-office/tests/_data/image/linked/model.word2016.html +++ b/packages/ckeditor5-paste-from-office/tests/_data/image/linked/model.word2016.html @@ -1,2 +1,2 @@ - + diff --git a/packages/ckeditor5-paste-from-office/tests/_data/image/offline/model.safari.word2016.html b/packages/ckeditor5-paste-from-office/tests/_data/image/offline/model.safari.word2016.html index 0d54939a0df..e3302b2696b 100644 --- a/packages/ckeditor5-paste-from-office/tests/_data/image/offline/model.safari.word2016.html +++ b/packages/ckeditor5-paste-from-office/tests/_data/image/offline/model.safari.word2016.html @@ -1,3 +1,3 @@ This word contains some pictures: - - + + diff --git a/packages/ckeditor5-paste-from-office/tests/_data/image/offline/model.word2016.html b/packages/ckeditor5-paste-from-office/tests/_data/image/offline/model.word2016.html index c1e0bd6df42..1c69545cd47 100644 --- a/packages/ckeditor5-paste-from-office/tests/_data/image/offline/model.word2016.html +++ b/packages/ckeditor5-paste-from-office/tests/_data/image/offline/model.word2016.html @@ -1,3 +1,3 @@ This word contains some pictures: - - + + diff --git a/packages/ckeditor5-paste-from-office/tests/_data/image/online-offline/model.safari.word2016.html b/packages/ckeditor5-paste-from-office/tests/_data/image/online-offline/model.safari.word2016.html index 8f9b8d6c9e0..d22e774e79c 100644 --- a/packages/ckeditor5-paste-from-office/tests/_data/image/online-offline/model.safari.word2016.html +++ b/packages/ckeditor5-paste-from-office/tests/_data/image/online-offline/model.safari.word2016.html @@ -1,4 +1,4 @@ -Kitty from internet: +Kitty from internet: -My drawing: hehehehe :D +My drawing: hehehehe :D diff --git a/packages/ckeditor5-paste-from-office/tests/_data/image/online-offline/model.word2016.html b/packages/ckeditor5-paste-from-office/tests/_data/image/online-offline/model.word2016.html index 9d1d30431b7..6486f412580 100644 --- a/packages/ckeditor5-paste-from-office/tests/_data/image/online-offline/model.word2016.html +++ b/packages/ckeditor5-paste-from-office/tests/_data/image/online-offline/model.word2016.html @@ -1,4 +1,4 @@ -Kitty from internet: +Kitty from internet: -My drawing: hehehehe :D +My drawing: hehehehe :D diff --git a/packages/ckeditor5-paste-from-office/tests/_data/image/reflection/model.safari.word2016.html b/packages/ckeditor5-paste-from-office/tests/_data/image/reflection/model.safari.word2016.html index aad2cb6fb6c..7b0b1aaad8b 100644 --- a/packages/ckeditor5-paste-from-office/tests/_data/image/reflection/model.safari.word2016.html +++ b/packages/ckeditor5-paste-from-office/tests/_data/image/reflection/model.safari.word2016.html @@ -1,2 +1,2 @@ - + diff --git a/packages/ckeditor5-paste-from-office/tests/_data/image/reflection/model.word2016.html b/packages/ckeditor5-paste-from-office/tests/_data/image/reflection/model.word2016.html index 81a00edc22a..54d84e85032 100644 --- a/packages/ckeditor5-paste-from-office/tests/_data/image/reflection/model.word2016.html +++ b/packages/ckeditor5-paste-from-office/tests/_data/image/reflection/model.word2016.html @@ -1,2 +1,2 @@ - + diff --git a/packages/ckeditor5-paste-from-office/tests/_data/image/rotated/model.safari.word2016.html b/packages/ckeditor5-paste-from-office/tests/_data/image/rotated/model.safari.word2016.html index ed952781172..9b309d21240 100644 --- a/packages/ckeditor5-paste-from-office/tests/_data/image/rotated/model.safari.word2016.html +++ b/packages/ckeditor5-paste-from-office/tests/_data/image/rotated/model.safari.word2016.html @@ -1 +1 @@ - + diff --git a/packages/ckeditor5-paste-from-office/tests/_data/image/rotated/model.word2016.html b/packages/ckeditor5-paste-from-office/tests/_data/image/rotated/model.word2016.html index 5c138b0658f..a31989ebcb2 100644 --- a/packages/ckeditor5-paste-from-office/tests/_data/image/rotated/model.word2016.html +++ b/packages/ckeditor5-paste-from-office/tests/_data/image/rotated/model.word2016.html @@ -1 +1 @@ - + diff --git a/packages/ckeditor5-paste-from-office/tests/_data/image/shapes-online-offline/model.safari.word2016.html b/packages/ckeditor5-paste-from-office/tests/_data/image/shapes-online-offline/model.safari.word2016.html index 62079fa7ed0..5d820af343c 100644 --- a/packages/ckeditor5-paste-from-office/tests/_data/image/shapes-online-offline/model.safari.word2016.html +++ b/packages/ckeditor5-paste-from-office/tests/_data/image/shapes-online-offline/model.safari.word2016.html @@ -1,7 +1,7 @@ -Kitty from internet: +Kitty from internet: -My drawing: hehehehe :D +My drawing: hehehehe :D -Additional shape made within Word is added here: +Additional shape made within Word is added here: diff --git a/packages/ckeditor5-paste-from-office/tests/_data/image/shapes-online-offline/model.word2016.html b/packages/ckeditor5-paste-from-office/tests/_data/image/shapes-online-offline/model.word2016.html index a2ca325c7b3..35232fafeab 100644 --- a/packages/ckeditor5-paste-from-office/tests/_data/image/shapes-online-offline/model.word2016.html +++ b/packages/ckeditor5-paste-from-office/tests/_data/image/shapes-online-offline/model.word2016.html @@ -1,6 +1,6 @@ -Kitty from internet: +Kitty from internet: -My drawing: hehehehe :D +My drawing: hehehehe :D Additional shape made within Word is added here: diff --git a/packages/ckeditor5-paste-from-office/tests/_data/image/wrapped/model.safari.word2016.html b/packages/ckeditor5-paste-from-office/tests/_data/image/wrapped/model.safari.word2016.html index cb74abc3605..f8426d8b90a 100644 --- a/packages/ckeditor5-paste-from-office/tests/_data/image/wrapped/model.safari.word2016.html +++ b/packages/ckeditor5-paste-from-office/tests/_data/image/wrapped/model.safari.word2016.html @@ -1 +1 @@ - + diff --git a/packages/ckeditor5-paste-from-office/tests/_data/image/wrapped/model.word2016.html b/packages/ckeditor5-paste-from-office/tests/_data/image/wrapped/model.word2016.html index 49847faff42..dade0ff3ef4 100644 --- a/packages/ckeditor5-paste-from-office/tests/_data/image/wrapped/model.word2016.html +++ b/packages/ckeditor5-paste-from-office/tests/_data/image/wrapped/model.word2016.html @@ -1 +1 @@ - +