UNPKG

@ckeditor/ckeditor5-image

Version:

Image feature for CKEditor 5.

6,275 lines • 217 kB
/**
 * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
 */
import { Command, Plugin } from "@ckeditor/ckeditor5-core";
import { Clipboard, ClipboardPipeline } from "@ckeditor/ckeditor5-clipboard";
import { ModelElement, ModelLivePosition, ModelLiveRange, Observer, ViewUpcastWriter, _isParagraphableModelNode, enableViewPlaceholder } from "@ckeditor/ckeditor5-engine";
import { Undo } from "@ckeditor/ckeditor5-undo";
import { Delete } from "@ckeditor/ckeditor5-typing";
import { CKEditorError, Collection, DomEmitterMixin, FocusTracker, KeystrokeHandler, Rect, _tryCastDimensionsToUnit, _tryParseDimensionWithUnit, env, first, global, logWarning, toArray } from "@ckeditor/ckeditor5-utils";
import { Widget, WidgetResize, WidgetToolbarRepository, calculateResizeHostAncestorWidth, findOptimalInsertionRange, isWidget, toWidget, toWidgetEditable } from "@ckeditor/ckeditor5-widget";
import { BalloonPanelView, ButtonView, CollapsibleView, ContextualBalloon, CssTransitionDisablerMixin, Dialog, DropdownButtonView, FileDialogButtonView, FocusCycler, FormHeaderView, FormRowView, LabeledFieldView, MenuBarMenuListItemButtonView, MenuBarMenuListItemFileDialogButtonView, MenuBarMenuListItemView, MenuBarMenuListView, MenuBarMenuView, Notification, SplitButtonView, UIModel, View, ViewCollection, addListToDropdown, addToolbarToDropdown, clickOutsideHandler, createDropdown, createLabeledInputNumber, createLabeledInputText, submitHandler } from "@ckeditor/ckeditor5-ui";
import { IconCaption, IconImage, IconImageUpload, IconImageUrl, IconObjectCenter, IconObjectFullWidth, IconObjectInline, IconObjectInlineLeft, IconObjectInlineRight, IconObjectLeft, IconObjectRight, IconObjectSizeCustom, IconObjectSizeFull, IconObjectSizeLarge, IconObjectSizeMedium, IconObjectSizeSmall, IconPreviousArrow, IconTextAlternative } from "@ckeditor/ckeditor5-icons";
import { FileRepository } from "@ckeditor/ckeditor5-upload";
import { identity, isEqual, isObject } from "es-toolkit/compat";

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/image/utils
*/
/**
* Creates a view element representing the inline image.
*
* ```html
* <span class="image-inline"><img></img></span>
* ```
*
* Note that `alt` and `src` attributes are converted separately, so they are not included.
*
* @internal
*/
function createInlineImageViewElement(writer) {
	return writer.createContainerElement("span", { class: "image-inline" }, writer.createEmptyElement("img"));
}
/**
* Creates a view element representing the block image.
*
* ```html
* <figure class="image"><img></img></figure>
* ```
*
* Note that `alt` and `src` attributes are converted separately, so they are not included.
*
* @internal
*/
function createBlockImageViewElement(writer) {
	return writer.createContainerElement("figure", { class: "image" }, [writer.createEmptyElement("img"), writer.createSlot("children")]);
}
/**
* A function returning a `MatcherPattern` for a particular type of View images.
*
* @deprecated
* @internal
* @param matchImageType The type of created image.
*/
function getImgViewElementMatcher(editor, matchImageType) {
	const imageUtils = editor.plugins.get("ImageUtils");
	const areBothImagePluginsLoaded = editor.plugins.has("ImageInlineEditing") && editor.plugins.has("ImageBlockEditing");
	return (element) => {
		if (!imageUtils.isInlineImageView(element)) return null;
		if (!areBothImagePluginsLoaded) return getPositiveMatchPattern(element);
		if (getViewImageType(element, imageUtils) !== matchImageType) return null;
		return getPositiveMatchPattern(element);
	};
	function getPositiveMatchPattern(element) {
		const pattern = { name: true };
		if (element.hasAttribute("src")) pattern.attributes = ["src"];
		return pattern;
	}
}
/**
* Resolves the model image type (`'imageBlock'` or `'imageInline'`) that a given `<img>` view element represents,
* based purely on its view structure.
*
* An `<img>` is treated as a block image when it has a `display: block` style or is wrapped in a block image figure
* (`<figure class="image">`, also `<figure class="image"><a>...</a></figure>` added by the `LinkImage` plugin).
* Otherwise it is treated as an inline image.
*
* Note that this only reflects the view representation - it does not check whether the resolved type is allowed by the
* schema at the insertion position.
*
* @internal
* @param element The `<img>` view element to resolve the type for.
* @param imageUtils The `ImageUtils` plugin instance.
*/
function getViewImageType(element, imageUtils) {
	return element.getStyle("display") == "block" || element.findAncestor(imageUtils.isBlockImageView) ? "imageBlock" : "imageInline";
}
/**
* Checks whether the given image type can be placed at the specified position - either directly (or after hoisting
* to an allowed ancestor) or, for inline images, wrapped in an auto-created paragraph.
*
* This is the predicate that decides whether a block image may land at a position or must degrade to an inline image
* (for example, inside an `$inlineRoot` that disallows block content) and, symmetrically, whether an inline image may
* become a block image. `isParagraphable()` is always `false` for `imageBlock` (a block object never fits in a
* paragraph), so the auto-paragraph relaxation only applies to the inline target.
*
* @internal
* @param schema The model schema to check against.
* @param position The position at which the image type should be placed.
* @param imageType The image type to check.
*/
function isImageTypePlaceable(schema, position, imageType) {
	return !!schema.findAllowedParent(position, imageType) || _isParagraphableModelNode(position, imageType, schema);
}
/**
* Considering the current model selection, it returns the name of the model image element
* (`'imageBlock'` or `'imageInline'`) that will make most sense from the UX perspective if a new
* image was inserted (also: uploaded, dropped, pasted) at that selection.
*
* The assumption is that inserting images into empty blocks or on other block widgets should
* produce block images. Inline images should be inserted in other cases, e.g. in paragraphs
* that already contain some text.
*
* @internal
*/
function determineImageTypeForInsertionAtSelection(schema, selection) {
	const firstBlock = first(selection.getSelectedBlocks());
	if (!firstBlock || schema.isObject(firstBlock)) return "imageBlock";
	if (firstBlock.isEmpty && firstBlock.name != "listItem") return "imageBlock";
	return "imageInline";
}
/**
* Returns parsed value of the size, but only if it contains unit: px.
*
* @internal
*/
function getSizeValueIfInPx(size) {
	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.
*
* @internal
*/
function widthAndHeightStylesAreBothSet(viewElement) {
	const widthStyle = getSizeValueIfInPx(viewElement.getStyle("width"));
	const heightStyle = getSizeValueIfInPx(viewElement.getStyle("height"));
	return !!(widthStyle && heightStyle);
}

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
const IMAGE_WIDGETS_CLASSES_MATCH_REGEXP = /^(image|image-inline)$/;
/**
* A set of helpers related to images.
*/
var ImageUtils = class extends Plugin {
	/**
	* DOM Emitter.
	*/
	_domEmitter = new (DomEmitterMixin())();
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageUtils";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* Checks if the provided model element is an `image` or `imageInline`.
	*/
	isImage(modelElement) {
		return this.isInlineImage(modelElement) || this.isBlockImage(modelElement);
	}
	/**
	* Checks if the provided view element represents an inline image.
	*
	* Also, see {@link module:image/imageutils~ImageUtils#isImageWidget}.
	*/
	isInlineImageView(element) {
		return !!element && element.is("element", "img");
	}
	/**
	* Checks if the provided view element represents a block image.
	*
	* Also, see {@link module:image/imageutils~ImageUtils#isImageWidget}.
	*/
	isBlockImageView(element) {
		return !!element && element.is("element", "figure") && element.hasClass("image");
	}
	/**
	* Handles inserting single file. This method unifies image insertion using {@link module:widget/utils~findOptimalInsertionRange}
	* method.
	*
	* ```ts
	* const imageUtils = editor.plugins.get( 'ImageUtils' );
	*
	* imageUtils.insertImage( { src: 'path/to/image.jpg' } );
	* ```
	*
	* @param attributes Attributes of the inserted image.
	* This method filters out the attributes which are disallowed by the {@link module:engine/model/schema~ModelSchema}.
	* @param selectable Place to insert the image. If not specified,
	* the {@link module:widget/utils~findOptimalInsertionRange} logic will be applied for the block images
	* and `model.document.selection` for the inline images.
	*
	* **Note**: If `selectable` is passed, this helper will not be able to set selection attributes (such as `linkHref`)
	* and apply them to the new image. In this case, make sure all selection attributes are passed in `attributes`.
	*
	* @param imageType Image type of inserted image. If not specified,
	* it will be determined automatically depending of editor config or place of the insertion.
	* @param options.setImageSizes Specifies whether the image `width` and `height` attributes should be set automatically.
	* The default is `true`.
	* @return The inserted model image element.
	*/
	insertImage(attributes = {}, selectable = null, imageType = null, options = {}) {
		const editor = this.editor;
		const model = editor.model;
		const selection = model.document.selection;
		const determinedImageType = determineImageTypeForInsertion(editor, selectable || selection, imageType);
		attributes = {
			...Object.fromEntries(selection.getAttributes()),
			...attributes
		};
		for (const attributeName in attributes) if (!model.schema.checkAttribute(determinedImageType, attributeName)) delete attributes[attributeName];
		return model.change((writer) => {
			const { setImageSizes = true } = options;
			const imageElement = writer.createElement(determinedImageType, attributes);
			model.insertObject(imageElement, selectable, null, {
				setSelection: "on",
				findOptimalPosition: !selectable && determinedImageType != "imageInline" ? "auto" : void 0
			});
			if (imageElement.parent) {
				if (setImageSizes) this.setImageNaturalSizeAttributes(imageElement);
				return imageElement;
			}
			return null;
		});
	}
	/**
	* 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.
	*/
	setImageNaturalSizeAttributes(imageElement) {
		const src = imageElement.getAttribute("src");
		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")) 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.
	*/
	getClosestSelectedImageWidget(selection) {
		const selectionPosition = selection.getFirstPosition();
		if (!selectionPosition) return null;
		const viewElement = selection.getSelectedElement();
		if (viewElement && this.isImageWidget(viewElement)) return viewElement;
		let parent = selectionPosition.parent;
		while (parent) {
			if (parent.is("element") && this.isImageWidget(parent)) return parent;
			parent = parent.parent;
		}
		return null;
	}
	/**
	* Returns a image model element if one is selected or is among the selection's ancestors.
	*/
	getClosestSelectedImageElement(selection) {
		const selectedElement = selection.getSelectedElement();
		return this.isImage(selectedElement) ? selectedElement : selection.getFirstPosition().findAncestor("imageBlock");
	}
	/**
	* Returns an image widget editing view based on the passed image view.
	*/
	getImageWidgetFromImageView(imageView) {
		return imageView.findAncestor({ classes: IMAGE_WIDGETS_CLASSES_MATCH_REGEXP });
	}
	/**
	* Checks if image can be inserted at current model selection.
	*
	* @internal
	*/
	isImageAllowed() {
		const selection = this.editor.model.document.selection;
		return isImageAllowedInParent(this.editor, selection) && isNotInsideImage(selection);
	}
	/**
	* Converts a given {@link module:engine/view/element~ViewElement} to an image widget:
	* * Adds a {@link module:engine/view/element~ViewElement#_setCustomProperty custom property} allowing to recognize the image widget
	* element.
	* * Calls the {@link module:widget/utils~toWidget} function with the proper element's label creator.
	*
	* @param writer An instance of the view writer.
	* @param label The element's label. It will be concatenated with the image `alt` attribute if one is present.
	*/
	toImageWidget(viewElement, writer, label) {
		writer.setCustomProperty("image", true, viewElement);
		const labelCreator = () => {
			const altText = this.findViewImgElement(viewElement).getAttribute("alt");
			return altText ? `${altText} ${label}` : label;
		};
		return toWidget(viewElement, writer, { label: labelCreator });
	}
	/**
	* Checks if a given view element is an image widget.
	*/
	isImageWidget(viewElement) {
		return !!viewElement.getCustomProperty("image") && isWidget(viewElement);
	}
	/**
	* Checks if the provided model element is an `image`.
	*/
	isBlockImage(modelElement) {
		return !!modelElement && modelElement.is("element", "imageBlock");
	}
	/**
	* Checks if the provided model element is an `imageInline`.
	*/
	isInlineImage(modelElement) {
		return !!modelElement && modelElement.is("element", "imageInline");
	}
	/**
	* Get the view `<img>` from another view element, e.g. a widget (`<figure class="image">`), a link (`<a>`).
	*
	* The `<img>` can be located deep in other elements, so this helper performs a deep tree search.
	*/
	findViewImgElement(figureView) {
		if (this.isInlineImageView(figureView)) return figureView;
		const editingView = this.editor.editing.view;
		for (const { item } of editingView.createRangeIn(figureView)) if (this.isInlineImageView(item)) return item;
	}
	/**
	* @inheritDoc
	*/
	destroy() {
		this._domEmitter.stopListening();
		return super.destroy();
	}
};
/**
* Checks if image is allowed by schema in optimal insertion parent.
*/
function isImageAllowedInParent(editor, selection) {
	if (determineImageTypeForInsertion(editor, selection, null) == "imageBlock") return isBlockImageAllowedAtInsertion(editor.model, selection);
	return editor.model.schema.checkChild(selection.focus, "imageInline");
}
/**
* Checks if selection is not placed inside an image (e.g. its caption).
*/
function isNotInsideImage(selection) {
	return [...selection.focus.getAncestors()].every((ancestor) => !ancestor.is("element", "imageBlock"));
}
/**
* Returns a node that will be used to insert image with `model.insertContent`.
*/
function getInsertImageParent(selection, model) {
	const parent = findOptimalInsertionRange(selection, model).start.parent;
	if (parent.isEmpty && !parent.is("rootElement")) return parent.parent;
	return parent;
}
/**
* Checks whether a block image is allowed by the schema at the optimal insertion point for the given selectable
* (e.g. it is not allowed inside `$inlineRoot` or any other inline-only container).
*/
function isBlockImageAllowedAtInsertion(model, selectable) {
	const insertionParent = getInsertImageParent(selectable.is("position") ? model.createSelection(selectable) : selectable, model);
	return model.schema.checkChild(insertionParent, "imageBlock");
}
/**
* Determine image element type name depending on editor config or place of insertion.
*
* @param imageType Image element type name. Used to force return of provided element name,
* but only if there is proper plugin enabled.
*/
function determineImageTypeForInsertion(editor, selectable, imageType) {
	const schema = editor.model.schema;
	const configImageInsertType = editor.config.get("image.insert.type");
	if (!editor.plugins.has("ImageBlockEditing")) return "imageInline";
	if (!editor.plugins.has("ImageInlineEditing")) return "imageBlock";
	if (imageType) return imageType;
	if (!isBlockImageAllowedAtInsertion(editor.model, selectable)) return "imageInline";
	if (configImageInsertType === "inline") return "imageInline";
	if (configImageInsertType !== "auto") return "imageBlock";
	if (selectable.is("selection")) return determineImageTypeForInsertionAtSelection(schema, selectable);
	return schema.checkChild(selectable, "imageInline") ? "imageInline" : "imageBlock";
}

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/autoimage
*/
const IMAGE_URL_REGEXP = new RegExp(String(/^(http(s)?:\/\/)?[\w-]+\.[\w.~:/[\]@!$&'()*+,;=%-]+/.source + /\.(jpg|jpeg|png|gif|ico|webp|JPG|JPEG|PNG|GIF|ICO|WEBP)/.source + /(\?[\w.~:/[\]@!$&'()*+,;=%-]*)?/.source + /(#[\w.~:/[\]@!$&'()*+,;=%-]*)?$/.source));
/**
* The auto-image plugin. It recognizes image links in the pasted content and embeds
* them shortly after they are injected into the document.
*/
var AutoImage = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [
			Clipboard,
			ImageUtils,
			Undo,
			Delete
		];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "AutoImage";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* The paste–to–embed `setTimeout` ID. Stored as a property to allow
	* cleaning of the timeout.
	*/
	_timeoutId;
	/**
	* The position where the `<imageBlock>` element will be inserted after the timeout,
	* determined each time a new content is pasted into the document.
	*/
	_positionToInsert;
	/**
	* @inheritDoc
	*/
	constructor(editor) {
		super(editor);
		this._timeoutId = null;
		this._positionToInsert = null;
	}
	/**
	* @inheritDoc
	*/
	init() {
		const editor = this.editor;
		const modelDocument = editor.model.document;
		const clipboardPipeline = editor.plugins.get("ClipboardPipeline");
		this.listenTo(clipboardPipeline, "inputTransformation", () => {
			const firstRange = modelDocument.selection.getFirstRange();
			const leftLivePosition = ModelLivePosition.fromPosition(firstRange.start);
			leftLivePosition.stickiness = "toPrevious";
			const rightLivePosition = ModelLivePosition.fromPosition(firstRange.end);
			rightLivePosition.stickiness = "toNext";
			modelDocument.once("change:data", () => {
				this._embedImageBetweenPositions(leftLivePosition, rightLivePosition);
				leftLivePosition.detach();
				rightLivePosition.detach();
			}, { priority: "high" });
		});
		editor.commands.get("undo").on("execute", () => {
			if (this._timeoutId) {
				global.window.clearTimeout(this._timeoutId);
				this._positionToInsert.detach();
				this._timeoutId = null;
				this._positionToInsert = null;
			}
		}, { priority: "high" });
	}
	/**
	* Analyzes the part of the document between provided positions in search for a URL representing an image.
	* When the URL is found, it is automatically converted into an image.
	*
	* @param leftPosition Left position of the selection.
	* @param rightPosition Right position of the selection.
	*/
	_embedImageBetweenPositions(leftPosition, rightPosition) {
		const editor = this.editor;
		const urlRange = new ModelLiveRange(leftPosition, rightPosition);
		const walker = urlRange.getWalker({ ignoreElementEnd: true });
		const selectionAttributes = Object.fromEntries(editor.model.document.selection.getAttributes());
		const imageUtils = this.editor.plugins.get("ImageUtils");
		let src = "";
		for (const node of walker) if (node.item.is("$textProxy")) src += node.item.data;
		src = src.trim();
		if (!src.match(IMAGE_URL_REGEXP)) {
			urlRange.detach();
			return;
		}
		this._positionToInsert = ModelLivePosition.fromPosition(leftPosition);
		this._timeoutId = setTimeout(() => {
			if (!editor.commands.get("insertImage").isEnabled) {
				urlRange.detach();
				return;
			}
			editor.model.change((writer) => {
				this._timeoutId = null;
				writer.remove(urlRange);
				urlRange.detach();
				let insertionPosition;
				if (this._positionToInsert.root.rootName !== "$graveyard") insertionPosition = this._positionToInsert.toPosition();
				imageUtils.insertImage({
					...selectionAttributes,
					src
				}, insertionPosition);
				this._positionToInsert.detach();
				this._positionToInsert = null;
			});
			editor.plugins.get("Delete").requestUndoOnBackspace();
		}, 100);
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imagetextalternative/imagetextalternativecommand
*/
/**
* The image text alternative command. It is used to change the `alt` attribute of `<imageBlock>` and `<imageInline>` model elements.
*/
var ImageTextAlternativeCommand = class extends Command {
	/**
	* @inheritDoc
	*/
	refresh() {
		const element = this.editor.plugins.get("ImageUtils").getClosestSelectedImageElement(this.editor.model.document.selection);
		this.isEnabled = !!element;
		if (this.isEnabled && element.hasAttribute("alt")) this.value = element.getAttribute("alt");
		else this.value = false;
	}
	/**
	* Executes the command.
	*
	* @fires execute
	* @param options
	* @param options.newValue The new value of the `alt` attribute to set.
	*/
	execute(options) {
		const editor = this.editor;
		const imageUtils = editor.plugins.get("ImageUtils");
		const model = editor.model;
		const imageElement = imageUtils.getClosestSelectedImageElement(model.document.selection);
		model.change((writer) => {
			writer.setAttribute("alt", options.newValue, imageElement);
		});
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imagetextalternative/imagetextalternativeediting
*/
/**
* The image text alternative editing plugin.
*
* Registers the `'imageTextAlternative'` command.
*/
var ImageTextAlternativeEditing = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [ImageUtils];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageTextAlternativeEditing";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	init() {
		this.editor.commands.add("imageTextAlternative", new ImageTextAlternativeCommand(this.editor));
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imagetextalternative/ui/textalternativeformview
*/
/**
* The TextAlternativeFormView class.
*
* @internal
*/
var TextAlternativeFormView = class extends View {
	/**
	* Tracks information about the DOM focus in the form.
	*/
	focusTracker;
	/**
	* An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
	*/
	keystrokes;
	/**
	* An input with a label.
	*/
	labeledInput;
	/**
	* The Back button view displayed in the header.
	*/
	backButtonView;
	/**
	* A button used to submit the form.
	*/
	saveButtonView;
	/**
	* A collection of child views.
	*/
	children;
	/**
	* A collection of views which can be focused in the form.
	*/
	_focusables;
	/**
	* Helps cycling over {@link #_focusables} in the form.
	*/
	_focusCycler;
	/**
	* @inheritDoc
	*/
	constructor(locale) {
		super(locale);
		this.focusTracker = new FocusTracker();
		this.keystrokes = new KeystrokeHandler();
		this.backButtonView = this._createBackButton();
		this.saveButtonView = this._createSaveButton();
		this.labeledInput = this._createLabeledInputView();
		this.children = this.createCollection([this._createHeaderView()]);
		this.children.add(new FormRowView(locale, {
			children: [this.labeledInput, this.saveButtonView],
			class: ["ck-form__row_with-submit", "ck-form__row_large-top-padding"]
		}));
		this._focusables = new ViewCollection();
		this.keystrokes.set("Esc", (data, cancel) => {
			this.fire("cancel");
			cancel();
		});
		this._focusCycler = new FocusCycler({
			focusables: this._focusables,
			focusTracker: this.focusTracker,
			keystrokeHandler: this.keystrokes,
			actions: {
				focusPrevious: "shift + tab",
				focusNext: "tab"
			}
		});
		this.setTemplate({
			tag: "form",
			attributes: {
				class: [
					"ck",
					"ck-form",
					"ck-text-alternative-form",
					"ck-responsive-form"
				],
				tabindex: "-1"
			},
			children: this.children
		});
	}
	/**
	* @inheritDoc
	*/
	render() {
		super.render();
		submitHandler({ view: this });
		[
			this.backButtonView,
			this.labeledInput,
			this.saveButtonView
		].forEach((v) => {
			this._focusables.add(v);
			this.focusTracker.add(v.element);
		});
		this.keystrokes.listenTo(this.element);
	}
	/**
	* @inheritDoc
	*/
	destroy() {
		super.destroy();
		this.focusTracker.destroy();
		this.keystrokes.destroy();
	}
	/**
	* Creates a back button view that cancels the form.
	*/
	_createBackButton() {
		const t = this.locale.t;
		const backButton = new ButtonView(this.locale);
		backButton.set({
			class: "ck-button-back",
			label: t("Back"),
			icon: IconPreviousArrow,
			tooltip: true
		});
		backButton.delegate("execute").to(this, "cancel");
		return backButton;
	}
	/**
	* Creates a save button view that text alternative the image.
	*/
	_createSaveButton() {
		const t = this.locale.t;
		const saveButton = new ButtonView(this.locale);
		saveButton.set({
			label: t("Save"),
			withText: true,
			type: "submit",
			class: "ck-button-action ck-button-bold"
		});
		return saveButton;
	}
	/**
	* Creates a header view for the form.
	*/
	_createHeaderView() {
		const t = this.locale.t;
		const header = new FormHeaderView(this.locale, { label: t("Text Alternative") });
		header.children.add(this.backButtonView, 0);
		return header;
	}
	/**
	* Creates an input with a label.
	*
	* @returns Labeled field view instance.
	*/
	_createLabeledInputView() {
		const t = this.locale.t;
		const labeledInput = new LabeledFieldView(this.locale, createLabeledInputText);
		labeledInput.label = t("Text alternative");
		labeledInput.class = "ck-labeled-field-view_full-width";
		return labeledInput;
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* A helper utility that positions the
* {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon} instance
* with respect to the image in the editor content, if one is selected.
*
* @param editor The editor instance.
* @internal
*/
function repositionContextualBalloon(editor) {
	const balloon = editor.plugins.get("ContextualBalloon");
	if (editor.plugins.get("ImageUtils").getClosestSelectedImageWidget(editor.editing.view.document.selection)) {
		const position = getBalloonPositionData(editor);
		balloon.updatePosition(position);
	}
}
/**
* Returns the positioning options that control the geometry of the
* {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon} with respect
* to the selected element in the editor content.
*
* @param editor The editor instance.
* @internal
*/
function getBalloonPositionData(editor) {
	const editingView = editor.editing.view;
	const defaultPositions = BalloonPanelView.defaultPositions;
	const imageUtils = editor.plugins.get("ImageUtils");
	return {
		target: editingView.domConverter.mapViewToDom(imageUtils.getClosestSelectedImageWidget(editingView.document.selection)),
		positions: [
			defaultPositions.northArrowSouth,
			defaultPositions.northArrowSouthWest,
			defaultPositions.northArrowSouthEast,
			defaultPositions.southArrowNorth,
			defaultPositions.southArrowNorthWest,
			defaultPositions.southArrowNorthEast,
			defaultPositions.viewportStickyNorth
		]
	};
}

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imagetextalternative/imagetextalternativeui
*/
/**
* The image text alternative UI plugin.
*
* The plugin uses the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon}.
*/
var ImageTextAlternativeUI = class extends Plugin {
	/**
	* The contextual balloon plugin instance.
	*/
	_balloon;
	/**
	* A form containing a textarea and buttons, used to change the `alt` text value.
	*/
	_form;
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [ContextualBalloon];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageTextAlternativeUI";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	init() {
		this._createButton();
	}
	/**
	* @inheritDoc
	*/
	destroy() {
		super.destroy();
		if (this._form) this._form.destroy();
	}
	/**
	* Creates a button showing the balloon panel for changing the image text alternative and
	* registers it in the editor {@link module:ui/componentfactory~ComponentFactory ComponentFactory}.
	*/
	_createButton() {
		const editor = this.editor;
		const t = editor.t;
		editor.ui.componentFactory.add("imageTextAlternative", (locale) => {
			const command = editor.commands.get("imageTextAlternative");
			const view = new ButtonView(locale);
			view.set({
				label: t("Change image text alternative"),
				icon: IconTextAlternative,
				tooltip: true
			});
			view.bind("isEnabled").to(command, "isEnabled");
			view.bind("isOn").to(command, "value", (value) => !!value);
			this.listenTo(view, "execute", () => {
				this._showForm();
			});
			return view;
		});
	}
	/**
	* Creates the {@link module:image/imagetextalternative/ui/textalternativeformview~TextAlternativeFormView}
	* form.
	*/
	_createForm() {
		const editor = this.editor;
		const viewDocument = editor.editing.view.document;
		const imageUtils = editor.plugins.get("ImageUtils");
		this._balloon = this.editor.plugins.get("ContextualBalloon");
		this._form = new (CssTransitionDisablerMixin(TextAlternativeFormView))(editor.locale);
		this._form.render();
		this.listenTo(this._form, "submit", () => {
			editor.execute("imageTextAlternative", { newValue: this._form.labeledInput.fieldView.element.value });
			this._hideForm(true);
		});
		this.listenTo(this._form, "cancel", () => {
			this._hideForm(true);
		});
		this.listenTo(editor.ui, "update", () => {
			if (!imageUtils.getClosestSelectedImageWidget(viewDocument.selection)) this._hideForm(true);
			else if (this._isVisible) repositionContextualBalloon(editor);
		});
		clickOutsideHandler({
			emitter: this._form,
			activator: () => this._isVisible,
			contextElements: () => [this._balloon.view.element],
			callback: () => this._hideForm()
		});
	}
	/**
	* Shows the {@link #_form} in the {@link #_balloon}.
	*/
	_showForm() {
		if (this._isVisible) return;
		if (!this._form) this._createForm();
		const editor = this.editor;
		const command = editor.commands.get("imageTextAlternative");
		const labeledInput = this._form.labeledInput;
		this._form.disableCssTransitions();
		if (!this._isInBalloon) this._balloon.add({
			view: this._form,
			position: getBalloonPositionData(editor)
		});
		labeledInput.fieldView.value = labeledInput.fieldView.element.value = command.value || "";
		this._form.labeledInput.fieldView.select();
		this._form.enableCssTransitions();
	}
	/**
	* Removes the {@link #_form} from the {@link #_balloon}.
	*
	* @param focusEditable Controls whether the editing view is focused afterwards.
	*/
	_hideForm(focusEditable = false) {
		if (!this._isInBalloon) return;
		if (this._form.focusTracker.isFocused) this._form.saveButtonView.focus();
		this._balloon.remove(this._form);
		if (focusEditable) this.editor.editing.view.focus();
	}
	/**
	* Returns `true` when the {@link #_form} is the visible view in the {@link #_balloon}.
	*/
	get _isVisible() {
		return !!this._balloon && this._balloon.visibleView === this._form;
	}
	/**
	* Returns `true` when the {@link #_form} is in the {@link #_balloon}.
	*/
	get _isInBalloon() {
		return !!this._balloon && this._balloon.hasView(this._form);
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imagetextalternative
*/
/**
* The image text alternative plugin.
*
* For a detailed overview, check the {@glink features/images/images-styles image styles} documentation.
*
* This is a "glue" plugin which loads the
*  {@link module:image/imagetextalternative/imagetextalternativeediting~ImageTextAlternativeEditing}
* and {@link module:image/imagetextalternative/imagetextalternativeui~ImageTextAlternativeUI} plugins.
*/
var ImageTextAlternative = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [ImageTextAlternativeEditing, ImageTextAlternativeUI];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageTextAlternative";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* Returns a function that converts the image view representation:
*
* ```html
* <figure class="image"><img src="..." alt="..."></img></figure>
* ```
*
* to the model representation:
*
* ```html
* <imageBlock src="..." alt="..."></imageBlock>
* ```
*
* The entire content of the `<figure>` element except the first `<img>` is being converted as children
* of the `<imageBlock>` model element.
*
* @internal
*/
function upcastImageFigure(imageUtils) {
	const converter = (evt, data, conversionApi) => {
		if (!conversionApi.consumable.test(data.viewItem, {
			name: true,
			classes: "image"
		})) return;
		const viewImage = imageUtils.findViewImgElement(data.viewItem);
		if (!viewImage || !conversionApi.consumable.test(viewImage, { name: true })) return;
		if (!isImageTypePlaceable(conversionApi.schema, data.modelCursor, "imageBlock")) return;
		conversionApi.consumable.consume(data.viewItem, {
			name: true,
			classes: "image"
		});
		const conversionResult = conversionApi.convertItem(viewImage, data.modelCursor);
		/* istanbul ignore if: defensive guard for the `ModelRange | null` return type -- @preserve */
		if (!conversionResult.modelRange) {
			conversionApi.consumable.revert(data.viewItem, {
				name: true,
				classes: "image"
			});
			return;
		}
		const modelImage = first(conversionResult.modelRange.getItems());
		if (!modelImage) {
			conversionApi.consumable.revert(data.viewItem, {
				name: true,
				classes: "image"
			});
			return;
		}
		conversionApi.convertChildren(data.viewItem, modelImage);
		conversionApi.updateConversionResult(modelImage, data);
	};
	return (dispatcher) => {
		dispatcher.on("element:figure", converter);
	};
}
/**
* Returns a function that upcasts an `<img>` element to either an `imageBlock` or an `imageInline` model element.
*
* The image type is first determined from the view structure (see {@link module:image/image/utils~getViewImageType}):
* an `<img>` wrapped in a `<figure class="image">` or styled with `display: block` becomes an `imageBlock`, otherwise
* an `imageInline`.
*
* That structural type is then verified against the schema at the insertion position. If it cannot be placed there -
* neither directly (or after hoisting to an allowed ancestor) nor wrapped in an auto-created paragraph - the converter
* falls back to `matchImageType`. This is what allows a block image to degrade to an inline image inside an inline root
* (and, symmetrically, an inline image to become a block image in a context that only accepts block images) instead of
* being dropped.
*
* Both `ImageBlockEditing` and `ImageInlineEditing` register this converter, each passing the type it falls back to.
* When only one of them is loaded, that single type is always produced.
*
* @internal
* @param matchImageType The image type to fall back to when the type resolved from the view cannot be placed at the
* insertion position.
* @param imageUtils The `ImageUtils` plugin instance.
*/
function upcastImg(matchImageType, imageUtils) {
	const converter = (evt, data, conversionApi) => {
		let imageType = getViewImageType(data.viewItem, imageUtils);
		const attributes = data.viewItem.hasAttribute("src") ? { src: data.viewItem.getAttribute("src") } : void 0;
		const consumables = {
			name: true,
			attributes: Object.keys(attributes || {})
		};
		if (!conversionApi.consumable.test(data.viewItem, consumables)) return;
		if (imageType != matchImageType && !isImageTypePlaceable(conversionApi.schema, data.modelCursor, imageType)) imageType = matchImageType;
		const modelElement = conversionApi.writer.createElement(imageType, attributes);
		if (!conversionApi.safeInsert(modelElement, data.modelCursor)) return;
		conversionApi.consumable.consume(data.viewItem, consumables);
		conversionApi.convertChildren(data.viewItem, modelElement);
		conversionApi.updateConversionResult(modelElement, data);
	};
	return (dispatcher) => {
		dispatcher.on("element:img", converter);
	};
}
/**
* Returns a function that converts the image view representation:
*
* ```html
* <picture><source ... /><source ... />...<img ... /></picture>
* ```
*
* to the model representation as the `sources` attribute:
*
* ```html
* <image[Block|Inline] ... sources="..."></image[Block|Inline]>
* ```
*
* @internal
*/
function upcastPicture(imageUtils) {
	const sourceAttributeNames = [
		"srcset",
		"media",
		"type",
		"sizes"
	];
	const converter = (evt, data, conversionApi) => {
		const pictureViewElement = data.viewItem;
		if (!conversionApi.consumable.test(pictureViewElement, { name: true })) return;
		const sources = /* @__PURE__ */ new Map();
		for (const childSourceElement of pictureViewElement.getChildren()) if (childSourceElement.is("element", "source")) {
			const attributes = {};
			for (const name of sourceAttributeNames) if (childSourceElement.hasAttribute(name)) {
				if (conversionApi.consumable.test(childSourceElement, { attributes: name })) attributes[name] = childSourceElement.getAttribute(name);
			}
			if (Object.keys(attributes).length) sources.set(childSourceElement, attributes);
		}
		const imgViewElement = imageUtils.findViewImgElement(pictureViewElement);
		if (!imgViewElement) return;
		let modelImage = data.modelCursor.parent;
		if (!modelImage.is("element", "imageBlock")) {
			const conversionResult = conversionApi.convertItem(imgViewElement, data.modelCursor);
			data.modelRange = conversionResult.modelRange;
			data.modelCursor = conversionResult.modelCursor;
			/* istanbul ignore if: defensive guard for the `ModelRange | null` return type -- @preserve */
			if (!conversionResult.modelRange) return;
			modelImage = first(conversionResult.modelRange.getItems());
			if (!modelImage) return;
		}
		conversionApi.consumable.consume(pictureViewElement, { name: true });
		for (const [sourceElement, attributes] of sources) conversionApi.consumable.consume(sourceElement, { attributes: Object.keys(attributes) });
		if (sources.size) conversionApi.writer.setAttribute("sources", Array.from(sources.values()), modelImage);
		conversionApi.convertChildren(pictureViewElement, modelImage);
	};
	return (dispatcher) => {
		dispatcher.on("element:picture", converter);
	};
}
/**
* 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.
*/
function downcastSrcsetAttribute(imageUtils, imageType) {
	const converter = (evt, data, conversionApi) => {
		if (!conversionApi.consumable.consume(data.item, evt.name)) return;
		const writer = conversionApi.writer;
		const element = conversionApi.mapper.toViewElement(data.item);
		const img = imageUtils.findViewImgElement(element);
		if (data.attributeNewValue === null) {
			writer.removeAttribute("srcset", img);
			writer.removeAttribute("sizes", img);
		} else if (data.attributeNewValue) {
			writer.setAttribute("srcset", data.attributeNewValue, img);
			writer.setAttribute("sizes", "100vw", img);
		}
	};
	return (dispatcher) => {
		dispatcher.on(`attribute:srcset:${imageType}`, converter);
	};
}
/**
* Converts the `source` model attribute to the `<picture><source /><source />...<img /></picture>`
* view structure.
*
* @internal
*/
function downcastSourcesAttribute(imageUtils) {
	const converter = (evt, data, conversionApi) => {
		if (!conversionApi.consumable.consume(data.item, evt.name)) return;
		const viewWriter = conversionApi.writer;
		const element = conversionApi.mapper.toViewElement(data.item);
		const imgElement = imageUtils.findViewImgElement(element);
		const attributeNewValue = data.attributeNewValue;
		if (attributeNewValue && attributeNewValue.length) {
			const attributeElements = [];
			let viewElement = imgElement.parent;
			while (viewElement && viewElement.is("attributeElement")) {
				const parentElement = viewElement.parent;
				viewWriter.unwrap(viewWriter.createRangeOn(imgElement), viewElement);
				attributeElements.unshift(viewElement);
				viewElement = parentElement;
			}
			const hasPictureElement = imgElement.parent.is("element", "picture");
			const pictureElement = hasPictureElement ? imgElement.parent : viewWriter.createContainerElement("picture", null);
			if (!hasPictureElement) viewWriter.insert(viewWriter.createPositionBefore(imgElement), pictureElement);
			viewWriter.remove(viewWriter.createRangeIn(pictureElement));
			viewWriter.insert(viewWriter.createPositionAt(pictureElement, "end"), attributeNewValue.map((sourceAttributes) => {
				return viewWriter.createEmptyElement("source", sourceAttributes);
			}));
			viewWriter.move(viewWriter.createRangeOn(imgElement), viewWriter.createPositionAt(pictureElement, "end"));
			for (const attributeElement of attributeElements) viewWriter.wrap(viewWriter.createRangeOn(pictureElement), attributeElement);
		} else if (imgElement.parent.is("element", "picture")) {
			const pictureElement = imgElement.parent;
			viewWriter.move(viewWriter.createRangeOn(imgElement), viewWriter.createPositionBefore(pictureElement));
			viewWriter.remove(pictureElement);
		}
	};
	return (dispatcher) => {
		dispatcher.on("attribute:sources:imageBlock", converter);
		dispatcher.on("attribute:sources:imageInline", converter);
	};
}
/**
* Converter used to convert a given image attribute from the model to the view.
*
* @internal
* @param imageType The type of the image.
* @param attributeKey The name of the attribute to convert.
*/
function downcastImageAttribute(imageUtils, imageType, attributeKey) {
	const converter = (evt, data, conversionApi) => {
		if (!conversionApi.consumable.consume(data.item, evt.name)) return;
		const viewWriter = conversionApi.writer;
		const element = conversionApi.mapper.toViewElement(data.item);
		const img = imageUtils.findViewImgElement(element);
		viewWriter.setAttribute(data.attributeKey, data.attributeNewValue || "", img);
	};
	return (dispatcher) => {
		dispatcher.on(`attribute:${attributeKey}:${imageType}`, converter);
	};
}

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/image/imageloadobserver
*/
/**
* Observes all new images added to the {@link module:engine/view/document~ViewDocument},
* fires {@link module:engine/view/document~ViewDocument#event:imageLoaded} and
* {@link module:engine/view/document~ViewDocument#event:layoutChanged} event every time when the new image
* has been loaded.
*
* **Note:** This event is not fired for images that has been added to the document and rendered as `complete` (already loaded).
*/
var ImageLoadObserver = class extends Observer {
	/**
	* @inheritDoc
	*/
	observe(domRoot) {
		this.listenTo(domRoot, "load", (event, domEvent) => {
			const domElement = domEvent.target;
			if (this.checkShouldIgnoreEventFromTarget(domElement)) return;
			if (domElement.tagName == "IMG") this._fireEvents(domEvent);
		}, { useCapture: true });
	}
	/**
	* @inheritDoc
	*/
	stopObserving(domRoot) {
		this.stopListening(domRoot);
	}
	/**
	* Fires {@link module:engine/view/document~ViewDocument#event:layoutChanged} and
	* {@link module:engine/view/document~ViewDocument#event:imageLoaded}
	* if observer {@link #isEnabled is enabled}.
	*
	* @param domEvent The DOM event.
	*/
	_fireEvents(domEvent) {
		if (this.isEnabled) {
			this.document.fire("layoutChanged");
			this.document.fire("imageLoaded", domEvent);
		}
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/image/insertimagecommand
*/
/**
* Insert image command.
*
* The command is registered by the {@link module:image/image/imageediting~ImageEditing} plugin as `insertImage`
* and it is also available via aliased `imageInsert` name.
*
* In order to insert an image at the current selection position
* (according to the {@link module:widget/utils~findOptimalInsertionRange} algorithm),
* execute the command and specify the image source:
*
* ```ts
* editor.execute( 'insertImage', { source: 'http://url.to.the/image' } );
* ```
*
* It is also possible to insert multiple images at once:
*
* ```ts
* editor.execute( 'insertImage', {
* 	source:  [
* 		'path/to/image.jpg',
* 		'path/to/other-image.jpg'
* 	]
* } );
* ```
*
* If you want to take the full control over the process, you can specify individual model attributes:
*
* ```ts
* editor.execute( 'insertImage', {
* 	source:  [
* 		{ src: 'path/to/image.jpg', alt: 'First alt text' },
* 		{ src: 'path/to/other-image.jpg', alt: 'Second alt text', customAttribute: 'My attribute value' }
* 	]
* } );
* ```
*/
var InsertImageCommand = class extends Command {
	/**
	* @inheritDoc
	*/
	constructor(editor) {
		super(editor);
		const configImageInsertType = editor.config.get("image.insert.type");
		if (!editor.plugins.has("ImageBlockEditing")) {
			if (configImageInsertType === "block")
 /**
			* The {@link module:image/imageblock~ImageBlock} plugin must be enabled to allow inserting block images. See
			* {@link module:image/imageconfig~ImageInsertConfig#type} to learn more.
			*
			* @error image-block-plugin-required
			*/
			logWarning("image-block-plugin-required");
		}
		if (!editor.plugins.has("ImageInlineEditing")) {
			if (configImageInsertType === "inline")
 /**
			* The {@link module:image/imageinline~ImageInline} plugin must be enabled to allow inserting inline images. See
			* {@link module:image/imageconfig~ImageInsertConfig#type} to learn more.
			*
			* @error image-inline-plugin-required
			*/
			logWarning("image-inline-plugin-required");
		}
	}
	/**
	* @inheritDoc
	*/
	refresh() {
		const imageUtils = this.editor.plugins.get("ImageUtils");
		this.isEnabled = imageUtils.isImageAllowed();
	}
	/**
	* Executes the command.
	*
	* @fires execute
	* @param options Options for the executed command.
	* @param options.imageType The type of the image to insert. If not specified, the type will be determined automatically.
	* @param options.source The image source or an array of image sources to insert.
	* @param options.breakBlock If set to `true`, the block at the selection start will be broken before inserting the image.
	* See the documentation of the command to learn more about accepted formats.
	*/
	execute(options) {
		const sourceDefinitions = toArray(options.source);
		const selection = this.editor.model.document.selection;
		const imageUtils = this.editor.plugins.get("ImageUtils");
		const selectionAttributes = Object.fromEntries(selection.getAttributes());
		sourceDefinitions.forEach((sourceDefinition, index) => {
			const selectedElement = selection.getSelectedElement();
			if (typeof sourceDefinition === "string") sourceDefinition = { src: sourceDefinition };
			if (index && selectedElement && imageUtils.isImage(selectedElement)) {
				const position = this.editor.model.createPositionAfter(selectedElement);
				imageUtils.insertImage({
					...sourceDefinition,
					...selectionAttributes
				}, position, options.imageType);
			} else if (options.breakBlock) imageUtils.insertImage({
				...sourceDefinition,
				...selectionAttributes
			}, selection.getFirstPosition(), options.imageType);
			else imageUtils.insertImage({
				...sourceDefinition,
				...selectionAttributes
			}, null, options.imageType);
		});
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/image/replaceimagesourcecommand
*/
/**
* Replace image source command.
*
* Changes image source to the one provided. Can be executed as follows:
*
* ```ts
* editor.execute( 'replaceImageSource', { source: 'http://url.to.the/image' } );
* ```
*/
var ReplaceImageSourceCommand = class extends Command {
	constructor(editor) {
		super(editor);
		this.decorate("cleanupImage");
	}
	/**
	* @inheritDoc
	*/
	refresh() {
		const imageUtils = this.editor.plugins.get("ImageUtils");
		const element = this.editor.model.document.selection.getSelectedElement();
		this.isEnabled = imageUtils.isImage(element);
		this.value = this.isEnabled ? element.getAttribute("src") : null;
	}
	/**
	* Executes the command.
	*
	* @fires execute
	* @param options Options for the executed command.
	* @param options.source The image source to replace.
	*/
	execute(options) {
		const image = this.editor.model.document.selection.getSelectedElement();
		const imageUtils = this.editor.plugins.get("ImageUtils");
		this.editor.model.change((writer) => {
			writer.setAttribute("src", options.source, image);
			this.cleanupImage(writer, image);
			imageUtils.setImageNaturalSizeAttributes(image);
		});
	}
	/**
	* Cleanup image attributes that are not relevant to the new source.
	*
	* Removed attributes are: 'srcset', 'sizes', 'sources', 'width', 'height', 'alt'.
	*
	* This method is decorated, to allow custom cleanup logic.
	* For example, to remove 'myImageId' attribute after 'src' has changed:
	*
	* ```ts
	* replaceImageSourceCommand.on( 'cleanupImage', ( eventInfo, [ writer, image ] ) => {
	* 	writer.removeAttribute( 'myImageId', image );
	* } );
	* ```
	*/
	cleanupImage(writer, image) {
		writer.removeAttribute("srcset", image);
		writer.removeAttribute("sizes", image);
		/**
		* In case responsive images some attributes should be cleaned up.
		* Check: https://github.com/ckeditor/ckeditor5/issues/15093
		*/
		writer.removeAttribute("sources", image);
		writer.removeAttribute("width", image);
		writer.removeAttribute("height", image);
		writer.removeAttribute("alt", image);
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/image/imageediting
*/
/**
* The image engine plugin. This module loads common code shared between
* {@link module:image/image/imageinlineediting~ImageInlineEditing} and
* {@link module:image/image/imageblockediting~ImageBlockEditing} plugins.
*
* This plugin registers the {@link module:image/image/insertimagecommand~InsertImageCommand 'insertImage'} command.
*/
var ImageEditing = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [ImageUtils];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageEditing";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	init() {
		const editor = this.editor;
		const conversion = editor.conversion;
		editor.editing.view.addObserver(ImageLoadObserver);
		conversion.for("upcast").attributeToAttribute({
			view: {
				name: "img",
				key: "alt"
			},
			model: "alt"
		}).attributeToAttribute({
			view: {
				name: "img",
				key: "srcset"
			},
			model: "srcset"
		});
		const insertImageCommand = new InsertImageCommand(editor);
		const replaceImageSourceCommand = new ReplaceImageSourceCommand(editor);
		editor.commands.add("insertImage", insertImageCommand);
		editor.commands.add("replaceImageSource", replaceImageSourceCommand);
		editor.commands.add("imageInsert", insertImageCommand);
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imagesizeattributes
*/
/**
* This plugin enables `width` and `height` attributes in inline and block image elements.
*/
var ImageSizeAttributes = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [ImageUtils];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageSizeAttributes";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	afterInit() {
		this._registerSchema();
		this._registerConverters("imageBlock");
		this._registerConverters("imageInline");
	}
	/**
	* Registers the `width` and `height` attributes for inline and block images.
	*/
	_registerSchema() {
		const schema = this.editor.model.schema;
		if (this.editor.plugins.has("ImageBlockEditing")) schema.extend("imageBlock", { allowAttributes: ["width", "height"] });
		if (this.editor.plugins.has("ImageInlineEditing")) schema.extend("imageInline", { allowAttributes: ["width", "height"] });
	}
	/**
	* Registers converters for `width` and `height` attributes.
	*/
	_registerConverters(imageType) {
		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) => {
					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) => {
					if (widthAndHeightStylesAreBothSet(viewElement)) return getSizeValueIfInPx(viewElement.getStyle("height"));
					return null;
				}
			}
		}).attributeToAttribute({
			view: {
				name: viewElementName,
				key: "height"
			},
			model: "height"
		});
		editor.conversion.for("editingDowncast").add((dispatcher) => {
			attachDowncastConverter(dispatcher, "width", "width", true, true);
			attachDowncastConverter(dispatcher, "height", "height", true, true);
		});
		editor.conversion.for("dataDowncast").add((dispatcher) => {
			attachDowncastConverter(dispatcher, "width", "width", false);
			attachDowncastConverter(dispatcher, "height", "height", false);
		});
		editor.conversion.for("upcast").add((dispatcher) => {
			dispatcher.on("element:img", (evt, data, conversionApi) => {
				const width = data.viewItem.getAttribute("width");
				const height = data.viewItem.getAttribute("height");
				if (width && height) conversionApi.consumable.consume(data.viewItem, { styles: ["aspect-ratio"] });
			});
		});
		function attachDowncastConverter(dispatcher, modelAttributeName, viewAttributeName, setRatioForInlineImage, isEditingDowncast = false) {
			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);
				const img = imageUtils.findViewImgElement(viewElement);
				if (data.attributeNewValue !== null) viewWriter.setAttribute(viewAttributeName, data.attributeNewValue, img);
				else viewWriter.removeAttribute(viewAttributeName, img);
				const width = data.item.getAttribute("width");
				const height = data.item.getAttribute("height");
				const hasSizes = width && height;
				if (hasSizes && isEditingDowncast) viewWriter.setAttribute("loading", "lazy", img);
				if (data.item.hasAttribute("sources")) return;
				const isResized = data.item.hasAttribute("resizedWidth");
				if (imageType === "imageInline" && !isResized && !setRatioForInlineImage) return;
				if (hasSizes) viewWriter.setStyle("aspect-ratio", `${width}/${height}`, img);
			});
		}
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* The image type command. It changes the type of a selected image, depending on the configuration.
*/
var ImageTypeCommand = class extends Command {
	/**
	* Model element name the command converts to.
	*/
	_modelElementName;
	/**
	* @inheritDoc
	*
	* @param modelElementName Model element name the command converts to.
	*/
	constructor(editor, modelElementName) {
		super(editor);
		this._modelElementName = modelElementName;
	}
	/**
	* @inheritDoc
	*/
	refresh() {
		const editor = this.editor;
		const model = editor.model;
		const schema = model.schema;
		const imageUtils = editor.plugins.get("ImageUtils");
		const element = imageUtils.getClosestSelectedImageElement(model.document.selection);
		if (!(this._modelElementName === "imageBlock" ? imageUtils.isInlineImage(element) : imageUtils.isBlockImage(element))) {
			this.isEnabled = false;
			return;
		}
		const position = model.createPositionBefore(element);
		this.isEnabled = isImageTypePlaceable(schema, position, this._modelElementName);
	}
	/**
	* Executes the command and changes the type of a selected image.
	*
	* @fires execute
	* @param options.setImageSizes Specifies whether the image `width` and `height` attributes should be set automatically.
	* The default is `true`.
	* @returns An object containing references to old and new model image elements
	* (for before and after the change) so external integrations can hook into the decorated
	* `execute` event and handle this change. `null` if the type change failed.
	*/
	execute(options = {}) {
		const editor = this.editor;
		const model = this.editor.model;
		const imageUtils = editor.plugins.get("ImageUtils");
		const oldElement = imageUtils.getClosestSelectedImageElement(model.document.selection);
		const attributes = Object.fromEntries(oldElement.getAttributes());
		if (!attributes.src && !attributes.uploadId) return null;
		return model.change((writer) => {
			const { setImageSizes = true } = options;
			const markers = Array.from(model.markers).filter((marker) => marker.getRange().containsItem(oldElement));
			const newElement = imageUtils.insertImage(attributes, model.createSelection(oldElement, "on"), this._modelElementName, { setImageSizes });
			if (!newElement) return null;
			const newElementRange = writer.createRangeOn(newElement);
			for (const marker of markers) {
				const markerRange = marker.getRange();
				const range = markerRange.root.rootName != "$graveyard" ? markerRange.getJoined(newElementRange, true) : newElementRange;
				writer.updateMarker(marker, { range });
			}
			return {
				oldElement,
				newElement
			};
		});
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/image/imageplaceholder
*/
/**
* Adds support for image placeholder that is automatically removed when the image is loaded.
*/
var ImagePlaceholder = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [ImageUtils];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImagePlaceholder";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	afterInit() {
		this._setupSchema();
		this._setupConversion();
		this._setupLoadListener();
	}
	/**
	* Extends model schema.
	*/
	_setupSchema() {
		const schema = this.editor.model.schema;
		if (schema.isRegistered("imageBlock")) schema.extend("imageBlock", { allowAttributes: ["placeholder"] });
		if (schema.isRegistered("imageInline")) schema.extend("imageInline", { allowAttributes: ["placeholder"] });
	}
	/**
	* Registers converters.
	*/
	_setupConversion() {
		const editor = this.editor;
		const conversion = editor.conversion;
		const imageUtils = editor.plugins.get("ImageUtils");
		conversion.for("editingDowncast").add((dispatcher) => {
			dispatcher.on("attribute:placeholder", (evt, data, conversionApi) => {
				if (!conversionApi.consumable.test(data.item, evt.name)) return;
				if (!data.item.is("element", "imageBlock") && !data.item.is("element", "imageInline")) return;
				conversionApi.consumable.consume(data.item, evt.name);
				const viewWriter = conversionApi.writer;
				const element = conversionApi.mapper.toViewElement(data.item);
				const img = imageUtils.findViewImgElement(element);
				if (data.attributeNewValue) {
					viewWriter.addClass("image_placeholder", img);
					viewWriter.setStyle("background-image", `url(${data.attributeNewValue})`, img);
					viewWriter.setCustomProperty("editingPipeline:doNotReuseOnce", true, img);
				} else {
					viewWriter.removeClass("image_placeholder", img);
					viewWriter.removeStyle("background-image", img);
				}
			});
		});
	}
	/**
	* Prepares listener for image load.
	*/
	_setupLoadListener() {
		const editor = this.editor;
		const model = editor.model;
		const editing = editor.editing;
		const editingView = editing.view;
		const imageUtils = editor.plugins.get("ImageUtils");
		editingView.addObserver(ImageLoadObserver);
		this.listenTo(editingView.document, "imageLoaded", (evt, domEvent) => {
			const imgViewElement = editingView.domConverter.mapDomToView(domEvent.target);
			if (!imgViewElement) return;
			const viewElement = imageUtils.getImageWidgetFromImageView(imgViewElement);
			if (!viewElement) return;
			const modelElement = editing.mapper.toModelElement(viewElement);
			if (!modelElement || !modelElement.hasAttribute("placeholder")) return;
			model.enqueueChange({ isUndoable: false }, (writer) => {
				writer.removeAttribute("placeholder", modelElement);
			});
		});
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/image/imageblockediting
*/
/**
* The image block plugin.
*
* It registers:
*
* * `<imageBlock>` as a block element in the document schema, and allows `alt`, `src` and `srcset` attributes.
* * converters for editing and data pipelines.,
* * {@link module:image/image/imagetypecommand~ImageTypeCommand `'imageTypeBlock'`} command that converts inline images into
* block images.
*/
var ImageBlockEditing = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [
			ImageEditing,
			ImageSizeAttributes,
			ImageUtils,
			ImagePlaceholder,
			ClipboardPipeline
		];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageBlockEditing";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	init() {
		const editor = this.editor;
		editor.model.schema.register("imageBlock", {
			inheritAllFrom: "$blockObject",
			allowAttributes: [
				"alt",
				"src",
				"srcset"
			]
		});
		this._setupConversion();
		if (editor.plugins.has("ImageInlineEditing")) {
			editor.commands.add("imageTypeBlock", new ImageTypeCommand(this.editor, "imageBlock"));
			this._setupClipboardIntegration();
		}
	}
	/**
	* Configures conversion pipelines to support upcasting and downcasting
	* block images (block image widgets) and their attributes.
	*/
	_setupConversion() {
		const editor = this.editor;
		const t = editor.t;
		const conversion = editor.conversion;
		const imageUtils = editor.plugins.get("ImageUtils");
		conversion.for("dataDowncast").elementToStructure({
			model: "imageBlock",
			view: (modelElement, { writer }) => createBlockImageViewElement(writer)
		});
		conversion.for("editingDowncast").elementToStructure({
			model: "imageBlock",
			view: (modelElement, { writer }) => imageUtils.toImageWidget(createBlockImageViewElement(writer), writer, t("image widget"))
		});
		conversion.for("downcast").add(downcastImageAttribute(imageUtils, "imageBlock", "src")).add(downcastImageAttribute(imageUtils, "imageBlock", "alt")).add(downcastSrcsetAttribute(imageUtils, "imageBlock"));
		conversion.for("upcast").add(upcastImg("imageBlock", imageUtils)).add(upcastImageFigure(imageUtils));
	}
	/**
	* Integrates the plugin with the clipboard pipeline.
	*
	* Idea is that the feature should recognize the user's intent when an **inline** image is
	* pasted or dropped. If such an image is pasted/dropped:
	*
	* * into an empty block (e.g. an empty paragraph),
	* * on another object (e.g. some block widget).
	*
	* it gets converted into a block image on the fly. We assume this is the user's intent
	* if they decided to put their image there.
	*
	* See the `ImageInlineEditing` for the similar integration that works in the opposite direction.
	*
	* The feature also sets image `width` and `height` attributes on paste.
	*/
	_setupClipboardIntegration() {
		const editor = this.editor;
		const model = editor.model;
		const editingView = editor.editing.view;
		const imageUtils = editor.plugins.get("ImageUtils");
		const clipboardPipeline = editor.plugins.get("ClipboardPipeline");
		this.listenTo(clipboardPipeline, "inputTransformation", (evt, data) => {
			const docFragmentChildren = Array.from(data.content.getChildren());
			let modelRange;
			if (!docFragmentChildren.every(imageUtils.isInlineImageView)) return;
			if (data.targetRanges) modelRange = editor.editing.mapper.toModelRange(data.targetRanges[0]);
			else modelRange = model.document.selection.getFirstRange();
			const selection = model.createSelection(modelRange);
			const selectionPosition = selection.getFirstPosition();
			if (!!selectionPosition && isImageTypePlaceable(model.schema, selectionPosition, "imageBlock") && determineImageTypeForInsertionAtSelection(model.schema, selection) === "imageBlock") {
				const writer = new ViewUpcastWriter(editingView.document);
				const blockViewImages = docFragmentChildren.map((inlineViewImage) => writer.createElement("figure", { class: "image" }, inlineViewImage));
				data.content = writer.createDocumentFragment(blockViewImages);
			}
		});
		this.listenTo(clipboardPipeline, "contentInsertion", (evt, data) => {
			if (data.method !== "paste") return;
			model.change((writer) => {
				const range = writer.createRangeIn(data.content);
				for (const item of range.getItems()) if (item.is("element", "imageBlock")) imageUtils.setImageNaturalSizeAttributes(item);
			});
		});
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imageinsert/ui/imageinsertformview
*/
/**
* The view displayed in the insert image dropdown.
*
* See {@link module:image/imageinsert/imageinsertui~ImageInsertUI}.
*
* @internal
*/
var ImageInsertFormView = class extends View {
	/**
	* Tracks information about DOM focus in the form.
	*/
	focusTracker;
	/**
	* An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
	*/
	keystrokes;
	/**
	* A collection of views that can be focused in the form.
	*/
	_focusables;
	/**
	* Helps cycling over {@link #_focusables} in the form.
	*/
	_focusCycler;
	/**
	* A collection of the defined integrations for inserting the images.
	*/
	children;
	/**
	* Creates a view for the dropdown panel of {@link module:image/imageinsert/imageinsertui~ImageInsertUI}.
	*
	* @param locale The localization services instance.
	* @param integrations An integrations object that contains components (or tokens for components) to be shown in the panel view.
	*/
	constructor(locale, integrations = []) {
		super(locale);
		this.focusTracker = new FocusTracker();
		this.keystrokes = new KeystrokeHandler();
		this._focusables = new ViewCollection();
		this.children = this.createCollection();
		this._focusCycler = new FocusCycler({
			focusables: this._focusables,
			focusTracker: this.focusTracker,
			keystrokeHandler: this.keystrokes,
			actions: {
				focusPrevious: "shift + tab",
				focusNext: "tab"
			}
		});
		for (const view of integrations) {
			this.children.add(view);
			this._focusables.add(view);
			if (view instanceof CollapsibleView) this._focusables.addMany(view.children);
		}
		this.setTemplate({
			tag: "form",
			attributes: {
				class: ["ck", "ck-image-insert-form"],
				tabindex: -1
			},
			children: this.children
		});
	}
	/**
	* @inheritDoc
	*/
	render() {
		super.render();
		submitHandler({ view: this });
		for (const view of this._focusables) this.focusTracker.add(view.element);
		this.keystrokes.listenTo(this.element);
		const stopPropagation = (data) => data.stopPropagation();
		this.keystrokes.set("arrowright", stopPropagation);
		this.keystrokes.set("arrowleft", stopPropagation);
		this.keystrokes.set("arrowup", stopPropagation);
		this.keystrokes.set("arrowdown", stopPropagation);
	}
	/**
	* @inheritDoc
	*/
	destroy() {
		super.destroy();
		this.focusTracker.destroy();
		this.keystrokes.destroy();
	}
	/**
	* Focuses the first {@link #_focusables focusable} in the form.
	*/
	focus() {
		this._focusCycler.focusFirst();
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imageinsert/imageinsertui
*/
/**
* The image insert dropdown plugin.
*
* For a detailed overview, check the {@glink features/images/image-upload/image-upload Image upload feature}
* and {@glink features/images/images-inserting Insert images via source URL} documentation.
*
* Adds the `'insertImage'` dropdown to the {@link module:ui/componentfactory~ComponentFactory UI component factory}
* and also the `imageInsert` dropdown as an alias for backward compatibility.
*
* Adds the `'menuBar:insertImage'` sub-menu to the {@link module:ui/componentfactory~ComponentFactory UI component factory}, which is
* by default added to the `'Insert'` menu.
*/
var ImageInsertUI = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageInsertUI";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [ImageUtils];
	}
	/**
	* The dropdown view responsible for displaying the image insert UI.
	*/
	dropdownView;
	/**
	* Registered integrations map.
	*/
	_integrations = /* @__PURE__ */ new Map();
	/**
	* @inheritDoc
	*/
	constructor(editor) {
		super(editor);
		editor.config.define("image.insert.integrations", [
			"upload",
			"assetManager",
			"url"
		]);
	}
	/**
	* @inheritDoc
	*/
	init() {
		const editor = this.editor;
		const selection = editor.model.document.selection;
		const imageUtils = editor.plugins.get("ImageUtils");
		this.set("isImageSelected", false);
		this.listenTo(editor.model.document, "change", () => {
			this.isImageSelected = imageUtils.isImage(selection.getSelectedElement());
		});
		const componentCreator = (locale) => this._createToolbarComponent(locale);
		const menuBarComponentCreator = (locale) => this._createMenuBarComponent(locale);
		editor.ui.componentFactory.add("insertImage", componentCreator);
		editor.ui.componentFactory.add("imageInsert", componentCreator);
		editor.ui.componentFactory.add("menuBar:insertImage", menuBarComponentCreator);
	}
	/**
	* Registers the insert image dropdown integration.
	*/
	registerIntegration({ name, observable, buttonViewCreator, formViewCreator, menuBarButtonViewCreator, requiresForm = false, override = false }) {
		if (this._integrations.has(name) && !override)
 /**
		* There are two insert-image integrations registered with the same name.
		*
		* Make sure that you do not load multiple asset manager plugins.
		*
		* @error image-insert-integration-exists
		*/
		logWarning("image-insert-integration-exists", { name });
		this._integrations.set(name, {
			observable,
			buttonViewCreator,
			menuBarButtonViewCreator,
			formViewCreator,
			requiresForm
		});
	}
	/**
	* Creates the toolbar component.
	*/
	_createToolbarComponent(locale) {
		const editor = this.editor;
		const t = locale.t;
		const integrations = this._prepareIntegrations();
		if (!integrations.length) return null;
		let dropdownButton;
		const firstIntegration = integrations[0];
		if (integrations.length == 1) {
			if (!firstIntegration.requiresForm) return firstIntegration.buttonViewCreator(true);
			dropdownButton = firstIntegration.buttonViewCreator(true);
		} else {
			dropdownButton = new SplitButtonView(locale, firstIntegration.buttonViewCreator(false));
			dropdownButton.tooltip = true;
			dropdownButton.bind("label").to(this, "isImageSelected", (isImageSelected) => isImageSelected ? t("Replace image") : t("Insert image"));
		}
		const dropdownView = this.dropdownView = createDropdown(locale, dropdownButton);
		const observables = integrations.map(({ observable }) => typeof observable == "function" ? observable() : observable);
		dropdownView.bind("isEnabled").toMany(observables, "isEnabled", (...isEnabled) => isEnabled.some((isEnabled) => isEnabled));
		dropdownView.once("change:isOpen", () => {
			const integrationViews = integrations.flatMap(({ formViewCreator }) => formViewCreator(integrations.length == 1));
			const imageInsertFormView = new ImageInsertFormView(editor.locale, integrationViews);
			dropdownView.panelView.children.add(imageInsertFormView);
		});
		return dropdownView;
	}
	/**
	* Creates the menu bar component.
	*/
	_createMenuBarComponent(locale) {
		const t = locale.t;
		const integrations = this._prepareIntegrations();
		if (!integrations.length) return null;
		const integrationViews = integrations.flatMap(({ menuBarButtonViewCreator }) => menuBarButtonViewCreator(integrations.length == 1));
		const resultView = new MenuBarMenuView(locale);
		const listView = new MenuBarMenuListView(locale);
		resultView.panelView.children.add(listView);
		resultView.buttonView.set({
			icon: IconImage,
			label: t("Image")
		});
		for (const integrationView of integrationViews) {
			const listItemView = new MenuBarMenuListItemView(locale, resultView);
			listItemView.children.add(integrationView);
			listView.items.add(listItemView);
			integrationView.delegate("execute").to(resultView);
		}
		return resultView;
	}
	/**
	* Validates the integrations list.
	*/
	_prepareIntegrations() {
		const items = this.editor.config.get("image.insert.integrations");
		const result = [];
		if (!items.length) {
			/**
			* The insert image feature requires a list of integrations to be provided in the editor configuration.
			*
			* The default list of integrations is `upload`, `assetManager`, `url`. Those integrations are included
			* in the insert image dropdown if the given feature plugin is loaded. You should omit the `integrations`
			* configuration key to use the default set or provide a selected list of integrations that should be used.
			*
			* @error image-insert-integrations-not-specified
			*/
			logWarning("image-insert-integrations-not-specified");
			return result;
		}
		for (const item of items) {
			if (!this._integrations.has(item)) {
				if (![
					"upload",
					"assetManager",
					"url"
				].includes(item))
 /**
				* The specified insert image integration name is unknown or the providing plugin is not loaded in the editor.
				*
				* @error image-insert-unknown-integration
				*/
				logWarning("image-insert-unknown-integration", { item });
				continue;
			}
			result.push(this._integrations.get(item));
		}
		if (!result.length)
 /**
		* The image insert feature requires integrations to be registered by separate features.
		*
		* The `insertImage` toolbar button requires integrations to be registered by other features.
		* For example {@link module:image/imageupload~ImageUpload ImageUpload},
		* {@link module:image/imageinsert~ImageInsert ImageInsert},
		* {@link module:image/imageinsertviaurl~ImageInsertViaUrl ImageInsertViaUrl},
		* {@link module:ckbox/ckbox~CKBox CKBox}
		*
		* @error image-insert-integrations-not-registered
		*/
		logWarning("image-insert-integrations-not-registered");
		return result;
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imageblock
*/
/**
* The image block plugin.
*
* This is a "glue" plugin which loads the following plugins:
*
* * {@link module:image/image/imageblockediting~ImageBlockEditing},
* * {@link module:image/imagetextalternative~ImageTextAlternative}.
*
* Usually, it is used in conjunction with other plugins from this package. See the {@glink api/image package page}
* for more information.
*/
var ImageBlock = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [
			ImageBlockEditing,
			Widget,
			ImageTextAlternative,
			ImageInsertUI
		];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageBlock";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/image/imageinlineediting
*/
/**
* The image inline plugin.
*
* It registers:
*
* * `<imageInline>` as an inline element in the document schema, and allows `alt`, `src` and `srcset` attributes.
* * converters for editing and data pipelines.
* * {@link module:image/image/imagetypecommand~ImageTypeCommand `'imageTypeInline'`} command that converts block images into
* inline images.
*/
var ImageInlineEditing = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [
			ImageEditing,
			ImageSizeAttributes,
			ImageUtils,
			ImagePlaceholder,
			ClipboardPipeline
		];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageInlineEditing";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	init() {
		const editor = this.editor;
		editor.model.schema.register("imageInline", {
			inheritAllFrom: "$inlineObject",
			allowAttributes: [
				"alt",
				"src",
				"srcset"
			],
			disallowIn: ["caption"]
		});
		this._setupConversion();
		if (editor.plugins.has("ImageBlockEditing")) {
			editor.commands.add("imageTypeInline", new ImageTypeCommand(this.editor, "imageInline"));
			this._setupClipboardIntegration();
		}
	}
	/**
	* Configures conversion pipelines to support upcasting and downcasting
	* inline images (inline image widgets) and their attributes.
	*/
	_setupConversion() {
		const editor = this.editor;
		const t = editor.t;
		const conversion = editor.conversion;
		const imageUtils = editor.plugins.get("ImageUtils");
		conversion.for("dataDowncast").elementToElement({
			model: "imageInline",
			view: (modelElement, { writer }) => writer.createEmptyElement("img")
		});
		conversion.for("editingDowncast").elementToStructure({
			model: "imageInline",
			view: (modelElement, { writer }) => imageUtils.toImageWidget(createInlineImageViewElement(writer), writer, t("image widget"))
		});
		conversion.for("downcast").add(downcastImageAttribute(imageUtils, "imageInline", "src")).add(downcastImageAttribute(imageUtils, "imageInline", "alt")).add(downcastSrcsetAttribute(imageUtils, "imageInline"));
		conversion.for("upcast").add(upcastImg("imageInline", imageUtils));
	}
	/**
	* Integrates the plugin with the clipboard pipeline.
	*
	* Idea is that the feature should recognize the user's intent when an **block** image is
	* pasted or dropped. If such an image is pasted/dropped into a non-empty block
	* (e.g. a paragraph with some text) it gets converted into an inline image on the fly.
	*
	* We assume this is the user's intent if they decided to put their image there.
	*
	* **Note**: If a block image has a caption, it will not be converted to an inline image
	* to avoid the confusion. Captions are added on purpose and they should never be lost
	* in the clipboard pipeline.
	*
	* See the `ImageBlockEditing` for the similar integration that works in the opposite direction.
	*
	* The feature also sets image `width` and `height` attributes when pasting.
	*/
	_setupClipboardIntegration() {
		const editor = this.editor;
		const model = editor.model;
		const editingView = editor.editing.view;
		const imageUtils = editor.plugins.get("ImageUtils");
		const clipboardPipeline = editor.plugins.get("ClipboardPipeline");
		this.listenTo(clipboardPipeline, "inputTransformation", (evt, data) => {
			const docFragmentChildren = Array.from(data.content.getChildren());
			let modelRange;
			if (!docFragmentChildren.every(imageUtils.isBlockImageView)) return;
			if (data.targetRanges) modelRange = editor.editing.mapper.toModelRange(data.targetRanges[0]);
			else modelRange = model.document.selection.getFirstRange();
			const selection = model.createSelection(modelRange);
			const selectionPosition = selection.getFirstPosition();
			if (!(!!selectionPosition && isImageTypePlaceable(model.schema, selectionPosition, "imageBlock")) || determineImageTypeForInsertionAtSelection(model.schema, selection) === "imageInline") {
				const writer = new ViewUpcastWriter(editingView.document);
				const inlineViewImages = docFragmentChildren.map((blockViewImage) => {
					if (blockViewImage.childCount === 1) {
						Array.from(blockViewImage.getAttributes()).forEach((attribute) => writer.setAttribute(...attribute, imageUtils.findViewImgElement(blockViewImage)));
						return blockViewImage.getChild(0);
					} else return blockViewImage;
				});
				data.content = writer.createDocumentFragment(inlineViewImages);
			}
		});
		this.listenTo(clipboardPipeline, "contentInsertion", (evt, data) => {
			if (data.method !== "paste") return;
			model.change((writer) => {
				const range = writer.createRangeIn(data.content);
				for (const item of range.getItems()) if (item.is("element", "imageInline")) imageUtils.setImageNaturalSizeAttributes(item);
			});
		});
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imageinline
*/
/**
* The image inline plugin.
*
* This is a "glue" plugin which loads the following plugins:
*
* * {@link module:image/image/imageinlineediting~ImageInlineEditing},
* * {@link module:image/imagetextalternative~ImageTextAlternative}.
*
* Usually, it is used in conjunction with other plugins from this package. See the {@glink api/image package page}
* for more information.
*/
var ImageInline = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [
			ImageInlineEditing,
			Widget,
			ImageTextAlternative,
			ImageInsertUI
		];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageInline";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/image
*/
/**
* The image plugin.
*
* For a detailed overview, check the {@glink features/images/images-overview image feature} documentation.
*
* This is a "glue" plugin which loads the following plugins:
*
* * {@link module:image/imageblock~ImageBlock},
* * {@link module:image/imageinline~ImageInline},
*
* Usually, it is used in conjunction with other plugins from this package. See the {@glink api/image package page}
* for more information.
*/
var Image = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [ImageBlock, ImageInline];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "Image";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* The image caption utilities plugin.
*/
var ImageCaptionUtils = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageCaptionUtils";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [ImageUtils];
	}
	/**
	* Returns the caption model element from a given image element. Returns `null` if no caption is found.
	*/
	getCaptionFromImageModelElement(imageModelElement) {
		for (const node of imageModelElement.getChildren()) if (!!node && node.is("element", "caption")) return node;
		return null;
	}
	/**
	* Returns the caption model element for a model selection. Returns `null` if the selection has no caption element ancestor.
	*/
	getCaptionFromModelSelection(selection) {
		const imageUtils = this.editor.plugins.get("ImageUtils");
		const captionElement = selection.getFirstPosition().findAncestor("caption");
		if (!captionElement) return null;
		if (imageUtils.isBlockImage(captionElement.parent)) return captionElement;
		return null;
	}
	/**
	* {@link module:engine/view/matcher~Matcher} pattern. Checks if a given element is a `<figcaption>` element that is placed
	* inside the image `<figure>` element.
	* @returns Returns the object accepted by {@link module:engine/view/matcher~Matcher} or `null` if the element
	* cannot be matched.
	*/
	matchImageCaptionViewElement(element) {
		const imageUtils = this.editor.plugins.get("ImageUtils");
		if (element.name == "figcaption" && imageUtils.isBlockImageView(element.parent)) return { name: true };
		return null;
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* The toggle image caption command.
*
* This command is registered by {@link module:image/imagecaption/imagecaptionediting~ImageCaptionEditing} as the
* `'toggleImageCaption'` editor command.
*
* Executing this command:
*
* * either adds or removes the image caption of a selected image (depending on whether the caption is present or not),
* * removes the image caption if the selection is anchored in one.
*
* ```ts
* // Toggle the presence of the caption.
* editor.execute( 'toggleImageCaption' );
* ```
*
* **Note**: Upon executing this command, the selection will be set on the image if previously anchored in the caption element.
*
* **Note**: You can move the selection to the caption right away as it shows up upon executing this command by using
* the `focusCaptionOnShow` option:
*
* ```ts
* editor.execute( 'toggleImageCaption', { focusCaptionOnShow: true } );
* ```
*/
var ToggleImageCaptionCommand = class extends Command {
	/**
	* @inheritDoc
	*/
	refresh() {
		const editor = this.editor;
		const imageCaptionUtils = editor.plugins.get("ImageCaptionUtils");
		const imageUtils = editor.plugins.get("ImageUtils");
		if (!editor.plugins.has(ImageBlockEditing)) {
			this.isEnabled = false;
			this.value = false;
			return;
		}
		const selection = editor.model.document.selection;
		const selectedElement = selection.getSelectedElement();
		if (!selectedElement) {
			const ancestorCaptionElement = imageCaptionUtils.getCaptionFromModelSelection(selection);
			this.isEnabled = !!ancestorCaptionElement;
			this.value = !!ancestorCaptionElement;
			return;
		}
		this.isEnabled = imageUtils.isImage(selectedElement);
		if (this.isEnabled && imageUtils.isInlineImage(selectedElement)) {
			const position = editor.model.createPositionBefore(selectedElement);
			this.isEnabled = isImageTypePlaceable(editor.model.schema, position, "imageBlock");
		}
		if (!this.isEnabled) this.value = false;
		else this.value = !!imageCaptionUtils.getCaptionFromImageModelElement(selectedElement);
	}
	/**
	* Executes the command.
	*
	* ```ts
	* editor.execute( 'toggleImageCaption' );
	* ```
	*
	* @param options Options for the executed command.
	* @param options.focusCaptionOnShow When true and the caption shows up, the selection will be moved into it straight away.
	* @fires execute
	*/
	execute(options = {}) {
		const { focusCaptionOnShow } = options;
		this.editor.model.change((writer) => {
			if (this.value) this._hideImageCaption(writer);
			else this._showImageCaption(writer, focusCaptionOnShow);
		});
	}
	/**
	* Shows the caption of the `<imageBlock>` or `<imageInline>`. Also:
	*
	* * it converts `<imageInline>` to `<imageBlock>` to show the caption,
	* * it attempts to restore the caption content from the `ImageCaptionEditing` caption registry,
	* * it moves the selection to the caption right away, it the `focusCaptionOnShow` option was set.
	*/
	_showImageCaption(writer, focusCaptionOnShow) {
		const selection = this.editor.model.document.selection;
		const imageCaptionEditing = this.editor.plugins.get("ImageCaptionEditing");
		const imageUtils = this.editor.plugins.get("ImageUtils");
		let selectedImage = selection.getSelectedElement();
		const savedCaption = imageCaptionEditing._getSavedCaption(selectedImage);
		if (imageUtils.isInlineImage(selectedImage)) {
			this.editor.execute("imageTypeBlock");
			selectedImage = selection.getSelectedElement();
		}
		const newCaptionElement = savedCaption || writer.createElement("caption");
		writer.append(newCaptionElement, selectedImage);
		if (focusCaptionOnShow) writer.setSelection(newCaptionElement, "in");
	}
	/**
	* Hides the caption of a selected image (or an image caption the selection is anchored to).
	*
	* The content of the caption is stored in the `ImageCaptionEditing` caption registry to make this
	* a reversible action.
	*/
	_hideImageCaption(writer) {
		const editor = this.editor;
		const selection = editor.model.document.selection;
		const imageCaptionEditing = editor.plugins.get("ImageCaptionEditing");
		const imageCaptionUtils = editor.plugins.get("ImageCaptionUtils");
		let selectedImage = selection.getSelectedElement();
		let captionElement;
		if (selectedImage) captionElement = imageCaptionUtils.getCaptionFromImageModelElement(selectedImage);
		else {
			captionElement = imageCaptionUtils.getCaptionFromModelSelection(selection);
			selectedImage = captionElement.parent;
		}
		imageCaptionEditing._saveCaption(selectedImage, captionElement);
		writer.setSelection(selectedImage, "on");
		writer.remove(captionElement);
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imagecaption/imagecaptionediting
*/
/**
* The image caption engine plugin. It is responsible for:
*
* * registering converters for the caption element,
* * registering converters for the caption model attribute,
* * registering the {@link module:image/imagecaption/toggleimagecaptioncommand~ToggleImageCaptionCommand `toggleImageCaption`} command.
*/
var ImageCaptionEditing = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [ImageUtils, ImageCaptionUtils];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageCaptionEditing";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* A map that keeps saved JSONified image captions and image model elements they are
	* associated with.
	*
	* To learn more about this system, see {@link #_saveCaption}.
	*/
	_savedCaptionsMap;
	/**
	* @inheritDoc
	*/
	constructor(editor) {
		super(editor);
		this._savedCaptionsMap = /* @__PURE__ */ new WeakMap();
	}
	/**
	* @inheritDoc
	*/
	init() {
		const editor = this.editor;
		const schema = editor.model.schema;
		if (!schema.isRegistered("caption")) schema.register("caption", {
			allowIn: "imageBlock",
			allowContentOf: "$block",
			isLimit: true
		});
		else schema.extend("caption", { allowIn: "imageBlock" });
		editor.commands.add("toggleImageCaption", new ToggleImageCaptionCommand(this.editor));
		this._setupConversion();
		this._setupImageTypeCommandsIntegration();
		this._registerCaptionReconversion();
	}
	/**
	* Configures conversion pipelines to support upcasting and downcasting
	* image captions.
	*/
	_setupConversion() {
		const editor = this.editor;
		const view = editor.editing.view;
		const imageUtils = editor.plugins.get("ImageUtils");
		const imageCaptionUtils = editor.plugins.get("ImageCaptionUtils");
		const t = editor.t;
		editor.conversion.for("upcast").elementToElement({
			view: (element) => imageCaptionUtils.matchImageCaptionViewElement(element),
			model: "caption"
		});
		editor.conversion.for("dataDowncast").elementToElement({
			model: "caption",
			view: (modelElement, { writer }) => {
				if (!imageUtils.isBlockImage(modelElement.parent)) return null;
				return writer.createContainerElement("figcaption");
			}
		});
		editor.conversion.for("editingDowncast").elementToElement({
			model: "caption",
			view: (modelElement, { writer }) => {
				if (!imageUtils.isBlockImage(modelElement.parent)) return null;
				const figcaptionElement = writer.createEditableElement("figcaption");
				writer.setCustomProperty("imageCaption", true, figcaptionElement);
				figcaptionElement.placeholder = t("Enter image caption");
				enableViewPlaceholder({
					view,
					element: figcaptionElement,
					keepOnFocus: true
				});
				const imageAlt = modelElement.parent.getAttribute("alt");
				return toWidgetEditable(figcaptionElement, writer, { label: imageAlt ? t("Caption for image: %0", [imageAlt]) : t("Caption for the image") });
			}
		});
	}
	/**
	* Integrates with {@link module:image/image/imagetypecommand~ImageTypeCommand image type commands}
	* to make sure the caption is preserved when the type of an image changes so it can be restored
	* in the future if the user decides they want their caption back.
	*/
	_setupImageTypeCommandsIntegration() {
		const editor = this.editor;
		const imageUtils = editor.plugins.get("ImageUtils");
		const imageCaptionUtils = editor.plugins.get("ImageCaptionUtils");
		const imageTypeInlineCommand = editor.commands.get("imageTypeInline");
		const imageTypeBlockCommand = editor.commands.get("imageTypeBlock");
		const handleImageTypeChange = (evt) => {
			if (!evt.return) return;
			const { oldElement, newElement } = evt.return;
			/* istanbul ignore if: paranoid check -- @preserve */
			if (!oldElement) return;
			if (imageUtils.isBlockImage(oldElement)) {
				const oldCaptionElement = imageCaptionUtils.getCaptionFromImageModelElement(oldElement);
				if (oldCaptionElement) {
					this._saveCaption(newElement, oldCaptionElement);
					return;
				}
			}
			const savedOldElementCaption = this._getSavedCaption(oldElement);
			if (savedOldElementCaption) this._saveCaption(newElement, savedOldElementCaption);
		};
		if (imageTypeInlineCommand) this.listenTo(imageTypeInlineCommand, "execute", handleImageTypeChange, { priority: "low" });
		if (imageTypeBlockCommand) this.listenTo(imageTypeBlockCommand, "execute", handleImageTypeChange, { priority: "low" });
	}
	/**
	* Returns the saved {@link module:engine/model/element~ModelElement#toJSON JSONified} caption
	* of an image model element.
	*
	* See {@link #_saveCaption}.
	*
	* @internal
	* @param imageModelElement The model element the caption should be returned for.
	* @returns The model caption element or `null` if there is none.
	*/
	_getSavedCaption(imageModelElement) {
		const jsonObject = this._savedCaptionsMap.get(imageModelElement);
		return jsonObject ? ModelElement.fromJSON(jsonObject) : null;
	}
	/**
	* Saves a {@link module:engine/model/element~ModelElement#toJSON JSONified} caption for
	* an image element to allow restoring it in the future.
	*
	* A caption is saved every time it gets hidden and/or the type of an image changes. The
	* user should be able to restore it on demand.
	*
	* **Note**: The caption cannot be stored in the image model element attribute because,
	* for instance, when the model state propagates to collaborators, the attribute would get
	* lost (mainly because it does not convert to anything when the caption is hidden) and
	* the states of collaborators' models would de-synchronize causing numerous issues.
	*
	* See {@link #_getSavedCaption}.
	*
	* @internal
	* @param imageModelElement The model element the caption is saved for.
	* @param caption The caption model element to be saved.
	*/
	_saveCaption(imageModelElement, caption) {
		this._savedCaptionsMap.set(imageModelElement, caption.toJSON());
	}
	/**
	* Reconverts image caption when image alt attribute changes.
	* The change of alt attribute is reflected in caption's aria-label attribute.
	*/
	_registerCaptionReconversion() {
		const editor = this.editor;
		const model = editor.model;
		const imageUtils = editor.plugins.get("ImageUtils");
		const imageCaptionUtils = editor.plugins.get("ImageCaptionUtils");
		model.document.on("change:data", () => {
			const changes = model.document.differ.getChanges();
			for (const change of changes) {
				if (change.attributeKey !== "alt") continue;
				const image = change.range.start.nodeAfter;
				if (imageUtils.isBlockImage(image)) {
					const caption = imageCaptionUtils.getCaptionFromImageModelElement(image);
					if (!caption) return;
					editor.editing.reconvertItem(caption);
				}
			}
		});
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imagecaption/imagecaptionui
*/
/**
* The image caption UI plugin. It introduces the `'toggleImageCaption'` UI button.
*/
var ImageCaptionUI = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [ImageCaptionUtils];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageCaptionUI";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	init() {
		const editor = this.editor;
		const editingView = editor.editing.view;
		const imageCaptionUtils = editor.plugins.get("ImageCaptionUtils");
		const t = editor.t;
		editor.ui.componentFactory.add("toggleImageCaption", (locale) => {
			const command = editor.commands.get("toggleImageCaption");
			const view = new ButtonView(locale);
			view.set({
				icon: IconCaption,
				tooltip: true,
				isToggleable: true
			});
			view.bind("isOn", "isEnabled").to(command, "value", "isEnabled");
			view.bind("label").to(command, "value", (value) => value ? t("Toggle caption off") : t("Toggle caption on"));
			this.listenTo(view, "execute", () => {
				editor.execute("toggleImageCaption", { focusCaptionOnShow: true });
				const modelCaptionElement = imageCaptionUtils.getCaptionFromModelSelection(editor.model.document.selection);
				if (modelCaptionElement) {
					const figcaptionElement = editor.editing.mapper.toViewElement(modelCaptionElement);
					editingView.scrollToTheSelection();
					editingView.change((writer) => {
						writer.addClass("image__caption_highlighted", figcaptionElement);
					});
				}
				editor.editing.view.focus();
			});
			return view;
		});
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imagecaption
*/
/**
* The image caption plugin.
*
* For a detailed overview, check the {@glink features/images/images-captions image caption} documentation.
*/
var ImageCaption = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [ImageCaptionEditing, ImageCaptionUI];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageCaption";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* Creates a regular expression used to test for image files.
*
* ```ts
* const imageType = createImageTypeRegExp( [ 'png', 'jpeg', 'svg+xml', 'vnd.microsoft.icon' ] );
*
* console.log( 'is supported image', imageType.test( file.type ) );
* ```
*/
function createImageTypeRegExp(types) {
	const regExpSafeNames = types.map((type) => type.replace("+", "\\+"));
	return new RegExp(`^image\\/(${regExpSafeNames.join("|")})$`);
}
/**
* Creates a promise that fetches the image local source (Base64 or blob) and resolves with a `File` object.
*
* @internal
* @param image Image whose source to fetch.
* @returns A promise which resolves when an image source is fetched and converted to a `File` instance.
* It resolves with a `File` object. If there were any errors during file processing, the promise will be rejected.
*/
function fetchLocalImage(image) {
	return new Promise((resolve, reject) => {
		const imageSrc = image.getAttribute("src");
		fetch(imageSrc).then((resource) => resource.blob()).then((blob) => {
			const mimeType = getImageMimeType(blob, imageSrc);
			const filename = `image.${mimeType.replace("image/", "")}`;
			resolve(new File([blob], filename, { type: mimeType }));
		}).catch((err) => {
			return err && err.name === "TypeError" ? convertLocalImageOnCanvas(imageSrc).then(resolve).catch(reject) : reject(err);
		});
	});
}
/**
* Checks whether a given node is an image element with a local source (Base64 or blob).
*
* @param node The node to check.
* @internal
*/
function isLocalImage(imageUtils, node) {
	if (!imageUtils.isInlineImageView(node) || !node.getAttribute("src")) return false;
	return !!node.getAttribute("src").match(/^data:image\/\w+;base64,/g) || !!node.getAttribute("src").match(/^blob:/g);
}
/**
* Extracts an image type based on its blob representation or its source.
* @param blob Image blob representation.
* @param src Image `src` attribute value.
*/
function getImageMimeType(blob, src) {
	if (blob.type) return blob.type;
	else if (src.match(/data:(image\/\w+);base64/)) return src.match(/data:(image\/\w+);base64/)[1].toLowerCase();
	else return "image/jpeg";
}
/**
* Creates a promise that converts the image local source (Base64 or blob) to a blob using canvas and resolves
* with a `File` object.
* @param imageSrc Image `src` attribute value.
* @returns A promise which resolves when an image source is converted to a `File` instance.
* It resolves with a `File` object. If there were any errors during file processing, the promise will be rejected.
*/
function convertLocalImageOnCanvas(imageSrc) {
	return getBlobFromCanvas(imageSrc).then((blob) => {
		const mimeType = getImageMimeType(blob, imageSrc);
		const filename = `image.${mimeType.replace("image/", "")}`;
		return new File([blob], filename, { type: mimeType });
	});
}
/**
* Creates a promise that resolves with a `Blob` object converted from the image source (Base64 or blob).
* @param imageSrc Image `src` attribute value.
*/
function getBlobFromCanvas(imageSrc) {
	return new Promise((resolve, reject) => {
		const image = global.document.createElement("img");
		image.addEventListener("load", () => {
			const canvas = global.document.createElement("canvas");
			canvas.width = image.width;
			canvas.height = image.height;
			canvas.getContext("2d").drawImage(image, 0, 0);
			canvas.toBlob((blob) => blob ? resolve(blob) : reject());
		});
		image.addEventListener("error", () => reject());
		image.src = imageSrc;
	});
}

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imageupload/imageuploadui
*/
/**
* The image upload button plugin.
*
* For a detailed overview, check the {@glink features/images/image-upload/image-upload Image upload feature} documentation.
*
* Adds the `'uploadImage'` button to the {@link module:ui/componentfactory~ComponentFactory UI component factory}
* and also the `imageUpload` button as an alias for backward compatibility.
*
* Adds the `'menuBar:uploadImage'` menu button to the {@link module:ui/componentfactory~ComponentFactory UI component factory}.
*
* It also integrates with the `insertImage` toolbar component and `menuBar:insertImage` menu component, which are the default components
* through which image upload is available.
*/
var ImageUploadUI = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageUploadUI";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	init() {
		const editor = this.editor;
		editor.ui.componentFactory.add("uploadImage", () => this._createToolbarButton());
		editor.ui.componentFactory.add("imageUpload", () => this._createToolbarButton());
		editor.ui.componentFactory.add("menuBar:uploadImage", () => this._createMenuBarButton("standalone"));
		if (editor.plugins.has("ImageInsertUI")) editor.plugins.get("ImageInsertUI").registerIntegration({
			name: "upload",
			observable: () => editor.commands.get("uploadImage"),
			buttonViewCreator: () => this._createToolbarButton(),
			formViewCreator: () => this._createDropdownButton(),
			menuBarButtonViewCreator: (isOnly) => this._createMenuBarButton(isOnly ? "insertOnly" : "insertNested")
		});
	}
	/**
	* Creates the base for various kinds of the button component provided by this feature.
	*/
	_createButton(ButtonClass) {
		const editor = this.editor;
		const locale = editor.locale;
		const command = editor.commands.get("uploadImage");
		const imageTypes = editor.config.get("image.upload.types");
		const imageTypesRegExp = createImageTypeRegExp(imageTypes);
		const view = new ButtonClass(editor.locale);
		const t = locale.t;
		view.set({
			acceptedType: imageTypes.map((type) => `image/${type}`).join(","),
			allowMultipleFiles: true,
			label: t("Upload from computer"),
			icon: IconImageUpload
		});
		view.bind("isEnabled").to(command);
		view.on("done", (evt, files) => {
			const imagesToUpload = Array.from(files).filter((file) => imageTypesRegExp.test(file.type));
			if (imagesToUpload.length) {
				editor.execute("uploadImage", { file: imagesToUpload });
				editor.editing.view.focus();
			}
		});
		return view;
	}
	/**
	* Creates a simple toolbar button, with an icon and a tooltip.
	*/
	_createToolbarButton() {
		const t = this.editor.locale.t;
		const imageInsertUI = this.editor.plugins.get("ImageInsertUI");
		const uploadImageCommand = this.editor.commands.get("uploadImage");
		const button = this._createButton(FileDialogButtonView);
		button.tooltip = true;
		button.bind("label").to(imageInsertUI, "isImageSelected", uploadImageCommand, "isAccessAllowed", (isImageSelected, isAccessAllowed) => {
			if (!isAccessAllowed) return t("You have no image upload permissions.");
			return isImageSelected ? t("Replace image from computer") : t("Upload image from computer");
		});
		return button;
	}
	/**
	* Creates a button for the dropdown view, with an icon, text and no tooltip.
	*/
	_createDropdownButton() {
		const t = this.editor.locale.t;
		const imageInsertUI = this.editor.plugins.get("ImageInsertUI");
		const button = this._createButton(FileDialogButtonView);
		button.withText = true;
		button.bind("label").to(imageInsertUI, "isImageSelected", (isImageSelected) => isImageSelected ? t("Replace from computer") : t("Upload from computer"));
		button.on("execute", () => {
			imageInsertUI.dropdownView.isOpen = false;
		});
		return button;
	}
	/**
	* Creates a button for the menu bar.
	*/
	_createMenuBarButton(type) {
		const t = this.editor.locale.t;
		const button = this._createButton(MenuBarMenuListItemFileDialogButtonView);
		button.withText = true;
		switch (type) {
			case "standalone":
				button.label = t("Image from computer");
				break;
			case "insertOnly":
				button.label = t("Image");
				break;
			case "insertNested":
				button.label = t("From computer");
				break;
		}
		return button;
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imageupload/imageuploadprogress
*/
/**
* The image upload progress plugin.
* It shows a placeholder when the image is read from the disk and a progress bar while the image is uploading.
*/
var ImageUploadProgress = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageUploadProgress";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* The image placeholder that is displayed before real image data can be accessed.
	*
	* For the record, this image is a 1x1 px GIF with an aspect ratio set by CSS.
	*/
	placeholder;
	/**
	* @inheritDoc
	*/
	constructor(editor) {
		super(editor);
		this.placeholder = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
	}
	/**
	* @inheritDoc
	*/
	init() {
		const editor = this.editor;
		if (editor.plugins.has("ImageBlockEditing")) editor.editing.downcastDispatcher.on("attribute:uploadStatus:imageBlock", this.uploadStatusChange);
		if (editor.plugins.has("ImageInlineEditing")) editor.editing.downcastDispatcher.on("attribute:uploadStatus:imageInline", this.uploadStatusChange);
	}
	/**
	* This method is called each time the image `uploadStatus` attribute is changed.
	*
	* @param evt An object containing information about the fired event.
	* @param data Additional information about the change.
	*/
	uploadStatusChange = (evt, data, conversionApi) => {
		const editor = this.editor;
		const modelImage = data.item;
		const uploadId = modelImage.getAttribute("uploadId");
		if (!conversionApi.consumable.consume(data.item, evt.name)) return;
		const imageUtils = editor.plugins.get("ImageUtils");
		const fileRepository = editor.plugins.get(FileRepository);
		const status = uploadId ? data.attributeNewValue : null;
		const placeholder = this.placeholder;
		const viewFigure = editor.editing.mapper.toViewElement(modelImage);
		const viewWriter = conversionApi.writer;
		if (status == "reading") {
			_startAppearEffect(viewFigure, viewWriter);
			_showPlaceholder(imageUtils, placeholder, viewFigure, viewWriter);
			return;
		}
		if (status == "uploading") {
			const loader = fileRepository.loaders.get(uploadId);
			_startAppearEffect(viewFigure, viewWriter);
			if (!loader) _showPlaceholder(imageUtils, placeholder, viewFigure, viewWriter);
			else {
				_hidePlaceholder(viewFigure, viewWriter);
				_showProgressBar(viewFigure, viewWriter, loader, editor.editing.view);
				_displayLocalImage(imageUtils, viewFigure, viewWriter, loader);
			}
			return;
		}
		if (status == "complete" && fileRepository.loaders.get(uploadId)) _showCompleteIcon(viewFigure, viewWriter, editor.editing.view);
		_hideProgressBar(viewFigure, viewWriter);
		_hidePlaceholder(viewFigure, viewWriter);
		_stopAppearEffect(viewFigure, viewWriter);
	};
};
/**
* Adds ck-appear class to the image figure if one is not already applied.
*/
function _startAppearEffect(viewFigure, writer) {
	if (!viewFigure.hasClass("ck-appear")) writer.addClass("ck-appear", viewFigure);
}
/**
* Removes ck-appear class to the image figure if one is not already removed.
*/
function _stopAppearEffect(viewFigure, writer) {
	writer.removeClass("ck-appear", viewFigure);
}
/**
* Shows placeholder together with infinite progress bar on given image figure.
*/
function _showPlaceholder(imageUtils, placeholder, viewFigure, writer) {
	if (!viewFigure.hasClass("ck-image-upload-placeholder")) writer.addClass("ck-image-upload-placeholder", viewFigure);
	const viewImg = imageUtils.findViewImgElement(viewFigure);
	if (viewImg.getAttribute("src") !== placeholder) writer.setAttribute("src", placeholder, viewImg);
	if (!_getUIElement(viewFigure, "placeholder")) writer.insert(writer.createPositionAfter(viewImg), _createPlaceholder(writer));
}
/**
* Removes placeholder together with infinite progress bar on given image figure.
*/
function _hidePlaceholder(viewFigure, writer) {
	if (viewFigure.hasClass("ck-image-upload-placeholder")) writer.removeClass("ck-image-upload-placeholder", viewFigure);
	_removeUIElement(viewFigure, writer, "placeholder");
}
/**
* Shows progress bar displaying upload progress.
* Attaches it to the file loader to update when upload percentace is changed.
*/
function _showProgressBar(viewFigure, writer, loader, view) {
	const progressBar = _createProgressBar(writer);
	writer.insert(writer.createPositionAt(viewFigure, "end"), progressBar);
	loader.on("change:uploadedPercent", (evt, name, value) => {
		view.change((writer) => {
			writer.setStyle("width", value + "%", progressBar);
		});
	});
}
/**
* Hides upload progress bar.
*/
function _hideProgressBar(viewFigure, writer) {
	_removeUIElement(viewFigure, writer, "progressBar");
}
/**
* Shows complete icon and hides after a certain amount of time.
*/
function _showCompleteIcon(viewFigure, writer, view) {
	const completeIcon = writer.createUIElement("div", { class: "ck-image-upload-complete-icon" });
	writer.insert(writer.createPositionAt(viewFigure, "end"), completeIcon);
	setTimeout(() => {
		view.change((writer) => writer.remove(writer.createRangeOn(completeIcon)));
	}, 3e3);
}
/**
* Create progress bar element using {@link module:engine/view/uielement~ViewUIElement}.
*/
function _createProgressBar(writer) {
	const progressBar = writer.createUIElement("div", { class: "ck-progress-bar" });
	writer.setCustomProperty("progressBar", true, progressBar);
	return progressBar;
}
/**
* Create placeholder element using {@link module:engine/view/uielement~ViewUIElement}.
*/
function _createPlaceholder(writer) {
	const placeholder = writer.createUIElement("div", { class: "ck-upload-placeholder-loader" });
	writer.setCustomProperty("placeholder", true, placeholder);
	return placeholder;
}
/**
* Returns {@link module:engine/view/uielement~ViewUIElement} of given unique property from image figure element.
* Returns `undefined` if element is not found.
*/
function _getUIElement(imageFigure, uniqueProperty) {
	for (const child of imageFigure.getChildren()) if (child.getCustomProperty(uniqueProperty)) return child;
}
/**
* Removes {@link module:engine/view/uielement~ViewUIElement} of given unique property from image figure element.
*/
function _removeUIElement(viewFigure, writer, uniqueProperty) {
	const element = _getUIElement(viewFigure, uniqueProperty);
	if (element) writer.remove(writer.createRangeOn(element));
}
/**
* Displays local data from file loader.
*/
function _displayLocalImage(imageUtils, viewFigure, writer, loader) {
	if (loader.data) {
		const viewImg = imageUtils.findViewImgElement(viewFigure);
		writer.setAttribute("src", loader.data, viewImg);
	}
}

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imageupload/uploadimagecommand
*/
/**
* The upload image command.
*
* The command is registered by the {@link module:image/imageupload/imageuploadediting~ImageUploadEditing} plugin as `uploadImage`
* and it is also available via aliased `imageUpload` name.
*
* In order to upload an image at the current selection position
* (according to the {@link module:widget/utils~findOptimalInsertionRange} algorithm),
* execute the command and pass the native image file instance:
*
* ```ts
* this.listenTo( editor.editing.view.document, 'clipboardInput', ( evt, data ) => {
* 	// Assuming that only images were pasted:
* 	const images = Array.from( data.dataTransfer.files );
*
* 	// Upload the first image:
* 	editor.execute( 'uploadImage', { file: images[ 0 ] } );
* } );
* ```
*
* It is also possible to insert multiple images at once:
*
* ```ts
* editor.execute( 'uploadImage', {
* 	file: [
* 		file1,
* 		file2
* 	]
* } );
* ```
*/
var UploadImageCommand = class extends Command {
	/**
	* Creates an instance of the `imageUlpoad` command. When executed, the command upload one of
	* the currently selected image from computer.
	*
	* @param editor The editor instance.
	*/
	constructor(editor) {
		super(editor);
		this.set("isAccessAllowed", true);
	}
	/**
	* @inheritDoc
	*/
	refresh() {
		const editor = this.editor;
		const imageUtils = editor.plugins.get("ImageUtils");
		const selectedElement = editor.model.document.selection.getSelectedElement();
		this.isEnabled = imageUtils.isImageAllowed() || imageUtils.isImage(selectedElement);
	}
	/**
	* Executes the command.
	*
	* @fires execute
	* @param options Options for the executed command.
	* @param options.file The image file or an array of image files to upload.
	*/
	execute(options) {
		const files = toArray(options.file);
		const selection = this.editor.model.document.selection;
		const imageUtils = this.editor.plugins.get("ImageUtils");
		const selectionAttributes = Object.fromEntries(selection.getAttributes());
		files.forEach((file, index) => {
			const selectedElement = selection.getSelectedElement();
			if (index && selectedElement && imageUtils.isImage(selectedElement)) {
				const position = this.editor.model.createPositionAfter(selectedElement);
				this._uploadImage(file, selectionAttributes, position);
			} else this._uploadImage(file, selectionAttributes);
		});
	}
	/**
	* Handles uploading single file.
	*/
	_uploadImage(file, attributes, position) {
		const editor = this.editor;
		const loader = editor.plugins.get(FileRepository).createLoader(file);
		const imageUtils = editor.plugins.get("ImageUtils");
		if (!loader) return;
		imageUtils.insertImage({
			...attributes,
			uploadId: loader.id
		}, position);
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imageupload/imageuploadediting
*/
/**
* The editing part of the image upload feature. It registers the `'uploadImage'` command
* and the `imageUpload` command as an aliased name.
*
* When an image is uploaded, it fires the {@link ~ImageUploadEditing#event:uploadComplete `uploadComplete`} event
* that allows adding custom attributes to the {@link module:engine/model/element~ModelElement image element}.
*/
var ImageUploadEditing = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [
			FileRepository,
			Notification,
			ClipboardPipeline,
			ImageUtils
		];
	}
	static get pluginName() {
		return "ImageUploadEditing";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* An internal mapping of {@link module:upload/filerepository~FileLoader#id file loader UIDs} and
	* model elements during the upload.
	*
	* Model element of the uploaded image can change, for instance, when {@link module:image/image/imagetypecommand~ImageTypeCommand}
	* is executed as a result of adding caption or changing image style. As a result, the upload logic must keep track of the model
	* element (reference) and resolve the upload for the correct model element (instead of the one that landed in the `$graveyard`
	* after image type changed).
	*/
	_uploadImageElements;
	/**
	* An internal mapping of {@link module:upload/filerepository~FileLoader#id file loader UIDs} and
	* upload responses for handling images dragged during their upload process. When such images are later
	* dropped, their original upload IDs no longer exist in the registry (as the original upload completed).
	* This map preserves the upload responses to properly handle such cases.
	*/
	_uploadedImages = /* @__PURE__ */ new Map();
	/**
	* @inheritDoc
	*/
	constructor(editor) {
		super(editor);
		editor.config.define("image", { upload: { types: [
			"jpeg",
			"png",
			"gif",
			"bmp",
			"webp",
			"tiff"
		] } });
		this._uploadImageElements = /* @__PURE__ */ new Map();
	}
	/**
	* @inheritDoc
	*/
	init() {
		const editor = this.editor;
		const doc = editor.model.document;
		const conversion = editor.conversion;
		const fileRepository = editor.plugins.get(FileRepository);
		const imageUtils = editor.plugins.get("ImageUtils");
		const clipboardPipeline = editor.plugins.get("ClipboardPipeline");
		const imageTypes = createImageTypeRegExp(editor.config.get("image.upload.types"));
		const uploadImageCommand = new UploadImageCommand(editor);
		editor.commands.add("uploadImage", uploadImageCommand);
		editor.commands.add("imageUpload", uploadImageCommand);
		conversion.for("upcast").attributeToAttribute({
			view: {
				name: "img",
				key: "uploadId"
			},
			model: "uploadId"
		}).add((dispatcher) => dispatcher.on("element:img", (evt, data, conversionApi) => {
			if (!conversionApi.consumable.test(data.viewItem, { attributes: ["data-ck-upload-id"] })) return;
			const uploadId = data.viewItem.getAttribute("data-ck-upload-id");
			if (!uploadId) return;
			if (!data.modelRange) return;
			const [modelElement] = Array.from(data.modelRange.getItems({ shallow: true }));
			const loader = fileRepository.loaders.get(uploadId);
			/* v8 ignore else -- @preserve */
			if (modelElement) {
				conversionApi.writer.setAttribute("uploadId", uploadId, modelElement);
				conversionApi.consumable.consume(data.viewItem, { attributes: ["data-ck-upload-id"] });
				if (loader && loader.data) conversionApi.writer.setAttribute("uploadStatus", loader.status, modelElement);
			}
		}, { priority: "low" }));
		this.listenTo(editor.editing.view.document, "clipboardInput", (evt, data) => {
			if (isHtmlInDataTransfer(data.dataTransfer)) return;
			const images = Array.from(data.dataTransfer.files).filter((file) => {
				if (!file) return false;
				return imageTypes.test(file.type);
			});
			if (!images.length) return;
			evt.stop();
			editor.model.change((writer) => {
				if (data.targetRanges) writer.setSelection(data.targetRanges.map((viewRange) => editor.editing.mapper.toModelRange(viewRange)));
				editor.execute("uploadImage", { file: images });
			});
			if (!editor.commands.get("uploadImage").isAccessAllowed) {
				const notification = editor.plugins.get("Notification");
				const t = editor.locale.t;
				notification.showWarning(t("You have no image upload permissions."), { namespace: "image" });
			}
		});
		this.listenTo(clipboardPipeline, "inputTransformation", (evt, data) => {
			const fetchableImages = Array.from(editor.editing.view.createRangeIn(data.content)).map((value) => value.item).filter((viewElement) => isLocalImage(imageUtils, viewElement) && !viewElement.getAttribute("uploadProcessed")).map((viewElement) => {
				return {
					promise: fetchLocalImage(viewElement),
					imageElement: viewElement
				};
			});
			if (!fetchableImages.length) return;
			const writer = new ViewUpcastWriter(editor.editing.view.document);
			for (const fetchableImage of fetchableImages) {
				writer.setAttribute("uploadProcessed", true, fetchableImage.imageElement);
				const loader = fileRepository.createLoader(fetchableImage.promise);
				if (loader) {
					writer.setAttribute("src", "", fetchableImage.imageElement);
					writer.setAttribute("uploadId", loader.id, fetchableImage.imageElement);
				}
			}
		});
		editor.editing.view.document.on("dragover", (evt, data) => {
			data.preventDefault();
		});
		doc.on("change", () => {
			const changes = doc.differ.getChanges({ includeChangesInGraveyard: true }).reverse();
			const insertedImagesIds = /* @__PURE__ */ new Set();
			for (const entry of changes) if (entry.type == "insert" && entry.name != "$text") {
				const item = entry.position.nodeAfter;
				const isInsertedInGraveyard = entry.position.root.rootName == "$graveyard";
				for (const imageElement of getImagesFromChangeItem(editor, item)) {
					const uploadId = imageElement.getAttribute("uploadId");
					const uploadStatus = imageElement.getAttribute("uploadStatus");
					if (!uploadId || uploadStatus == "complete") continue;
					const loader = fileRepository.loaders.get(uploadId);
					if (!loader) {
						if (!isInsertedInGraveyard && this._uploadedImages.has(uploadId)) editor.model.enqueueChange({ isUndoable: false }, (writer) => {
							writer.setAttribute("uploadStatus", "complete", imageElement);
							this.fire("uploadComplete", {
								data: this._uploadedImages.get(uploadId),
								imageElement
							});
						});
						continue;
					}
					if (isInsertedInGraveyard) {
						if (!insertedImagesIds.has(uploadId)) {
							/* v8 ignore else -- @preserve */
							if (Array.from(this._uploadImageElements.get(uploadId)).every((element) => element.root.rootName == "$graveyard")) loader.abort();
						}
					} else {
						insertedImagesIds.add(uploadId);
						if (!this._uploadImageElements.has(uploadId)) this._uploadImageElements.set(uploadId, /* @__PURE__ */ new Set([imageElement]));
						else this._uploadImageElements.get(uploadId).add(imageElement);
						if (loader.status == "idle") this._readAndUpload(loader);
					}
				}
			}
		});
		this.on("uploadComplete", (evt, { imageElement, data }) => {
			const urls = data.urls ? data.urls : data;
			this.editor.model.change((writer) => {
				writer.setAttribute("src", urls.default, imageElement);
				this._parseAndSetSrcsetAttributeOnImage(urls, imageElement, writer);
				imageUtils.setImageNaturalSizeAttributes(imageElement);
			});
		}, { priority: "low" });
	}
	/**
	* @inheritDoc
	*/
	afterInit() {
		const schema = this.editor.model.schema;
		if (this.editor.plugins.has("ImageBlockEditing")) {
			schema.extend("imageBlock", { allowAttributes: ["uploadId", "uploadStatus"] });
			this._registerConverters("imageBlock");
		}
		if (this.editor.plugins.has("ImageInlineEditing")) {
			schema.extend("imageInline", { allowAttributes: ["uploadId", "uploadStatus"] });
			this._registerConverters("imageInline");
		}
	}
	/**
	* Reads and uploads an image.
	*
	* The image is read from the disk and as a Base64-encoded string it is set temporarily to
	* `image[src]`. When the image is successfully uploaded, the temporary data is replaced with the target
	* image's URL (the URL to the uploaded image on the server).
	*/
	_readAndUpload(loader) {
		const editor = this.editor;
		const model = editor.model;
		const t = editor.locale.t;
		const fileRepository = editor.plugins.get(FileRepository);
		const notification = editor.plugins.get(Notification);
		const imageUtils = editor.plugins.get("ImageUtils");
		const imageUploadElements = this._uploadImageElements;
		model.enqueueChange({ isUndoable: false }, (writer) => {
			const elements = imageUploadElements.get(loader.id);
			for (const element of elements) writer.setAttribute("uploadStatus", "reading", element);
		});
		return loader.read().then(() => {
			const promise = loader.upload();
			/* v8 ignore else -- @preserve */
			if (editor.ui) editor.ui.ariaLiveAnnouncer.announce(t("Uploading image"));
			for (const imageElement of imageUploadElements.get(loader.id)) {
				/* istanbul ignore next -- @preserve */
				if (env.isSafari) {
					const viewFigure = editor.editing.mapper.toViewElement(imageElement);
					const viewImg = imageUtils.findViewImgElement(viewFigure);
					editor.editing.view.once("render", () => {
						if (!viewImg.parent) return;
						const domFigure = editor.editing.view.domConverter.mapViewToDom(viewImg.parent);
						if (!domFigure) return;
						const originalDisplay = domFigure.style.display;
						domFigure.style.display = "none";
						domFigure._ckHack = domFigure.offsetHeight;
						domFigure.style.display = originalDisplay;
					});
				}
				model.enqueueChange({ isUndoable: false }, (writer) => {
					writer.setAttribute("uploadStatus", "uploading", imageElement);
				});
			}
			return promise;
		}).then((data) => {
			model.enqueueChange({ isUndoable: false }, (writer) => {
				for (const imageElement of imageUploadElements.get(loader.id)) {
					writer.setAttribute("uploadStatus", "complete", imageElement);
					this.fire("uploadComplete", {
						data,
						imageElement
					});
				}
				/* v8 ignore else -- @preserve */
				if (editor.ui) editor.ui.ariaLiveAnnouncer.announce(t("Image upload complete"));
				this._uploadedImages.set(loader.id, data);
			});
			clean();
		}).catch((error) => {
			/* v8 ignore else -- @preserve */
			if (editor.ui) editor.ui.ariaLiveAnnouncer.announce(t("Error during image upload"));
			if (loader.status !== "error" && loader.status !== "aborted") throw error;
			if (loader.status == "error" && error) notification.showWarning(error, {
				title: t("Upload failed"),
				namespace: "upload"
			});
			model.enqueueChange({ isUndoable: false }, (writer) => {
				for (const imageElement of imageUploadElements.get(loader.id)) if (imageElement.root.rootName !== "$graveyard") writer.remove(imageElement);
			});
			clean();
		});
		function clean() {
			model.enqueueChange({ isUndoable: false }, (writer) => {
				for (const imageElement of imageUploadElements.get(loader.id)) {
					writer.removeAttribute("uploadId", imageElement);
					writer.removeAttribute("uploadStatus", imageElement);
				}
				imageUploadElements.delete(loader.id);
			});
			fileRepository.destroyLoader(loader);
		}
	}
	/**
	* Creates the `srcset` attribute based on a given file upload response and sets it as an attribute to a specific image element.
	*
	* @param data Data object from which `srcset` will be created.
	* @param image The image element on which the `srcset` attribute will be set.
	*/
	_parseAndSetSrcsetAttributeOnImage(data, image, writer) {
		let maxWidth = 0;
		const srcsetAttribute = Object.keys(data).filter((key) => {
			const width = parseInt(key, 10);
			if (!isNaN(width)) {
				maxWidth = Math.max(maxWidth, width);
				return true;
			}
		}).map((key) => `${data[key]} ${key}w`).join(", ");
		if (srcsetAttribute != "") {
			const attributes = { srcset: srcsetAttribute };
			if (!image.hasAttribute("width") && !image.hasAttribute("height")) attributes.width = maxWidth;
			writer.setAttributes(attributes, image);
		}
	}
	/**
	* Registers image upload converters.
	*
	* @param imageType The type of the image.
	*/
	_registerConverters(imageType) {
		const { conversion, plugins } = this.editor;
		const fileRepository = plugins.get(FileRepository);
		const imageUtils = plugins.get(ImageUtils);
		conversion.for("dataDowncast").add((dispatcher) => {
			dispatcher.on(`attribute:uploadId:${imageType}`, (evt, data, conversionApi) => {
				if (!conversionApi.consumable.test(data.item, evt.name)) return;
				const loader = fileRepository.loaders.get(data.attributeNewValue);
				if (!loader || !loader.data) return null;
				const viewElement = conversionApi.mapper.toViewElement(data.item);
				const img = imageUtils.findViewImgElement(viewElement);
				/* v8 ignore else -- @preserve */
				if (img) {
					conversionApi.consumable.consume(data.item, evt.name);
					conversionApi.writer.setAttribute("data-ck-upload-id", loader.id, img);
				}
			});
		});
	}
};
/**
* TODO move this to the clipboard package.
*
* Returns `true` if non-empty `text/html` is included in the data transfer.
*/
function isHtmlInDataTransfer(dataTransfer) {
	return Array.from(dataTransfer.types).includes("text/html") && dataTransfer.getData("text/html") !== "";
}
function getImagesFromChangeItem(editor, item) {
	const imageUtils = editor.plugins.get("ImageUtils");
	return Array.from(editor.model.createRangeOn(item)).filter((value) => imageUtils.isImage(value.item)).map((value) => value.item);
}

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imageupload
*/
/**
* The image upload plugin.
*
* For a detailed overview, check the {@glink features/images/image-upload/image-upload image upload feature} documentation.
*
* This plugin does not do anything directly, but it loads a set of specific plugins to enable image uploading:
*
* * {@link module:image/imageupload/imageuploadediting~ImageUploadEditing},
* * {@link module:image/imageupload/imageuploadui~ImageUploadUI},
* * {@link module:image/imageupload/imageuploadprogress~ImageUploadProgress}.
*/
var ImageUpload = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageUpload";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [
			ImageUploadEditing,
			ImageUploadUI,
			ImageUploadProgress
		];
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imageinsert/ui/imageinserturlview
*/
/**
* The insert an image via URL view.
*
* See {@link module:image/imageinsert/imageinsertviaurlui~ImageInsertViaUrlUI}.
*
* @internal
*/
var ImageInsertUrlView = class extends View {
	/**
	* The URL input field view.
	*/
	urlInputView;
	/**
	* An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
	*/
	keystrokes;
	/**
	* Creates a view for the dropdown panel of {@link module:image/imageinsert/imageinsertui~ImageInsertUI}.
	*
	* @param locale The localization services instance.
	*/
	constructor(locale) {
		super(locale);
		this.set("imageURLInputValue", "");
		this.set("isImageSelected", false);
		this.set("isEnabled", true);
		this.keystrokes = new KeystrokeHandler();
		this.urlInputView = this._createUrlInputView();
		this.setTemplate({
			tag: "form",
			attributes: {
				class: ["ck", "ck-image-insert-url"],
				tabindex: "-1"
			},
			children: [this.urlInputView, {
				tag: "div",
				attributes: { class: ["ck", "ck-image-insert-url__action-row"] }
			}]
		});
	}
	/**
	* @inheritDoc
	*/
	render() {
		super.render();
		submitHandler({ view: this });
		this.keystrokes.listenTo(this.element);
	}
	/**
	* @inheritDoc
	*/
	destroy() {
		super.destroy();
		this.keystrokes.destroy();
	}
	/**
	* Creates the {@link #urlInputView}.
	*/
	_createUrlInputView() {
		const locale = this.locale;
		const t = locale.t;
		const urlInputView = new LabeledFieldView(locale, createLabeledInputText);
		urlInputView.bind("label").to(this, "isImageSelected", (value) => value ? t("Update image URL") : t("Insert image via URL"));
		urlInputView.bind("isEnabled").to(this);
		urlInputView.fieldView.inputMode = "url";
		urlInputView.fieldView.placeholder = "https://example.com/image.png";
		urlInputView.fieldView.bind("value").to(this, "imageURLInputValue", (value) => value || "");
		urlInputView.fieldView.on("input", () => {
			this.imageURLInputValue = urlInputView.fieldView.element.value.trim();
		});
		return urlInputView;
	}
	/**
	* Focuses the view.
	*/
	focus() {
		this.urlInputView.focus();
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imageinsert/imageinsertviaurlui
*/
/**
* The image insert via URL plugin (UI part).
*
* The plugin introduces two UI components to the {@link module:ui/componentfactory~ComponentFactory UI component factory}:
*
* * the `'insertImageViaUrl'` toolbar button,
* * the `'menuBar:insertImageViaUrl'` menu bar component.
*
* It also integrates with the `insertImage` toolbar component and `menuBar:insertImage` menu component, which are default components
* through which inserting image via URL is available.
*/
var ImageInsertViaUrlUI = class extends Plugin {
	_imageInsertUI;
	_formView;
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageInsertViaUrlUI";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [ImageInsertUI, Dialog];
	}
	init() {
		this.editor.ui.componentFactory.add("insertImageViaUrl", () => this._createToolbarButton());
		this.editor.ui.componentFactory.add("menuBar:insertImageViaUrl", () => this._createMenuBarButton("standalone"));
	}
	/**
	* @inheritDoc
	*/
	afterInit() {
		this._imageInsertUI = this.editor.plugins.get("ImageInsertUI");
		this._imageInsertUI.registerIntegration({
			name: "url",
			observable: () => this.editor.commands.get("insertImage"),
			buttonViewCreator: () => this._createToolbarButton(),
			formViewCreator: () => this._createDropdownButton(),
			menuBarButtonViewCreator: (isOnly) => this._createMenuBarButton(isOnly ? "insertOnly" : "insertNested")
		});
	}
	/**
	* Creates the base for various kinds of the button component provided by this feature.
	*/
	_createInsertUrlButton(ButtonClass) {
		const button = new ButtonClass(this.editor.locale);
		button.icon = IconImageUrl;
		button.on("execute", () => {
			this._showModal();
		});
		return button;
	}
	/**
	* Creates a simple toolbar button, with an icon and a tooltip.
	*/
	_createToolbarButton() {
		const t = this.editor.locale.t;
		const button = this._createInsertUrlButton(ButtonView);
		button.tooltip = true;
		button.bind("label").to(this._imageInsertUI, "isImageSelected", (isImageSelected) => isImageSelected ? t("Update image URL") : t("Insert image via URL"));
		return button;
	}
	/**
	* Creates a button for the dropdown view, with an icon, text and no tooltip.
	*/
	_createDropdownButton() {
		const t = this.editor.locale.t;
		const button = this._createInsertUrlButton(ButtonView);
		button.withText = true;
		button.bind("label").to(this._imageInsertUI, "isImageSelected", (isImageSelected) => isImageSelected ? t("Update image URL") : t("Insert via URL"));
		return button;
	}
	/**
	* Creates a button for the menu bar.
	*/
	_createMenuBarButton(type) {
		const t = this.editor.locale.t;
		const button = this._createInsertUrlButton(MenuBarMenuListItemButtonView);
		button.withText = true;
		switch (type) {
			case "standalone":
				button.label = t("Image via URL");
				break;
			case "insertOnly":
				button.label = t("Image");
				break;
			case "insertNested":
				button.label = t("Via URL");
				break;
		}
		return button;
	}
	/**
	* Creates the form view used to submit the image URL.
	*/
	_createInsertUrlView() {
		const editor = this.editor;
		const locale = editor.locale;
		const replaceImageSourceCommand = editor.commands.get("replaceImageSource");
		const insertImageCommand = editor.commands.get("insertImage");
		const imageInsertUrlView = new ImageInsertUrlView(locale);
		imageInsertUrlView.bind("isImageSelected").to(this._imageInsertUI);
		imageInsertUrlView.bind("isEnabled").toMany([insertImageCommand, replaceImageSourceCommand], "isEnabled", (...isEnabled) => isEnabled.some((isCommandEnabled) => isCommandEnabled));
		return imageInsertUrlView;
	}
	/**
	* Shows the insert image via URL form view in a modal.
	*/
	_showModal() {
		const editor = this.editor;
		const t = editor.locale.t;
		const dialog = editor.plugins.get("Dialog");
		if (!this._formView) {
			this._formView = this._createInsertUrlView();
			this._formView.on("submit", () => this._handleSave());
		}
		const replaceImageSourceCommand = editor.commands.get("replaceImageSource");
		this._formView.imageURLInputValue = replaceImageSourceCommand.value || "";
		dialog.show({
			id: "insertImageViaUrl",
			title: t("Image via URL"),
			isModal: true,
			content: this._formView,
			actionButtons: [{
				label: t("Cancel"),
				withText: true,
				onExecute: () => dialog.hide()
			}, {
				label: this._imageInsertUI.isImageSelected ? t("Save") : t("Insert"),
				class: "ck-button-action",
				withText: true,
				onExecute: () => this._handleSave()
			}]
		});
	}
	/**
	* Executes appropriate command depending on selection and form value.
	*/
	_handleSave() {
		if (this.editor.commands.get("replaceImageSource").isEnabled) this.editor.execute("replaceImageSource", { source: this._formView.imageURLInputValue });
		else this.editor.execute("insertImage", { source: this._formView.imageURLInputValue });
		this.editor.plugins.get("Dialog").hide();
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imageinsertviaurl
*/
/**
* The image insert via URL plugin.
*
* For a detailed overview, check the {@glink features/images/images-inserting
* Insert images via source URL} documentation.
*
* This plugin does not do anything directly, but it loads a set of specific plugins
* to enable image inserting via implemented integrations:
*
* * {@link module:image/imageinsert/imageinsertui~ImageInsertUI},
*/
var ImageInsertViaUrl = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageInsertViaUrl";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [ImageInsertViaUrlUI, ImageInsertUI];
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imageinsert
*/
/**
* The image insert plugin.
*
* For a detailed overview, check the {@glink features/images/image-upload/image-upload Image upload feature}
* and {@glink features/images/images-inserting Insert images via source URL} documentation.
*
* This plugin does not do anything directly, but it loads a set of specific plugins
* to enable image uploading or inserting via implemented integrations:
*
* * {@link module:image/imageupload~ImageUpload}
* * {@link module:image/imageinsert/imageinsertui~ImageInsertUI}
*/
var ImageInsert = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageInsert";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [
			ImageUpload,
			ImageInsertViaUrl,
			ImageInsertUI
		];
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imageresize/resizeimagecommand
*/
/**
* The resize image command. Currently, it only supports the width attribute.
*/
var ResizeImageCommand = class extends Command {
	/**
	* @inheritDoc
	*/
	refresh() {
		const editor = this.editor;
		const element = editor.plugins.get("ImageUtils").getClosestSelectedImageElement(editor.model.document.selection);
		this.isEnabled = !!element;
		if (!element || !element.hasAttribute("resizedWidth")) this.value = null;
		else this.value = {
			width: element.getAttribute("resizedWidth"),
			height: null
		};
	}
	/**
	* Executes the command.
	*
	* ```ts
	* // Sets the width to 50%:
	* editor.execute( 'resizeImage', { width: '50%' } );
	*
	* // Removes the width attribute:
	* editor.execute( 'resizeImage', { width: null } );
	* ```
	*
	* @param options
	* @param options.width The new width of the image.
	* @fires execute
	*/
	execute(options) {
		const editor = this.editor;
		const model = editor.model;
		const imageUtils = editor.plugins.get("ImageUtils");
		const imageElement = imageUtils.getClosestSelectedImageElement(model.document.selection);
		this.value = {
			width: options.width,
			height: null
		};
		if (imageElement) model.change((writer) => {
			writer.setAttribute("resizedWidth", options.width, imageElement);
			writer.removeAttribute("resizedHeight", imageElement);
			imageUtils.setImageNaturalSizeAttributes(imageElement);
		});
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* The image resize editing feature.
*
* It adds the ability to resize each image using handles or manually by
* {@link module:image/imageresize/imageresizebuttons~ImageResizeButtons} buttons.
*/
var ImageResizeEditing = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [ImageUtils];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageResizeEditing";
	}
	/**
	* @inheritDoc
	* @internal
	*/
	static get licenseFeatureCode() {
		return "IR";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	static get isPremiumPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	constructor(editor) {
		super(editor);
		editor.config.define("image", {
			resizeUnit: "%",
			resizeOptions: [
				{
					name: "resizeImage:original",
					value: null,
					icon: "original"
				},
				{
					name: "resizeImage:custom",
					value: "custom",
					icon: "custom"
				},
				{
					name: "resizeImage:25",
					value: "25",
					icon: "small"
				},
				{
					name: "resizeImage:50",
					value: "50",
					icon: "medium"
				},
				{
					name: "resizeImage:75",
					value: "75",
					icon: "large"
				}
			]
		});
	}
	/**
	* @inheritDoc
	*/
	init() {
		const editor = this.editor;
		const resizeImageCommand = new ResizeImageCommand(editor);
		this._registerConverters("imageBlock");
		this._registerConverters("imageInline");
		editor.commands.add("resizeImage", resizeImageCommand);
		editor.commands.add("imageResize", resizeImageCommand);
	}
	/**
	* @inheritDoc
	*/
	afterInit() {
		this._registerSchema();
	}
	_registerSchema() {
		const schema = this.editor.model.schema;
		if (this.editor.plugins.has("ImageBlockEditing")) {
			schema.extend("imageBlock", { allowAttributes: ["resizedWidth", "resizedHeight"] });
			schema.setAttributeProperties("resizedWidth", { isFormatting: true });
			schema.setAttributeProperties("resizedHeight", { isFormatting: true });
		}
		if (this.editor.plugins.has("ImageInlineEditing")) {
			schema.extend("imageInline", { allowAttributes: ["resizedWidth", "resizedHeight"] });
			schema.setAttributeProperties("resizedWidth", { isFormatting: true });
			schema.setAttributeProperties("resizedHeight", { isFormatting: true });
		}
	}
	/**
	* Registers image resize converters.
	*
	* @param imageType The type of the image.
	*/
	_registerConverters(imageType) {
		const editor = this.editor;
		const imageUtils = editor.plugins.get("ImageUtils");
		editor.conversion.for("downcast").add((dispatcher) => dispatcher.on(`attribute:resizedWidth:${imageType}`, (evt, data, conversionApi) => {
			if (!conversionApi.consumable.consume(data.item, evt.name)) return;
			const viewWriter = conversionApi.writer;
			const viewImg = conversionApi.mapper.toViewElement(data.item);
			if (data.attributeNewValue !== null) {
				viewWriter.setStyle("width", data.attributeNewValue, viewImg);
				viewWriter.addClass("image_resized", viewImg);
			} else {
				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);
		}));
		editor.conversion.for("upcast").attributeToAttribute({
			view: {
				name: imageType === "imageBlock" ? "figure" : "img",
				styles: { width: /.+/ }
			},
			model: {
				key: "resizedWidth",
				value: (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) => {
					if (widthAndHeightStylesAreBothSet(viewElement)) return null;
					return viewElement.getStyle("height");
				}
			}
		});
		editor.conversion.for("upcast").add((dispatcher) => {
			dispatcher.on(`element:${imageType === "imageBlock" ? "figure" : "img"}`, (evt, data, conversionApi) => {
				conversionApi.consumable.consume(data.viewItem, { classes: ["image_resized"] });
			});
		});
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imageresize/imageresizebuttons
*/
const RESIZE_ICONS = /* #__PURE__ */ (() => ({
	small: IconObjectSizeSmall,
	medium: IconObjectSizeMedium,
	large: IconObjectSizeLarge,
	custom: IconObjectSizeCustom,
	original: IconObjectSizeFull
}))();
/**
* The image resize buttons plugin.
*
* It adds a possibility to resize images using the toolbar dropdown or individual buttons, depending on the plugin configuration.
*/
var ImageResizeButtons = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [ImageResizeEditing];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageResizeButtons";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* The resize unit.
	* @default '%'
	*/
	_resizeUnit;
	/**
	* @inheritDoc
	*/
	constructor(editor) {
		super(editor);
		this._resizeUnit = editor.config.get("image.resizeUnit");
	}
	/**
	* @inheritDoc
	*/
	init() {
		const editor = this.editor;
		const options = editor.config.get("image.resizeOptions");
		const command = editor.commands.get("resizeImage");
		this.bind("isEnabled").to(command);
		for (const option of options) this._registerImageResizeButton(option);
		this._registerImageResizeDropdown(options);
	}
	/**
	* A helper function that creates a standalone button component for the plugin.
	*
	* @param option A model of the resize option.
	*/
	_registerImageResizeButton(option) {
		const editor = this.editor;
		const { name, value, icon } = option;
		editor.ui.componentFactory.add(name, (locale) => {
			const button = new ButtonView(locale);
			const command = editor.commands.get("resizeImage");
			const labelText = this._getOptionLabelValue(option, true);
			if (!RESIZE_ICONS[icon])
 /**
			* When configuring {@link module:image/imageconfig~ImageConfig#resizeOptions `config.image.resizeOptions`} for standalone
			* buttons, a valid `icon` token must be set for each option.
			*
			* See all valid options described in the
			* {@link module:image/imageconfig~ImageResizeOption plugin configuration}.
			*
			* @error imageresizebuttons-missing-icon
			* @param {module:image/imageconfig~ImageResizeOption} option Invalid image resize option.
			*/
			throw new CKEditorError("imageresizebuttons-missing-icon", editor, option);
			button.set({
				label: labelText,
				icon: RESIZE_ICONS[icon],
				tooltip: labelText,
				isToggleable: true
			});
			button.bind("isEnabled").to(this);
			if (editor.plugins.has("ImageCustomResizeUI") && isCustomImageResizeOption(option)) {
				const customResizeUI = editor.plugins.get("ImageCustomResizeUI");
				this.listenTo(button, "execute", () => {
					customResizeUI._showForm(this._resizeUnit);
				});
			} else {
				const optionValueWithUnit = value ? value + this._resizeUnit : null;
				button.bind("isOn").to(command, "value", command, "isEnabled", getIsOnButtonCallback(optionValueWithUnit));
				this.listenTo(button, "execute", () => {
					editor.execute("resizeImage", { width: optionValueWithUnit });
				});
			}
			return button;
		});
	}
	/**
	* A helper function that creates a dropdown component for the plugin containing all the resize options defined in
	* the editor configuration.
	*
	* @param options An array of configured options.
	*/
	_registerImageResizeDropdown(options) {
		const editor = this.editor;
		const t = editor.t;
		const originalSizeOption = options.find((option) => !option.value);
		const componentCreator = (locale) => {
			const command = editor.commands.get("resizeImage");
			const dropdownView = createDropdown(locale, DropdownButtonView);
			const dropdownButton = dropdownView.buttonView;
			const accessibleLabel = t("Resize image");
			dropdownButton.set({
				tooltip: accessibleLabel,
				commandValue: originalSizeOption.value,
				icon: RESIZE_ICONS.medium,
				isToggleable: true,
				label: this._getOptionLabelValue(originalSizeOption),
				withText: true,
				class: "ck-resize-image-button",
				ariaLabel: accessibleLabel,
				ariaLabelledBy: void 0
			});
			dropdownButton.bind("label").to(command, "value", (commandValue) => {
				if (commandValue && commandValue.width) return commandValue.width;
				else return this._getOptionLabelValue(originalSizeOption);
			});
			dropdownView.bind("isEnabled").to(this);
			addListToDropdown(dropdownView, () => this._getResizeDropdownListItemDefinitions(options, command), {
				ariaLabel: t("Image resize list"),
				role: "menu"
			});
			this.listenTo(dropdownView, "execute", (evt) => {
				if ("onClick" in evt.source) evt.source.onClick();
				else {
					editor.execute(evt.source.commandName, { width: evt.source.commandValue });
					editor.editing.view.focus();
				}
			});
			return dropdownView;
		};
		editor.ui.componentFactory.add("resizeImage", componentCreator);
		editor.ui.componentFactory.add("imageResize", componentCreator);
	}
	/**
	* A helper function for creating an option label value string.
	*
	* @param option A resize option object.
	* @param forTooltip An optional flag for creating a tooltip label.
	* @returns A user-defined label combined from the numeric value and the resize unit or the default label
	* for reset options (`Original`).
	*/
	_getOptionLabelValue(option, forTooltip = false) {
		const t = this.editor.t;
		if (option.label) return option.label;
		else if (forTooltip) if (isCustomImageResizeOption(option)) return t("Custom image size");
		else if (option.value) return t("Resize image to %0", option.value + this._resizeUnit);
		else return t("Resize image to the original size");
		else if (isCustomImageResizeOption(option)) return t("Custom");
		else if (option.value) return option.value + this._resizeUnit;
		else return t("Original");
	}
	/**
	* A helper function that parses the resize options and returns list item definitions ready for use in the dropdown.
	*
	* @param options The resize options.
	* @param command The resize image command.
	* @returns Dropdown item definitions.
	*/
	_getResizeDropdownListItemDefinitions(options, command) {
		const { editor } = this;
		const itemDefinitions = new Collection();
		const optionsWithSerializedValues = options.map((option) => {
			if (isCustomImageResizeOption(option)) return {
				...option,
				valueWithUnits: "custom"
			};
			if (!option.value) return {
				...option,
				valueWithUnits: null
			};
			return {
				...option,
				valueWithUnits: `${option.value}${this._resizeUnit}`
			};
		});
		for (const option of optionsWithSerializedValues) {
			let definition;
			if (editor.plugins.has("ImageCustomResizeUI") && isCustomImageResizeOption(option)) {
				const customResizeUI = editor.plugins.get("ImageCustomResizeUI");
				definition = {
					type: "button",
					model: new UIModel({
						label: this._getOptionLabelValue(option),
						role: "menuitemradio",
						withText: true,
						icon: null,
						onClick: () => {
							customResizeUI._showForm(this._resizeUnit);
						}
					})
				};
				const allDropdownValues = Object.values(optionsWithSerializedValues).map((option) => option.valueWithUnits);
				definition.model.bind("isOn").to(command, "value", command, "isEnabled", getIsOnCustomButtonCallback(allDropdownValues));
			} else {
				definition = {
					type: "button",
					model: new UIModel({
						commandName: "resizeImage",
						commandValue: option.valueWithUnits,
						label: this._getOptionLabelValue(option),
						role: "menuitemradio",
						withText: true,
						icon: null
					})
				};
				definition.model.bind("isOn").to(command, "value", command, "isEnabled", getIsOnButtonCallback(option.valueWithUnits));
			}
			definition.model.bind("isEnabled").to(command, "isEnabled");
			itemDefinitions.add(definition);
		}
		return itemDefinitions;
	}
};
/**
* A helper that checks if provided option triggers custom resize balloon.
*/
function isCustomImageResizeOption(option) {
	return option.value === "custom";
}
/**
* A helper function for setting the `isOn` state of buttons in value bindings.
*/
function getIsOnButtonCallback(value) {
	return (commandValue, isEnabled) => {
		const objectCommandValue = commandValue;
		if (objectCommandValue === void 0 || !isEnabled) return false;
		if (value === null && objectCommandValue === value) return true;
		return objectCommandValue !== null && objectCommandValue.width === value;
	};
}
/**
* A helper function for setting the `isOn` state of custom size button in value bindings.
*/
function getIsOnCustomButtonCallback(allDropdownValues) {
	return (commandValue, isEnabled) => !allDropdownValues.some((dropdownValue) => getIsOnButtonCallback(dropdownValue)(commandValue, isEnabled));
}

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
const RESIZABLE_IMAGES_CSS_SELECTOR = "figure.image.ck-widget > img,figure.image.ck-widget > picture > img,figure.image.ck-widget > a > img,figure.image.ck-widget > a > picture > img,span.image-inline.ck-widget > img,span.image-inline.ck-widget > picture > img";
const RESIZED_IMAGE_CLASS = "image_resized";
/**
* The image resize by handles feature.
*
* It adds the ability to resize each image using handles or manually by
* {@link module:image/imageresize/imageresizebuttons~ImageResizeButtons} buttons.
*/
var ImageResizeHandles = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [WidgetResize, ImageUtils];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageResizeHandles";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	init() {
		const command = this.editor.commands.get("resizeImage");
		this.bind("isEnabled").to(command);
		this._setupResizerCreator();
	}
	/**
	* Attaches the listeners responsible for creating a resizer for each image, except for images inside the HTML embed preview.
	*/
	_setupResizerCreator() {
		const editor = this.editor;
		const editingView = editor.editing.view;
		const imageUtils = editor.plugins.get("ImageUtils");
		editingView.addObserver(ImageLoadObserver);
		this.listenTo(editingView.document, "imageLoaded", (evt, domEvent) => {
			if (!domEvent.target.matches(RESIZABLE_IMAGES_CSS_SELECTOR)) return;
			const domConverter = editor.editing.view.domConverter;
			const imageView = domConverter.domToView(domEvent.target);
			const widgetView = imageUtils.getImageWidgetFromImageView(imageView);
			let resizer = this.editor.plugins.get(WidgetResize).getResizerByViewElement(widgetView);
			if (resizer) {
				resizer.redraw();
				return;
			}
			const mapper = editor.editing.mapper;
			const imageModel = mapper.toModelElement(widgetView);
			resizer = editor.plugins.get(WidgetResize).attachTo({
				unit: editor.config.get("image.resizeUnit"),
				modelElement: imageModel,
				viewElement: widgetView,
				editor,
				getHandleHost(domWidgetElement) {
					return domWidgetElement.querySelector("img");
				},
				getResizeHost() {
					return domConverter.mapViewToDom(mapper.toViewElement(imageModel));
				},
				isCentered() {
					return imageModel.getAttribute("imageStyle") == "alignCenter";
				},
				onCommit(newValue) {
					editingView.change((writer) => {
						writer.removeClass(RESIZED_IMAGE_CLASS, widgetView);
					});
					editor.execute("resizeImage", { width: newValue });
				}
			});
			resizer.on("updateSize", () => {
				if (!widgetView.hasClass(RESIZED_IMAGE_CLASS)) editingView.change((writer) => {
					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);
		});
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* Finds model, view and DOM element for selected image element. Returns `null` if there is no image selected.
*
* @param editor Editor instance.
* @internal
*/
function getSelectedImageEditorNodes(editor) {
	const { editing } = editor;
	const imageModelElement = editor.plugins.get("ImageUtils").getClosestSelectedImageElement(editor.model.document.selection);
	if (!imageModelElement) return null;
	const imageViewElement = editing.mapper.toViewElement(imageModelElement);
	return {
		model: imageModelElement,
		view: imageViewElement,
		dom: editing.view.domConverter.mapViewToDom(imageViewElement)
	};
}

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imageresize/utils/getselectedimagewidthinunits
*/
/**
* Returns image width in specified units. It is width of image after resize.
*
* 	* If image is not selected or command is disabled then `null` will be returned.
* 	* If image is not fully loaded (and it is impossible to determine its natural size) then `null` will be returned.
*	* If `targetUnit` percentage is passed then it will return width percentage of image related to its accessors.
*
* @param editor Editor instance.
* @param targetUnit Unit in which dimension will be returned.
* @returns Parsed image width after resize (with unit).
* @internal
*/
function getSelectedImageWidthInUnits(editor, targetUnit) {
	const imageNodes = getSelectedImageEditorNodes(editor);
	if (!imageNodes) return null;
	const parsedResizedWidth = _tryParseDimensionWithUnit(imageNodes.model.getAttribute("resizedWidth") || null);
	if (!parsedResizedWidth) return null;
	if (parsedResizedWidth.unit === targetUnit) return parsedResizedWidth;
	return _tryCastDimensionsToUnit(calculateResizeHostAncestorWidth(imageNodes.dom), {
		unit: "px",
		value: new Rect(imageNodes.dom).width
	}, targetUnit);
}

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imageresize/ui/imagecustomresizeformview
*/
/**
* The ImageCustomResizeFormView class.
*
* @internal
*/
var ImageCustomResizeFormView = class extends View {
	/**
	* Tracks information about the DOM focus in the form.
	*/
	focusTracker;
	/**
	* An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
	*/
	keystrokes;
	/**
	* Resize unit shortcut.
	*/
	unit;
	/**
	* The Back button view displayed in the header.
	*/
	backButtonView;
	/**
	* A button used to submit the form.
	*/
	saveButtonView;
	/**
	* An input with a label.
	*/
	labeledInput;
	/**
	* A collection of child views.
	*/
	children;
	/**
	* A collection of views which can be focused in the form.
	*/
	_focusables;
	/**
	* Helps cycling over {@link #_focusables} in the form.
	*/
	_focusCycler;
	/**
	* An array of form validators used by {@link #isValid}.
	*/
	_validators;
	/**
	* @inheritDoc
	*/
	constructor(locale, unit, validators) {
		super(locale);
		this.focusTracker = new FocusTracker();
		this.keystrokes = new KeystrokeHandler();
		this.unit = unit;
		this.backButtonView = this._createBackButton();
		this.saveButtonView = this._createSaveButton();
		this.labeledInput = this._createLabeledInputView();
		this.children = this.createCollection([this._createHeaderView()]);
		this.children.add(new FormRowView(locale, {
			children: [this.labeledInput, this.saveButtonView],
			class: ["ck-form__row_with-submit", "ck-form__row_large-top-padding"]
		}));
		this._focusables = new ViewCollection();
		this._validators = validators;
		this.keystrokes.set("Esc", (data, cancel) => {
			this.fire("cancel");
			cancel();
		});
		this._focusCycler = new FocusCycler({
			focusables: this._focusables,
			focusTracker: this.focusTracker,
			keystrokeHandler: this.keystrokes,
			actions: {
				focusPrevious: "shift + tab",
				focusNext: "tab"
			}
		});
		this.setTemplate({
			tag: "form",
			attributes: {
				class: [
					"ck",
					"ck-form",
					"ck-image-custom-resize-form",
					"ck-responsive-form"
				],
				tabindex: "-1"
			},
			children: this.children
		});
	}
	/**
	* @inheritDoc
	*/
	render() {
		super.render();
		submitHandler({ view: this });
		[
			this.backButtonView,
			this.labeledInput,
			this.saveButtonView
		].forEach((v) => {
			this._focusables.add(v);
			this.focusTracker.add(v.element);
		});
		this.keystrokes.listenTo(this.element);
	}
	/**
	* @inheritDoc
	*/
	destroy() {
		super.destroy();
		this.focusTracker.destroy();
		this.keystrokes.destroy();
	}
	/**
	* Creates a back button view that cancels the form.
	*/
	_createBackButton() {
		const t = this.locale.t;
		const backButton = new ButtonView(this.locale);
		backButton.set({
			class: "ck-button-back",
			label: t("Back"),
			icon: IconPreviousArrow,
			tooltip: true
		});
		backButton.delegate("execute").to(this, "cancel");
		return backButton;
	}
	/**
	* Creates a save button view that resize the image.
	*/
	_createSaveButton() {
		const t = this.locale.t;
		const saveButton = new ButtonView(this.locale);
		saveButton.set({
			label: t("Save"),
			withText: true,
			type: "submit",
			class: "ck-button-action ck-button-bold"
		});
		return saveButton;
	}
	/**
	* Creates a header view for the form.
	*/
	_createHeaderView() {
		const t = this.locale.t;
		const header = new FormHeaderView(this.locale, { label: t("Image Resize") });
		header.children.add(this.backButtonView, 0);
		return header;
	}
	/**
	* Creates an input with a label.
	*
	* @returns Labeled field view instance.
	*/
	_createLabeledInputView() {
		const t = this.locale.t;
		const labeledInput = new LabeledFieldView(this.locale, createLabeledInputNumber);
		labeledInput.label = t("Resize image (in %0)", this.unit);
		labeledInput.class = "ck-labeled-field-view_full-width";
		labeledInput.fieldView.set({ step: .1 });
		return labeledInput;
	}
	/**
	* Validates the form and returns `false` when some fields are invalid.
	*/
	isValid() {
		this.resetFormStatus();
		for (const validator of this._validators) {
			const errorText = validator(this);
			if (errorText) {
				this.labeledInput.errorText = errorText;
				return false;
			}
		}
		return true;
	}
	/**
	* Cleans up the supplementary error and information text of the {@link #labeledInput}
	* bringing them back to the state when the form has been displayed for the first time.
	*
	* See {@link #isValid}.
	*/
	resetFormStatus() {
		this.labeledInput.errorText = null;
	}
	/**
	* The native DOM `value` of the input element of {@link #labeledInput}.
	*/
	get rawSize() {
		const { element } = this.labeledInput.fieldView;
		if (!element) return null;
		return element.value;
	}
	/**
	* Get numeric value of size. Returns `null` if value of size input element in {@link #labeledInput}.is not a number.
	*/
	get parsedSize() {
		const { rawSize } = this;
		if (rawSize === null) return null;
		const parsed = Number.parseFloat(rawSize);
		if (Number.isNaN(parsed)) return null;
		return parsed;
	}
	/**
	* Returns serialized image input size with unit.
	* Returns `null` if value of size input element in {@link #labeledInput}.is not a number.
	*/
	get sizeWithUnits() {
		const { parsedSize, unit } = this;
		if (parsedSize === null) return null;
		return `${parsedSize}${unit}`;
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* Returns min and max value of resize image in specified unit.
*
* @param editor Editor instance.
* @param targetUnit Unit in which dimension will be returned.
* @returns Possible resize range in numeric form.
* @internal
*/
function getSelectedImagePossibleResizeRange(editor, targetUnit) {
	const imageNodes = getSelectedImageEditorNodes(editor);
	if (!imageNodes) return null;
	const imageParentWidthPx = calculateResizeHostAncestorWidth(imageNodes.dom);
	const minimumImageWidth = _tryParseDimensionWithUnit(window.getComputedStyle(imageNodes.dom).minWidth) || {
		value: 1,
		unit: "px"
	};
	return {
		unit: targetUnit,
		lower: Math.max(.1, _tryCastDimensionsToUnit(imageParentWidthPx, minimumImageWidth, targetUnit).value),
		upper: targetUnit === "px" ? imageParentWidthPx : 100
	};
}

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imageresize/imagecustomresizeui
*/
/**
* The custom resize image UI plugin.
*
* The plugin uses the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon}.
*/
var ImageCustomResizeUI = class extends Plugin {
	/**
	* The contextual balloon plugin instance.
	*/
	_balloon;
	/**
	* A form containing a textarea and buttons, used to change the `alt` text value.
	*/
	_form;
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [ContextualBalloon];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageCustomResizeUI";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	destroy() {
		super.destroy();
		if (this._form) this._form.destroy();
	}
	/**
	* Creates the {@link module:image/imageresize/ui/imagecustomresizeformview~ImageCustomResizeFormView}
	* form.
	*/
	_createForm(unit) {
		const editor = this.editor;
		this._balloon = this.editor.plugins.get("ContextualBalloon");
		this._form = new (CssTransitionDisablerMixin(ImageCustomResizeFormView))(editor.locale, unit, getFormValidators(editor));
		this._form.render();
		this.listenTo(this._form, "submit", () => {
			if (this._form.isValid()) {
				editor.execute("resizeImage", { width: this._form.sizeWithUnits });
				this._hideForm(true);
			}
		});
		this.listenTo(this._form.labeledInput, "change:errorText", () => {
			editor.ui.update();
		});
		this.listenTo(this._form, "cancel", () => {
			this._hideForm(true);
		});
		clickOutsideHandler({
			emitter: this._form,
			activator: () => this._isVisible,
			contextElements: () => [this._balloon.view.element],
			callback: () => this._hideForm()
		});
	}
	/**
	* Shows the {@link #_form} in the {@link #_balloon}.
	*
	* @internal
	*/
	_showForm(unit) {
		if (this._isVisible) return;
		if (!this._form) this._createForm(unit);
		const editor = this.editor;
		const labeledInput = this._form.labeledInput;
		this._form.disableCssTransitions();
		this._form.resetFormStatus();
		if (!this._isInBalloon) this._balloon.add({
			view: this._form,
			position: getBalloonPositionData(editor)
		});
		const currentParsedWidth = getSelectedImageWidthInUnits(editor, unit);
		const initialInputValue = currentParsedWidth ? currentParsedWidth.value.toFixed(1) : "";
		const possibleRange = getSelectedImagePossibleResizeRange(editor, unit);
		labeledInput.fieldView.value = labeledInput.fieldView.element.value = initialInputValue;
		if (possibleRange) Object.assign(labeledInput.fieldView, {
			min: possibleRange.lower.toFixed(1),
			max: Math.ceil(possibleRange.upper).toFixed(1)
		});
		this._form.labeledInput.fieldView.select();
		this._form.enableCssTransitions();
	}
	/**
	* Removes the {@link #_form} from the {@link #_balloon}.
	*
	* @param focusEditable Controls whether the editing view is focused afterwards.
	*/
	_hideForm(focusEditable = false) {
		if (!this._isInBalloon) return;
		if (this._form.focusTracker.isFocused) this._form.saveButtonView.focus();
		this._balloon.remove(this._form);
		if (focusEditable) this.editor.editing.view.focus();
	}
	/**
	* Returns `true` when the {@link #_form} is the visible view in the {@link #_balloon}.
	*/
	get _isVisible() {
		return !!this._balloon && this._balloon.visibleView === this._form;
	}
	/**
	* Returns `true` when the {@link #_form} is in the {@link #_balloon}.
	*/
	get _isInBalloon() {
		return !!this._balloon && this._balloon.hasView(this._form);
	}
};
/**
* Returns image resize form validation callbacks.
*
* @param editor Editor instance.
*/
function getFormValidators(editor) {
	const t = editor.t;
	return [(form) => {
		if (form.rawSize.trim() === "") return t("The value must not be empty.");
		if (form.parsedSize === null) return t("The value should be a plain number.");
	}];
}

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imageresize
*/
/**
* The image resize plugin.
*
* It adds a possibility to resize each image using handles.
*/
var ImageResize = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [
			ImageResizeEditing,
			ImageResizeHandles,
			ImageCustomResizeUI,
			ImageResizeButtons
		];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageResize";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* The image style command. It is used to apply {@link module:image/imageconfig~ImageStyleConfig#options image style option}
* to a selected image.
*
* **Note**: Executing this command may change the image model element if the desired style requires an image of a different
* type. See {@link module:image/imagestyle/imagestylecommand~ImageStyleCommand#execute} to learn more.
*/
var ImageStyleCommand = class extends Command {
	/**
	* An object containing names of default style options for the inline and block images.
	* If there is no default style option for the given image type in the configuration,
	* the name will be `false`.
	*/
	_defaultStyles;
	/**
	* The styles handled by this command.
	*/
	_styles;
	/**
	* Creates an instance of the image style command. When executed, the command applies one of
	* {@link module:image/imageconfig~ImageStyleConfig#options style options} to the currently selected image.
	*
	* @param editor The editor instance.
	* @param styles The style options that this command supports.
	*/
	constructor(editor, styles) {
		super(editor);
		this._defaultStyles = {
			imageBlock: false,
			imageInline: false
		};
		this._styles = new Map(styles.map((style) => {
			if (style.isDefault) for (const modelElementName of style.modelElements) this._defaultStyles[modelElementName] = style.name;
			return [style.name, style];
		}));
	}
	/**
	* @inheritDoc
	*/
	refresh() {
		const element = this.editor.plugins.get("ImageUtils").getClosestSelectedImageElement(this.editor.model.document.selection);
		this.isEnabled = !!element;
		if (!this.isEnabled) this.value = false;
		else if (element.hasAttribute("imageStyle")) this.value = element.getAttribute("imageStyle");
		else this.value = this._defaultStyles[element.name];
	}
	/**
	* Returns whether the style with the given name can be applied to the currently selected image.
	*
	* Applying a style may internally trigger a type conversion (block ↔ inline) when the style
	* supports only the opposite type. If that conversion would be rejected by the schema (for
	* example, converting an inline image to a block image inside `$inlineRoot`), this style
	* cannot be applied even though the command itself is enabled. UI components representing
	* individual styles should reflect this per-style state in their `isEnabled`.
	*/
	isStyleEnabled(styleName) {
		if (!this.isEnabled) return false;
		const imageUtils = this.editor.plugins.get("ImageUtils");
		const element = imageUtils.getClosestSelectedImageElement(this.editor.model.document.selection);
		if (!element || !this._styles.has(styleName)) return false;
		if (!this.shouldConvertImageType(styleName, element)) return true;
		const typeCommandName = imageUtils.isBlockImage(element) ? "imageTypeInline" : "imageTypeBlock";
		const typeCommand = this.editor.commands.get(typeCommandName);
		return !!typeCommand && typeCommand.isEnabled;
	}
	/**
	* Executes the command and applies the style to the currently selected image:
	*
	* ```ts
	* editor.execute( 'imageStyle', { value: 'side' } );
	* ```
	*
	* **Note**: Executing this command may change the image model element if the desired style requires an image
	* of a different type. Learn more about {@link module:image/imageconfig~ImageStyleOptionDefinition#modelElements model element}
	* configuration for the style option.
	*
	* @param options.value The name of the style (as configured in {@link module:image/imageconfig~ImageStyleConfig#options}).
	* @param options.setImageSizes Specifies whether the image `width` and `height` attributes should be set automatically.
	* The default is `true`.
	* @fires execute
	*/
	execute(options = {}) {
		const editor = this.editor;
		const model = editor.model;
		const imageUtils = editor.plugins.get("ImageUtils");
		if (options.value && !this.isStyleEnabled(options.value)) return;
		model.change((writer) => {
			const requestedStyle = options.value;
			const { setImageSizes = true } = options;
			let imageElement = imageUtils.getClosestSelectedImageElement(model.document.selection);
			if (requestedStyle && this.shouldConvertImageType(requestedStyle, imageElement)) {
				this.editor.execute(imageUtils.isBlockImage(imageElement) ? "imageTypeInline" : "imageTypeBlock", { setImageSizes });
				imageElement = imageUtils.getClosestSelectedImageElement(model.document.selection);
			}
			if (!requestedStyle || this._styles.get(requestedStyle).isDefault) writer.removeAttribute("imageStyle", imageElement);
			else writer.setAttribute("imageStyle", requestedStyle, imageElement);
			if (setImageSizes) imageUtils.setImageNaturalSizeAttributes(imageElement);
		});
	}
	/**
	* Returns `true` if requested style change would trigger the image type change.
	*
	* @param requestedStyle The name of the style (as configured in {@link module:image/imageconfig~ImageStyleConfig#options}).
	* @param imageElement The image model element.
	*/
	shouldConvertImageType(requestedStyle, imageElement) {
		return !this._styles.get(requestedStyle).modelElements.includes(imageElement.name);
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imagestyle/utils
*/
/**
* Default image style options provided by the plugin that can be referred in the {@link module:image/imageconfig~ImageConfig#styles}
* configuration.
*
* There are available 5 styles focused on formatting:
*
* * **`'alignLeft'`** aligns the inline or block image to the left and wraps it with the text using the `image-style-align-left` class,
* * **`'alignRight'`** aligns the inline or block image to the right and wraps it with the text using the `image-style-align-right` class,
* * **`'alignCenter'`** centers the block image using the `image-style-align-center` class,
* * **`'alignBlockLeft'`** aligns the block image to the left using the `image-style-block-align-left` class,
* * **`'alignBlockRight'`** aligns the block image to the right using the `image-style-block-align-right` class,
*
* and 3 semantic styles:
*
* * **`'inline'`** is an inline image without any CSS class,
* * **`'block'`** is a block image without any CSS class,
* * **`'side'`** is a block image styled with the `image-style-side` CSS class.
*
* @internal
*/
const DEFAULT_OPTIONS = {
	get inline() {
		return {
			name: "inline",
			title: "In line",
			icon: IconObjectInline,
			modelElements: ["imageInline"],
			isDefault: true
		};
	},
	get alignLeft() {
		return {
			name: "alignLeft",
			title: "Left aligned image",
			icon: IconObjectInlineLeft,
			modelElements: ["imageBlock", "imageInline"],
			className: "image-style-align-left"
		};
	},
	get alignBlockLeft() {
		return {
			name: "alignBlockLeft",
			title: "Left aligned image",
			icon: IconObjectLeft,
			modelElements: ["imageBlock"],
			className: "image-style-block-align-left"
		};
	},
	get alignCenter() {
		return {
			name: "alignCenter",
			title: "Centered image",
			icon: IconObjectCenter,
			modelElements: ["imageBlock"],
			className: "image-style-align-center"
		};
	},
	get alignRight() {
		return {
			name: "alignRight",
			title: "Right aligned image",
			icon: IconObjectInlineRight,
			modelElements: ["imageBlock", "imageInline"],
			className: "image-style-align-right"
		};
	},
	get alignBlockRight() {
		return {
			name: "alignBlockRight",
			title: "Right aligned image",
			icon: IconObjectRight,
			modelElements: ["imageBlock"],
			className: "image-style-block-align-right"
		};
	},
	get block() {
		return {
			name: "block",
			title: "Centered image",
			icon: IconObjectCenter,
			modelElements: ["imageBlock"],
			isDefault: true
		};
	},
	get side() {
		return {
			name: "side",
			title: "Side image",
			icon: IconObjectInlineRight,
			modelElements: ["imageBlock"],
			className: "image-style-side"
		};
	}
};
/**
* Default image style icons provided by the plugin that can be referred in the {@link module:image/imageconfig~ImageConfig#styles}
* configuration.
*
* See {@link module:image/imageconfig~ImageStyleOptionDefinition#icon} to learn more.
*
* There are 7 default icons available: `'full'`, `'left'`, `'inlineLeft'`, `'center'`, `'right'`, `'inlineRight'`, and `'inline'`.
*
* @internal
*/
const DEFAULT_ICONS = /* #__PURE__ */ (() => ({
	full: IconObjectFullWidth,
	left: IconObjectLeft,
	right: IconObjectRight,
	center: IconObjectCenter,
	inlineLeft: IconObjectInlineLeft,
	inlineRight: IconObjectInlineRight,
	inline: IconObjectInline
}))();
/**
* Default drop-downs provided by the plugin that can be referred in the {@link module:image/imageconfig~ImageConfig#toolbar}
* configuration. The drop-downs are containers for the {@link module:image/imageconfig~ImageStyleConfig#options image style options}.
*
* If both of the `ImageEditing` plugins are loaded, there are 2 predefined drop-downs available:
*
* * **`'imageStyle:wrapText'`**, which contains the `alignLeft` and `alignRight` options, that is,
* those that wraps the text around the image,
* * **`'imageStyle:breakText'`**, which contains the `alignBlockLeft`, `alignCenter` and `alignBlockRight` options, that is,
* those that breaks the text around the image.
*
* @internal
*/
const DEFAULT_DROPDOWN_DEFINITIONS = [{
	name: "imageStyle:wrapText",
	title: "Wrap text",
	defaultItem: "imageStyle:alignLeft",
	items: ["imageStyle:alignLeft", "imageStyle:alignRight"]
}, {
	name: "imageStyle:breakText",
	title: "Break text",
	defaultItem: "imageStyle:block",
	items: [
		"imageStyle:alignBlockLeft",
		"imageStyle:block",
		"imageStyle:alignBlockRight"
	]
}];
/**
* Returns the style with a given `name` from an array of styles.
*
* @internal
*/
function getStyleDefinitionByName(name, styles) {
	for (const style of styles) if (style.name === name) return style;
}
/**
* Returns a list of the normalized and validated image style options.
*
* @param config
* @param config.isInlinePluginLoaded
* Determines whether the {@link module:image/image/imageblockediting~ImageBlockEditing `ImageBlockEditing`} plugin has been loaded.
* @param config.isBlockPluginLoaded
* Determines whether the {@link module:image/image/imageinlineediting~ImageInlineEditing `ImageInlineEditing`} plugin has been loaded.
* @param config.configuredStyles
* The image styles configuration provided in the image styles {@link module:image/imageconfig~ImageConfig#styles configuration}
* as a default or custom value.
* @returns
* * Each of options contains a complete icon markup.
* * The image style options not supported by any of the loaded plugins are filtered out.
*/
function normalizeStyles(config) {
	return (config.configuredStyles.options || []).map((arrangement) => normalizeDefinition(arrangement)).filter((arrangement) => isValidOption(arrangement, config));
}
/**
* Returns the default image styles configuration depending on the loaded image editing plugins.
*
* @param isInlinePluginLoaded
* Determines whether the {@link module:image/image/imageblockediting~ImageBlockEditing `ImageBlockEditing`} plugin has been loaded.
*
* @param isBlockPluginLoaded
* Determines whether the {@link module:image/image/imageinlineediting~ImageInlineEditing `ImageInlineEditing`} plugin has been loaded.
*
* @returns
* It returns an object with the lists of the image style options and groups defined as strings related to the
* {@link module:image/imagestyle/utils#DEFAULT_OPTIONS default options}
*/
function getDefaultStylesConfiguration(isBlockPluginLoaded, isInlinePluginLoaded) {
	if (isBlockPluginLoaded && isInlinePluginLoaded) return { options: [
		"inline",
		"alignLeft",
		"alignRight",
		"alignCenter",
		"alignBlockLeft",
		"alignBlockRight",
		"block",
		"side"
	] };
	else if (isBlockPluginLoaded) return { options: ["block", "side"] };
	else if (isInlinePluginLoaded) return { options: [
		"inline",
		"alignLeft",
		"alignRight"
	] };
	return {};
}
/**
* Returns a list of the available predefined drop-downs' definitions depending on the loaded image editing plugins.
*/
function getDefaultDropdownDefinitions(pluginCollection) {
	if (pluginCollection.has("ImageBlockEditing") && pluginCollection.has("ImageInlineEditing")) return [...DEFAULT_DROPDOWN_DEFINITIONS];
	else return [];
}
/**
* Normalizes an image style option or group provided in the {@link module:image/imageconfig~ImageConfig#styles}
* and returns it in a {@link module:image/imageconfig~ImageStyleOptionDefinition}/
*/
function normalizeDefinition(definition) {
	if (typeof definition === "string") if (!DEFAULT_OPTIONS[definition]) definition = { name: definition };
	else definition = { ...DEFAULT_OPTIONS[definition] };
	else definition = extendStyle(DEFAULT_OPTIONS[definition.name], definition);
	if (typeof definition.icon === "string") definition.icon = DEFAULT_ICONS[definition.icon] || definition.icon;
	return definition;
}
/**
* Checks if the image style option is valid:
* * if it has the modelElements fields defined and filled,
* * if the defined modelElements are supported by any of the loaded image editing plugins.
* It also displays a console warning these conditions are not met.
*
* @param option image style option
*/
function isValidOption(option, { isBlockPluginLoaded, isInlinePluginLoaded }) {
	const { modelElements, name } = option;
	if (!modelElements || !modelElements.length || !name) {
		warnInvalidStyle({ style: option });
		return false;
	} else {
		const supportedElements = [isBlockPluginLoaded ? "imageBlock" : null, isInlinePluginLoaded ? "imageInline" : null];
		if (!modelElements.some((elementName) => supportedElements.includes(elementName))) {
			/**
			* In order to work correctly, each image style {@link module:image/imageconfig~ImageStyleOptionDefinition option}
			* requires specific model elements (also: types of images) to be supported by the editor.
			*
			* Model element names to which the image style option can be applied are defined in the
			* {@link module:image/imageconfig~ImageStyleOptionDefinition#modelElements} property of the style option
			* definition.
			*
			* Explore the warning in the console to find out precisely which option is not supported and which editor plugins
			* are missing. Make sure these plugins are loaded in your editor to get this image style option working.
			*
			* @error image-style-missing-dependency
			* @param {string} style The name of the unsupported option.
			* @param {Array.<string>} missingPlugins The names of the plugins one of which has to be loaded for the particular option.
			*/
			logWarning("image-style-missing-dependency", {
				style: option,
				missingPlugins: modelElements.map((name) => name === "imageBlock" ? "ImageBlockEditing" : "ImageInlineEditing")
			});
			return false;
		}
	}
	return true;
}
/**
* Extends the default style with a style provided by the developer.
* Note: Don't override the custom–defined style object, clone it instead.
*/
function extendStyle(source, style) {
	const extendedStyle = { ...style };
	for (const prop in source) if (!Object.prototype.hasOwnProperty.call(style, prop)) extendedStyle[prop] = source[prop];
	return extendedStyle;
}
/**
* Displays a console warning with the 'image-style-configuration-definition-invalid' error.
*/
function warnInvalidStyle(info) {
	/**
	* The image style definition provided in the configuration is invalid.
	*
	* Please make sure the definition implements properly one of the following:
	*
	* * {@link module:image/imageconfig~ImageStyleOptionDefinition image style option definition},
	* * {@link module:image/imageconfig~ImageStyleDropdownDefinition image style dropdown definition}
	*
	* @error image-style-configuration-definition-invalid
	* @param {object} info The information about the invalid definition.
	*/
	logWarning("image-style-configuration-definition-invalid", info);
}
/**
* @internal
*/
const utils = {
	normalizeStyles,
	getDefaultStylesConfiguration,
	getDefaultDropdownDefinitions,
	warnInvalidStyle
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imagestyle/converters
*/
/**
* Returns a converter for the `imageStyle` attribute. It can be used for adding, changing and removing the attribute.
*
* @param styles An array containing available image style options.
* @returns A model-to-view attribute converter.
* @internal
*/
function modelToViewStyleAttribute(styles) {
	return (evt, data, conversionApi) => {
		if (!conversionApi.consumable.consume(data.item, evt.name)) return;
		const newStyle = getStyleDefinitionByName(data.attributeNewValue, styles);
		const oldStyle = getStyleDefinitionByName(data.attributeOldValue, styles);
		const viewElement = conversionApi.mapper.toViewElement(data.item);
		const viewWriter = conversionApi.writer;
		if (oldStyle) viewWriter.removeClass(oldStyle.className, viewElement);
		if (newStyle) viewWriter.addClass(newStyle.className, viewElement);
	};
}
/**
* Returns a view-to-model converter converting image CSS classes to a proper value in the model.
*
* @param styles Image style options for which the converter is created.
* @returns A view-to-model converter.
* @internal
*/
function viewToModelStyleAttribute(styles) {
	const nonDefaultStyles = {
		imageInline: styles.filter((style) => !style.isDefault && style.modelElements.includes("imageInline")),
		imageBlock: styles.filter((style) => !style.isDefault && style.modelElements.includes("imageBlock"))
	};
	return (evt, data, conversionApi) => {
		if (!data.modelRange) return;
		const viewElement = data.viewItem;
		const modelImageElement = first(data.modelRange.getItems());
		if (!modelImageElement) return;
		if (!conversionApi.schema.checkAttribute(modelImageElement, "imageStyle")) return;
		for (const style of nonDefaultStyles[modelImageElement.name]) if (conversionApi.consumable.consume(viewElement, { classes: style.className })) conversionApi.writer.setAttribute("imageStyle", style.name, modelImageElement);
		normalizeFloatToDefinitionStyle(conversionApi, viewElement, modelImageElement, styles);
	};
}
/**
* A helper function that attempts to convert the `float` CSS style into a corresponding `imageStyle` attribute.
*
* It maps `float: left` and `float: right` to standard alignment styles (e.g. `'alignLeft'`, `'alignRight'`),
* but only if the target style definition matches one of the {@link module:image/image/utils~DEFAULT_OPTIONS default options}.
*/
function normalizeFloatToDefinitionStyle(conversionApi, viewElement, modelElement, styles) {
	if (!conversionApi.consumable.test(viewElement, { styles: ["float"] })) return;
	let floatStyleName = null;
	switch (viewElement.getStyle("float")) {
		case "left":
			floatStyleName = "alignLeft";
			break;
		case "right":
			floatStyleName = "alignRight";
			break;
	}
	if (!floatStyleName) return;
	const definition = getStyleDefinitionByName(floatStyleName, styles);
	if (!definition) return;
	const builtinDefinition = DEFAULT_OPTIONS[definition.name];
	if (!isEqual(definition, builtinDefinition)) return;
	conversionApi.writer.setAttribute("imageStyle", floatStyleName, modelElement);
	conversionApi.consumable.consume(viewElement, { styles: ["float"] });
}

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imagestyle/imagestyleediting
*/
/**
* The image style engine plugin. It sets the default configuration, creates converters and registers
* {@link module:image/imagestyle/imagestylecommand~ImageStyleCommand ImageStyleCommand}.
*/
var ImageStyleEditing = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageStyleEditing";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [ImageUtils];
	}
	/**
	* It contains a list of the normalized and validated style options.
	*
	* * Each option contains a complete icon markup.
	* * The style options not supported by any of the loaded image editing plugins (
	* {@link module:image/image/imageinlineediting~ImageInlineEditing `ImageInlineEditing`} or
	* {@link module:image/image/imageblockediting~ImageBlockEditing `ImageBlockEditing`}) are filtered out.
	*
	* @internal
	* @readonly
	*/
	normalizedStyles;
	/**
	* @inheritDoc
	*/
	init() {
		const editor = this.editor;
		const isBlockPluginLoaded = editor.plugins.has("ImageBlockEditing");
		const isInlinePluginLoaded = editor.plugins.has("ImageInlineEditing");
		editor.config.define("image.styles", utils.getDefaultStylesConfiguration(isBlockPluginLoaded, isInlinePluginLoaded));
		this.normalizedStyles = utils.normalizeStyles({
			configuredStyles: editor.config.get("image.styles"),
			isBlockPluginLoaded,
			isInlinePluginLoaded
		});
		this._setupConversion(isBlockPluginLoaded, isInlinePluginLoaded);
		this._setupPostFixer();
		editor.commands.add("imageStyle", new ImageStyleCommand(editor, this.normalizedStyles));
	}
	/**
	* Sets the editor conversion taking the presence of
	* {@link module:image/image/imageinlineediting~ImageInlineEditing `ImageInlineEditing`}
	* and {@link module:image/image/imageblockediting~ImageBlockEditing `ImageBlockEditing`} plugins into consideration.
	*/
	_setupConversion(isBlockPluginLoaded, isInlinePluginLoaded) {
		const editor = this.editor;
		const schema = editor.model.schema;
		const modelToViewConverter = modelToViewStyleAttribute(this.normalizedStyles);
		const viewToModelConverter = viewToModelStyleAttribute(this.normalizedStyles);
		editor.editing.downcastDispatcher.on("attribute:imageStyle", modelToViewConverter);
		editor.data.downcastDispatcher.on("attribute:imageStyle", modelToViewConverter);
		if (isBlockPluginLoaded) {
			schema.extend("imageBlock", { allowAttributes: "imageStyle" });
			schema.setAttributeProperties("imageStyle", {
				isFormatting: true,
				blockAlignment: getBlockAlignmentAttributeProperty(this.normalizedStyles)
			});
			editor.data.upcastDispatcher.on("element:figure", viewToModelConverter, { priority: "low" });
		}
		if (isInlinePluginLoaded) {
			schema.extend("imageInline", { allowAttributes: "imageStyle" });
			schema.setAttributeProperties("imageStyle", { isFormatting: true });
			editor.data.upcastDispatcher.on("element:img", viewToModelConverter, { priority: "low" });
		}
	}
	/**
	* Registers a post-fixer that will make sure that the style attribute value is correct for a specific image type (block vs inline).
	*/
	_setupPostFixer() {
		const editor = this.editor;
		const document = editor.model.document;
		const imageUtils = editor.plugins.get(ImageUtils);
		const stylesMap = new Map(this.normalizedStyles.map((style) => [style.name, style]));
		document.registerPostFixer((writer) => {
			let changed = false;
			for (const change of document.differ.getChanges()) if (change.type == "insert" || change.type == "attribute" && change.attributeKey == "imageStyle") {
				let element = change.type == "insert" ? change.position.nodeAfter : change.range.start.nodeAfter;
				if (element && element.is("element", "paragraph") && element.childCount > 0) element = element.getChild(0);
				if (!imageUtils.isImage(element)) continue;
				const imageStyle = element.getAttribute("imageStyle");
				if (!imageStyle) continue;
				const imageStyleDefinition = stylesMap.get(imageStyle);
				if (!imageStyleDefinition || !imageStyleDefinition.modelElements.includes(element.name)) {
					writer.removeAttribute("imageStyle", element);
					changed = true;
				}
			}
			return changed;
		});
	}
};
/**
* Returns a mapping of generic alignment values ('left', 'right', 'center') to their corresponding
* block image alignment style names.
*
* This function is particularly useful for scenarios like `td[align]` conversion, which needs to know
* exactly how to map a generic `align` attribute to the correct alignment value. This mapping is necessary
* because the actual alignment value can differ depending on the target element type (e.g., block images
* vs. tables).
*
* @param styles An array of available image style option definitions.
* @returns A record mapping basic alignments to their valid, block-specific style names.
*/
function getBlockAlignmentAttributeProperty(styles) {
	const pickFirstSupported = (...names) => {
		const found = names.map((name) => getStyleDefinitionByName(name, styles)).find((definition) => definition?.modelElements.includes("imageBlock"));
		if (!found) return;
		return {
			isDefault: !!found.isDefault,
			value: found.name
		};
	};
	return Object.fromEntries(Object.entries({
		left: ["alignBlockLeft", "alignLeft"],
		right: ["alignBlockRight", "alignRight"],
		center: ["block", "alignCenter"]
	}).map(([align, names]) => [align, pickFirstSupported(...names)]).filter(([, resolved]) => resolved !== void 0));
}

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imagestyle/imagestyleui
*/
/**
* The image style UI plugin.
*
* It registers buttons corresponding to the {@link module:image/imageconfig~ImageConfig#styles} configuration.
* It also registers the {@link module:image/imagestyle/utils#DEFAULT_DROPDOWN_DEFINITIONS default drop-downs} and the
* custom drop-downs defined by the developer in the {@link module:image/imageconfig~ImageConfig#toolbar} configuration.
*/
var ImageStyleUI = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [ImageStyleEditing];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageStyleUI";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* Returns the default localized style titles provided by the plugin.
	*
	* The following localized titles corresponding with
	* {@link module:image/imagestyle/utils#DEFAULT_OPTIONS} are available:
	*
	* * `'Wrap text'`,
	* * `'Break text'`,
	* * `'In line'`,
	* * `'Full size image'`,
	* * `'Side image'`,
	* * `'Left aligned image'`,
	* * `'Centered image'`,
	* * `'Right aligned image'`
	*/
	get localizedDefaultStylesTitles() {
		const t = this.editor.t;
		return {
			"Wrap text": t("Wrap text"),
			"Break text": t("Break text"),
			"In line": t("In line"),
			"Full size image": t("Full size image"),
			"Side image": t("Side image"),
			"Left aligned image": t("Left aligned image"),
			"Centered image": t("Centered image"),
			"Right aligned image": t("Right aligned image")
		};
	}
	/**
	* @inheritDoc
	*/
	init() {
		const plugins = this.editor.plugins;
		const toolbarConfig = this.editor.config.get("image.toolbar") || [];
		const definedStyles = translateStyles(plugins.get("ImageStyleEditing").normalizedStyles, this.localizedDefaultStylesTitles);
		for (const styleConfig of definedStyles) this._createButton(styleConfig);
		const definedDropdowns = translateStyles([...toolbarConfig.filter(isObject), ...utils.getDefaultDropdownDefinitions(plugins)], this.localizedDefaultStylesTitles);
		for (const dropdownConfig of definedDropdowns) this._createDropdown(dropdownConfig, definedStyles);
	}
	/**
	* Creates a dropdown and stores it in the editor {@link module:ui/componentfactory~ComponentFactory}.
	*/
	_createDropdown(dropdownConfig, definedStyles) {
		const factory = this.editor.ui.componentFactory;
		factory.add(dropdownConfig.name, (locale) => {
			let defaultButton;
			const { defaultItem, items, title } = dropdownConfig;
			const buttonViews = items.filter((itemName) => definedStyles.find(({ name }) => getUIComponentName(name) === itemName)).map((buttonName) => {
				const button = factory.create(buttonName);
				if (buttonName === defaultItem) defaultButton = button;
				return button;
			});
			if (items.length !== buttonViews.length) utils.warnInvalidStyle({ dropdown: dropdownConfig });
			const dropdownView = createDropdown(locale, SplitButtonView);
			const splitButtonView = dropdownView.buttonView;
			const splitButtonViewArrow = splitButtonView.arrowView;
			addToolbarToDropdown(dropdownView, buttonViews, { enableActiveItemFocusOnDropdownOpen: true });
			splitButtonView.set({
				label: getDropdownButtonTitle(title, defaultButton.label),
				class: null,
				tooltip: true
			});
			splitButtonViewArrow.unbind("label");
			splitButtonViewArrow.set({ label: title });
			splitButtonView.bind("icon").toMany(buttonViews, "isOn", (...areOn) => {
				const index = areOn.findIndex(identity);
				return index < 0 ? defaultButton.icon : buttonViews[index].icon;
			});
			splitButtonView.bind("label").toMany(buttonViews, "isOn", (...areOn) => {
				const index = areOn.findIndex(identity);
				return getDropdownButtonTitle(title, index < 0 ? defaultButton.label : buttonViews[index].label);
			});
			splitButtonView.bind("isOn").toMany(buttonViews, "isOn", (...areOn) => areOn.some(identity));
			splitButtonView.bind("class").toMany(buttonViews, "isOn", (...areOn) => areOn.some(identity) ? "ck-splitbutton_flatten" : void 0);
			splitButtonView.on("execute", () => {
				if (!buttonViews.some(({ isOn }) => isOn)) defaultButton.fire("execute");
				else dropdownView.isOpen = !dropdownView.isOpen;
			});
			dropdownView.bind("isEnabled").toMany(buttonViews, "isEnabled", (...areEnabled) => areEnabled.some(identity));
			this.listenTo(dropdownView, "execute", () => {
				this.editor.editing.view.focus();
			});
			return dropdownView;
		});
	}
	/**
	* Creates a button and stores it in the editor {@link module:ui/componentfactory~ComponentFactory}.
	*/
	_createButton(buttonConfig) {
		const buttonName = buttonConfig.name;
		this.editor.ui.componentFactory.add(getUIComponentName(buttonName), (locale) => {
			const editor = this.editor;
			const command = editor.commands.get("imageStyle");
			const view = new ButtonView(locale);
			view.set({
				label: buttonConfig.title,
				icon: buttonConfig.icon,
				tooltip: true,
				isToggleable: true
			});
			const reactiveSources = [command];
			const imageTypeBlock = editor.commands.get("imageTypeBlock");
			const imageTypeInline = editor.commands.get("imageTypeInline");
			if (imageTypeBlock) reactiveSources.push(imageTypeBlock);
			if (imageTypeInline) reactiveSources.push(imageTypeInline);
			view.bind("isEnabled").toMany(reactiveSources, "isEnabled", () => command.isStyleEnabled(buttonName));
			view.bind("isOn").to(command, "value", (value) => value === buttonName);
			view.on("execute", this._executeCommand.bind(this, buttonName));
			return view;
		});
	}
	_executeCommand(name) {
		this.editor.execute("imageStyle", { value: name });
		this.editor.editing.view.focus();
	}
};
/**
* Returns the translated `title` from the passed styles array.
*/
function translateStyles(styles, titles) {
	for (const style of styles) if (titles[style.title]) style.title = titles[style.title];
	return styles;
}
/**
* Returns the image style component name with the "imageStyle:" prefix.
*/
function getUIComponentName(name) {
	return `imageStyle:${name}`;
}
/**
* Returns title for the splitbutton containing the dropdown title and default action item title.
*/
function getDropdownButtonTitle(dropdownTitle, buttonTitle) {
	return (dropdownTitle ? dropdownTitle + ": " : "") + buttonTitle;
}

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imagestyle
*/
/**
* The image style plugin.
*
* For a detailed overview of the image styles feature, check the {@glink features/images/images-styles documentation}.
*
* This is a "glue" plugin which loads the following plugins:
* * {@link module:image/imagestyle/imagestyleediting~ImageStyleEditing},
* * {@link module:image/imagestyle/imagestyleui~ImageStyleUI}
*
* It provides a default configuration, which can be extended or overwritten.
* Read more about the {@link module:image/imageconfig~ImageConfig#styles image styles configuration}.
*/
var ImageStyle = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [ImageStyleEditing, ImageStyleUI];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageStyle";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/imagetoolbar
*/
/**
* The image toolbar plugin. It creates and manages the image toolbar (the toolbar displayed when an image is selected).
*
* For an overview, check the {@glink features/images/images-overview#image-contextual-toolbar image contextual toolbar} documentation.
*
* Instances of toolbar components (e.g. buttons) are created using the editor's
* {@link module:ui/componentfactory~ComponentFactory component factory}
* based on the {@link module:image/imageconfig~ImageConfig#toolbar `image.toolbar` configuration option}.
*
* The toolbar uses the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon}.
*/
var ImageToolbar = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [WidgetToolbarRepository, ImageUtils];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "ImageToolbar";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	afterInit() {
		const editor = this.editor;
		const t = editor.t;
		const widgetToolbarRepository = editor.plugins.get(WidgetToolbarRepository);
		const imageUtils = editor.plugins.get("ImageUtils");
		widgetToolbarRepository.register("image", {
			ariaLabel: t("Image toolbar"),
			items: normalizeDeclarativeConfig(editor.config.get("image.toolbar") || []),
			getRelatedElement: (selection) => imageUtils.getClosestSelectedImageWidget(selection)
		});
	}
};
/**
* Convert the dropdown definitions to their keys registered in the ComponentFactory.
* The registration precess should be handled by the plugin which handles the UI of a particular feature.
*/
function normalizeDeclarativeConfig(config) {
	return config.map((item) => isObject(item) ? item.name : item);
}

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module image/pictureediting
*/
/**
* This plugin enables the [`<picture>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture) element support in the editor.
*
* * It enables the `sources` model attribute on `imageBlock` and `imageInline` model elements
* (brought by {@link module:image/imageblock~ImageBlock} and {@link module:image/imageinline~ImageInline}, respectively).
* * It translates the `sources` model element to the view (also: data) structure that may look as follows:
*
* ```html
* <p>Inline image using picture:
* 	<picture>
* 		<source media="(min-width: 800px)" srcset="image-large.webp" type="image/webp">
* 		<source media="(max-width: 800px)" srcset="image-small.webp" type="image/webp">
* 		<!-- Other sources as specified in the "sources" model attribute... -->
* 		<img src="image.png" alt="An image using picture" />
* 	</picture>
* </p>
*
* <p>Block image using picture:</p>
* <figure class="image">
* 	<picture>
* 		<source media="(min-width: 800px)" srcset="image-large.webp" type="image/webp">
* 		<source media="(max-width: 800px)" srcset="image-small.webp" type="image/webp">
* 		<!-- Other sources as specified in the "sources" model attribute... -->
* 		<img src="image.png" alt="An image using picture" />
* 	</picture>
* 	<figcaption>Caption of the image</figcaption>
* </figure>
* ```
*
* **Note:** The value of the `sources` {@glink framework/architecture/editing-engine#changing-the-model model attribute}
* in both examples equals:
*
* ```css
* [
* 	{
* 		media: '(min-width: 800px)',
* 		srcset: 'image-large.webp',
* 		type: 'image/webp'
* 	},
* 	{
* 		media: '(max-width: 800px)',
* 		srcset: 'image-small.webp',
* 		type: 'image/webp'
* 	}
* ]
* ```
*
* * It integrates with the {@link module:image/imageupload~ImageUpload} plugin so images uploaded in the editor
* automatically render using `<picture>` if the {@glink features/images/image-upload/image-upload upload adapter}
* supports image sources and provides neccessary data.
*
* @private
*/
var PictureEditing = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [ImageEditing, ImageUtils];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "PictureEditing";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	afterInit() {
		const editor = this.editor;
		if (editor.plugins.has("ImageBlockEditing")) editor.model.schema.extend("imageBlock", { allowAttributes: ["sources"] });
		if (editor.plugins.has("ImageInlineEditing")) editor.model.schema.extend("imageInline", { allowAttributes: ["sources"] });
		this._setupConversion();
		this._setupImageUploadEditingIntegration();
	}
	/**
	* Configures conversion pipelines to support upcasting and downcasting images using the `<picture>` view element
	* and the model `sources` attribute.
	*/
	_setupConversion() {
		const editor = this.editor;
		const conversion = editor.conversion;
		const imageUtils = editor.plugins.get("ImageUtils");
		conversion.for("upcast").add(upcastPicture(imageUtils));
		conversion.for("downcast").add(downcastSourcesAttribute(imageUtils));
	}
	/**
	* Makes it possible for uploaded images to get the `sources` model attribute and the `<picture>...</picture>`
	* view structure out-of-the-box if relevant data is provided along the
	* {@link module:image/imageupload/imageuploadediting~ImageUploadEditing#event:uploadComplete} event.
	*/
	_setupImageUploadEditingIntegration() {
		const editor = this.editor;
		if (!editor.plugins.has("ImageUploadEditing")) return;
		const imageUploadEditing = editor.plugins.get("ImageUploadEditing");
		this.listenTo(imageUploadEditing, "uploadComplete", (evt, { imageElement, data }) => {
			const sources = data.sources;
			if (!sources) return;
			editor.model.change((writer) => {
				writer.setAttributes({ sources }, imageElement);
			});
		});
	}
};

/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/

export { AutoImage, Image, ImageBlock, ImageBlockEditing, ImageCaption, ImageCaptionEditing, ImageCaptionUI, ImageCaptionUtils, ImageCustomResizeUI, ImageEditing, ImageInline, ImageInlineEditing, ImageInsert, ImageInsertUI, ImageInsertViaUrl, ImageInsertViaUrlUI, ImageLoadObserver, ImagePlaceholder, ImageResize, ImageResizeButtons, ImageResizeEditing, ImageResizeHandles, ImageSizeAttributes, ImageStyle, ImageStyleCommand, ImageStyleEditing, ImageStyleUI, ImageTextAlternative, ImageTextAlternativeCommand, ImageTextAlternativeEditing, ImageTextAlternativeUI, ImageToolbar, ImageTypeCommand, ImageUpload, ImageUploadEditing, ImageUploadProgress, ImageUploadUI, ImageUtils, InsertImageCommand, PictureEditing, ReplaceImageSourceCommand, ResizeImageCommand, ToggleImageCaptionCommand, UploadImageCommand, DEFAULT_DROPDOWN_DEFINITIONS as _IMAGE_DEFAULT_DROPDOWN_DEFINITIONS, DEFAULT_ICONS as _IMAGE_DEFAULT_ICONS, DEFAULT_OPTIONS as _IMAGE_DEFAULT_OPTIONS, ImageCustomResizeFormView as _ImageCustomResizeFormView, ImageInsertFormView as _ImageInsertFormView, ImageInsertUrlView as _ImageInsertUrlView, utils as _ImageStyleUtils, TextAlternativeFormView as _ImageTextAlternativeFormView, widthAndHeightStylesAreBothSet as _checkIfImageWidthAndHeightStylesAreBothSet, createBlockImageViewElement as _createBlockImageViewElement, createInlineImageViewElement as _createInlineImageViewElement, determineImageTypeForInsertionAtSelection as _determineImageTypeForInsertionAtSelection, downcastImageAttribute as _downcastImageAttribute, downcastSourcesAttribute as _downcastImageSourcesAttribute, downcastSrcsetAttribute as _downcastImageSrcsetAttribute, fetchLocalImage as _fetchLocalImage, getBalloonPositionData as _getImageBalloonPositionData, getSizeValueIfInPx as _getImageSizeValueIfInPx, getImgViewElementMatcher as _getImageViewElementMatcher, getSelectedImageEditorNodes as _getSelectedImageEditorNodes, getSelectedImagePossibleResizeRange as _getSelectedImagePossibleResizeRange, getSelectedImageWidthInUnits as _getSelectedImageWidthInUnits, isLocalImage as _isLocalImage, modelToViewStyleAttribute as _modelToViewImageStyleAttribute, repositionContextualBalloon as _repositionImageContextualBalloon, upcastImageFigure as _upcastImageFigure, upcastPicture as _upcastImagePicture, viewToModelStyleAttribute as _viewToModelImageStyleAttribute, createImageTypeRegExp, isHtmlInDataTransfer };
//# sourceMappingURL=index.js.map