UNPKG

@ckeditor/ckeditor5-table

Version:

Table feature for CKEditor 5.

13,933 lines 507 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 { Widget, WidgetToolbarRepository, isWidget, toWidget, toWidgetEditable } from "@ckeditor/ckeditor5-widget";
import { CKEditorError, Collection, DomEmitterMixin, FocusTracker, KeystrokeHandler, Rect, first, getLocalizedArrowKeyCodeDirection, global, priorities, toUnit, uid } from "@ckeditor/ckeditor5-utils";
import { debounce, isEqual, isObject, throttle } from "es-toolkit/compat";
import { DomEventObserver, Matcher, ModelDocumentSelection, ModelElement, addBackgroundStylesRules, addBorderStylesRules, addMarginStylesRules, addPaddingStylesRules, enableViewPlaceholder, isColorStyleValue, isLengthStyleValue, isPercentageStyleValue } from "@ckeditor/ckeditor5-engine";
import { _getCopyOnEnterAttributes } from "@ckeditor/ckeditor5-enter";
import { IconAlignBottom, IconAlignCenter, IconAlignJustify, IconAlignLeft, IconAlignMiddle, IconAlignRight, IconAlignTop, IconCaption, IconObjectCenter, IconObjectInlineLeft, IconObjectInlineRight, IconObjectLeft, IconObjectRight, IconPreviousArrow, IconTable, IconTableCellProperties, IconTableColumn, IconTableLayout, IconTableMergeCell, IconTableProperties, IconTableRow } from "@ckeditor/ckeditor5-icons";
import { BalloonPanelView, ButtonView, ColorSelectorView, ContextualBalloon, DropdownButtonView, FocusCycler, FormHeaderView, FormRowView, InputTextView, LabelView, LabeledFieldView, MenuBarMenuView, SplitButtonView, SwitchButtonView, ToolbarView, UIModel, View, ViewCollection, addKeyboardHandlingForGrid, addListToDropdown, clickOutsideHandler, createDropdown, createLabeledDropdown, createLabeledInputText, getLocalizedColorOptions, normalizeColorOptions, submitHandler } from "@ckeditor/ckeditor5-ui";
import { ClipboardMarkersUtils, ClipboardPipeline } from "@ckeditor/ckeditor5-clipboard";

/**
* @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 ALIGN_VALUES_REG_EXP$1 = /^(left|center|right)$/;
const FLOAT_VALUES_REG_EXP = /^(left|none|right)$/;
/**
* Conversion helper for upcasting attributes using normalized styles.
*
* @param options.modelAttribute The attribute to set.
* @param options.styleName The style name to convert.
* @param options.attributeName The HTML attribute name to convert.
* @param options.attributeType The HTML attribute type for value normalization.
* @param options.viewElement The view element name that should be converted.
* @param options.defaultValue The default value for the specified `modelAttribute`.
* @param options.shouldUpcast The function which returns `true` if style should be upcasted from this element.
* @internal
*/
function upcastStyleToAttribute(conversion, options) {
	const { modelAttribute, styleName, attributeName, attributeType, viewElement, defaultValue, shouldUpcast = () => true, reduceBoxSides = false } = options;
	conversion.for("upcast").attributeToAttribute({
		view: {
			name: viewElement,
			styles: { [styleName]: /[\s\S]+/ }
		},
		model: {
			key: modelAttribute,
			value: (viewElement, conversionApi, data) => {
				if (!shouldUpcast(viewElement)) return;
				const localDefaultValue = getDefaultValueAdjusted(defaultValue, "", data);
				const normalized = viewElement.getNormalizedStyle(styleName);
				const value = reduceBoxSides ? reduceBoxSidesValue(normalized) : normalized;
				if (localDefaultValue !== value) return value;
				conversionApi.consumable.consume(viewElement, { styles: styleName });
			}
		}
	});
	if (attributeName) conversion.for("upcast").attributeToAttribute({
		view: {
			name: viewElement,
			attributes: { [attributeName]: /.+/ }
		},
		model: {
			key: modelAttribute,
			value: (viewElement, conversionApi, data) => {
				if (viewElement.name == "figure" || viewElement.hasStyle(styleName) || viewElement.name == "table" && viewElement.parent.name == "figure" && viewElement.parent.hasStyle(styleName)) return;
				const localDefaultValue = getDefaultValueAdjusted(defaultValue, "", data);
				let value = viewElement.getAttribute(attributeName);
				if (value && attributeType == "length") {
					const parsedValue = parseFloat(value);
					if (isNaN(parsedValue)) value = localDefaultValue;
					else value = parsedValue + (value.includes("%") ? "%" : "px");
				}
				if (localDefaultValue !== value) return value;
				conversionApi.consumable.consume(viewElement, { attributes: attributeName });
			}
		}
	});
}
/**
* Conversion helper for upcasting border styles for view elements.
*
* @param editor The editor instance.
* @param defaultBorder The default border values.
* @param defaultBorder.color The default `borderColor` value.
* @param defaultBorder.style The default `borderStyle` value.
* @param defaultBorder.width The default `borderWidth` value.
* @internal
*/
function upcastBorderStyles(editor, viewElementName, modelAttributes, defaultBorder) {
	const { conversion } = editor;
	conversion.for("upcast").add((dispatcher) => {
		dispatcher.on(`element:${viewElementName}`, (evt, data, conversionApi) => {
			const { modelRange, viewItem } = data;
			if (!modelRange) return;
			const stylesToConsume = [
				"border-top-width",
				"border-top-color",
				"border-top-style",
				"border-bottom-width",
				"border-bottom-color",
				"border-bottom-style",
				"border-right-width",
				"border-right-color",
				"border-right-style",
				"border-left-width",
				"border-left-color",
				"border-left-style"
			].filter((styleName) => viewItem.hasStyle(styleName));
			const viewTable = viewItem.is("element", "table") ? viewItem : viewItem.findAncestor("table");
			const hasTableBorderAttribute = viewTable.hasAttribute("border");
			if (!stylesToConsume.length && !hasTableBorderAttribute) return;
			const matcherPattern = { styles: stylesToConsume };
			if (!conversionApi.consumable.test(viewItem, matcherPattern)) return;
			const modelElement = first(modelRange.getItems({ shallow: true }));
			const tableModelElement = modelElement.findAncestor("table", { includeSelf: true });
			let localDefaultBorder = defaultBorder;
			if (tableModelElement && tableModelElement.getAttribute("tableType") == "layout") localDefaultBorder = {
				style: "none",
				color: "",
				width: ""
			};
			conversionApi.consumable.consume(viewItem, matcherPattern);
			const normalizedBorder = {
				style: viewItem.getNormalizedStyle("border-style"),
				color: viewItem.getNormalizedStyle("border-color"),
				width: viewItem.getNormalizedStyle("border-width")
			};
			if (hasTableBorderAttribute && conversionApi.consumable.test(viewTable, { attributes: "border" })) {
				const borderValue = parseFloat(viewTable.getAttribute("border") || "1");
				const borderPx = Number.isNaN(borderValue) || !Number.isFinite(borderValue) || borderValue < 0 || viewItem.name != "table" && borderValue > 1 ? "1px" : `${borderValue}px`;
				normalizedBorder.width = {
					top: borderPx,
					bottom: borderPx,
					right: borderPx,
					left: borderPx,
					...normalizedBorder.width || {}
				};
				if (viewItem.is("element", "table")) conversionApi.consumable.consume(viewTable, { attributes: "border" });
			}
			const reducedBorder = {
				style: reduceBoxSidesValue(normalizedBorder.style),
				color: reduceBoxSidesValue(normalizedBorder.color),
				width: reduceBoxSidesValue(normalizedBorder.width)
			};
			if (reducedBorder.style !== localDefaultBorder.style) conversionApi.writer.setAttribute(modelAttributes.style, reducedBorder.style, modelElement);
			if (reducedBorder.color !== localDefaultBorder.color) conversionApi.writer.setAttribute(modelAttributes.color, reducedBorder.color, modelElement);
			if (reducedBorder.width !== localDefaultBorder.width) conversionApi.writer.setAttribute(modelAttributes.width, reducedBorder.width, modelElement);
		});
	});
}
/**
* Conversion helper for downcasting an attribute to a style.
*
* @internal
*/
function downcastAttributeToStyle(conversion, options) {
	const { modelElement, modelAttribute, styleName } = options;
	conversion.for("downcast").attributeToAttribute({
		model: {
			name: modelElement,
			key: modelAttribute
		},
		view: (modelAttributeValue) => ({
			key: "style",
			value: { [styleName]: modelAttributeValue }
		})
	});
}
/**
* Conversion helper for downcasting attributes from the model table to a view table (not to `<figure>`).
*
* @internal
*/
function downcastTableAttribute(conversion, options) {
	const { modelAttribute, styleName } = options;
	conversion.for("downcast").add((dispatcher) => {
		dispatcher.on(`attribute:${modelAttribute}:table`, (evt, data, conversionApi) => {
			const { item, attributeNewValue } = data;
			const { mapper, writer } = conversionApi;
			if (!conversionApi.consumable.consume(data.item, evt.name)) return;
			const table = Array.from(mapper.toViewElement(item).getChildren()).find((child) => child.is("element", "table"));
			if (attributeNewValue) writer.setStyle(styleName, attributeNewValue, table);
			else writer.removeStyle(styleName, table);
		});
	});
}
/**
* Returns the default value for table or table cell property adjusted for layout tables.
*
* @internal
*/
function getDefaultValueAdjusted(defaultValue, layoutTableDefault, data) {
	const modelElement = data.modelRange && first(data.modelRange.getItems({ shallow: true }));
	const tableElement = modelElement && modelElement.is("element") && modelElement.findAncestor("table", { includeSelf: true });
	if (tableElement && tableElement.getAttribute("tableType") === "layout") return layoutTableDefault;
	return defaultValue;
}
/**
* Reduces the full top, right, bottom, left object to a single string if all sides are equal.
* Returns original style otherwise.
*/
function reduceBoxSidesValue(style) {
	if (!style) return;
	const sides = [
		"top",
		"right",
		"bottom",
		"left"
	];
	if (!sides.every((side) => style[side])) return style;
	const topSideStyle = style.top;
	if (!sides.every((side) => style[side] === topSideStyle)) return style;
	return topSideStyle;
}
/**
* Conversion helper for upcasting the `cellpadding` table attribute.
*
* @param editor The editor instance.
* @param viewElementName The view element name that should be converted.
* @param defaultPadding The default padding value.
* @internal
*/
function upcastTableCellPaddingAttribute(editor, viewElementName, defaultPadding) {
	const { conversion } = editor;
	conversion.for("upcast").add((dispatcher) => {
		dispatcher.on(`element:${viewElementName}`, (evt, data, conversionApi) => {
			const { modelRange, viewItem } = data;
			if (!modelRange) return;
			if (viewItem.is("element", "table")) {
				conversionApi.consumable.consume(viewItem, { attributes: "cellpadding" });
				return;
			}
			const viewTable = viewItem.findAncestor("table");
			if (!viewTable.hasAttribute("cellpadding") || !conversionApi.consumable.test(viewTable, { attributes: "cellpadding" })) return;
			const modelElement = modelRange?.start?.nodeAfter;
			const cellpaddingValue = parseFloat(viewTable.getAttribute("cellpadding") || "1");
			const cellpaddingPx = Number.isNaN(cellpaddingValue) || !Number.isFinite(cellpaddingValue) || cellpaddingValue < 0 ? "0px" : `${cellpaddingValue}px`;
			const tableCellPaddings = modelElement.getAttribute("tableCellPadding");
			if (!tableCellPaddings) {
				if (defaultPadding !== cellpaddingPx) conversionApi.writer.setAttribute("tableCellPadding", cellpaddingPx, modelElement);
			} else if (typeof tableCellPaddings === "object") {
				const normalizedPaddings = {
					...defaultPadding !== cellpaddingPx && { top: cellpaddingPx },
					...defaultPadding !== cellpaddingPx && { right: cellpaddingPx },
					...defaultPadding !== cellpaddingPx && { bottom: cellpaddingPx },
					...defaultPadding !== cellpaddingPx && { left: cellpaddingPx },
					...tableCellPaddings
				};
				conversionApi.writer.setAttribute("tableCellPadding", normalizedPaddings, modelElement);
			}
		}, { priority: "low" });
	});
}
/**
* Default table alignment options.
*/
const DEFAULT_TABLE_ALIGNMENT_OPTIONS = {
	left: { className: "table-style-align-left" },
	center: { className: "table-style-align-center" },
	right: { className: "table-style-align-right" },
	blockLeft: { className: "table-style-block-align-left" },
	blockRight: { className: "table-style-block-align-right" }
};
/**
* Configuration for upcasting table alignment from view to model.
*/
const upcastTableAlignmentConfig = [
	{
		view: {
			name: /^(table|figure)$/,
			styles: { float: FLOAT_VALUES_REG_EXP }
		},
		getAlign: (viewElement) => {
			let align = viewElement.getStyle("float");
			if (align === "none") align = "center";
			return align;
		},
		getConsumables(viewElement) {
			const float = viewElement.getStyle("float");
			const styles = ["float"];
			if (float === "left" && viewElement.hasStyle("margin-right")) styles.push("margin-right");
			else if (float === "right" && viewElement.hasStyle("margin-left")) styles.push("margin-left");
			return { styles };
		}
	},
	{
		view: {
			name: /^(table|figure)$/,
			styles: {
				"margin-left": "auto",
				"margin-right": "auto"
			}
		},
		getAlign: () => "center",
		getConsumables: () => {
			return { styles: ["margin-left", "margin-right"] };
		}
	},
	{
		view: {
			name: /^(table|figure)$/,
			key: "class",
			value: "table-style-align-left"
		},
		getAlign: () => "left",
		getConsumables() {
			return { classes: DEFAULT_TABLE_ALIGNMENT_OPTIONS.left.className };
		}
	},
	{
		view: {
			name: /^(table|figure)$/,
			key: "class",
			value: DEFAULT_TABLE_ALIGNMENT_OPTIONS.right.className
		},
		getAlign: () => "right",
		getConsumables() {
			return { classes: DEFAULT_TABLE_ALIGNMENT_OPTIONS.right.className };
		}
	},
	{
		view: {
			name: /^(table|figure)$/,
			key: "class",
			value: DEFAULT_TABLE_ALIGNMENT_OPTIONS.center.className
		},
		getAlign: () => "center",
		getConsumables() {
			return { classes: DEFAULT_TABLE_ALIGNMENT_OPTIONS.center.className };
		}
	},
	{
		view: {
			name: /^(table|figure)$/,
			key: "class",
			value: DEFAULT_TABLE_ALIGNMENT_OPTIONS.blockLeft.className
		},
		getAlign: () => "blockLeft",
		getConsumables() {
			return { classes: DEFAULT_TABLE_ALIGNMENT_OPTIONS.blockLeft.className };
		}
	},
	{
		view: {
			name: /^(table|figure)$/,
			key: "class",
			value: DEFAULT_TABLE_ALIGNMENT_OPTIONS.blockRight.className
		},
		getAlign: () => "blockRight",
		getConsumables() {
			return { classes: DEFAULT_TABLE_ALIGNMENT_OPTIONS.blockRight.className };
		}
	},
	{
		view: {
			name: /^(table|figure)$/,
			styles: {
				"margin-left": "0",
				"margin-right": "auto"
			}
		},
		getAlign: () => "blockLeft",
		getConsumables() {
			return { styles: ["margin-left", "margin-right"] };
		}
	},
	{
		view: {
			name: /^(table|figure)$/,
			styles: {
				"margin-left": "auto",
				"margin-right": "0"
			}
		},
		getAlign: () => "blockRight",
		getConsumables() {
			return { styles: ["margin-left", "margin-right"] };
		}
	},
	{
		view: {
			name: "table",
			attributes: { align: ALIGN_VALUES_REG_EXP$1 }
		},
		getAlign: (viewElement) => viewElement.getAttribute("align"),
		getConsumables() {
			return { attributes: "align" };
		}
	}
];
const downcastTableAlignmentConfig = {
	center: {
		align: "center",
		style: "margin-left: auto; margin-right: auto;",
		className: "table-style-align-center"
	},
	left: {
		align: "left",
		style: "float: left;",
		className: "table-style-align-left"
	},
	right: {
		align: "right",
		style: "float: right;",
		className: "table-style-align-right"
	},
	blockLeft: {
		align: void 0,
		style: "margin-left: 0; margin-right: auto;",
		className: DEFAULT_TABLE_ALIGNMENT_OPTIONS.blockLeft.className
	},
	blockRight: {
		align: void 0,
		style: "margin-left: auto; margin-right: 0;",
		className: DEFAULT_TABLE_ALIGNMENT_OPTIONS.blockRight.className
	}
};

/**
* @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 table iterator class. It allows to iterate over table cells. For each cell the iterator yields
* {@link module:table/tablewalker~TableSlot} with proper table cell attributes.
*/
var TableWalker = class {
	/**
	* The walker's table element.
	*
	* @internal
	*/
	_table;
	/**
	* A row index from which this iterator will start.
	*/
	_startRow;
	/**
	* A row index at which this iterator will end.
	*/
	_endRow;
	/**
	* If set, the table walker will only output cells from a given column and following ones or cells that overlap them.
	*/
	_startColumn;
	/**
	* If set, the table walker will only output cells up to a given column.
	*/
	_endColumn;
	/**
	* Enables output of spanned cells that are normally not yielded.
	*/
	_includeAllSlots;
	/**
	* Row indexes to skip from the iteration.
	*/
	_skipRows;
	/**
	* The current row index.
	*
	* @internal
	*/
	_row;
	/**
	* The index of the current row element in the table.
	*
	* @internal
	*/
	_rowIndex;
	/**
	* The current column index.
	*
	* @internal
	*/
	_column;
	/**
	* The cell index in a parent row. For spanned cells when {@link #_includeAllSlots} is set to `true`,
	* this represents the index of the next table cell.
	*
	* @internal
	*/
	_cellIndex;
	/**
	* Holds a map of spanned cells in a table.
	*/
	_spannedCells;
	/**
	* Index of the next column where a cell is anchored.
	*/
	_nextCellAtColumn;
	/**
	* Indicates whether the iterator jumped to (or close to) the start row, ignoring rows that don't need to be traversed.
	*/
	_jumpedToStartRow = false;
	/**
	* Creates an instance of the table walker.
	*
	* The table walker iterates internally by traversing the table from row index = 0 and column index = 0.
	* It walks row by row and column by column in order to output values defined in the constructor.
	* By default it will output only the locations that are occupied by a cell. To include also spanned rows and columns,
	* pass the `includeAllSlots` option to the constructor.
	*
	* The most important values of the iterator are column and row indexes of a cell.
	*
	* See {@link module:table/tablewalker~TableSlot} what values are returned by the table walker.
	*
	* To iterate over a given row:
	*
	* ```ts
	* const tableWalker = new TableWalker( table, { startRow: 1, endRow: 2 } );
	*
	* for ( const tableSlot of tableWalker ) {
	*   console.log( 'A cell at row', tableSlot.row, 'and column', tableSlot.column );
	* }
	* ```
	*
	* For instance the code above for the following table:
	*
	*  +----+----+----+----+----+----+
	*  | 00      | 02 | 03 | 04 | 05 |
	*  |         +----+----+----+----+
	*  |         | 12      | 14 | 15 |
	*  |         +----+----+----+    +
	*  |         | 22           |    |
	*  |----+----+----+----+----+    +
	*  | 30 | 31 | 32 | 33 | 34 |    |
	*  +----+----+----+----+----+----+
	*
	* will log in the console:
	*
	*  'A cell at row 1 and column 2'
	*  'A cell at row 1 and column 4'
	*  'A cell at row 1 and column 5'
	*  'A cell at row 2 and column 2'
	*
	* To also iterate over spanned cells:
	*
	* ```ts
	* const tableWalker = new TableWalker( table, { row: 1, includeAllSlots: true } );
	*
	* for ( const tableSlot of tableWalker ) {
	*   console.log( 'Slot at', tableSlot.row, 'x', tableSlot.column, ':', tableSlot.isAnchor ? 'is anchored' : 'is spanned' );
	* }
	* ```
	*
	* will log in the console for the table from the previous example:
	*
	*  'Cell at 1 x 0 : is spanned'
	*  'Cell at 1 x 1 : is spanned'
	*  'Cell at 1 x 2 : is anchored'
	*  'Cell at 1 x 3 : is spanned'
	*  'Cell at 1 x 4 : is anchored'
	*  'Cell at 1 x 5 : is anchored'
	*
	* **Note**: Option `row` is a shortcut that sets both `startRow` and `endRow` to the same row.
	* (Use either `row` or `startRow` and `endRow` but never together). Similarly the `column` option sets both `startColumn`
	* and `endColumn` to the same column (Use either `column` or `startColumn` and `endColumn` but never together).
	*
	* @param table A table over which the walker iterates.
	* @param options An object with configuration.
	* @param options.row A row index for which this iterator will output cells. Can't be used together with `startRow` and `endRow`.
	* @param options.startRow A row index from which this iterator should start. Can't be used together with `row`. Default value is 0.
	* @param options.endRow A row index at which this iterator should end. Can't be used together with `row`.
	* @param options.column A column index for which this iterator will output cells.
	* Can't be used together with `startColumn` and `endColumn`.
	* @param options.startColumn A column index from which this iterator should start.
	* Can't be used together with `column`. Default value is 0.
	* @param options.endColumn A column index at which this iterator should end. Can't be used together with `column`.
	* @param options.includeAllSlots Also return values for spanned cells. Default value is "false".
	*/
	constructor(table, options = {}) {
		this._table = table;
		this._startRow = options.row !== void 0 ? options.row : options.startRow || 0;
		this._endRow = options.row !== void 0 ? options.row : options.endRow;
		this._startColumn = options.column !== void 0 ? options.column : options.startColumn || 0;
		this._endColumn = options.column !== void 0 ? options.column : options.endColumn;
		this._includeAllSlots = !!options.includeAllSlots;
		this._skipRows = /* @__PURE__ */ new Set();
		this._row = 0;
		this._rowIndex = 0;
		this._column = 0;
		this._cellIndex = 0;
		this._spannedCells = /* @__PURE__ */ new Map();
		this._nextCellAtColumn = -1;
	}
	/**
	* Iterable interface.
	*/
	[Symbol.iterator]() {
		return this;
	}
	/**
	* Gets the next table walker's value.
	*
	* @returns The next table walker's value.
	*/
	next() {
		if (this._canJumpToStartRow()) this._jumpToNonSpannedRowClosestToStartRow();
		const row = this._table.getChild(this._rowIndex);
		if (!row || this._isOverEndRow()) return {
			done: true,
			value: void 0
		};
		if (!row.is("element", "tableRow")) {
			this._rowIndex++;
			return this.next();
		}
		if (this._isOverEndColumn()) return this._advanceToNextRow();
		let outValue = null;
		const spanData = this._getSpanned();
		if (spanData) {
			if (this._includeAllSlots && !this._shouldSkipSlot()) outValue = this._formatOutValue(spanData.cell, spanData.row, spanData.column);
		} else {
			const cell = row.getChild(this._cellIndex);
			if (!cell) return this._advanceToNextRow();
			const colspan = parseInt(cell.getAttribute("colspan") || "1");
			const rowspan = parseInt(cell.getAttribute("rowspan") || "1");
			if (colspan > 1 || rowspan > 1) this._recordSpans(cell, rowspan, colspan);
			if (!this._shouldSkipSlot()) outValue = this._formatOutValue(cell);
			this._nextCellAtColumn = this._column + colspan;
		}
		this._column++;
		if (this._column == this._nextCellAtColumn) this._cellIndex++;
		return outValue || this.next();
	}
	/**
	* Marks a row to skip in the next iteration. It will also skip cells from the current row if there are any cells from the current row
	* to output.
	*
	* @param row The row index to skip.
	*/
	skipRow(row) {
		this._skipRows.add(row);
	}
	/**
	* Advances internal cursor to the next row.
	*/
	_advanceToNextRow() {
		this._row++;
		this._rowIndex++;
		this._column = 0;
		this._cellIndex = 0;
		this._nextCellAtColumn = -1;
		return this.next();
	}
	/**
	* Checks if the current row is over {@link #_endRow}.
	*/
	_isOverEndRow() {
		return this._endRow !== void 0 && this._row > this._endRow;
	}
	/**
	* Checks if the current cell is over {@link #_endColumn}
	*/
	_isOverEndColumn() {
		return this._endColumn !== void 0 && this._column > this._endColumn;
	}
	/**
	* A common method for formatting the iterator's output value.
	*
	* @param cell The table cell to output.
	* @param anchorRow The row index of a cell anchor slot.
	* @param anchorColumn The column index of a cell anchor slot.
	*/
	_formatOutValue(cell, anchorRow = this._row, anchorColumn = this._column) {
		return {
			done: false,
			value: new TableSlot(this, cell, anchorRow, anchorColumn)
		};
	}
	/**
	* Checks if the current slot should be skipped.
	*/
	_shouldSkipSlot() {
		const rowIsMarkedAsSkipped = this._skipRows.has(this._row);
		const rowIsBeforeStartRow = this._row < this._startRow;
		const columnIsBeforeStartColumn = this._column < this._startColumn;
		const columnIsAfterEndColumn = this._endColumn !== void 0 && this._column > this._endColumn;
		return rowIsMarkedAsSkipped || rowIsBeforeStartRow || columnIsBeforeStartColumn || columnIsAfterEndColumn;
	}
	/**
	* Returns the cell element that is spanned over the current cell location.
	*/
	_getSpanned() {
		const rowMap = this._spannedCells.get(this._row);
		if (!rowMap) return null;
		return rowMap.get(this._column) || null;
	}
	/**
	* Updates spanned cells map relative to the current cell location and its span dimensions.
	*
	* @param cell A cell that is spanned.
	* @param rowspan Cell height.
	* @param colspan Cell width.
	*/
	_recordSpans(cell, rowspan, colspan) {
		const data = {
			cell,
			row: this._row,
			column: this._column
		};
		for (let rowToUpdate = this._row; rowToUpdate < this._row + rowspan; rowToUpdate++) for (let columnToUpdate = this._column; columnToUpdate < this._column + colspan; columnToUpdate++) if (rowToUpdate != this._row || columnToUpdate != this._column) this._markSpannedCell(rowToUpdate, columnToUpdate, data);
	}
	/**
	* Marks the cell location as spanned by another cell.
	*
	* @param row The row index of the cell location.
	* @param column The column index of the cell location.
	* @param data A spanned cell details (cell element, anchor row and column).
	*/
	_markSpannedCell(row, column, data) {
		if (!this._spannedCells.has(row)) this._spannedCells.set(row, /* @__PURE__ */ new Map());
		this._spannedCells.get(row).set(column, data);
	}
	/**
	* Checks if part of the table can be skipped.
	*/
	_canJumpToStartRow() {
		return !!this._startRow && this._startRow > 0 && !this._jumpedToStartRow;
	}
	/**
	* Sets the current row to `this._startRow` or the first row before it that has the number of cells
	* equal to the number of columns in the table.
	*
	* Example:
	* 	+----+----+----+
	*  | 00 | 01 | 02 |
	*  |----+----+----+
	*  | 10      | 12 |
	*  |         +----+
	*  |         | 22 |
	*  |         +----+
	*  |         | 32 | <--- Start row
	*  +----+----+----+
	*  | 40 | 41 | 42 |
	*  +----+----+----+
	*
	* If the 4th row is a `this._startRow`, this method will:
	* 1.) Count the number of columns this table has based on the first row (3 columns in this case).
	* 2.) Check if the 4th row contains 3 cells. It doesn't, so go to the row before it.
	* 3.) Check if the 3rd row contains 3 cells. It doesn't, so go to the row before it.
	* 4.) Check if the 2nd row contains 3 cells. It does, so set the current row to that row.
	*
	* Setting the current row this way is necessary to let the `next()`  method loop over the cells
	* spanning multiple rows or columns and update the `this._spannedCells` property.
	*/
	_jumpToNonSpannedRowClosestToStartRow() {
		const firstRowLength = this._getRowLength(0);
		for (let i = this._startRow; !this._jumpedToStartRow; i--) if (firstRowLength === this._getRowLength(i)) {
			this._row = i;
			this._rowIndex = i;
			this._jumpedToStartRow = true;
		}
	}
	/**
	* Returns a number of columns in a row taking `colspan` into consideration.
	*/
	_getRowLength(rowIndex) {
		return [...this._table.getChild(rowIndex).getChildren()].reduce((cols, row) => {
			return cols + parseInt(row.getAttribute("colspan") || "1");
		}, 0);
	}
};
/**
* An object returned by {@link module:table/tablewalker~TableWalker} when traversing table cells.
*/
var TableSlot = class {
	/**
	* The current table cell.
	*/
	cell;
	/**
	* The row index of a table slot.
	*/
	row;
	/**
	* The column index of a table slot.
	*/
	column;
	/**
	* The row index of a cell anchor slot.
	*/
	cellAnchorRow;
	/**
	* The column index of a cell anchor slot.
	*/
	cellAnchorColumn;
	/**
	* The index of the current cell in the parent row.
	*/
	_cellIndex;
	/**
	* The index of the current row element in the table.
	*/
	_rowIndex;
	/**
	* The table element.
	*/
	_table;
	/**
	* Creates an instance of the table walker value.
	*
	* @param tableWalker The table walker instance.
	* @param cell The current table cell.
	* @param anchorRow The row index of a cell anchor slot.
	* @param anchorColumn The column index of a cell anchor slot.
	*/
	constructor(tableWalker, cell, anchorRow, anchorColumn) {
		this.cell = cell;
		this.row = tableWalker._row;
		this.column = tableWalker._column;
		this.cellAnchorRow = anchorRow;
		this.cellAnchorColumn = anchorColumn;
		this._cellIndex = tableWalker._cellIndex;
		this._rowIndex = tableWalker._rowIndex;
		this._table = tableWalker._table;
	}
	/**
	* Whether the cell is anchored in the current slot.
	*/
	get isAnchor() {
		return this.row === this.cellAnchorRow && this.column === this.cellAnchorColumn;
	}
	/**
	* The width of a cell defined by a `colspan` attribute. If the model attribute is not present, it is set to `1`.
	*/
	get cellWidth() {
		return parseInt(this.cell.getAttribute("colspan") || "1");
	}
	/**
	* The height of a cell defined by a `rowspan` attribute. If the model attribute is not present, it is set to `1`.
	*/
	get cellHeight() {
		return parseInt(this.cell.getAttribute("rowspan") || "1");
	}
	/**
	* The index of the current row element in the table.
	*/
	get rowIndex() {
		return this._rowIndex;
	}
	/**
	* Returns the {@link module:engine/model/position~ModelPosition} before the table slot.
	*/
	getPositionBefore() {
		return this._table.root.document.model.createPositionAt(this._table.getChild(this.row), this._cellIndex);
	}
};
/**
* This `TableSlot`'s getter (property) was removed in CKEditor 5 v20.0.0.
*
* Check out the new `TableWalker`'s API in the documentation.
*
* @error tableslot-getter-removed
* @param getterName
*/

/**
* @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
*/
/**
* Checks if the given cell type represents a header cell.
*
* @param cellType The type of the table cell.
* @returns `true` if the cell type represents a header cell, `false` otherwise.
*/
function isTableHeaderCellType(cellType) {
	return cellType === "header" || cellType === "header-row" || cellType === "header-column";
}

/**
* @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 common method to update the numeric value. If a value is the default one, it will be unset.
*
* @internal
* @param key An attribute key.
* @param value The new attribute value.
* @param item A model item on which the attribute will be set.
* @param defaultValue The default attribute value. If a value is lower or equal, it will be unset.
*/
function updateNumericAttribute(key, value, item, writer, defaultValue = 1) {
	if (value !== void 0 && value !== null && defaultValue !== void 0 && defaultValue !== null && value > defaultValue) writer.setAttribute(key, value, item);
	else writer.removeAttribute(key, item);
}
/**
* A common method to create an empty table cell. It creates a proper model structure as a table cell must have at least one block inside.
*
* @internal
* @param writer The model writer.
* @param insertPosition The position at which the table cell should be inserted.
* @param attributes The element attributes.
* @returns Created table cell.
*/
function createEmptyTableCell(writer, insertPosition, attributes = {}) {
	const tableCell = writer.createElement("tableCell", attributes);
	writer.insertElement("paragraph", tableCell);
	writer.insert(tableCell, insertPosition);
	return tableCell;
}
/**
* Checks if a table cell belongs to the heading column section.
*
* @internal
*/
function isHeadingColumnCell(tableUtils, tableCell) {
	const table = tableCell.parent.parent;
	const headingColumns = parseInt(table.getAttribute("headingColumns") || "0");
	const { column } = tableUtils.getCellLocation(tableCell);
	return !!headingColumns && column < headingColumns;
}
/**
* Enables conversion for an attribute for simple view-model mappings.
*
* @internal
* @param options.defaultValue The default value for the specified `modelAttribute`.
*/
function enableProperty(schema, conversion, options) {
	const { modelAttribute } = options;
	schema.extend("tableCell", { allowAttributes: [modelAttribute] });
	schema.setAttributeProperties(modelAttribute, { isFormatting: true });
	upcastStyleToAttribute(conversion, {
		viewElement: /^(td|th)$/,
		...options
	});
	downcastAttributeToStyle(conversion, {
		modelElement: "tableCell",
		...options
	});
}
/**
* Depending on the position of the selection we either return the table under cursor or look for the table higher in the hierarchy.
*
* @internal
*/
function getSelectionAffectedTable(selection) {
	const selectedElement = selection.getSelectedElement();
	if (selectedElement && selectedElement.is("element", "table")) return selectedElement;
	return selection.getFirstPosition().findAncestor("table");
}
/**
* Groups table cells by their parent table.
*
* @internal
*/
function groupCellsByTable(tableCells) {
	const tableMap = /* @__PURE__ */ new Map();
	for (const tableCell of tableCells) {
		const table = tableCell.findAncestor("table");
		if (!tableMap.has(table)) tableMap.set(table, []);
		tableMap.get(table).push(tableCell);
	}
	return tableMap;
}
/**
* Checks if all cells in a given row or column are header cells.
*
* @internal
*/
function isEntireCellsLineHeader({ table, row, column }) {
	const tableWalker = new TableWalker(table, {
		row,
		column
	});
	for (const { cell } of tableWalker) if (!isTableHeaderCellType(cell.getAttribute("tableCellType"))) return false;
	return true;
}
/**
* Checks whether the `tableCellType` attribute is enabled in the editor schema and the experimental flag is set.
*
* @internal
*/
function isTableCellTypeEnabled(editor) {
	return editor.model.schema.checkAttribute("tableCell", "tableCellType");
}
/**
* Yields every empty block (typically a `paragraph`) found inside the given table's cells.
*
* @internal
*/
function* getEmptyTableCellBlocks(table) {
	for (const row of table.getChildren()) {
		if (!row.is("element", "tableRow")) continue;
		for (const cell of row.getChildren()) {
			if (!cell.is("element", "tableCell")) continue;
			for (const block of cell.getChildren()) if (block.is("element") && block.isEmpty) yield block;
		}
	}
}

/**
* @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 cropped table according to given dimensions.

* To return a cropped table that starts at first row and first column and end in third row and column:
*
* ```ts
* const croppedTable = cropTableToDimensions( table, {
*   startRow: 1,
*   endRow: 3,
*   startColumn: 1,
*   endColumn: 3
* }, writer );
* ```
*
* Calling the code above for the table below:
*
*        0   1   2   3   4                      0   1   2
*      ┌───┬───┬───┬───┬───┐
*   0  │ a │ b │ c │ d │ e │
*      ├───┴───┤   ├───┴───┤                  ┌───┬───┬───┐
*   1  │ f     │   │ g     │                  │   │   │ g │  0
*      ├───┬───┴───┼───┬───┤   will return:   ├───┴───┼───┤
*   2  │ h │ i     │ j │ k │                  │ i     │ j │  1
*      ├───┤       ├───┤   │                  │       ├───┤
*   3  │ l │       │ m │   │                  │       │ m │  2
*      ├───┼───┬───┤   ├───┤                  └───────┴───┘
*   4  │ n │ o │ p │   │ q │
*      └───┴───┴───┴───┴───┘
*
* @internal
*/
function cropTableToDimensions(sourceTable, cropDimensions, writer) {
	const { startRow, startColumn, endRow, endColumn } = cropDimensions;
	const croppedTable = writer.createElement("table");
	const sourceTableType = sourceTable.getAttribute("tableType");
	if (sourceTableType) writer.setAttribute("tableType", sourceTableType, croppedTable);
	const cropHeight = endRow - startRow + 1;
	for (let i = 0; i < cropHeight; i++) writer.insertElement("tableRow", croppedTable, "end");
	const tableMap = [...new TableWalker(sourceTable, {
		startRow,
		endRow,
		startColumn,
		endColumn,
		includeAllSlots: true
	})];
	for (const { row: sourceRow, column: sourceColumn, cell: tableCell, isAnchor, cellAnchorRow, cellAnchorColumn } of tableMap) {
		const rowInCroppedTable = sourceRow - startRow;
		const row = croppedTable.getChild(rowInCroppedTable);
		if (!isAnchor) {
			if (cellAnchorRow < startRow || cellAnchorColumn < startColumn) createEmptyTableCell(writer, writer.createPositionAt(row, "end"));
		} else {
			const tableCellCopy = writer.cloneElement(tableCell);
			writer.append(tableCellCopy, row);
			trimTableCellIfNeeded(tableCellCopy, sourceRow, sourceColumn, endRow, endColumn, writer);
		}
	}
	addHeadingsToCroppedTable(croppedTable, sourceTable, startRow, startColumn, writer);
	addFootersToCroppedTable(croppedTable, sourceTable, startRow, endRow, writer);
	return croppedTable;
}
/**
* Returns slot info of cells that starts above and overlaps a given row.
*
* In a table below, passing `overlapRow = 3`
*
*     ┌───┬───┬───┬───┬───┐
*  0  │ a │ b │ c │ d │ e │
*     │   ├───┼───┼───┼───┤
*  1  │   │ f │ g │ h │ i │
*     ├───┤   ├───┼───┤   │
*  2  │ j │   │ k │ l │   │
*     │   │   │   ├───┼───┤
*  3  │   │   │   │ m │ n │  <- overlap row to check
*     ├───┼───┤   │   ├───│
*  4  │ o │ p │   │   │ q │
*     └───┴───┴───┴───┴───┘
*
* will return slot info for cells: "j", "f", "k".
*
* @internal
* @param table The table to check.
* @param overlapRow The index of the row to check.
* @param startRow row to start analysis. Use it when it is known that the cells above that row will not overlap. Default value is 0.
*/
function getVerticallyOverlappingCells(table, overlapRow, startRow = 0) {
	const cells = [];
	const tableWalker = new TableWalker(table, {
		startRow,
		endRow: overlapRow - 1
	});
	for (const slotInfo of tableWalker) {
		const { row, cellHeight } = slotInfo;
		const cellEndRow = row + cellHeight - 1;
		if (row < overlapRow && overlapRow <= cellEndRow) cells.push(slotInfo);
	}
	return cells;
}
/**
* Splits the table cell horizontally.
*
* @internal
* @returns Created table cell, if any were created.
*/
function splitHorizontally(tableCell, splitRow, writer) {
	const tableRow = tableCell.parent;
	const table = tableRow.parent;
	const rowIndex = tableRow.index;
	const rowspan = parseInt(tableCell.getAttribute("rowspan"));
	const newRowspan = splitRow - rowIndex;
	const newCellAttributes = {};
	const newCellRowSpan = rowspan - newRowspan;
	if (newCellRowSpan > 1) newCellAttributes.rowspan = newCellRowSpan;
	const colspan = parseInt(tableCell.getAttribute("colspan") || "1");
	if (colspan > 1) newCellAttributes.colspan = colspan;
	const startRow = rowIndex;
	const endRow = startRow + newRowspan;
	const tableMap = [...new TableWalker(table, {
		startRow,
		endRow,
		includeAllSlots: true
	})];
	let newCell = null;
	let columnIndex;
	for (const tableSlot of tableMap) {
		const { row, column, cell } = tableSlot;
		if (cell === tableCell && columnIndex === void 0) columnIndex = column;
		if (columnIndex !== void 0 && columnIndex === column && row === endRow) newCell = createEmptyTableCell(writer, tableSlot.getPositionBefore(), newCellAttributes);
	}
	updateNumericAttribute("rowspan", newRowspan, tableCell, writer);
	return newCell;
}
/**
* Returns slot info of cells that starts before and overlaps a given column.
*
* In a table below, passing `overlapColumn = 3`
*
*    0   1   2   3   4
*  ┌───────┬───────┬───┐
*  │ a     │ b     │ c │
*  │───┬───┴───────┼───┤
*  │ d │ e         │ f │
*  ├───┼───┬───────┴───┤
*  │ g │ h │ i         │
*  ├───┼───┼───┬───────┤
*  │ j │ k │ l │ m     │
*  ├───┼───┴───┼───┬───┤
*  │ n │ o     │ p │ q │
*  └───┴───────┴───┴───┘
*                ^
*                Overlap column to check
*
* will return slot info for cells: "b", "e", "i".
*
* @internal
* @param table The table to check.
* @param overlapColumn The index of the column to check.
*/
function getHorizontallyOverlappingCells(table, overlapColumn) {
	const cellsToSplit = [];
	const tableWalker = new TableWalker(table);
	for (const slotInfo of tableWalker) {
		const { column, cellWidth } = slotInfo;
		const cellEndColumn = column + cellWidth - 1;
		if (column < overlapColumn && overlapColumn <= cellEndColumn) cellsToSplit.push(slotInfo);
	}
	return cellsToSplit;
}
/**
* Splits the table cell vertically.
*
* @internal
* @param columnIndex The table cell column index.
* @param splitColumn The index of column to split cell on.
* @returns Created table cell.
*/
function splitVertically(tableCell, columnIndex, splitColumn, writer) {
	const colspan = parseInt(tableCell.getAttribute("colspan"));
	const newColspan = splitColumn - columnIndex;
	const newCellAttributes = {};
	const newCellColSpan = colspan - newColspan;
	if (newCellColSpan > 1) newCellAttributes.colspan = newCellColSpan;
	const rowspan = parseInt(tableCell.getAttribute("rowspan") || "1");
	if (rowspan > 1) newCellAttributes.rowspan = rowspan;
	const newCell = createEmptyTableCell(writer, writer.createPositionAfter(tableCell), newCellAttributes);
	updateNumericAttribute("colspan", newColspan, tableCell, writer);
	return newCell;
}
/**
* Adjusts table cell dimensions to not exceed limit row and column.
*
* If table cell width (or height) covers a column (or row) that is after a limit column (or row)
* this method will trim "colspan" (or "rowspan") attribute so the table cell will fit in a defined limits.
*
* @internal
*/
function trimTableCellIfNeeded(tableCell, cellRow, cellColumn, limitRow, limitColumn, writer) {
	const colspan = parseInt(tableCell.getAttribute("colspan") || "1");
	const rowspan = parseInt(tableCell.getAttribute("rowspan") || "1");
	if (cellColumn + colspan - 1 > limitColumn) updateNumericAttribute("colspan", limitColumn - cellColumn + 1, tableCell, writer, 1);
	if (cellRow + rowspan - 1 > limitRow) updateNumericAttribute("rowspan", limitRow - cellRow + 1, tableCell, writer, 1);
}
/**
* Sets proper heading attributes to a cropped table.
*/
function addHeadingsToCroppedTable(croppedTable, sourceTable, startRow, startColumn, writer) {
	const headingRows = parseInt(sourceTable.getAttribute("headingRows") || "0");
	if (headingRows > 0) updateNumericAttribute("headingRows", headingRows - startRow, croppedTable, writer, 0);
	const headingColumns = parseInt(sourceTable.getAttribute("headingColumns") || "0");
	if (headingColumns > 0) updateNumericAttribute("headingColumns", headingColumns - startColumn, croppedTable, writer, 0);
}
/**
* Sets footer row attributes to a cropped table.
*/
function addFootersToCroppedTable(croppedTable, sourceTable, startRow, endRow, writer) {
	const maxRows = Array.from(sourceTable.getChildren()).reduce((count, row) => row.is("element", "tableRow") ? count + 1 : count, 0);
	const footerRows = parseInt(sourceTable.getAttribute("footerRows") || "0");
	const footerIndex = maxRows - footerRows;
	if (footerRows < 1) return;
	let footerRowsInCrop = 0;
	if (endRow >= footerIndex) footerRowsInCrop = endRow - Math.max(footerIndex, startRow) + 1;
	updateNumericAttribute("footerRows", footerRowsInCrop, croppedTable, writer, 0);
}
/**
* Removes columns that have no cells anchored.
*
* In table below:
*
*     +----+----+----+----+----+----+----+
*     | 00 | 01      | 03 | 04      | 06 |
*     +----+----+----+----+         +----+
*     | 10 | 11      | 13 |         | 16 |
*     +----+----+----+----+----+----+----+
*     | 20 | 21      | 23 | 24      | 26 |
*     +----+----+----+----+----+----+----+
*                  ^--- empty ---^
*
* Will remove columns 2 and 5.
*
* **Note:** This is a low-level helper method for clearing invalid model state when doing table modifications.
* To remove a column from a table use {@link module:table/tableutils~TableUtils#removeColumns `TableUtils.removeColumns()`}.
*
* @internal
* @returns True if removed some columns.
*/
function removeEmptyColumns(table, tableUtils) {
	const width = tableUtils.getColumns(table);
	const columnsMap = new Array(width).fill(0);
	for (const { column } of new TableWalker(table)) columnsMap[column]++;
	const emptyColumns = columnsMap.reduce((result, cellsCount, column) => {
		return cellsCount ? result : [...result, column];
	}, []);
	if (emptyColumns.length > 0) {
		const emptyColumn = emptyColumns[emptyColumns.length - 1];
		tableUtils.removeColumns(table, { at: emptyColumn });
		return true;
	}
	return false;
}
/**
* Removes rows that have no cells anchored.
*
* In table below:
*
*     +----+----+----+
*     | 00 | 01 | 02 |
*     +----+----+----+
*     | 10 | 11 | 12 |
*     +    +    +    +
*     |    |    |    | <-- empty
*     +----+----+----+
*     | 30 | 31 | 32 |
*     +----+----+----+
*     | 40      | 42 |
*     +         +    +
*     |         |    | <-- empty
*     +----+----+----+
*     | 60 | 61 | 62 |
*     +----+----+----+
*
* Will remove rows 2 and 5.
*
* **Note:** This is a low-level helper method for clearing invalid model state when doing table modifications.
* To remove a row from a table use {@link module:table/tableutils~TableUtils#removeRows `TableUtils.removeRows()`}.
*
* @internal
* @returns True if removed some rows.
*/
function removeEmptyRows(table, tableUtils) {
	const emptyRows = [];
	const tableRowCount = tableUtils.getRows(table);
	for (let rowIndex = 0; rowIndex < tableRowCount; rowIndex++) if (table.getChild(rowIndex).isEmpty) emptyRows.push(rowIndex);
	if (emptyRows.length > 0) {
		const emptyRow = emptyRows[emptyRows.length - 1];
		tableUtils.removeRows(table, { at: emptyRow });
		return true;
	}
	return false;
}
/**
* Removes rows and columns that have no cells anchored.
*
* In table below:
*
*     +----+----+----+----+
*     | 00      | 02      |
*     +----+----+         +
*     | 10      |         |
*     +----+----+----+----+
*     | 20      | 22 | 23 |
*     +         +    +    +
*     |         |    |    | <-- empty row
*     +----+----+----+----+
*             ^--- empty column
*
* Will remove row 3 and column 1.
*
* **Note:** This is a low-level helper method for clearing invalid model state when doing table modifications.
* To remove a rows from a table use {@link module:table/tableutils~TableUtils#removeRows `TableUtils.removeRows()`} and
* {@link module:table/tableutils~TableUtils#removeColumns `TableUtils.removeColumns()`} to remove a column.
*
* @internal
*/
function removeEmptyRowsColumns(table, tableUtils) {
	if (!removeEmptyColumns(table, tableUtils)) removeEmptyRows(table, tableUtils);
}
/**
* Returns adjusted last row index if selection covers part of a row with empty slots (spanned by other cells).
* The `dimensions.lastRow` is equal to last row index but selection might be bigger.
*
* This happens *only* on rectangular selection so we analyze a case like this:
*
*        +---+---+---+---+
*      0 | a | b | c | d |
*        +   +   +---+---+
*      1 |   | e | f | g |
*        +   +---+   +---+
*      2 |   | h |   | i | <- last row, each cell has rowspan = 2,
*        +   +   +   +   +    so we need to return 3, not 2
*      3 |   |   |   |   |
*        +---+---+---+---+
*
* @internal
* @returns Adjusted last row index.
*/
function adjustLastRowIndex(table, dimensions) {
	const lastRowMap = Array.from(new TableWalker(table, {
		startColumn: dimensions.firstColumn,
		endColumn: dimensions.lastColumn,
		row: dimensions.lastRow
	}));
	if (lastRowMap.every(({ cellHeight }) => cellHeight === 1)) return dimensions.lastRow;
	const rowspanAdjustment = lastRowMap[0].cellHeight - 1;
	return dimensions.lastRow + rowspanAdjustment;
}
/**
* Returns adjusted last column index if selection covers part of a column with empty slots (spanned by other cells).
* The `dimensions.lastColumn` is equal to last column index but selection might be bigger.
*
* This happens *only* on rectangular selection so we analyze a case like this:
*
*       0   1   2   3
*     +---+---+---+---+
*     | a             |
*     +---+---+---+---+
*     | b | c | d     |
*     +---+---+---+---+
*     | e     | f     |
*     +---+---+---+---+
*     | g | h         |
*     +---+---+---+---+
*               ^
*              last column, each cell has colspan = 2, so we need to return 3, not 2
*
* @internal
* @returns Adjusted last column index.
*/
function adjustLastColumnIndex(table, dimensions) {
	const lastColumnMap = Array.from(new TableWalker(table, {
		startRow: dimensions.firstRow,
		endRow: dimensions.lastRow,
		column: dimensions.lastColumn
	}));
	if (lastColumnMap.every(({ cellWidth }) => cellWidth === 1)) return dimensions.lastColumn;
	const colspanAdjustment = lastColumnMap[0].cellWidth - 1;
	return dimensions.lastColumn + colspanAdjustment;
}
/**
* Get view `<table>` element from the wrapper.
*/
function getViewTableFromWrapper(wrapperView) {
	for (const wrapperChild of wrapperView.getChildren()) if (wrapperChild.is("element", "table")) return wrapperChild;
}

/**
* @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 table view representation:
*
* ```xml
* <figure class="table"><table>...</table></figure>
* ```
*
* to the model representation:
*
* ```xml
* <table></table>
* ```
*
* @internal
*/
function upcastTableFigure() {
	return (dispatcher) => {
		dispatcher.on("element:figure", (evt, data, conversionApi) => {
			if (!conversionApi.consumable.test(data.viewItem, {
				name: true,
				classes: "table"
			})) return;
			const viewTable = getViewTableFromWrapper(data.viewItem);
			if (!viewTable || !conversionApi.consumable.test(viewTable, { name: true })) return;
			conversionApi.consumable.consume(data.viewItem, {
				name: true,
				classes: "table"
			});
			const conversionResult = conversionApi.convertItem(viewTable, 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: "table"
				});
				return;
			}
			const modelTable = first(conversionResult.modelRange.getItems());
			if (!modelTable || !modelTable.is("element", "table")) {
				conversionApi.consumable.revert(data.viewItem, {
					name: true,
					classes: "table"
				});
				if (!conversionResult.modelRange.isCollapsed) {
					data.modelRange = conversionResult.modelRange;
					data.modelCursor = conversionResult.modelCursor;
				}
				return;
			}
			conversionApi.convertChildren(data.viewItem, conversionApi.writer.createPositionAt(modelTable, "end"));
			conversionApi.updateConversionResult(modelTable, data);
		});
	};
}
/**
* View table element to model table element conversion helper.
*
* This conversion helper converts the table element as well as table rows.
*
* @param options Conversion options.
* @param options.enableFooters If set to `true` the `footerRows` attribute will be upcasted.
* @returns Conversion helper.
* @internal
*/
function upcastTable(options) {
	return (dispatcher) => {
		dispatcher.on("element:table", (evt, data, conversionApi) => {
			const viewTable = data.viewItem;
			if (!conversionApi.consumable.test(viewTable, { name: true })) return;
			const { rows, headingRows, headingColumns, footerRows } = scanTable(viewTable);
			const attributes = {};
			if (headingColumns) attributes.headingColumns = headingColumns;
			if (headingRows) attributes.headingRows = headingRows;
			if (options.enableFooters && footerRows) attributes.footerRows = footerRows;
			const table = conversionApi.writer.createElement("table", attributes);
			if (!conversionApi.safeInsert(table, data.modelCursor)) return;
			conversionApi.consumable.consume(viewTable, { name: true });
			rows.forEach((row) => conversionApi.convertItem(row, conversionApi.writer.createPositionAt(table, "end")));
			conversionApi.convertChildren(viewTable, conversionApi.writer.createPositionAt(table, "end"));
			if (table.isEmpty) {
				const row = conversionApi.writer.createElement("tableRow");
				conversionApi.writer.insert(row, conversionApi.writer.createPositionAt(table, "end"));
				createEmptyTableCell(conversionApi.writer, conversionApi.writer.createPositionAt(row, "end"));
			}
			conversionApi.updateConversionResult(table, data);
		});
	};
}
/**
* A conversion helper that skips empty <tr> elements from upcasting at the beginning of the table.
*
* An empty row is considered a table model error but when handling clipboard data there could be rows that contain only row-spanned cells
* and empty TR-s are used to maintain the table structure (also {@link module:table/tablewalker~TableWalker} assumes that there are only
* rows that have related `tableRow` elements).
*
* *Note:* Only the first empty rows are removed because they have no meaning and it solves the issue
* of an improper table with all empty rows.
*
* @internal
* @returns Conversion helper.
*/
function skipEmptyTableRow() {
	return (dispatcher) => {
		dispatcher.on("element:tr", (evt, data) => {
			if (data.viewItem.isEmpty && data.modelCursor.index == 0) evt.stop();
		}, { priority: "high" });
	};
}
/**
* A converter that ensures an empty paragraph is inserted in a table cell if no other content was converted.
*
* @internal
* @returns Conversion helper.
*/
function ensureParagraphInTableCell(elementName) {
	return (dispatcher) => {
		dispatcher.on(`element:${elementName}`, (evt, data, { writer }) => {
			if (!data.modelRange) return;
			const tableCell = data.modelRange.start.nodeAfter;
			const modelCursor = writer.createPositionAt(tableCell, 0);
			if (data.viewItem.isEmpty) {
				writer.insertElement("paragraph", modelCursor);
				return;
			}
			const childNodes = Array.from(tableCell.getChildren());
			if (childNodes.every((node) => node.is("element", "$marker"))) {
				const paragraph = writer.createElement("paragraph");
				writer.insert(paragraph, writer.createPositionAt(tableCell, 0));
				for (const node of childNodes) writer.move(writer.createRangeOn(node), writer.createPositionAt(paragraph, "end"));
			}
		}, { priority: "low" });
	};
}
/**
* Scans table rows and extracts required metadata from the table:
*
* headingRows    - The number of rows that go as table headers.
* headingColumns - The maximum number of row headings.
* rows           - Sorted `<tr>` elements as they should go into the model - ie. if `<thead>` is inserted after `<tbody>` in the view.
*
* @param viewTable The view table element.
* @returns The table metadata.
*/
function scanTable(viewTable) {
	let headingColumns = void 0;
	let shouldAccumulateHeadingRows = true;
	const headRows = [];
	const bodyRows = [];
	const footRows = [];
	let firstTheadElement = null;
	let firstTfoot = null;
	const tableChildren = Array.from(viewTable.getChildren());
	for (let childIndex = 0; childIndex < tableChildren.length; childIndex++) {
		const tableChild = tableChildren[childIndex];
		if (tableChild.name !== "tbody" && tableChild.name !== "thead" && tableChild.name !== "tfoot") continue;
		if (tableChild.name === "thead" && !firstTheadElement) {
			shouldAccumulateHeadingRows = true;
			firstTheadElement = tableChild;
		}
		const trs = Array.from(tableChild.getChildren()).filter((el) => el.is("element", "tr"));
		let maxPrevColumns = null;
		let arePrecedingChildrenFooters = null;
		for (const tr of trs) {
			const trColumns = Array.from(tr.getChildren()).filter((el) => el.is("element", "td") || el.is("element", "th"));
			if (tableChild.name === "tfoot") {
				firstTfoot ||= {
					element: tableChild,
					rows: trs
				};
				shouldAccumulateHeadingRows = false;
				const isFirstTfoot = firstTfoot.element === tableChild;
				if (!isFirstTfoot && arePrecedingChildrenFooters === null) for (let i = childIndex; i < tableChildren.length; i++) {
					arePrecedingChildrenFooters = tableChildren[i].name === "tfoot";
					if (!arePrecedingChildrenFooters) break;
				}
				if (isFirstTfoot) {
					footRows.push(tr);
					continue;
				}
				if (arePrecedingChildrenFooters !== false) {
					footRows.splice(footRows.length - firstTfoot.rows.length, 0, tr);
					continue;
				}
			}
			if (firstTheadElement && tableChild === firstTheadElement || tableChild.name === "tbody" && trColumns.length > 0 && (maxPrevColumns === null || trColumns.length === maxPrevColumns) && trColumns.every((e) => e.is("element", "th")) && shouldAccumulateHeadingRows) {
				headRows.push(tr);
				shouldAccumulateHeadingRows = true;
			} else {
				bodyRows.push(tr);
				shouldAccumulateHeadingRows = false;
			}
			maxPrevColumns = Math.max(maxPrevColumns || 0, trColumns.length);
		}
	}
	const bodyMatrix = generateCellMatrix(bodyRows);
	for (const rowSlots of bodyMatrix) {
		let index = 0;
		while (index < rowSlots.length) {
			if (rowSlots[index]?.name !== "th") break;
			index += 1;
		}
		if (headingColumns === void 0 || index < headingColumns) headingColumns = index;
	}
	return {
		headingRows: headRows.length,
		headingColumns: headingColumns || 0,
		footerRows: footRows.length,
		rows: [
			...headRows,
			...bodyRows,
			...footRows
		]
	};
}
/**
* Takes an array of `<tr>` elements and generates a "matrix" (square
* two-dimensional array) describing which `<th>`s and `<td>`s fill which
* "slots", factoring in `rowspan`s and `colspan`s. For example, given
*
* ```xml
* <table>
*   <tr> <td>11</td> <td rowspan="2">12-22</td> <td>13</td> </tr>
*   <tr> <td>21</td> <td>23</td> </tr>
*   <tr> <td colspan="2">31-32</td> <td>33</rd> </tr>
* </table>
* ```
*
* The result would be (with cell elements' text content in place of the element
* objects for readability):
*
* ```js
* [
*   [ '11', '12-22', '13' ],
*   [ '21', '12-22', '23' ],
*   [ '31-32', '31-32', '33' ],
* ]
* ```
*
* This allows for a computation of heading columns that factors in the case
* where a cell from a previous rows with a `rowspan` attribute effectively adds
* an additional header cell to a subsequent row.
*
* There are also cases where cells are "missing" from a row. A simple one is
* the case where a row simply has fewer cells than another row in the same
* table. But another is one where a row has a cell with a `rowspan` that
* effectively adds a cell to a subsequent row "off the end" of the row. In this
* case, there will be a `null` value instead of an element object in that
* position. For example,
*
* ```xml
* <table>
*   <tr> <td>11</td> <td>12</td> <td rowspan="2">13-23</td> </tr>
*   <tr> <td>21</td> </tr>
*   <tr> <td>31</td> </tr>
* </table>
* ```
*
* would result in
*
* ```js
* [
*   [ '11', '12', '13-23' ],
*   [ '21', null, '13-23' ],
*   [ '31', null, null ]
* ]
* ```
*
* @param trs the array of `<tr>` elements
* @returns the cell matrix
*/
function generateCellMatrix(trs) {
	let prevRowspans = /* @__PURE__ */ new Map();
	let maxColumns = 0;
	const slots = trs.map((tr) => {
		const curSlots = [];
		const children = Array.from(tr.getChildren()).filter((child) => child.name === "th" || child.name === "td");
		const curRowspans = /* @__PURE__ */ new Map();
		while (children.length || curSlots.length < maxColumns) {
			const rowSpan = prevRowspans.get(curSlots.length);
			if (rowSpan && rowSpan.remaining > 0) curSlots.push(rowSpan.cell);
			else {
				const cell = children.shift();
				if (cell) {
					const colspan = parseInt(cell.getAttribute("colspan") || "1");
					const rowspan = parseInt(cell.getAttribute("rowspan") || "1");
					for (let i = 0; i < colspan; i++) {
						if (rowspan > 1) curRowspans.set(curSlots.length, {
							cell,
							remaining: rowspan - 1
						});
						curSlots.push(cell);
					}
				} else {
					curSlots.push(null);
					continue;
				}
			}
		}
		for (const [index, entry] of prevRowspans.entries()) {
			entry.remaining -= 1;
			if (entry.remaining > 0 && !curRowspans.has(index)) curRowspans.set(index, entry);
		}
		prevRowspans = curRowspans;
		maxColumns = Math.max(maxColumns, curSlots.length);
		return curSlots;
	});
	for (const rowSlots of slots) while (rowSlots.length < maxColumns) rowSlots.push(null);
	return slots;
}

/**
* @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 table/tablecolumnresize/constants
*/
/**
* The minimum column width given as a percentage value. Used in situations when the table is not yet rendered, so it is impossible to
* calculate how many percentage of the table width would be {@link ~COLUMN_MIN_WIDTH_IN_PIXELS minimum column width in pixels}.
*
* @internal
*/
const COLUMN_MIN_WIDTH_AS_PERCENTAGE = 5;
/**
* The minimum column width in pixels when the maximum table width is known.
* This value is an equivalent of `10%` of the default editor width (600px).
*
* @internal
*/
const COLUMN_MIN_WIDTH_IN_PIXELS = 40;
/**
* Determines how many digits after the decimal point are used to store the column width as a percentage value.
*
* @internal
*/
const COLUMN_WIDTH_PRECISION = 2;
/**
* The distance in pixels that the mouse has to move to start resizing the column.
*
* @internal
*/
const COLUMN_RESIZE_DISTANCE_THRESHOLD = 3;

/**
* @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 all the inserted or changed table model elements in a given change set. Only the tables
* with 'columnsWidth' attribute are taken into account. The returned set may be empty.
*
* Most notably if an entire table is removed it will not be included in returned set.
*
* @internal
* @param model The model to collect the affected elements from.
* @returns A set of table model elements.
*/
function getChangedResizedTables(model) {
	const affectedTables = /* @__PURE__ */ new Set();
	for (const change of model.document.differ.getChanges()) {
		let referencePosition = null;
		switch (change.type) {
			case "insert":
				referencePosition = [
					"table",
					"tableRow",
					"tableCell"
				].includes(change.name) ? change.position : null;
				break;
			case "remove":
				referencePosition = ["tableRow", "tableCell"].includes(change.name) ? change.position : null;
				break;
			case "attribute":
				if (change.range.start.nodeAfter) referencePosition = [
					"table",
					"tableRow",
					"tableCell"
				].includes(change.range.start.nodeAfter.name) ? change.range.start : null;
				break;
		}
		if (!referencePosition) continue;
		const tableNode = referencePosition.nodeAfter && referencePosition.nodeAfter.is("element", "table") ? referencePosition.nodeAfter : referencePosition.findAncestor("table");
		for (const node of model.createRangeOn(tableNode).getItems()) {
			if (!node.is("element", "table")) continue;
			if (!getColumnGroupElement(node)) continue;
			affectedTables.add(node);
		}
	}
	return affectedTables;
}
/**
* Calculates the percentage of the minimum column width given in pixels for a given table.
*
* @internal
* @param modelTable A table model element.
* @param editor The editor instance.
* @returns The minimal column width in percentage.
*/
function getColumnMinWidthAsPercentage(modelTable, editor) {
	return 40 * 100 / getTableWidthInPixels(modelTable, editor);
}
/**
* Calculates the table width in pixels.
*
* @internal
* @param modelTable A table model element.
* @param editor The editor instance.
* @returns The width of the table in pixels.
*/
function getTableWidthInPixels(modelTable, editor) {
	const referenceElement = getChildrenViewElement(modelTable, "tbody", editor) || getChildrenViewElement(modelTable, "thead", editor) || getChildrenViewElement(modelTable, "tfoot", editor);
	return getElementWidthInPixels(editor.editing.view.domConverter.mapViewToDom(referenceElement));
}
/**
* Returns the a view element with a given name that is nested directly in a `<table>` element
* related to a given `modelTable`.
*
* @param elementName Name of a view to be looked for, e.g. `'colgroup`', `'thead`'.
* @returns Matched view or `undefined` otherwise.
*/
function getChildrenViewElement(modelTable, elementName, editor) {
	return [...[...editor.editing.mapper.toViewElement(modelTable).getChildren()].find((node) => node.is("element", "table")).getChildren()].find((node) => node.is("element", elementName));
}
/**
* Returns the computed width (in pixels) of the DOM element without padding and borders.
*
* @internal
* @param domElement A DOM element.
* @returns The width of the DOM element in pixels.
*/
function getElementWidthInPixels(domElement) {
	const styles = global.window.getComputedStyle(domElement);
	if (styles.boxSizing === "border-box") return parseFloat(styles.width) - parseFloat(styles.paddingLeft) - parseFloat(styles.paddingRight) - parseFloat(styles.borderLeftWidth) - parseFloat(styles.borderRightWidth);
	else return parseFloat(styles.width);
}
/**
* Returns the inner pixel width of a given editing root, or `null` if the root has no
* DOM element attached yet (e.g. it hasn't been rendered for the first time).
*
* @internal
*/
function getEditableWidth(editor, rootName) {
	const domRoot = editor.editing.view.getDomRoot(rootName);
	return domRoot ? getElementWidthInPixels(domRoot) : null;
}
/**
* Returns the column indexes on the left and right edges of a cell. They differ if the cell spans
* across multiple columns.
*
* @internal
* @param cell A table cell model element.
* @param tableUtils The Table Utils plugin instance.
* @returns An object containing the indexes of the left and right edges of the cell.
*/
function getColumnEdgesIndexes(cell, tableUtils) {
	const cellColumnIndex = tableUtils.getCellLocation(cell).column;
	return {
		leftEdge: cellColumnIndex,
		rightEdge: cellColumnIndex + (cell.getAttribute("colspan") || 1) - 1
	};
}
/**
* Rounds the provided value to a fixed-point number with defined number of digits after the decimal point.
*
* @internal
* @param value A number to be rounded.
* @returns The rounded number.
*/
function toPrecision(value) {
	const multiplier = Math.pow(10, 2);
	return Math.round((typeof value === "number" ? value : parseFloat(value)) * multiplier) / multiplier;
}
/**
* Clamps the number within the inclusive lower (min) and upper (max) bounds. Returned number is rounded using the
* {@link ~toPrecision `toPrecision()`} function.
*
* @internal
* @param number A number to be clamped.
* @param min A lower bound.
* @param max An upper bound.
* @returns The clamped number.
*/
function clamp(number, min, max) {
	if (number <= min) return toPrecision(min);
	if (number >= max) return toPrecision(max);
	return toPrecision(number);
}
/**
* Creates an array with defined length and fills all elements with defined value.
*
* @internal
* @param length The length of the array.
* @param value The value to fill the array with.
* @returns An array with defined length and filled with defined value.
*/
function createFilledArray(length, value) {
	return Array(length).fill(value);
}
/**
* Sums all array values that can be parsed to a float.
*
* @internal
* @param array An array of numbers.
* @returns The sum of all array values.
*/
function sumArray(array) {
	return array.map((value) => typeof value === "number" ? value : parseFloat(value)).filter((value) => !Number.isNaN(value)).reduce((result, item) => result + item, 0);
}
/**
* Makes sure that the sum of the widths from all columns is 100%. If the sum of all the widths is not equal 100%, all the widths are
* changed proportionally so that they all sum back to 100%. If there are columns without specified width, the amount remaining
* after assigning the known widths will be distributed equally between them.
*
* @internal
* @param columnWidths An array of column widths.
* @returns An array of column widths guaranteed to sum up to 100%.
*/
function normalizeColumnWidths(columnWidths) {
	const hasPixels = columnWidths.some((width) => typeof width === "string" && width.endsWith("px"));
	const hasPercentages = columnWidths.some((width) => typeof width === "string" && width.endsWith("%"));
	if (hasPixels && !hasPercentages) return columnWidths.map((width) => width === "auto" || width === void 0 ? "auto" : `${toPrecision(width)}px`);
	let normalizedWidths = calculateMissingColumnWidths(columnWidths.map((width) => {
		if (width === "auto" || width === void 0) return "auto";
		return parseFloat(width.replace("%", ""));
	}));
	const totalWidth = sumArray(normalizedWidths);
	if (totalWidth !== 100) normalizedWidths = normalizedWidths.map((width) => toPrecision(width * 100 / totalWidth)).map((columnWidth, columnIndex, width) => {
		if (!(columnIndex === width.length - 1)) return columnWidth;
		const totalWidth = sumArray(width);
		return toPrecision(columnWidth + 100 - totalWidth);
	});
	return normalizedWidths.map((width) => width + "%");
}
/**
* Initializes the column widths by parsing the attribute value and calculating the uninitialized column widths. The special value 'auto'
* indicates that width for the column must be calculated. The width of such uninitialized column is calculated as follows:
* - If there is enough free space in the table for all uninitialized columns to have at least the minimum allowed width for all of them,
*   then set this width equally for all uninitialized columns.
* - Otherwise, just set the minimum allowed width for all uninitialized columns. The sum of all column widths will be greater than 100%,
*   but then it will be adjusted proportionally to 100% in {@link #normalizeColumnWidths `normalizeColumnWidths()`}.
*
* @param columnWidths An array of column widths.
* @returns An array with 'auto' values replaced with calculated widths.
*/
function calculateMissingColumnWidths(columnWidths) {
	const numberOfUninitializedColumns = columnWidths.filter((columnWidth) => columnWidth === "auto").length;
	if (numberOfUninitializedColumns === 0) return columnWidths.map((columnWidth) => toPrecision(columnWidth));
	const totalWidthOfInitializedColumns = sumArray(columnWidths);
	const widthForUninitializedColumn = Math.max((100 - totalWidthOfInitializedColumns) / numberOfUninitializedColumns, 5);
	return columnWidths.map((columnWidth) => columnWidth === "auto" ? widthForUninitializedColumn : columnWidth).map((columnWidth) => toPrecision(columnWidth));
}
/**
* Calculates the total horizontal space taken by the cell. That includes:
*  * width,
*  * left and red padding,
*  * border width.
*
* @internal
* @param domCell A DOM cell element.
* @returns Width in pixels without `px` at the end.
*/
function getDomCellOuterWidth(domCell) {
	const styles = global.window.getComputedStyle(domCell);
	if (styles.boxSizing === "border-box") return parseInt(styles.width);
	else return parseFloat(styles.width) + parseFloat(styles.paddingLeft) + parseFloat(styles.paddingRight) + parseFloat(styles.borderWidth);
}
/**
* Updates column elements to match columns widths.
*
* @internal
* @param columns
* @param tableColumnGroup
* @param normalizedWidths
* @param writer
*/
function updateColumnElements(columns, tableColumnGroup, normalizedWidths, writer) {
	for (let i = 0; i < Math.max(normalizedWidths.length, columns.length); i++) {
		const column = columns[i];
		const columnWidth = normalizedWidths[i];
		if (!columnWidth) writer.remove(column);
		else if (!column) writer.appendElement("tableColumn", { columnWidth }, tableColumnGroup);
		else writer.setAttribute("columnWidth", columnWidth, column);
	}
}
/**
* Returns a 'tableColumnGroup' element from the 'table'.
*
* @internal
* @param element A 'table' or 'tableColumnGroup' element.
* @returns A 'tableColumnGroup' element.
*/
function getColumnGroupElement(element) {
	if (element.is("element", "tableColumnGroup")) return element;
	const children = element.getChildren();
	return Array.from(children).find((element) => element.is("element", "tableColumnGroup"));
}
/**
* Returns an array of 'tableColumn' elements. It may be empty if there's no `tableColumnGroup` element.
*
* @internal
* @param element A 'table' or 'tableColumnGroup' element.
* @returns An array of 'tableColumn' elements.
*/
function getTableColumnElements(element) {
	const columnGroupElement = getColumnGroupElement(element);
	if (!columnGroupElement) return [];
	return Array.from(columnGroupElement.getChildren());
}
/**
* Returns an array of table column widths.
*
* @internal
* @param element A 'table' or 'tableColumnGroup' element.
* @returns An array of table column widths.
*/
function getTableColumnsWidths(element) {
	return getTableColumnElements(element).map((column) => column.getAttribute("columnWidth"));
}
/**
* Tells whether a table is in the pixel width mode, that is, its `tableWidth` attribute is expressed in pixels.
* The `tableWidth` unit is the single source of truth for the whole table's width mode.
*
* @internal
* @param table A 'table' model element.
*/
function isTableWidthInPixels(table) {
	const tableWidth = table.getAttribute("tableWidth");
	return typeof tableWidth === "string" && tableWidth.trim().endsWith("px");
}
/**
* Tells whether the given column widths are expressed in pixels.
*
* @internal
* @param columnWidths An array of column widths.
*/
function isColumnWidthsInPixels(columnWidths) {
	return columnWidths.some((width) => typeof width === "string" && width.endsWith("px"));
}
/**
* Translates the `colSpan` model attribute into additional column widths and returns the resulting array.
*
* @internal
* @param element A 'table' or 'tableColumnGroup' element.
* @param writer A writer instance.
* @returns An array of table column widths.
*/
function translateColSpanAttribute(element, writer) {
	return getTableColumnElements(element).reduce((acc, element) => {
		const columnWidth = element.getAttribute("columnWidth");
		const colSpan = element.getAttribute("colSpan");
		if (!colSpan) {
			acc.push(columnWidth);
			return acc;
		}
		for (let i = 0; i < colSpan; i++) acc.push(columnWidth);
		writer.removeAttribute("colSpan", element);
		return acc;
	}, []);
}
/**
* Removes the `tableCellWidth` attribute from every cell of the given table. Once a column is resized (with the resize
* handler or the column-width command), a per-cell width is obsolete - the column width governs the layout - so it is
* dropped to keep the model clean.
*
* @internal
*/
function removeCellWidthsFromTable(writer, table) {
	for (const row of table.getChildren()) {
		if (!row.is("element", "tableRow")) continue;
		for (const cell of row.getChildren()) writer.removeAttribute("tableCellWidth", cell);
	}
}

/**
* @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 table/tableutils
*/
/**
* The table utilities plugin.
*/
var TableUtils = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "TableUtils";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	init() {
		this.decorate("insertColumns");
		this.decorate("insertRows");
	}
	/**
	* Returns the table cell location as an object with table row and table column indexes.
	*
	* For instance, in the table below:
	*
	*      0   1   2   3
	*    +---+---+---+---+
	*  0 | a     | b | c |
	*    +       +   +---+
	*  1 |       |   | d |
	*    +---+---+   +---+
	*  2 | e     |   | f |
	*    +---+---+---+---+
	*
	* the method will return:
	*
	* ```ts
	* const cellA = table.getNodeByPath( [ 0, 0 ] );
	* editor.plugins.get( 'TableUtils' ).getCellLocation( cellA );
	* // will return { row: 0, column: 0 }
	*
	* const cellD = table.getNodeByPath( [ 1, 0 ] );
	* editor.plugins.get( 'TableUtils' ).getCellLocation( cellD );
	* // will return { row: 1, column: 3 }
	* ```
	*
	* @returns Returns a `{row, column}` object.
	*/
	getCellLocation(tableCell) {
		const tableRow = tableCell.parent;
		const table = tableRow.parent;
		const tableWalker = new TableWalker(table, { row: table.getChildIndex(tableRow) });
		for (const { cell, row, column } of tableWalker) if (cell === tableCell) return {
			row,
			column
		};
	}
	/**
	* Creates an empty table with a proper structure. The table needs to be inserted into the model,
	* for example, by using the {@link module:engine/model/model~Model#insertContent} function.
	*
	* ```ts
	* model.change( ( writer ) => {
	*   // Create a table of 2 rows and 7 columns:
	*   const table = tableUtils.createTable( writer, { rows: 2, columns: 7 } );
	*
	*   // Insert a table to the model at the best position taking the current selection:
	*   model.insertContent( table );
	* }
	* ```
	*
	* @param writer The model writer.
	* @param options.rows The number of rows to create. Default value is 2.
	* @param options.columns The number of columns to create. Default value is 2.
	* @param options.headingRows The number of heading rows. Default value is 0.
	* @param options.headingColumns The number of heading columns. Default value is 0.
	* @param options.footerRows The number of footer rows. Default value is 0.
	* @returns The created table element.
	*/
	createTable(writer, options) {
		const table = writer.createElement("table");
		createEmptyRows(writer, table, 0, options.rows || 2, options.columns || 2);
		if (options.footerRows) this.setFooterRowsCount(writer, table, options.footerRows);
		if (options.headingRows) this.setHeadingRowsCount(writer, table, options.headingRows);
		if (options.headingColumns) this.setHeadingColumnsCount(writer, table, options.headingColumns);
		return table;
	}
	/**
	* Inserts rows into a table.
	*
	* ```ts
	* editor.plugins.get( 'TableUtils' ).insertRows( table, { at: 1, rows: 2 } );
	* ```
	*
	* Assuming the table on the left, the above code will transform it to the table on the right:
	*
	*  row index
	*    0 +---+---+---+       `at` = 1,      +---+---+---+ 0
	*      | a | b | c |       `rows` = 2,    | a | b | c |
	*    1 +   +---+---+   <-- insert here    +   +---+---+ 1
	*      |   | d | e |                      |   |   |   |
	*    2 +   +---+---+       will give:     +   +---+---+ 2
	*      |   | f | g |                      |   |   |   |
	*    3 +---+---+---+                      +   +---+---+ 3
	*                                         |   | d | e |
	*                                         +   +---+---+ 4
	*                                         +   + f | g |
	*                                         +---+---+---+ 5
	*
	* @param table The table model element where the rows will be inserted.
	* @param options.at The row index at which the rows will be inserted.  Default value is 0.
	* @param options.rows The number of rows to insert.  Default value is 1.
	* @param options.copyStructureFromAbove The flag for copying row structure. Note that
	* the row structure will not be copied if this option is not provided.
	*/
	insertRows(table, options = {}) {
		const model = this.editor.model;
		const insertAt = options.at || 0;
		const rowsToInsert = options.rows || 1;
		const isCopyStructure = options.copyStructureFromAbove !== void 0;
		const copyStructureFrom = options.copyStructureFromAbove ? insertAt - 1 : insertAt;
		const cellTypeEnabled = isTableCellTypeEnabled(this.editor);
		const scopedHeaders = !!this.editor.config.get("table.tableCellProperties.scopedHeaders");
		const rows = this.getRows(table);
		const columns = this.getColumns(table);
		if (insertAt > rows)
 /**
		* The `options.at` points at a row position that does not exist.
		*
		* @error tableutils-insertrows-insert-out-of-range
		*/
		throw new CKEditorError("tableutils-insertrows-insert-out-of-range", this, { options });
		model.change((writer) => {
			let headingRows = table.getAttribute("headingRows") || 0;
			let footerRows = table.getAttribute("footerRows") || 0;
			if (headingRows > insertAt) headingRows += rowsToInsert;
			if (footerRows && insertAt > rows - footerRows) footerRows += rowsToInsert;
			if (!isCopyStructure && (insertAt === 0 || insertAt === rows)) {
				const rows = createEmptyRows(writer, table, insertAt, rowsToInsert, columns);
				if (cellTypeEnabled) for (let rowOffset = 0; rowOffset < rows.length; rowOffset++) {
					const row = rows[rowOffset];
					for (let columnIndex = 0; columnIndex < columns; columnIndex++) updateTableCellType({
						table,
						writer,
						cell: row[columnIndex],
						row: insertAt + rowOffset,
						column: columnIndex,
						scopedHeaders
					});
				}
			} else {
				const tableIterator = new TableWalker(table, { endRow: isCopyStructure ? Math.max(insertAt, copyStructureFrom) : insertAt });
				const rowColSpansMap = new Array(columns).fill(1);
				for (const { row, column, cellHeight, cellWidth, cell } of tableIterator) {
					const lastCellRow = row + cellHeight - 1;
					const isOverlappingInsertedRow = row < insertAt && insertAt <= lastCellRow;
					const isReferenceRow = row <= copyStructureFrom && copyStructureFrom <= lastCellRow;
					if (isOverlappingInsertedRow) {
						writer.setAttribute("rowspan", cellHeight + rowsToInsert, cell);
						rowColSpansMap[column] = -cellWidth;
					} else if (isCopyStructure && isReferenceRow) rowColSpansMap[column] = cellWidth;
				}
				for (let rowIndex = 0; rowIndex < rowsToInsert; rowIndex++) {
					const tableRow = writer.createElement("tableRow");
					writer.insert(tableRow, table, insertAt);
					for (let cellIndex = 0; cellIndex < rowColSpansMap.length; cellIndex++) {
						const colspan = rowColSpansMap[cellIndex];
						const insertPosition = writer.createPositionAt(tableRow, "end");
						if (colspan > 0) {
							const insertedCell = createEmptyTableCell(writer, insertPosition, colspan > 1 ? { colspan } : void 0);
							if (cellTypeEnabled) updateTableCellType({
								table,
								writer,
								cell: insertedCell,
								row: insertAt + rowIndex,
								column: cellIndex,
								scopedHeaders
							});
						}
						cellIndex += Math.abs(colspan) - 1;
					}
				}
			}
			this.setFooterRowsCount(writer, table, footerRows);
			this.setHeadingRowsCount(writer, table, headingRows, { updateCellType: false });
		});
	}
	/**
	* Inserts columns into a table.
	*
	* ```ts
	* editor.plugins.get( 'TableUtils' ).insertColumns( table, { at: 1, columns: 2 } );
	* ```
	*
	* Assuming the table on the left, the above code will transform it to the table on the right:
	*
	*  0   1   2   3                   0   1   2   3   4   5
	*  +---+---+---+                   +---+---+---+---+---+
	*  | a     | b |                   | a             | b |
	*  +       +---+                   +               +---+
	*  |       | c |                   |               | c |
	*  +---+---+---+     will give:    +---+---+---+---+---+
	*  | d | e | f |                   | d |   |   | e | f |
	*  +---+   +---+                   +---+---+---+   +---+
	*  | g |   | h |                   | g |   |   |   | h |
	*  +---+---+---+                   +---+---+---+---+---+
	*  | i         |                   | i                 |
	*  +---+---+---+                   +---+---+---+---+---+
	*      ^---- insert here, `at` = 1, `columns` = 2
	*
	* @param table The table model element where the columns will be inserted.
	* @param options.at The column index at which the columns will be inserted. Default value is 0.
	* @param options.columns The number of columns to insert. Default value is 1.
	*/
	insertColumns(table, options = {}) {
		const model = this.editor.model;
		const insertAt = options.at || 0;
		const columnsToInsert = options.columns || 1;
		const cellTypeEnabled = isTableCellTypeEnabled(this.editor);
		const scopedHeaders = !!this.editor.config.get("table.tableCellProperties.scopedHeaders");
		model.change((writer) => {
			let headingColumns = table.getAttribute("headingColumns");
			if (insertAt < headingColumns) headingColumns += columnsToInsert;
			const tableColumns = this.getColumns(table);
			if (insertAt === 0 || tableColumns === insertAt) {
				let rowIndex = 0;
				for (const tableRow of table.getChildren()) {
					if (!tableRow.is("element", "tableRow")) continue;
					const insertedCells = createCells(columnsToInsert, writer, writer.createPositionAt(tableRow, insertAt ? "end" : 0));
					if (cellTypeEnabled) for (let columnOffset = 0; columnOffset < insertedCells.length; columnOffset++) updateTableCellType({
						table,
						writer,
						cell: insertedCells[columnOffset],
						row: rowIndex,
						column: insertAt + columnOffset,
						scopedHeaders
					});
					rowIndex++;
				}
			} else {
				const tableWalker = new TableWalker(table, {
					column: insertAt,
					includeAllSlots: true
				});
				for (const tableSlot of tableWalker) {
					const { row, cell, cellAnchorColumn, cellAnchorRow, cellWidth, cellHeight } = tableSlot;
					if (cellAnchorColumn < insertAt) {
						writer.setAttribute("colspan", cellWidth + columnsToInsert, cell);
						const lastCellRow = cellAnchorRow + cellHeight - 1;
						for (let i = row; i <= lastCellRow; i++) tableWalker.skipRow(i);
					} else {
						const insertedCells = createCells(columnsToInsert, writer, tableSlot.getPositionBefore());
						if (cellTypeEnabled) for (let columnOffset = 0; columnOffset < insertedCells.length; columnOffset++) updateTableCellType({
							table,
							writer,
							cell: insertedCells[columnOffset],
							row,
							column: insertAt + columnOffset,
							scopedHeaders
						});
					}
				}
			}
			this.setHeadingColumnsCount(writer, table, headingColumns, { updateCellType: false });
		});
	}
	/**
	* Removes rows from the given `table`.
	*
	* This method re-calculates the table geometry including `rowspan` attribute of table cells overlapping removed rows
	* and table headings values.
	*
	* ```ts
	* editor.plugins.get( 'TableUtils' ).removeRows( table, { at: 1, rows: 2 } );
	* ```
	*
	* Executing the above code in the context of the table on the left will transform its structure as presented on the right:
	*
	*  row index
	*      ┌───┬───┬───┐        `at` = 1        ┌───┬───┬───┐
	*    0 │ a │ b │ c │        `rows` = 2      │ a │ b │ c │ 0
	*      │   ├───┼───┤                        │   ├───┼───┤
	*    1 │   │ d │ e │  <-- remove from here  │   │ d │ g │ 1
	*      │   │   ├───┤        will give:      ├───┼───┼───┤
	*    2 │   │   │ f │                        │ h │ i │ j │ 2
	*      │   │   ├───┤                        └───┴───┴───┘
	*    3 │   │   │ g │
	*      ├───┼───┼───┤
	*    4 │ h │ i │ j │
	*      └───┴───┴───┘
	*
	* @param options.at The row index at which the removing rows will start.
	* @param options.rows The number of rows to remove. Default value is 1.
	*/
	removeRows(table, options) {
		const model = this.editor.model;
		const rowsToRemove = options.rows || 1;
		const rowCount = this.getRows(table);
		const first = options.at;
		const last = first + rowsToRemove - 1;
		if (last > rowCount - 1)
 /**
		* The `options.at` param must point at existing row and `options.rows` must not exceed the rows in the table.
		*
		* @error tableutils-removerows-row-index-out-of-range
		*/
		throw new CKEditorError("tableutils-removerows-row-index-out-of-range", this, {
			table,
			options
		});
		model.change((writer) => {
			const indexesObject = {
				first,
				last
			};
			const { cellsToMove, cellsToTrim } = getCellsToMoveAndTrimOnRemoveRow(table, indexesObject);
			if (cellsToMove.size) moveCellsToRow(table, last + 1, cellsToMove, writer);
			for (let i = last; i >= first; i--) writer.remove(table.getChild(i));
			for (const { rowspan, cell } of cellsToTrim) updateNumericAttribute("rowspan", rowspan, cell, writer);
			updateHeadingRows(table, indexesObject, writer);
			updateFooterRows(table, rowCount, indexesObject, writer);
			if (!removeEmptyColumns(table, this)) removeEmptyRows(table, this);
			if (isTableCellTypeEnabled(this.editor)) {
				let headingRows = table.getAttribute("headingRows") || 0;
				const totalRows = this.getRows(table);
				while (headingRows < totalRows && isEntireCellsLineHeader({
					table,
					row: headingRows
				})) headingRows++;
				this.setHeadingRowsCount(writer, table, headingRows, { updateCellType: false });
			}
		});
	}
	/**
	* Removes columns from the given `table`.
	*
	* This method re-calculates the table geometry including the `colspan` attribute of table cells overlapping removed columns
	* and table headings values.
	*
	* ```ts
	* editor.plugins.get( 'TableUtils' ).removeColumns( table, { at: 1, columns: 2 } );
	* ```
	*
	* Executing the above code in the context of the table on the left will transform its structure as presented on the right:
	*
	*    0   1   2   3   4                       0   1   2
	*  ┌───────────────┬───┐                   ┌───────┬───┐
	*  │ a             │ b │                   │ a     │ b │
	*  │               ├───┤                   │       ├───┤
	*  │               │ c │                   │       │ c │
	*  ├───┬───┬───┬───┼───┤     will give:    ├───┬───┼───┤
	*  │ d │ e │ f │ g │ h │                   │ d │ g │ h │
	*  ├───┼───┼───┤   ├───┤                   ├───┤   ├───┤
	*  │ i │ j │ k │   │ l │                   │ i │   │ l │
	*  ├───┴───┴───┴───┴───┤                   ├───┴───┴───┤
	*  │ m                 │                   │ m         │
	*  └───────────────────┘                   └───────────┘
	*        ^---- remove from here, `at` = 1, `columns` = 2
	*
	* @param options.at The row index at which the removing columns will start.
	* @param options.columns The number of columns to remove.
	*/
	removeColumns(table, options) {
		const model = this.editor.model;
		const first = options.at;
		const columnsToRemove = options.columns || 1;
		const last = options.at + columnsToRemove - 1;
		model.change((writer) => {
			adjustHeadingColumns(table, {
				first,
				last
			}, writer);
			const tableColumns = getTableColumnElements(table);
			for (let removedColumnIndex = last; removedColumnIndex >= first; removedColumnIndex--) {
				for (const { cell, column, cellWidth } of [...new TableWalker(table)]) if (column <= removedColumnIndex && cellWidth > 1 && column + cellWidth > removedColumnIndex) updateNumericAttribute("colspan", cellWidth - 1, cell, writer);
				else if (column === removedColumnIndex) writer.remove(cell);
				if (tableColumns[removedColumnIndex]) {
					const adjacentColumn = removedColumnIndex === 0 ? tableColumns[1] : tableColumns[removedColumnIndex - 1];
					const removedColumnWidthAttribute = tableColumns[removedColumnIndex].getAttribute("columnWidth");
					const removedColumnWidth = parseFloat(removedColumnWidthAttribute);
					const adjacentColumnWidth = parseFloat(adjacentColumn.getAttribute("columnWidth"));
					writer.remove(tableColumns[removedColumnIndex]);
					const unit = removedColumnWidthAttribute.trim().endsWith("px") ? "px" : "%";
					writer.setAttribute("columnWidth", `${removedColumnWidth + adjacentColumnWidth}${unit}`, adjacentColumn);
				}
			}
			if (!removeEmptyRows(table, this)) removeEmptyColumns(table, this);
			if (isTableCellTypeEnabled(this.editor)) {
				let headingColumns = table.getAttribute("headingColumns") || 0;
				const totalColumns = this.getColumns(table);
				while (headingColumns < totalColumns && isEntireCellsLineHeader({
					table,
					column: headingColumns
				})) headingColumns++;
				this.setHeadingColumnsCount(writer, table, headingColumns, { updateCellType: false });
			}
		});
	}
	/**
	* Divides a table cell vertically into several ones.
	*
	* The cell will be visually split into more cells by updating colspans of other cells in a column
	* and inserting cells (columns) after that cell.
	*
	* In the table below, if cell "a" is split into 3 cells:
	*
	*  +---+---+---+
	*  | a | b | c |
	*  +---+---+---+
	*  | d | e | f |
	*  +---+---+---+
	*
	* it will result in the table below:
	*
	*  +---+---+---+---+---+
	*  | a |   |   | b | c |
	*  +---+---+---+---+---+
	*  | d         | e | f |
	*  +---+---+---+---+---+
	*
	* So cell "d" will get its `colspan` updated to `3` and 2 cells will be added (2 columns will be created).
	*
	* Splitting a cell that already has a `colspan` attribute set will distribute the cell `colspan` evenly and the remainder
	* will be left to the original cell:
	*
	*  +---+---+---+
	*  | a         |
	*  +---+---+---+
	*  | b | c | d |
	*  +---+---+---+
	*
	* Splitting cell "a" with `colspan=3` into 2 cells will create 1 cell with a `colspan=a` and cell "a" that will have `colspan=2`:
	*
	*  +---+---+---+
	*  | a     |   |
	*  +---+---+---+
	*  | b | c | d |
	*  +---+---+---+
	*/
	splitCellVertically(tableCell, numberOfCells = 2) {
		const model = this.editor.model;
		const table = tableCell.parent.parent;
		const rowspan = parseInt(tableCell.getAttribute("rowspan") || "1");
		const colspan = parseInt(tableCell.getAttribute("colspan") || "1");
		model.change((writer) => {
			if (colspan > 1) {
				const { newCellsSpan, updatedSpan } = breakSpanEvenly(colspan, numberOfCells);
				updateNumericAttribute("colspan", updatedSpan, tableCell, writer);
				const newCellsAttributes = {};
				if (newCellsSpan > 1) newCellsAttributes.colspan = newCellsSpan;
				if (rowspan > 1) newCellsAttributes.rowspan = rowspan;
				createCells(colspan > numberOfCells ? numberOfCells - 1 : colspan - 1, writer, writer.createPositionAfter(tableCell), newCellsAttributes);
			}
			if (colspan < numberOfCells) {
				const cellsToInsert = numberOfCells - colspan;
				const tableMap = [...new TableWalker(table)];
				const { column: splitCellColumn } = tableMap.find(({ cell }) => cell === tableCell);
				const cellsToUpdate = tableMap.filter(({ cell, cellWidth, column }) => {
					const isOnSameColumn = cell !== tableCell && column === splitCellColumn;
					const spansOverColumn = column < splitCellColumn && column + cellWidth > splitCellColumn;
					return isOnSameColumn || spansOverColumn;
				});
				for (const { cell, cellWidth } of cellsToUpdate) writer.setAttribute("colspan", cellWidth + cellsToInsert, cell);
				const newCellsAttributes = {};
				if (rowspan > 1) newCellsAttributes.rowspan = rowspan;
				createCells(cellsToInsert, writer, writer.createPositionAfter(tableCell), newCellsAttributes);
				const headingColumns = table.getAttribute("headingColumns") || 0;
				if (headingColumns > splitCellColumn) updateNumericAttribute("headingColumns", headingColumns + cellsToInsert, table, writer);
			}
		});
	}
	/**
	* Divides a table cell horizontally into several ones.
	*
	* The cell will be visually split into more cells by updating rowspans of other cells in the row and inserting rows with a single cell
	* below.
	*
	* If in the table below cell "b" is split into 3 cells:
	*
	*  +---+---+---+
	*  | a | b | c |
	*  +---+---+---+
	*  | d | e | f |
	*  +---+---+---+
	*
	* It will result in the table below:
	*
	*  +---+---+---+
	*  | a | b | c |
	*  +   +---+   +
	*  |   |   |   |
	*  +   +---+   +
	*  |   |   |   |
	*  +---+---+---+
	*  | d | e | f |
	*  +---+---+---+
	*
	* So cells "a" and "b" will get their `rowspan` updated to `3` and 2 rows with a single cell will be added.
	*
	* Splitting a cell that already has a `rowspan` attribute set will distribute the cell `rowspan` evenly and the remainder
	* will be left to the original cell:
	*
	*  +---+---+---+
	*  | a | b | c |
	*  +   +---+---+
	*  |   | d | e |
	*  +   +---+---+
	*  |   | f | g |
	*  +   +---+---+
	*  |   | h | i |
	*  +---+---+---+
	*
	* Splitting cell "a" with `rowspan=4` into 3 cells will create 2 cells with a `rowspan=1` and cell "a" will have `rowspan=2`:
	*
	*  +---+---+---+
	*  | a | b | c |
	*  +   +---+---+
	*  |   | d | e |
	*  +---+---+---+
	*  |   | f | g |
	*  +---+---+---+
	*  |   | h | i |
	*  +---+---+---+
	*/
	splitCellHorizontally(tableCell, numberOfCells = 2) {
		const model = this.editor.model;
		const tableRow = tableCell.parent;
		const table = tableRow.parent;
		const splitCellRow = table.getChildIndex(tableRow);
		const rowspan = parseInt(tableCell.getAttribute("rowspan") || "1");
		const colspan = parseInt(tableCell.getAttribute("colspan") || "1");
		model.change((writer) => {
			if (rowspan > 1) {
				const tableMap = [...new TableWalker(table, {
					startRow: splitCellRow,
					endRow: splitCellRow + rowspan - 1,
					includeAllSlots: true
				})];
				const { newCellsSpan, updatedSpan } = breakSpanEvenly(rowspan, numberOfCells);
				updateNumericAttribute("rowspan", updatedSpan, tableCell, writer);
				const { column: cellColumn } = tableMap.find(({ cell }) => cell === tableCell);
				const newCellsAttributes = {};
				if (newCellsSpan > 1) newCellsAttributes.rowspan = newCellsSpan;
				if (colspan > 1) newCellsAttributes.colspan = colspan;
				let distanceFromLastCellSpan = 0;
				for (const tableSlot of tableMap) {
					const { column, row } = tableSlot;
					const isAfterSplitCell = row >= splitCellRow + updatedSpan;
					const isOnSameColumn = column === cellColumn;
					if (distanceFromLastCellSpan >= newCellsSpan && isOnSameColumn) distanceFromLastCellSpan = 0;
					if (isAfterSplitCell && isOnSameColumn) {
						if (!distanceFromLastCellSpan) createCells(1, writer, tableSlot.getPositionBefore(), newCellsAttributes);
						distanceFromLastCellSpan++;
					}
				}
			}
			if (rowspan < numberOfCells) {
				const cellsToInsert = numberOfCells - rowspan;
				const rowCountBefore = this.getRows(table);
				const tableMap = [...new TableWalker(table, {
					startRow: 0,
					endRow: splitCellRow
				})];
				for (const { cell, cellHeight, row } of tableMap) if (cell !== tableCell && row + cellHeight > splitCellRow) {
					const rowspanToSet = cellHeight + cellsToInsert;
					writer.setAttribute("rowspan", rowspanToSet, cell);
				}
				const newCellsAttributes = {};
				if (colspan > 1) newCellsAttributes.colspan = colspan;
				createEmptyRows(writer, table, splitCellRow + 1, cellsToInsert, 1, newCellsAttributes);
				const headingRows = table.getAttribute("headingRows") || 0;
				if (headingRows > splitCellRow) updateNumericAttribute("headingRows", headingRows + cellsToInsert, table, writer);
				const footerRows = table.getAttribute("footerRows") || 0;
				if (rowCountBefore - footerRows <= splitCellRow) updateNumericAttribute("footerRows", footerRows + cellsToInsert, table, writer);
			}
		});
	}
	/**
	* Returns the number of columns for a given table.
	*
	* ```ts
	* editor.plugins.get( 'TableUtils' ).getColumns( table );
	* ```
	*
	* @param table The table to analyze.
	*/
	getColumns(table) {
		return [...table.getChild(0).getChildren()].filter((node) => node.is("element", "tableCell")).reduce((columns, row) => {
			return columns + parseInt(row.getAttribute("colspan") || "1");
		}, 0);
	}
	/**
	* Returns the number of rows for a given table. Any other element present in the table model is omitted.
	*
	* ```ts
	* editor.plugins.get( 'TableUtils' ).getRows( table );
	* ```
	*
	* @param table The table to analyze.
	*/
	getRows(table) {
		return Array.from(table.getChildren()).reduce((rowCount, child) => child.is("element", "tableRow") ? rowCount + 1 : rowCount, 0);
	}
	/**
	* Creates an instance of the table walker.
	*
	* The table walker iterates internally by traversing the table from row index = 0 and column index = 0.
	* It walks row by row and column by column in order to output values defined in the options.
	* By default it will output only the locations that are occupied by a cell. To include also spanned rows and columns,
	* pass the `includeAllSlots` option.
	*
	* @internal
	* @param table A table over which the walker iterates.
	* @param options An object with configuration.
	*/
	createTableWalker(table, options = {}) {
		return new TableWalker(table, options);
	}
	/**
	* Returns all model table cells that are fully selected (from the outside)
	* within the provided model selection's ranges.
	*
	* To obtain the cells selected from the inside, use
	* {@link #getTableCellsContainingSelection}.
	*/
	getSelectedTableCells(selection) {
		const cells = [];
		for (const range of this.sortRanges(selection.getRanges())) {
			const element = range.getContainedElement();
			if (element && element.is("element", "tableCell")) cells.push(element);
		}
		return cells;
	}
	/**
	* Sets the number of footer rows for the given `table`.
	*
	* * If number of footer rows is greater than the number of rows in the table, it will be truncated to the number of rows.
	* * If footer rows and heading rows overlap, the number of heading rows will be adjusted
	*
	* It'll have no effect if {@link module:table/tableconfig~TableConfig#enableFooters `table.enableFooters`} is set to `false`.
	*
	* @param writer The model writer.
	* @param table The table model element.
	* @param footerRows The number of footer rows to set.
	*/
	setFooterRowsCount(writer, table, footerRows) {
		if (!this.editor.config.get("table.enableFooters")) return;
		const headingRows = table.getAttribute("headingRows") || 0;
		const totalRows = this.getRows(table);
		const truncatedFooterRows = Math.min(footerRows, totalRows);
		updateNumericAttribute("footerRows", truncatedFooterRows, table, writer, 0);
		if (headingRows + truncatedFooterRows > totalRows) {
			const newHeadingRows = totalRows - truncatedFooterRows;
			this.setHeadingRowsCount(writer, table, newHeadingRows);
		}
	}
	/**
	* Sets the number of heading rows for the given `table`.
	*
	* If number of heading rows is greater than the number of rows in the table,
	* it will be truncated to the number of rows.
	*
	* @param writer The model writer.
	* @param table The table model element.
	* @param headingRows The number of heading rows to set.
	* @param options Additional options.
	* @param options.updateCellType If set to `false` it will only update the `headingRows` attribute
	* without updating the cell types in the table. Default is `true`.
	* @param options.resetFormerHeadingCells If set to `true`, it will check if the rows that are no longer in the heading section
	* should be updated to body cells. Default is `true`.
	* @param options.autoExpand If set to `true`, it will check if the following rows look like a header and expand the heading section.
	* Default is `true`.
	*/
	setHeadingRowsCount(writer, table, headingRows, options = {}) {
		const { updateCellType = true, resetFormerHeadingCells = true, autoExpand = true } = options;
		const totalRows = this.getRows(table);
		const scopedHeaders = !!this.editor.config.get("table.tableCellProperties.scopedHeaders");
		const oldHeadingRows = table.getAttribute("headingRows") || 0;
		let truncatedHeadingRows = Math.min(headingRows, totalRows);
		if (truncatedHeadingRows === oldHeadingRows) return;
		updateNumericAttribute("headingRows", truncatedHeadingRows, table, writer, 0);
		const footerRows = table.getAttribute("footerRows") || 0;
		if (truncatedHeadingRows + footerRows > totalRows) {
			const newFooterRows = totalRows - truncatedHeadingRows;
			this.setFooterRowsCount(writer, table, newFooterRows);
		}
		if (!isTableCellTypeEnabled(this.editor)) return;
		if (updateCellType) {
			for (const { cell, row, column } of new TableWalker(table, { endRow: truncatedHeadingRows - 1 })) updateTableCellType({
				table,
				writer,
				cell,
				row,
				column,
				scopedHeaders
			});
			if (resetFormerHeadingCells && truncatedHeadingRows < oldHeadingRows) for (let row = truncatedHeadingRows; row < oldHeadingRows; row++) {
				if (!isEntireCellsLineHeader({
					table,
					row
				})) break;
				for (const { cell, row: cellRow, column } of new TableWalker(table, { row })) updateTableCellType({
					table,
					writer,
					cell,
					row: cellRow,
					column,
					scopedHeaders
				});
			}
		}
		if (autoExpand && truncatedHeadingRows > oldHeadingRows) {
			while (truncatedHeadingRows < totalRows && isEntireCellsLineHeader({
				table,
				row: truncatedHeadingRows
			})) truncatedHeadingRows++;
			updateNumericAttribute("headingRows", truncatedHeadingRows, table, writer, 0);
		}
	}
	/**
	* Sets the number of heading columns for the given `table`.
	*
	* If number of heading columns is greater than the number of columns in the table,
	* it will be truncated to the number of columns.
	*
	* @param writer The model writer to use.
	* @param table The table model element.
	* @param headingColumns The number of heading columns to set.
	* @param options Additional options.
	* @param options.updateCellType If set to `false` it will only update the `headingColumns` attribute
	* without updating the cell types in the table. Default is `true`.
	* @param options.resetFormerHeadingCells If set to `true`, it will check if the columns that are no longer in the heading section
	* should be updated to body cells. Default is `true`.
	* @param options.autoExpand If set to `true`, it will check if the following columns look like a header and expand the heading section.
	* Default is `true`.
	*/
	setHeadingColumnsCount(writer, table, headingColumns, options = {}) {
		const { updateCellType = true, resetFormerHeadingCells = true, autoExpand = true } = options;
		const totalColumns = this.getColumns(table);
		const oldHeadingColumns = table.getAttribute("headingColumns") || 0;
		const scopedHeaders = !!this.editor.config.get("table.tableCellProperties.scopedHeaders");
		let truncatedHeadingColumns = Math.min(headingColumns, totalColumns);
		if (truncatedHeadingColumns === oldHeadingColumns) return;
		updateNumericAttribute("headingColumns", truncatedHeadingColumns, table, writer, 0);
		if (!isTableCellTypeEnabled(this.editor)) return;
		if (updateCellType) {
			for (const { cell, row, column } of new TableWalker(table, { endColumn: truncatedHeadingColumns - 1 })) updateTableCellType({
				table,
				writer,
				cell,
				row,
				column,
				scopedHeaders
			});
			if (resetFormerHeadingCells && truncatedHeadingColumns < oldHeadingColumns) for (let column = truncatedHeadingColumns; column < oldHeadingColumns; column++) {
				if (!isEntireCellsLineHeader({
					table,
					column
				})) break;
				for (const { cell, row, column: cellColumn } of new TableWalker(table, { column })) updateTableCellType({
					table,
					writer,
					cell,
					row,
					column: cellColumn,
					scopedHeaders
				});
			}
		}
		if (autoExpand && truncatedHeadingColumns > oldHeadingColumns) {
			while (truncatedHeadingColumns < totalColumns && isEntireCellsLineHeader({
				table,
				column: truncatedHeadingColumns
			})) truncatedHeadingColumns++;
			updateNumericAttribute("headingColumns", truncatedHeadingColumns, table, writer, 0);
		}
	}
	/**
	* Returns all model table cells that the provided model selection's ranges
	* {@link module:engine/model/range~ModelRange#start} inside.
	*
	* To obtain the cells selected from the outside, use
	* {@link #getSelectedTableCells}.
	*/
	getTableCellsContainingSelection(selection) {
		const cells = [];
		for (const range of selection.getRanges()) {
			const cellWithSelection = range.start.findAncestor("tableCell");
			if (cellWithSelection) cells.push(cellWithSelection);
		}
		return cells;
	}
	/**
	* Returns all model table cells that are either completely selected
	* by selection ranges or host selection range
	* {@link module:engine/model/range~ModelRange#start start positions} inside them.
	*
	* Combines {@link #getTableCellsContainingSelection} and
	* {@link #getSelectedTableCells}.
	*/
	getSelectionAffectedTableCells(selection) {
		const selectedCells = this.getSelectedTableCells(selection);
		if (selectedCells.length) return selectedCells;
		return this.getTableCellsContainingSelection(selection);
	}
	/**
	* Returns an object with the `first` and `last` row index contained in the given `tableCells`.
	*
	* ```ts
	* const selectedTableCells = getSelectedTableCells( editor.model.document.selection );
	*
	* const { first, last } = getRowIndexes( selectedTableCells );
	*
	* console.log( `Selected rows: ${ first } to ${ last }` );
	* ```
	*
	* @returns Returns an object with the `first` and `last` table row indexes.
	*/
	getRowIndexes(tableCells) {
		const indexes = tableCells.map((cell) => cell.parent.index);
		return this._getFirstLastIndexesObject(indexes);
	}
	/**
	* Returns an object with the `first` and `last` column index contained in the given `tableCells`.
	*
	* ```ts
	* const selectedTableCells = getSelectedTableCells( editor.model.document.selection );
	*
	* const { first, last } = getColumnIndexes( selectedTableCells );
	*
	* console.log( `Selected columns: ${ first } to ${ last }` );
	* ```
	*
	* @returns Returns an object with the `first` and `last` table column indexes.
	*/
	getColumnIndexes(tableCells) {
		const indexes = [...new TableWalker(tableCells[0].findAncestor("table"))].filter((entry) => tableCells.includes(entry.cell)).map((entry) => entry.column);
		return this._getFirstLastIndexesObject(indexes);
	}
	/**
	* Checks if the selection contains cells that do not exceed rectangular selection.
	*
	* In a table below:
	*
	*  ┌───┬───┬───┬───┐
	*  │ a │ b │ c │ d │
	*  ├───┴───┼───┤   │
	*  │ e     │ f │   │
	*  │       ├───┼───┤
	*  │       │ g │ h │
	*  └───────┴───┴───┘
	*
	* Valid selections are these which create a solid rectangle (without gaps), such as:
	*   - a, b (two horizontal cells)
	*   - c, f (two vertical cells)
	*   - a, b, e (cell "e" spans over four cells)
	*   - c, d, f (cell d spans over a cell in the row below)
	*
	* While an invalid selection would be:
	*   - a, c (the unselected cell "b" creates a gap)
	*   - f, g, h (cell "d" spans over a cell from the row of "f" cell - thus creates a gap)
	*/
	isSelectionRectangular(selectedTableCells) {
		if (selectedTableCells.length < 2 || !this._areCellInTheSameTableSection(selectedTableCells)) return false;
		const rows = /* @__PURE__ */ new Set();
		const columns = /* @__PURE__ */ new Set();
		let areaOfSelectedCells = 0;
		for (const tableCell of selectedTableCells) {
			const { row, column } = this.getCellLocation(tableCell);
			const rowspan = parseInt(tableCell.getAttribute("rowspan")) || 1;
			const colspan = parseInt(tableCell.getAttribute("colspan")) || 1;
			rows.add(row);
			columns.add(column);
			if (rowspan > 1) rows.add(row + rowspan - 1);
			if (colspan > 1) columns.add(column + colspan - 1);
			areaOfSelectedCells += rowspan * colspan;
		}
		return getBiggestRectangleArea(rows, columns) == areaOfSelectedCells;
	}
	/**
	* Returns array of sorted ranges.
	*/
	sortRanges(ranges) {
		return Array.from(ranges).sort(compareRangeOrder);
	}
	/**
	* Helper method to get an object with `first` and `last` indexes from an unsorted array of indexes.
	*/
	_getFirstLastIndexesObject(indexes) {
		const allIndexesSorted = indexes.sort((indexA, indexB) => indexA - indexB);
		return {
			first: allIndexesSorted[0],
			last: allIndexesSorted[allIndexesSorted.length - 1]
		};
	}
	/**
	* Checks if the selection does not mix a header (column or row) with other cells.
	*
	* For instance, in the table below valid selections consist of cells with the same letter only.
	* So, a-a (same heading row and column) or d-d (body cells) are valid while c-d or a-b are not.
	*
	* header columns
	*    ↓   ↓
	*  ┌───┬───┬───┬───┐
	*  │ a │ a │ b │ b │  ← header row
	*  ├───┼───┼───┼───┤
	*  │ c │ c │ d │ d │
	*  ├───┼───┼───┼───┤
	*  │ c │ c │ d │ d │
	*  └───┴───┴───┴───┘
	*/
	_areCellInTheSameTableSection(tableCells) {
		const table = tableCells[0].findAncestor("table");
		const totalRows = this.getRows(table);
		const rowIndexes = this.getRowIndexes(tableCells);
		const headingRows = parseInt(table.getAttribute("headingRows")) || 0;
		const footerRows = parseInt(table.getAttribute("footerRows")) || 0;
		if (!this._areIndexesInSameHeadingSection(rowIndexes, headingRows) || !this._areIndexesInSameFooterSection(rowIndexes, totalRows, footerRows)) return false;
		const columnIndexes = this.getColumnIndexes(tableCells);
		const headingColumns = parseInt(table.getAttribute("headingColumns")) || 0;
		return this._areIndexesInSameHeadingSection(columnIndexes, headingColumns);
	}
	/**
	* Unified check if table rows/columns indexes are in the same heading/body section.
	*/
	_areIndexesInSameHeadingSection({ first, last }, headingSectionSize) {
		return first < headingSectionSize === last < headingSectionSize;
	}
	/**
	* Unified check if table rows indexes are in the same footer/body section.
	*/
	_areIndexesInSameFooterSection({ first, last }, totalRows, footerRows) {
		const footerStartIndex = totalRows - footerRows;
		return first >= footerStartIndex === last >= footerStartIndex;
	}
};
/**
* Creates empty rows at the given index in an existing table.
*
* @param insertAt The row index of row insertion.
* @param rows The number of rows to create.
* @param tableCellToInsert The number of cells to insert in each row.
*/
function createEmptyRows(writer, table, insertAt, rows, tableCellToInsert, attributes = {}) {
	const insertedRows = [];
	for (let i = 0; i < rows; i++) {
		const tableRow = writer.createElement("tableRow");
		writer.insert(tableRow, table, insertAt);
		insertedRows.push(createCells(tableCellToInsert, writer, writer.createPositionAt(tableRow, "end"), attributes));
	}
	return insertedRows;
}
/**
* Creates cells at a given position.
*
* @param cells The number of cells to create
*/
function createCells(cells, writer, insertPosition, attributes = {}) {
	const createdCells = [];
	let currentPosition = insertPosition;
	for (let i = 0; i < cells; i++) {
		const cell = createEmptyTableCell(writer, currentPosition, attributes);
		createdCells.push(cell);
		currentPosition = writer.createPositionAfter(cell);
	}
	return createdCells;
}
/**
* Evenly distributes the span of a cell to a number of provided cells.
* The resulting spans will always be integer values.
*
* For instance breaking a span of 7 into 3 cells will return:
*
* ```ts
* { newCellsSpan: 2, updatedSpan: 3 }
* ```
*
* as two cells will have a span of 2 and the remainder will go the first cell so its span will change to 3.
*
* @param span The span value do break.
* @param numberOfCells The number of resulting spans.
*/
function breakSpanEvenly(span, numberOfCells) {
	if (span < numberOfCells) return {
		newCellsSpan: 1,
		updatedSpan: 1
	};
	const newCellsSpan = Math.floor(span / numberOfCells);
	return {
		newCellsSpan,
		updatedSpan: span - newCellsSpan * numberOfCells + newCellsSpan
	};
}
/**
* Updates heading columns attribute if removing a row from head section.
*/
function adjustHeadingColumns(table, removedColumnIndexes, writer) {
	const headingColumns = table.getAttribute("headingColumns") || 0;
	if (headingColumns && removedColumnIndexes.first < headingColumns) {
		const headingsRemoved = Math.min(headingColumns - 1, removedColumnIndexes.last) - removedColumnIndexes.first + 1;
		writer.setAttribute("headingColumns", headingColumns - headingsRemoved, table);
	}
}
/**
* Calculates a new heading rows value for removing rows from heading section.
*/
function updateHeadingRows(table, { first, last }, writer) {
	const headingRows = table.getAttribute("headingRows") || 0;
	if (first < headingRows) updateNumericAttribute("headingRows", last < headingRows ? headingRows - (last - first + 1) : first, table, writer, 0);
}
/**
* Calculates a new footer rows value for removing rows from footer section.
*/
function updateFooterRows(table, totalRows, { first, last }, writer) {
	if (!table.hasAttribute("footerRows")) return;
	const footerRows = table.getAttribute("footerRows");
	const footerIndex = totalRows - footerRows;
	if (last >= footerIndex) updateNumericAttribute("footerRows", first >= footerIndex ? footerRows - (last - first + 1) : totalRows - 1 - last, table, writer, 0);
}
/**
* Finds cells that will be:
* - trimmed - Cells that are "above" removed rows sections and overlap the removed section - their rowspan must be trimmed.
* - moved - Cells from removed rows section might stick out of. These cells are moved to the next row after a removed section.
*
* Sample table with overlapping & sticking out cells:
*
*      +----+----+----+----+----+
*      | 00 | 01 | 02 | 03 | 04 |
*      +----+    +    +    +    +
*      | 10 |    |    |    |    |
*      +----+----+    +    +    +
*      | 20 | 21 |    |    |    | <-- removed row
*      +    +    +----+    +    +
*      |    |    | 32 |    |    | <-- removed row
*      +----+    +    +----+    +
*      | 40 |    |    | 43 |    |
*      +----+----+----+----+----+
*
* In a table above:
* - cells to trim: '02', '03' & '04'.
* - cells to move: '21' & '32'.
*/
function getCellsToMoveAndTrimOnRemoveRow(table, { first, last }) {
	const cellsToMove = /* @__PURE__ */ new Map();
	const cellsToTrim = [];
	for (const { row, column, cellHeight, cell } of new TableWalker(table, { endRow: last })) {
		const lastRowOfCell = row + cellHeight - 1;
		if (row >= first && row <= last && lastRowOfCell > last) {
			const rowSpanToSet = cellHeight - (last - row + 1);
			cellsToMove.set(column, {
				cell,
				rowspan: rowSpanToSet
			});
		}
		if (row < first && lastRowOfCell >= first) {
			let rowspanAdjustment;
			if (lastRowOfCell >= last) rowspanAdjustment = last - first + 1;
			else rowspanAdjustment = lastRowOfCell - first + 1;
			cellsToTrim.push({
				cell,
				rowspan: cellHeight - rowspanAdjustment
			});
		}
	}
	return {
		cellsToMove,
		cellsToTrim
	};
}
function moveCellsToRow(table, targetRowIndex, cellsToMove, writer) {
	const tableRowMap = [...new TableWalker(table, {
		includeAllSlots: true,
		row: targetRowIndex
	})];
	const row = table.getChild(targetRowIndex);
	let previousCell;
	for (const { column, cell, isAnchor } of tableRowMap) if (cellsToMove.has(column)) {
		const { cell: cellToMove, rowspan } = cellsToMove.get(column);
		const targetPosition = previousCell ? writer.createPositionAfter(previousCell) : writer.createPositionAt(row, 0);
		writer.move(writer.createRangeOn(cellToMove), targetPosition);
		updateNumericAttribute("rowspan", rowspan, cellToMove, writer);
		previousCell = cellToMove;
	} else if (isAnchor) previousCell = cell;
}
function compareRangeOrder(rangeA, rangeB) {
	const posA = rangeA.start;
	const posB = rangeB.start;
	return posA.isBefore(posB) ? -1 : 1;
}
/**
* Calculates the area of a maximum rectangle that can span over the provided row & column indexes.
*/
function getBiggestRectangleArea(rows, columns) {
	const rowsIndexes = Array.from(rows.values());
	const columnIndexes = Array.from(columns.values());
	const lastRow = Math.max(...rowsIndexes);
	const firstRow = Math.min(...rowsIndexes);
	const lastColumn = Math.max(...columnIndexes);
	const firstColumn = Math.min(...columnIndexes);
	return (lastRow - firstRow + 1) * (lastColumn - firstColumn + 1);
}
/**
* Updates the `tableCellType` attribute of a table cell based on its position in the table
* and the table's `headingRows` and `headingColumns` attributes.
*/
function updateTableCellType({ writer, table, row, column, cell, scopedHeaders }) {
	const headingRows = table.getAttribute("headingRows") || 0;
	const headingColumns = table.getAttribute("headingColumns") || 0;
	if (row >= headingRows && column >= headingColumns) {
		writer.removeAttribute("tableCellType", cell);
		return;
	}
	let headerCellType = "header";
	if (scopedHeaders) if (row < headingRows) headerCellType = "header-column";
	else headerCellType = "header-row";
	writer.setAttribute("tableCellType", headerCellType, cell);
}

/**
* @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 string if all four values of box sides are equal.
*
* If a string is passed, it is treated as a single value (pass-through).
*
* ```ts
* // Returns 'foo':
* getSingleValue( { top: 'foo', right: 'foo', bottom: 'foo', left: 'foo' } );
* getSingleValue( 'foo' );
*
* // Returns undefined:
* getSingleValue( { top: 'foo', right: 'foo', bottom: 'bar', left: 'foo' } );
* getSingleValue( { top: 'foo', right: 'foo' } );
* ```
*
* @internal
*/
function getSingleValue(objectOrString) {
	if (!objectOrString || !isObject(objectOrString)) return objectOrString;
	const { top, right, bottom, left } = objectOrString;
	if (top == right && right == bottom && bottom == left) return top;
}
/**
* Adds a unit to a value if the value is a number or a string representing a number.
*
* **Note**: It does nothing to non-numeric values.
*
* ```ts
* getSingleValue( 25, 'px' ); // '25px'
* getSingleValue( 25, 'em' ); // '25em'
* getSingleValue( '25em', 'px' ); // '25em'
* getSingleValue( 'foo', 'px' ); // 'foo'
* ```
*
* @internal
* @param defaultUnit A default unit added to a numeric value.
*/
function addDefaultUnitToNumericValue(value, defaultUnit) {
	const numericValue = parseFloat(value);
	if (Number.isNaN(numericValue)) return value;
	if (String(numericValue) !== String(value)) return value;
	return `${numericValue}${defaultUnit}`;
}
/**
* Returns the normalized configuration.
*
* @internal
* @param config The configuration to normalize.
* @param options Options used to determine which properties should be added.
*/
function getNormalizedDefaultProperties(config, options = {}) {
	const normalizedConfig = {
		borderStyle: "none",
		borderWidth: "",
		borderColor: "",
		backgroundColor: "",
		width: "",
		height: "",
		...config
	};
	if (options.includeAlignmentProperty && !normalizedConfig.alignment) normalizedConfig.alignment = "center";
	if (options.includePaddingProperty && !normalizedConfig.padding) normalizedConfig.padding = "";
	if (options.includeVerticalAlignmentProperty && !normalizedConfig.verticalAlignment) normalizedConfig.verticalAlignment = "middle";
	if (options.includeHorizontalAlignmentProperty && !normalizedConfig.horizontalAlignment) normalizedConfig.horizontalAlignment = options.isRightToLeftContent ? "right" : "left";
	return normalizedConfig;
}
/**
* Returns the normalized default table properties.
*
* @internal
* @param config The configuration to normalize.
* @param options Options used to determine which properties should be added.
*/
function getNormalizedDefaultTableProperties(config, options) {
	return getNormalizedDefaultProperties({
		borderStyle: "double",
		borderColor: "hsl(0, 0%, 70%)",
		borderWidth: "1px",
		...config
	}, options);
}
/**
* Returns the normalized default cell properties.
*
* @internal
* @param config The configuration to normalize.
* @param options Options used to determine which properties should be added.
*/
function getNormalizedDefaultCellProperties(config, options) {
	return getNormalizedDefaultProperties({
		borderStyle: "solid",
		borderColor: "hsl(0, 0%, 75%)",
		borderWidth: "1px",
		...config
	}, options);
}

/**
* @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
*/
/**
* Model table element to view table element conversion helper.
*
* @internal
*/
function downcastTable(tableUtils, options) {
	return (table, { writer }) => {
		const headingRows = table.getAttribute("headingRows") || 0;
		const footerRows = table.getAttribute("footerRows") || 0;
		const tableElement = writer.createContainerElement("table", null, []);
		const figureElement = writer.createContainerElement("figure", { class: "table" }, tableElement);
		const totalRows = tableUtils.getRows(table);
		if (headingRows > 0) writer.insert(writer.createPositionAt(tableElement, "end"), writer.createContainerElement("thead", null, writer.createSlot((element) => element.is("element", "tableRow") && element.index < headingRows)));
		if (headingRows + footerRows < totalRows) writer.insert(writer.createPositionAt(tableElement, "end"), writer.createContainerElement("tbody", null, writer.createSlot((element) => element.is("element", "tableRow") && element.index >= headingRows && element.index < totalRows - footerRows)));
		if (footerRows > 0) writer.insert(writer.createPositionAt(tableElement, "end"), writer.createContainerElement("tfoot", null, writer.createSlot((element) => element.is("element", "tableRow") && element.index >= totalRows - footerRows)));
		for (const { positionOffset, filter } of options.additionalSlots) writer.insert(writer.createPositionAt(tableElement, positionOffset), writer.createSlot(filter));
		writer.insert(writer.createPositionAt(tableElement, "after"), writer.createSlot((element) => {
			if (element.is("element", "tableRow")) return false;
			return !options.additionalSlots.some(({ filter }) => filter(element));
		}));
		return options.asWidget ? toTableWidget(figureElement, writer) : figureElement;
	};
}
/**
* Model table row element to view `<tr>` element conversion helper.
*
* @internal
* @returns Element creator.
*/
function downcastRow() {
	return (tableRow, { writer }) => {
		return tableRow.isEmpty ? writer.createEmptyElement("tr") : writer.createContainerElement("tr");
	};
}
/**
* Model table cell element to view `<td>` or `<th>` element conversion helper.
*
* This conversion helper will create proper `<th>` elements for table cells that are in the heading section (heading row or column)
* and `<td>` otherwise.
*
* @internal
* @param options.asWidget If set to `true`, the downcast conversion will produce a widget.
* @param options.cellTypeEnabled If returns `true`, the downcast conversion will use the `tableCellType` attribute to determine cell type.
* @returns Element creator.
*/
function downcastCell(options) {
	return (tableCell, { writer }) => {
		if (options.cellTypeEnabled?.()) return createCellElement(writer, isTableHeaderCellType(tableCell.getAttribute("tableCellType")) ? "th" : "td");
		const tableRow = tableCell.parent;
		const table = tableRow.parent;
		const tableWalker = new TableWalker(table, { row: table.getChildIndex(tableRow) });
		const headingRows = table.getAttribute("headingRows") || 0;
		const headingColumns = table.getAttribute("headingColumns") || 0;
		let result = null;
		for (const tableSlot of tableWalker) if (tableSlot.cell == tableCell) {
			result = createCellElement(writer, tableSlot.row < headingRows || tableSlot.column < headingColumns ? "th" : "td");
			break;
		}
		return result;
	};
	function createCellElement(writer, name) {
		return options.asWidget ? toWidgetEditable(writer.createEditableElement(name), writer, { withAriaRole: false }) : writer.createContainerElement(name);
	}
}
/**
* Overrides paragraph inside table cell conversion.
*
* This converter:
* * should be used to override default paragraph conversion.
* * It will only convert `<paragraph>` placed directly inside `<tableCell>`.
* * For a single paragraph without attributes it returns `<span>` to simulate data table.
* * For all other cases it returns `<p>` element.
*
* @internal
* @param options.asWidget If set to `true`, the downcast conversion will produce a widget.
* @returns Element creator.
*/
function convertParagraphInTableCell(options = {}) {
	return (modelElement, { writer }) => {
		if (!modelElement.parent.is("element", "tableCell")) return null;
		if (!isSingleParagraphWithoutAttributes(modelElement)) return null;
		if (options.asWidget) return writer.createContainerElement("span", { class: "ck-table-bogus-paragraph" });
		else {
			const viewElement = writer.createContainerElement("p");
			writer.setCustomProperty("dataPipeline:transparentRendering", true, viewElement);
			return viewElement;
		}
	};
}
/**
* Checks if given model `<paragraph>` is an only child of a parent (`<tableCell>`) and if it has any attribute set.
*
* The paragraph should be converted in the editing view to:
*
* * If returned `true` - to a `<span class="ck-table-bogus-paragraph">`
* * If returned `false` - to a `<p>`
*
* @internal
*/
function isSingleParagraphWithoutAttributes(modelElement) {
	return modelElement.parent.childCount == 1 && !hasAnyAttribute(modelElement);
}
/**
* Converts a given {@link module:engine/view/element~ViewElement} to a table widget:
* * Adds a {@link module:engine/view/element~ViewElement#_setCustomProperty custom property}
* allowing to recognize the table 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 table `alt` attribute if one is present.
*/
function toTableWidget(viewElement, writer) {
	writer.setCustomProperty("table", true, viewElement);
	return toWidget(viewElement, writer, { hasSelectionHandle: true });
}
/**
* Checks if an element has any attributes set.
*/
function hasAnyAttribute(element) {
	for (const attributeKey of element.getAttributeKeys()) {
		if (attributeKey.startsWith("selection:") || attributeKey == "htmlEmptyBlock") continue;
		return true;
	}
	return false;
}
/**
* Downcasts a plain table (also used in the clipboard pipeline).
*/
function convertPlainTable(editor) {
	return (table, conversionApi) => {
		const hasPlainTableOutput = editor.plugins.has("PlainTableOutput");
		const isClipboardPipeline = conversionApi.options.isClipboardPipeline;
		const stripFigureTagWithLayoutTable = shouldStripFigureTagWithLayoutTable(editor, table);
		if (hasPlainTableOutput || stripFigureTagWithLayoutTable || isClipboardPipeline) return downcastPlainTable(table, conversionApi, editor);
		return null;
	};
}
/**
* Downcasts a plain table caption (also used in the clipboard pipeline).
*/
function convertPlainTableCaption(editor) {
	return (modelElement, { writer, options }) => {
		const hasPlainTableOutput = editor.plugins.has("PlainTableOutput");
		const isClipboardPipeline = options.isClipboardPipeline;
		const stripFigureTagWithLayoutTable = shouldStripFigureTagWithLayoutTable(editor, modelElement);
		if (!(hasPlainTableOutput || stripFigureTagWithLayoutTable || isClipboardPipeline)) return null;
		if (modelElement.parent.name === "table") return writer.createContainerElement("caption");
		return null;
	};
}
/**
* Downcasts a plain table.
*
* @param table Table model element.
* @param conversionApi The conversion API object.
* @param editor The editor instance.
* @returns Created element.
*/
function downcastPlainTable(table, conversionApi, editor) {
	const tableUtils = editor.plugins.get(TableUtils);
	const writer = conversionApi.writer;
	const totalRows = tableUtils.getRows(table);
	const headingRows = table.getAttribute("headingRows") || 0;
	const footerRows = table.getAttribute("footerRows") || 0;
	const footerIndex = totalRows - footerRows;
	const headRowsSlot = writer.createSlot((element) => element.is("element", "tableRow") && element.index < headingRows);
	const bodyRowsSlot = writer.createSlot((element) => element.is("element", "tableRow") && element.index >= headingRows && element.index < footerIndex);
	const footerRowsSlot = writer.createSlot((element) => element.is("element", "tableRow") && element.index >= footerIndex);
	const childrenSlot = writer.createSlot((element) => !element.is("element", "tableRow"));
	const theadElement = writer.createContainerElement("thead", null, headRowsSlot);
	const tbodyElement = writer.createContainerElement("tbody", null, bodyRowsSlot);
	const tfootElement = writer.createContainerElement("tfoot", null, footerRowsSlot);
	const tableContentElements = [];
	if (headingRows) tableContentElements.push(theadElement);
	if (headingRows + footerRows < totalRows) tableContentElements.push(tbodyElement);
	if (footerRows) tableContentElements.push(tfootElement);
	const tableAttributes = { class: "table" };
	if (editor.plugins.has("TablePropertiesEditing") && conversionApi.options.isClipboardPipeline) {
		const defaultTableProperties = getNormalizedDefaultTableProperties(editor.config.get("table.tableProperties.defaultProperties"), { includeAlignmentProperty: true });
		const tableAlignment = table.getAttribute("tableAlignment");
		let localDefaultValue = defaultTableProperties.alignment;
		if (table.getAttribute("tableType") === "layout") localDefaultValue = "";
		const tableAlignmentValue = tableAlignment || localDefaultValue;
		if (tableAlignmentValue) {
			tableAttributes.class += " " + downcastTableAlignmentConfig[tableAlignmentValue].className;
			tableAttributes.style = downcastTableAlignmentConfig[tableAlignmentValue].style;
			if (downcastTableAlignmentConfig[tableAlignmentValue].align !== void 0) tableAttributes.align = downcastTableAlignmentConfig[tableAlignmentValue].align;
		}
	}
	return writer.createContainerElement("table", tableAttributes, [childrenSlot, ...tableContentElements]);
}
/**
* Registers border and background attributes converters for plain tables or when the clipboard pipeline is used.
*/
function downcastTableBorderAndBackgroundAttributes(editor) {
	for (const [styleName, modelAttribute] of Object.entries({
		"border-width": "tableBorderWidth",
		"border-color": "tableBorderColor",
		"border-style": "tableBorderStyle",
		"background-color": "tableBackgroundColor"
	})) editor.conversion.for("dataDowncast").add((dispatcher) => {
		return dispatcher.on(`attribute:${modelAttribute}:table`, (evt, data, conversionApi) => {
			const { item, attributeNewValue } = data;
			const { mapper, writer } = conversionApi;
			const hasPlainTableOutput = editor.plugins.has("PlainTableOutput");
			const isClipboardPipeline = conversionApi.options.isClipboardPipeline;
			const stripFigureTagWithLayoutTable = shouldStripFigureTagWithLayoutTable(editor, item);
			if (!(hasPlainTableOutput || stripFigureTagWithLayoutTable || isClipboardPipeline)) return;
			if (!conversionApi.consumable.consume(item, evt.name)) return;
			const table = mapper.toViewElement(item);
			if (attributeNewValue) writer.setStyle(styleName, attributeNewValue, table);
			else writer.removeStyle(styleName, table);
		}, { priority: "high" });
	});
}
/**
* Returns `true` if the figure tag should be stripped when using layout tables and when `tableType` is `layout`
* or `stripFigureFromContentTable` option is set to `true`, `false` otherwise.
*
* @param editor The editor instance.
* @param modelElement The model element to check.
* @returns `true` if the figure tag should be stripped, `false` otherwise.
*/
function shouldStripFigureTagWithLayoutTable(editor, modelElement) {
	const hasTableLayout = editor.plugins.has("TableLayoutEditing");
	const stripFigureFromContentTable = editor.config.get("table.tableLayout.stripFigureFromContentTable") ?? false;
	const tableType = modelElement.findAncestor("table", { includeSelf: true })?.getAttribute("tableType");
	return hasTableLayout && (stripFigureFromContentTable || tableType === "layout");
}

/**
* @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 table/commands/inserttablecommand
*/
/**
* The insert table command.
*
* The command is registered by {@link module:table/tableediting~TableEditing} as the `'insertTable'` editor command.
*
* To insert a table at the current selection, execute the command and specify the dimensions:
*
* ```ts
* editor.execute( 'insertTable', { rows: 20, columns: 5 } );
* ```
*/
var InsertTableCommand = class extends Command {
	/**
	* @inheritDoc
	*/
	refresh() {
		const model = this.editor.model;
		const selection = model.document.selection;
		const schema = model.schema;
		this.isEnabled = isAllowedInParent$1(selection, schema);
	}
	/**
	* Executes the command.
	*
	* Inserts a table with the given number of rows and columns into the editor.
	*
	* @param options.rows The number of rows to create in the inserted table. Default value is 2.
	* @param options.columns The number of columns to create in the inserted table. Default value is 2.
	* @param options.headingRows The number of heading rows. If not provided it will default to
	* {@link module:table/tableconfig~TableConfig#defaultHeadings `config.table.defaultHeadings.rows`} table config.
	* @param options.headingColumns The number of heading columns. If not provided it will default to
	* {@link module:table/tableconfig~TableConfig#defaultHeadings `config.table.defaultHeadings.columns`} table config.
	* @param options.footerRows The number of footer rows. If not provided it will default to
	* {@link module:table/tableconfig~TableConfig#defaultFooters `config.table.defaultFooters`} table config.
	* This option is ignored when {@link module:table/tableconfig~TableConfig#enableFooters `config.table.enableFooters`} is `false`.
	* @param options.inheritTextFormattingAttributes Whether every empty cell should inherit the `copyOnEnter` text
	* formatting attributes (e.g. bold, font color) that were uniformly active in the content right before
	* the table, so that whichever cell the user starts typing in first continues that formatting. Defaults
	* to `true`.
	* @fires execute
	*/
	execute(options = {}) {
		const editor = this.editor;
		const model = editor.model;
		const selection = model.document.selection;
		const tableUtils = editor.plugins.get("TableUtils");
		const areTableFootersEnabled = !!editor.config.get("table.enableFooters");
		const defaultRows = editor.config.get("table.defaultHeadings.rows");
		const defaultColumns = editor.config.get("table.defaultHeadings.columns");
		const defaultFooterRows = editor.config.get("table.defaultFooters");
		if (options.headingRows === void 0 && defaultRows) options.headingRows = defaultRows;
		if (options.headingColumns === void 0 && defaultColumns) options.headingColumns = defaultColumns;
		if (areTableFootersEnabled && options.footerRows === void 0 && defaultFooterRows) options.footerRows = defaultFooterRows;
		if (!areTableFootersEnabled && "footerRows" in options) delete options.footerRows;
		model.change((writer) => {
			const selectionAttributesToCopy = Array.from(_getCopyOnEnterAttributes(model.schema, selection.getAttributes()));
			const table = tableUtils.createTable(writer, options);
			model.insertObject(table, null, null, { findOptimalPosition: "auto" });
			writer.setSelection(writer.createPositionAt(table.getNodeByPath([
				0,
				0,
				0
			]), 0));
			if (options.inheritTextFormattingAttributes !== false && selectionAttributesToCopy.length) for (const cellBlock of getEmptyTableCellBlocks(table)) for (const [key, value] of selectionAttributesToCopy) writer.setAttribute(ModelDocumentSelection._getStoreAttributeKey(key), value, cellBlock);
		});
	}
};
/**
* Checks if the table is allowed in the parent.
*/
function isAllowedInParent$1(selection, schema) {
	const positionParent = selection.getFirstPosition().parent;
	const validParent = positionParent === positionParent.root ? positionParent : positionParent.parent;
	return schema.checkChild(validParent, "table");
}

/**
* @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 table/commands/insertrowcommand
*/
/**
* The insert row command.
*
* The command is registered by {@link module:table/tableediting~TableEditing} as the `'insertTableRowBelow'` and
* `'insertTableRowAbove'` editor commands.
*
* To insert a row below the selected cell, execute the following command:
*
* ```ts
* editor.execute( 'insertTableRowBelow' );
* ```
*
* To insert a row above the selected cell, execute the following command:
*
* ```ts
* editor.execute( 'insertTableRowAbove' );
* ```
*/
var InsertRowCommand = class extends Command {
	/**
	* The order of insertion relative to the row in which the caret is located.
	*/
	order;
	/**
	* Creates a new `InsertRowCommand` instance.
	*
	* @param editor The editor on which this command will be used.
	* @param options.order The order of insertion relative to the row in which the caret is located.
	* Possible values: `"above"` and `"below"`. Default value is "below"
	*/
	constructor(editor, options = {}) {
		super(editor);
		this.order = options.order || "below";
	}
	/**
	* @inheritDoc
	*/
	refresh() {
		const selection = this.editor.model.document.selection;
		const isAnyCellSelected = !!this.editor.plugins.get("TableUtils").getSelectionAffectedTableCells(selection).length;
		this.isEnabled = isAnyCellSelected;
	}
	/**
	* Executes the command.
	*
	* Depending on the command's {@link #order} value, it inserts a row `'below'` or `'above'` the row in which selection is set.
	*
	* @fires execute
	*/
	execute() {
		const editor = this.editor;
		const selection = editor.model.document.selection;
		const tableUtils = editor.plugins.get("TableUtils");
		const insertAbove = this.order === "above";
		const affectedTableCells = tableUtils.getSelectionAffectedTableCells(selection);
		const rowIndexes = tableUtils.getRowIndexes(affectedTableCells);
		const row = insertAbove ? rowIndexes.first : rowIndexes.last;
		const table = affectedTableCells[0].findAncestor("table");
		tableUtils.insertRows(table, {
			at: insertAbove ? row : row + 1,
			copyStructureFromAbove: !insertAbove
		});
	}
};

/**
* @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 table/commands/insertcolumncommand
*/
/**
* The insert column command.
*
* The command is registered by {@link module:table/tableediting~TableEditing} as the `'insertTableColumnLeft'` and
* `'insertTableColumnRight'` editor commands.
*
* To insert a column to the left of the selected cell, execute the following command:
*
* ```ts
* editor.execute( 'insertTableColumnLeft' );
* ```
*
* To insert a column to the right of the selected cell, execute the following command:
*
* ```ts
* editor.execute( 'insertTableColumnRight' );
* ```
*/
var InsertColumnCommand = class extends Command {
	/**
	* The order of insertion relative to the column in which the caret is located.
	*/
	order;
	/**
	* Creates a new `InsertColumnCommand` instance.
	*
	* @param editor An editor on which this command will be used.
	* @param options.order The order of insertion relative to the column in which the caret is located.
	* Possible values: `"left"` and `"right"`. Default value is "right".
	*/
	constructor(editor, options = {}) {
		super(editor);
		this.order = options.order || "right";
	}
	/**
	* @inheritDoc
	*/
	refresh() {
		const selection = this.editor.model.document.selection;
		const isAnyCellSelected = !!this.editor.plugins.get("TableUtils").getSelectionAffectedTableCells(selection).length;
		this.isEnabled = isAnyCellSelected;
	}
	/**
	* Executes the command.
	*
	* Depending on the command's {@link #order} value, it inserts a column to the `'left'` or `'right'` of the column
	* in which the selection is set.
	*
	* @fires execute
	*/
	execute() {
		const editor = this.editor;
		const selection = editor.model.document.selection;
		const tableUtils = editor.plugins.get("TableUtils");
		const insertBefore = this.order === "left";
		const affectedTableCells = tableUtils.getSelectionAffectedTableCells(selection);
		const columnIndexes = tableUtils.getColumnIndexes(affectedTableCells);
		const column = insertBefore ? columnIndexes.first : columnIndexes.last;
		const table = affectedTableCells[0].findAncestor("table");
		tableUtils.insertColumns(table, {
			columns: 1,
			at: insertBefore ? column : column + 1
		});
	}
};

/**
* @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 table/commands/splitcellcommand
*/
/**
* The split cell command.
*
* The command is registered by {@link module:table/tableediting~TableEditing} as the `'splitTableCellVertically'`
* and `'splitTableCellHorizontally'`  editor commands.
*
* You can split any cell vertically or horizontally by executing this command. When multiple cells are selected, each of them
* is split, and the whole operation is a single undo step. For example, to split the selected table cells vertically:
*
* ```ts
* editor.execute( 'splitTableCellVertically' );
* ```
*/
var SplitCellCommand = class extends Command {
	/**
	* The direction that indicates which cell will be split.
	*/
	direction;
	/**
	* Creates a new `SplitCellCommand` instance.
	*
	* @param editor The editor on which this command will be used.
	* @param options.direction Indicates whether the command should split cells `'horizontally'` or `'vertically'`.
	*/
	constructor(editor, options = {}) {
		super(editor);
		this.direction = options.direction || "horizontally";
	}
	/**
	* @inheritDoc
	*/
	refresh() {
		const selectedCells = this.editor.plugins.get("TableUtils").getSelectionAffectedTableCells(this.editor.model.document.selection);
		this.isEnabled = selectedCells.length > 0;
	}
	/**
	* @inheritDoc
	*/
	execute() {
		const tableUtils = this.editor.plugins.get("TableUtils");
		const tableCells = tableUtils.getSelectionAffectedTableCells(this.editor.model.document.selection);
		const isHorizontal = this.direction === "horizontally";
		this.editor.model.change(() => {
			for (const tableCell of tableCells) if (isHorizontal) tableUtils.splitCellHorizontally(tableCell, 2);
			else tableUtils.splitCellVertically(tableCell, 2);
		});
	}
};

/**
* @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 merge cell command.
*
* The command is registered by {@link module:table/tableediting~TableEditing} as the `'mergeTableCellRight'`, `'mergeTableCellLeft'`,
* `'mergeTableCellUp'` and `'mergeTableCellDown'` editor commands.
*
* To merge a table cell at the current selection with another cell, execute the command corresponding with the preferred direction.
*
* For example, to merge with a cell to the right:
*
* ```ts
* editor.execute( 'mergeTableCellRight' );
* ```
*
* **Note**: If a table cell has a different [`rowspan`](https://www.w3.org/TR/html50/tabular-data.html#attr-tdth-rowspan)
* (for `'mergeTableCellRight'` and `'mergeTableCellLeft'`) or [`colspan`](https://www.w3.org/TR/html50/tabular-data.html#attr-tdth-colspan)
* (for `'mergeTableCellUp'` and `'mergeTableCellDown'`), the command will be disabled.
*/
var MergeCellCommand = class extends Command {
	/**
	* The direction that indicates which cell will be merged with the currently selected one.
	*/
	direction;
	/**
	* Whether the merge is horizontal (left/right) or vertical (up/down).
	*/
	isHorizontal;
	/**
	* Creates a new `MergeCellCommand` instance.
	*
	* @param editor The editor on which this command will be used.
	* @param options.direction Indicates which cell to merge with the currently selected one.
	* Possible values are: `'left'`, `'right'`, `'up'` and `'down'`.
	*/
	constructor(editor, options) {
		super(editor);
		this.direction = options.direction;
		this.isHorizontal = this.direction == "right" || this.direction == "left";
	}
	/**
	* @inheritDoc
	*/
	refresh() {
		const cellToMerge = this._getMergeableCell();
		this.value = cellToMerge;
		this.isEnabled = !!cellToMerge;
	}
	/**
	* Executes the command.
	*
	* Depending on the command's {@link #direction} value, it will merge the cell that is to the `'left'`, `'right'`, `'up'` or `'down'`.
	*
	* @fires execute
	*/
	execute() {
		const model = this.editor.model;
		const doc = model.document;
		const tableCell = this.editor.plugins.get("TableUtils").getTableCellsContainingSelection(doc.selection)[0];
		const cellToMerge = this.value;
		const direction = this.direction;
		model.change((writer) => {
			const isMergeNext = direction == "right" || direction == "down";
			const cellToExpand = isMergeNext ? tableCell : cellToMerge;
			const cellToRemove = isMergeNext ? cellToMerge : tableCell;
			const removedTableCellRow = cellToRemove.parent;
			mergeTableCells$1(cellToRemove, cellToExpand, writer);
			const spanAttribute = this.isHorizontal ? "colspan" : "rowspan";
			const cellSpan = parseInt(tableCell.getAttribute(spanAttribute) || "1");
			const cellToMergeSpan = parseInt(cellToMerge.getAttribute(spanAttribute) || "1");
			writer.setAttribute(spanAttribute, cellSpan + cellToMergeSpan, cellToExpand);
			writer.setSelection(writer.createRangeIn(cellToExpand));
			const tableUtils = this.editor.plugins.get("TableUtils");
			removeEmptyRowsColumns(removedTableCellRow.findAncestor("table"), tableUtils);
		});
	}
	/**
	* Returns a cell that can be merged with the current cell depending on the command's direction.
	*/
	_getMergeableCell() {
		const doc = this.editor.model.document;
		const tableUtils = this.editor.plugins.get("TableUtils");
		const tableCell = tableUtils.getTableCellsContainingSelection(doc.selection)[0];
		if (!tableCell) return;
		const cellToMerge = this.isHorizontal ? getHorizontalCell(tableCell, this.direction, tableUtils) : getVerticalCell(tableCell, this.direction, tableUtils);
		if (!cellToMerge) return;
		const spanAttribute = this.isHorizontal ? "rowspan" : "colspan";
		const span = parseInt(tableCell.getAttribute(spanAttribute) || "1");
		if (parseInt(cellToMerge.getAttribute(spanAttribute) || "1") === span) return cellToMerge;
	}
};
/**
* Returns the cell that can be merged horizontally.
*/
function getHorizontalCell(tableCell, direction, tableUtils) {
	const table = tableCell.parent.parent;
	const horizontalCell = direction == "right" ? tableCell.nextSibling : tableCell.previousSibling;
	const hasHeadingColumns = (table.getAttribute("headingColumns") || 0) > 0;
	if (!horizontalCell) return;
	const cellOnLeft = direction == "right" ? tableCell : horizontalCell;
	const cellOnRight = direction == "right" ? horizontalCell : tableCell;
	const { column: leftCellColumn } = tableUtils.getCellLocation(cellOnLeft);
	const { column: rightCellColumn } = tableUtils.getCellLocation(cellOnRight);
	const leftCellSpan = parseInt(cellOnLeft.getAttribute("colspan") || "1");
	const isCellOnLeftInHeadingColumn = isHeadingColumnCell(tableUtils, cellOnLeft);
	const isCellOnRightInHeadingColumn = isHeadingColumnCell(tableUtils, cellOnRight);
	if (hasHeadingColumns && isCellOnLeftInHeadingColumn != isCellOnRightInHeadingColumn) return;
	return leftCellColumn + leftCellSpan === rightCellColumn ? horizontalCell : void 0;
}
/**
* Returns the cell that can be merged vertically.
*/
function getVerticalCell(tableCell, direction, tableUtils) {
	const tableRow = tableCell.parent;
	const table = tableRow.parent;
	const rowIndex = table.getChildIndex(tableRow);
	const rows = tableUtils.getRows(table);
	if (direction == "down" && rowIndex === rows - 1 || direction == "up" && rowIndex === 0) return null;
	const rowspan = parseInt(tableCell.getAttribute("rowspan") || "1");
	const headingRows = table.getAttribute("headingRows") || 0;
	const footerRows = table.getAttribute("footerRows") || 0;
	const footerIndex = rows - footerRows;
	const isMergeUpWithBodyCell = direction == "up" && rowIndex === footerIndex;
	const isMergeUpWithHeadCell = direction == "up" && rowIndex === headingRows;
	const isMergeDownWithBodyCell = direction == "down" && rowIndex + rowspan === headingRows;
	const isMergeDownWithFootCell = direction == "down" && rowIndex + rowspan === footerIndex;
	if (headingRows && (isMergeDownWithBodyCell || isMergeUpWithHeadCell)) return null;
	if (footerRows && (isMergeUpWithBodyCell || isMergeDownWithFootCell)) return null;
	const currentCellRowSpan = parseInt(tableCell.getAttribute("rowspan") || "1");
	const rowOfCellToMerge = direction == "down" ? rowIndex + currentCellRowSpan : rowIndex;
	const tableMap = [...new TableWalker(table, { endRow: rowOfCellToMerge })];
	const mergeColumn = tableMap.find((value) => value.cell === tableCell).column;
	const cellToMergeData = tableMap.find(({ row, cellHeight, column }) => {
		if (column !== mergeColumn) return false;
		if (direction == "down") return row === rowOfCellToMerge;
		else return rowOfCellToMerge === row + cellHeight;
	});
	return cellToMergeData && cellToMergeData.cell ? cellToMergeData.cell : null;
}
/**
* Merges two table cells. It will ensure that after merging cells with an empty paragraph, the resulting table cell will only have one
* paragraph. If one of the merged table cells is empty, the merged table cell will have the contents of the non-empty table cell.
* If both are empty, the merged table cell will have only one empty paragraph.
*/
function mergeTableCells$1(cellToRemove, cellToExpand, writer) {
	if (!isEmpty$2(cellToRemove)) {
		if (isEmpty$2(cellToExpand)) writer.remove(writer.createRangeIn(cellToExpand));
		writer.move(writer.createRangeIn(cellToRemove), writer.createPositionAt(cellToExpand, "end"));
	}
	writer.remove(cellToRemove);
}
/**
* Checks if the passed table cell contains an empty paragraph.
*/
function isEmpty$2(tableCell) {
	const firstTableChild = tableCell.getChild(0);
	return tableCell.childCount == 1 && firstTableChild.is("element", "paragraph") && firstTableChild.isEmpty;
}

/**
* @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 table/commands/removerowcommand
*/
/**
* The remove row command.
*
* The command is registered by {@link module:table/tableediting~TableEditing} as the `'removeTableRow'` editor command.
*
* To remove the row containing the selected cell, execute the command:
*
* ```ts
* editor.execute( 'removeTableRow' );
* ```
*/
var RemoveRowCommand = class extends Command {
	/**
	* @inheritDoc
	*/
	refresh() {
		const tableUtils = this.editor.plugins.get("TableUtils");
		const selectedCells = tableUtils.getSelectionAffectedTableCells(this.editor.model.document.selection);
		const firstCell = selectedCells[0];
		if (firstCell) {
			const table = firstCell.findAncestor("table");
			const lastRowIndex = tableUtils.getRows(table) - 1;
			const selectedRowIndexes = tableUtils.getRowIndexes(selectedCells);
			const areAllRowsSelected = selectedRowIndexes.first === 0 && selectedRowIndexes.last === lastRowIndex;
			this.isEnabled = !areAllRowsSelected;
		} else this.isEnabled = false;
	}
	/**
	* @inheritDoc
	*/
	execute() {
		const model = this.editor.model;
		const tableUtils = this.editor.plugins.get("TableUtils");
		const referenceCells = tableUtils.getSelectionAffectedTableCells(model.document.selection);
		const removedRowIndexes = tableUtils.getRowIndexes(referenceCells);
		const firstCell = referenceCells[0];
		const table = firstCell.findAncestor("table");
		const columnIndexToFocus = tableUtils.getCellLocation(firstCell).column;
		model.change((writer) => {
			const rowsToRemove = removedRowIndexes.last - removedRowIndexes.first + 1;
			tableUtils.removeRows(table, {
				at: removedRowIndexes.first,
				rows: rowsToRemove
			});
			const cellToFocus = getCellToFocus$1(table, removedRowIndexes.first, columnIndexToFocus, tableUtils.getRows(table));
			writer.setSelection(writer.createPositionAt(cellToFocus, 0));
		});
	}
};
/**
* Returns a cell that should be focused before removing the row, belonging to the same column as the currently focused cell.
* - If the row was not the last one, the cell to focus will be in the row that followed it (before removal).
* - If the row was the last one, the cell to focus will be in the row that preceded it (before removal).
*/
function getCellToFocus$1(table, removedRowIndex, columnToFocus, tableRowCount) {
	const row = table.getChild(Math.min(removedRowIndex, tableRowCount - 1));
	let cellToFocus = row.getChild(0);
	let column = 0;
	for (const tableCell of row.getChildren()) {
		if (column > columnToFocus) return cellToFocus;
		cellToFocus = tableCell;
		column += parseInt(tableCell.getAttribute("colspan") || "1");
	}
	return cellToFocus;
}

/**
* @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 table/commands/removecolumncommand
*/
/**
* The remove column command.
*
* The command is registered by {@link module:table/tableediting~TableEditing} as the `'removeTableColumn'` editor command.
*
* To remove the column containing the selected cell, execute the command:
*
* ```ts
* editor.execute( 'removeTableColumn' );
* ```
*/
var RemoveColumnCommand = class extends Command {
	/**
	* @inheritDoc
	*/
	refresh() {
		const tableUtils = this.editor.plugins.get("TableUtils");
		const selectedCells = tableUtils.getSelectionAffectedTableCells(this.editor.model.document.selection);
		const firstCell = selectedCells[0];
		if (firstCell) {
			const table = firstCell.findAncestor("table");
			const tableColumnCount = tableUtils.getColumns(table);
			const { first, last } = tableUtils.getColumnIndexes(selectedCells);
			this.isEnabled = last - first < tableColumnCount - 1;
		} else this.isEnabled = false;
	}
	/**
	* @inheritDoc
	*/
	execute() {
		const tableUtils = this.editor.plugins.get("TableUtils");
		const [firstCell, lastCell] = getBoundaryCells(this.editor.model.document.selection, tableUtils);
		const table = firstCell.parent.parent;
		const tableMap = [...new TableWalker(table)];
		const removedColumnIndexes = {
			first: tableMap.find((value) => value.cell === firstCell).column,
			last: tableMap.find((value) => value.cell === lastCell).column
		};
		const cellToFocus = getCellToFocus(tableMap, firstCell, lastCell, removedColumnIndexes);
		this.editor.model.change((writer) => {
			const columnsToRemove = removedColumnIndexes.last - removedColumnIndexes.first + 1;
			tableUtils.removeColumns(table, {
				at: removedColumnIndexes.first,
				columns: columnsToRemove
			});
			writer.setSelection(writer.createPositionAt(cellToFocus, 0));
		});
	}
};
/**
* Returns a proper table cell to focus after removing a column.
* - selection is on last table cell it will return previous cell.
*/
function getCellToFocus(tableMap, firstCell, lastCell, removedColumnIndexes) {
	if (parseInt(lastCell.getAttribute("colspan") || "1") > 1) return lastCell;
	else if (firstCell.previousSibling || lastCell.nextSibling) return lastCell.nextSibling || firstCell.previousSibling;
	else if (removedColumnIndexes.first) return tableMap.reverse().find(({ column }) => {
		return column < removedColumnIndexes.first;
	}).cell;
	else return tableMap.reverse().find(({ column }) => {
		return column > removedColumnIndexes.last;
	}).cell;
}
/**
* Returns helper object returning the first and the last cell contained in given selection, based on DOM order.
*/
function getBoundaryCells(selection, tableUtils) {
	const referenceCells = tableUtils.getSelectionAffectedTableCells(selection);
	const firstCell = referenceCells[0];
	const lastCell = referenceCells.pop();
	const returnValue = [firstCell, lastCell];
	return firstCell.isBefore(lastCell) ? returnValue : returnValue.reverse();
}

/**
* @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 table/commands/setheaderrowcommand
*/
/**
* The header row command.
*
* The command is registered by {@link module:table/tableediting~TableEditing} as the `'setTableColumnHeader'` editor command.
*
* You can make the row containing the selected cell a [header](https://www.w3.org/TR/html50/tabular-data.html#the-th-element) by executing:
*
* ```ts
* editor.execute( 'setTableRowHeader' );
* ```
*
* **Note:** All preceding rows will also become headers. If the current row is already a header, executing this command
* will make it a regular row back again (including the following rows).
*/
var SetHeaderRowCommand = class extends Command {
	/**
	* @inheritDoc
	*/
	refresh() {
		const tableUtils = this.editor.plugins.get("TableUtils");
		const model = this.editor.model;
		const selectedCells = tableUtils.getSelectionAffectedTableCells(model.document.selection);
		if (selectedCells.length === 0) {
			this.isEnabled = false;
			this.value = false;
			return;
		}
		const table = selectedCells[0].findAncestor("table");
		this.isEnabled = model.schema.checkAttribute(table, "headingRows");
		this.value = selectedCells.every((cell) => this._isInHeading(cell, cell.parent.parent));
	}
	/**
	* Executes the command.
	*
	* When the selection is in a non-header row, the command will set the `headingRows` table attribute to cover that row.
	*
	* When the selection is already in a header row, it will set `headingRows` so the heading section will end before that row.
	*
	* @fires execute
	* @param options.forceValue If set, the command will set (`true`) or unset (`false`) the header rows according to
	* the `forceValue` parameter instead of the current model state.
	*/
	execute(options = {}) {
		if (options.forceValue === this.value) return;
		const tableUtils = this.editor.plugins.get("TableUtils");
		const model = this.editor.model;
		const selectedCells = tableUtils.getSelectionAffectedTableCells(model.document.selection);
		const table = selectedCells[0].findAncestor("table");
		const { first, last } = tableUtils.getRowIndexes(selectedCells);
		const headingRowsToSet = this.value ? first : last + 1;
		const currentHeadingRows = table.getAttribute("headingRows") || 0;
		model.change((writer) => {
			if (headingRowsToSet) {
				const overlappingCells = getVerticallyOverlappingCells(table, headingRowsToSet, headingRowsToSet > currentHeadingRows ? currentHeadingRows : 0);
				for (const { cell } of overlappingCells) splitHorizontally(cell, headingRowsToSet, writer);
			}
			tableUtils.setHeadingRowsCount(writer, table, headingRowsToSet);
		});
	}
	/**
	* Checks if a table cell is in the heading section.
	*/
	_isInHeading(tableCell, table) {
		const headingRows = parseInt(table.getAttribute("headingRows") || "0");
		return !!headingRows && tableCell.parent.index < headingRows;
	}
};

/**
* @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 table/commands/setfooterrowcommand
*/
/**
* The footer row command.
*
* The command is registered by {@link module:table/tableediting~TableEditing} as the `'setTableFooterRow'` editor command.
*
* You can make the row containing the selected cell a footer by executing:
*
* ```ts
* editor.execute( 'setTableFooterRow' );
* ```
*
* **Note:** All following rows will also become footers. If the current row is already a footer, executing this command
* will make it a regular row back again (including the preceding rows).
*/
var SetFooterRowCommand = class extends Command {
	/**
	* @inheritDoc
	*/
	refresh() {
		const tableUtils = this.editor.plugins.get("TableUtils");
		const model = this.editor.model;
		const selectedCells = tableUtils.getSelectionAffectedTableCells(model.document.selection);
		if (selectedCells.length === 0) {
			this.isEnabled = false;
			this.value = false;
			return;
		}
		const table = selectedCells[0].findAncestor("table");
		this.isEnabled = model.schema.checkAttribute(table, "footerRows");
		this.value = selectedCells.every((cell) => this._isInFooter(cell, table));
	}
	/**
	* Executes the command.
	*
	* When the selection is in a non-footer row, the command will set the `footerRows` table attribute to cover that row.
	*
	* When the selection is already in a footer row, it will set `footerRows` so the footer section will start after that row.
	*
	* @fires execute
	* @param options.forceValue If set, the command will set (`true`) or unset (`false`) the footer rows according to
	* the `forceValue` parameter instead of the current model state.
	*/
	execute(options = {}) {
		if (options.forceValue === this.value) return;
		const tableUtils = this.editor.plugins.get("TableUtils");
		const model = this.editor.model;
		const selectedCells = tableUtils.getSelectionAffectedTableCells(model.document.selection);
		const table = selectedCells[0].findAncestor("table");
		const { first, last } = tableUtils.getRowIndexes(selectedCells);
		const totalRows = tableUtils.getRows(table);
		const footerRowsToSet = this.value ? totalRows - (last + 1) : totalRows - first;
		const currentFooterRows = table.getAttribute("footerRows") || 0;
		model.change((writer) => {
			if (footerRowsToSet) {
				const splitRow = totalRows - footerRowsToSet;
				const currentSplitRow = totalRows - currentFooterRows;
				const overlappingCells = getVerticallyOverlappingCells(table, splitRow, splitRow > currentSplitRow ? currentSplitRow : 0);
				for (const { cell } of overlappingCells) splitHorizontally(cell, splitRow, writer);
			}
			tableUtils.setFooterRowsCount(writer, table, footerRowsToSet);
		});
	}
	/**
	* Checks if a table cell is in the footer section.
	*/
	_isInFooter(tableCell, table) {
		const footerRows = parseInt(table.getAttribute("footerRows") || "0");
		const totalRows = this.editor.plugins.get("TableUtils").getRows(table);
		const rowIndex = tableCell.parent.index;
		return !!footerRows && rowIndex >= totalRows - footerRows;
	}
};

/**
* @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 table/commands/setheadercolumncommand
*/
/**
* The header column command.
*
* The command is registered by {@link module:table/tableediting~TableEditing} as the `'setTableColumnHeader'` editor command.
*
* You can make the column containing the selected cell a [header](https://www.w3.org/TR/html50/tabular-data.html#the-th-element)
* by executing:
*
* ```ts
* editor.execute( 'setTableColumnHeader' );
* ```
*
* **Note:** All preceding columns will also become headers. If the current column is already a header, executing this command
* will make it a regular column back again (including the following columns).
*/
var SetHeaderColumnCommand = class extends Command {
	/**
	* @inheritDoc
	*/
	refresh() {
		const tableUtils = this.editor.plugins.get("TableUtils");
		const model = this.editor.model;
		const selectedCells = tableUtils.getSelectionAffectedTableCells(model.document.selection);
		if (selectedCells.length === 0) {
			this.isEnabled = false;
			this.value = false;
			return;
		}
		const table = selectedCells[0].findAncestor("table");
		this.isEnabled = model.schema.checkAttribute(table, "headingColumns");
		this.value = selectedCells.every((cell) => isHeadingColumnCell(tableUtils, cell));
	}
	/**
	* Executes the command.
	*
	* When the selection is in a non-header column, the command will set the `headingColumns` table attribute to cover that column.
	*
	* When the selection is already in a header column, it will set `headingColumns` so the heading section will end before that column.
	*
	* @fires execute
	* @param options.forceValue If set, the command will set (`true`) or unset (`false`) the header columns according to
	* the `forceValue` parameter instead of the current model state.
	*/
	execute(options = {}) {
		if (options.forceValue === this.value) return;
		const tableUtils = this.editor.plugins.get("TableUtils");
		const model = this.editor.model;
		const selectedCells = tableUtils.getSelectionAffectedTableCells(model.document.selection);
		const table = selectedCells[0].findAncestor("table");
		const { first, last } = tableUtils.getColumnIndexes(selectedCells);
		const headingColumnsToSet = this.value ? first : last + 1;
		model.change((writer) => {
			if (headingColumnsToSet) {
				const overlappingCells = getHorizontallyOverlappingCells(table, headingColumnsToSet);
				for (const { cell, column } of overlappingCells) splitVertically(cell, column, headingColumnsToSet, writer);
			}
			tableUtils.setHeadingColumnsCount(writer, table, headingColumnsToSet);
		});
	}
};

/**
* @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 merge cells command.
*
* The command is registered by {@link module:table/tableediting~TableEditing} as the `'mergeTableCells'` editor command.
*
* For example, to merge selected table cells:
*
* ```ts
* editor.execute( 'mergeTableCells' );
* ```
*/
var MergeCellsCommand = class extends Command {
	/**
	* @inheritDoc
	*/
	refresh() {
		const tableUtils = this.editor.plugins.get(TableUtils);
		const selectedTableCells = tableUtils.getSelectedTableCells(this.editor.model.document.selection);
		this.isEnabled = tableUtils.isSelectionRectangular(selectedTableCells);
	}
	/**
	* Executes the command.
	*
	* @fires execute
	*/
	execute() {
		const model = this.editor.model;
		const tableUtils = this.editor.plugins.get(TableUtils);
		model.change((writer) => {
			const selectedTableCells = tableUtils.getSelectedTableCells(model.document.selection);
			const firstTableCell = selectedTableCells.shift();
			const { mergeWidth, mergeHeight } = getMergeDimensions(firstTableCell, selectedTableCells, tableUtils);
			updateNumericAttribute("colspan", mergeWidth, firstTableCell, writer);
			updateNumericAttribute("rowspan", mergeHeight, firstTableCell, writer);
			for (const tableCell of selectedTableCells) mergeTableCells(tableCell, firstTableCell, writer);
			removeEmptyRowsColumns(firstTableCell.findAncestor("table"), tableUtils);
			writer.setSelection(firstTableCell, "in");
		});
	}
};
/**
*  Merges two table cells. It will ensure that after merging cells with empty paragraphs the resulting table cell will only have one
* paragraph. If one of the merged table cells is empty, the merged table cell will have contents of the non-empty table cell.
* If both are empty, the merged table cell will have only one empty paragraph.
*/
function mergeTableCells(cellBeingMerged, targetCell, writer) {
	if (!isEmpty$1(cellBeingMerged)) {
		if (isEmpty$1(targetCell)) writer.remove(writer.createRangeIn(targetCell));
		writer.move(writer.createRangeIn(cellBeingMerged), writer.createPositionAt(targetCell, "end"));
	}
	writer.remove(cellBeingMerged);
}
/**
* Checks if the passed table cell contains an empty paragraph.
*/
function isEmpty$1(tableCell) {
	const firstTableChild = tableCell.getChild(0);
	return tableCell.childCount == 1 && firstTableChild.is("element", "paragraph") && firstTableChild.isEmpty;
}
function getMergeDimensions(firstTableCell, selectedTableCells, tableUtils) {
	let maxWidthOffset = 0;
	let maxHeightOffset = 0;
	for (const tableCell of selectedTableCells) {
		const { row, column } = tableUtils.getCellLocation(tableCell);
		maxWidthOffset = getMaxOffset(tableCell, column, maxWidthOffset, "colspan");
		maxHeightOffset = getMaxOffset(tableCell, row, maxHeightOffset, "rowspan");
	}
	const { row: firstCellRow, column: firstCellColumn } = tableUtils.getCellLocation(firstTableCell);
	return {
		mergeWidth: maxWidthOffset - firstCellColumn,
		mergeHeight: maxHeightOffset - firstCellRow
	};
}
function getMaxOffset(tableCell, start, currentMaxOffset, which) {
	const dimensionValue = parseInt(tableCell.getAttribute(which) || "1");
	return Math.max(currentMaxOffset, start + dimensionValue);
}

/**
* @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 table/commands/selectrowcommand
*/
/**
* The select row command.
*
* The command is registered by {@link module:table/tableediting~TableEditing} as the `'selectTableRow'` editor command.
*
* To select the rows containing the selected cells, execute the command:
*
* ```ts
* editor.execute( 'selectTableRow' );
* ```
*/
var SelectRowCommand = class extends Command {
	/**
	* @inheritDoc
	*/
	constructor(editor) {
		super(editor);
		this.affectsData = false;
	}
	/**
	* @inheritDoc
	*/
	refresh() {
		const selectedCells = this.editor.plugins.get("TableUtils").getSelectionAffectedTableCells(this.editor.model.document.selection);
		this.isEnabled = selectedCells.length > 0;
	}
	/**
	* @inheritDoc
	*/
	execute() {
		const model = this.editor.model;
		const tableUtils = this.editor.plugins.get("TableUtils");
		const referenceCells = tableUtils.getSelectionAffectedTableCells(model.document.selection);
		const rowIndexes = tableUtils.getRowIndexes(referenceCells);
		const table = referenceCells[0].findAncestor("table");
		const rangesToSelect = [];
		for (let rowIndex = rowIndexes.first; rowIndex <= rowIndexes.last; rowIndex++) for (const cell of table.getChild(rowIndex).getChildren()) rangesToSelect.push(model.createRangeOn(cell));
		model.change((writer) => {
			writer.setSelection(rangesToSelect);
		});
	}
};

/**
* @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 table/commands/selectcolumncommand
*/
/**
* The select column command.
*
* The command is registered by {@link module:table/tableediting~TableEditing} as the `'selectTableColumn'` editor command.
*
* To select the columns containing the selected cells, execute the command:
*
* ```ts
* editor.execute( 'selectTableColumn' );
* ```
*/
var SelectColumnCommand = class extends Command {
	/**
	* @inheritDoc
	*/
	constructor(editor) {
		super(editor);
		this.affectsData = false;
	}
	/**
	* @inheritDoc
	*/
	refresh() {
		const selectedCells = this.editor.plugins.get("TableUtils").getSelectionAffectedTableCells(this.editor.model.document.selection);
		this.isEnabled = selectedCells.length > 0;
	}
	/**
	* @inheritDoc
	*/
	execute() {
		const tableUtils = this.editor.plugins.get("TableUtils");
		const model = this.editor.model;
		const referenceCells = tableUtils.getSelectionAffectedTableCells(model.document.selection);
		const firstCell = referenceCells[0];
		const lastCell = referenceCells.pop();
		const table = firstCell.findAncestor("table");
		const startLocation = tableUtils.getCellLocation(firstCell);
		const endLocation = tableUtils.getCellLocation(lastCell);
		const startColumn = Math.min(startLocation.column, endLocation.column);
		const endColumn = Math.max(startLocation.column, endLocation.column);
		const rangesToSelect = [];
		for (const cellInfo of new TableWalker(table, {
			startColumn,
			endColumn
		})) rangesToSelect.push(model.createRangeOn(cellInfo.cell));
		model.change((writer) => {
			writer.setSelection(rangesToSelect);
		});
	}
};

/**
* @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
*/
/**
* Injects a table layout post-fixer into the model.
*
* The role of the table layout post-fixer is to ensure that the table rows have the correct structure
* after a {@link module:engine/model/model~Model#change `change()`} block was executed.
*
* The correct structure means that:
*
* * All table rows have the same size.
* * None of the table cells extend vertically beyond their section (either header or body).
* * A table cell has always at least one element as a child.
*
* If the table structure is not correct, the post-fixer will automatically correct it in two steps:
*
* 1. It will clip table cells that extend beyond their section.
* 2. It will add empty table cells to the rows that are narrower than the widest table row.
*
* ## Clipping overlapping table cells
*
* Such situation may occur when pasting a table (or a part of a table) to the editor from external sources.
*
* For example, see the following table which has a cell (FOO) with the rowspan attribute (2):
*
* ```xml
* <table headingRows="1">
*   <tableRow>
*     <tableCell rowspan="2"><paragraph>FOO</paragraph></tableCell>
*     <tableCell colspan="2"><paragraph>BAR</paragraph></tableCell>
*   </tableRow>
*   <tableRow>
*     <tableCell><paragraph>BAZ</paragraph></tableCell>
*     <tableCell><paragraph>XYZ</paragraph></tableCell>
*   </tableRow>
* </table>
* ```
*
* It will be rendered in the view as:
*
* ```xml
* <table>
*   <thead>
*     <tr>
*       <td rowspan="2">FOO</td>
*       <td colspan="2">BAR</td>
*     </tr>
*   </thead>
*   <tbody>
*     <tr>
*       <td>BAZ</td>
*       <td>XYZ</td>
*     </tr>
*   </tbody>
* </table>
* ```
*
* In the above example the table will be rendered as a table with two rows: one in the header and second one in the body.
* The table cell (FOO) cannot span over multiple rows as it would extend from the header to the body section.
* The `rowspan` attribute must be changed to (1). The value (1) is the default value of the `rowspan` attribute
* so the `rowspan` attribute will be removed from the model.
*
* The table cell with BAZ in the content will be in the first column of the table.
*
* ## Adding missing table cells
*
* The table post-fixer will insert empty table cells to equalize table row sizes (the number of columns).
* The size of a table row is calculated by counting column spans of table cells, both horizontal (from the same row) and
* vertical (from the rows above).
*
* In the above example, the table row in the body section of the table is narrower then the row from the header: it has two cells
* with the default colspan (1). The header row has one cell with colspan (1) and the second with colspan (2).
* The table cell (FOO) does not extend beyond the head section (and as such will be fixed in the first step of this post-fixer).
* The post-fixer will add a missing table cell to the row in the body section of the table.
*
* The table from the above example will be fixed and rendered to the view as below:
*
* ```xml
* <table>
*   <thead>
*     <tr>
*       <td rowspan="2">FOO</td>
*       <td colspan="2">BAR</td>
*     </tr>
*   </thead>
*   <tbody>
*     <tr>
*       <td>BAZ</td>
*       <td>XYZ</td>
*     </tr>
*   </tbody>
* </table>
* ```
*
* ## Collaboration and undo - Expectations vs post-fixer results
*
* The table post-fixer only ensures proper structure without a deeper analysis of the nature of the change. As such, it might lead
* to a structure which was not intended by the user. In particular, it will also fix undo steps (in conjunction with collaboration)
* in which the editor content might not return to the original state.
*
* This will usually happen when one or more users change the size of the table.
*
* As an example see the table below:
*
* ```xml
* <table>
*   <tbody>
*     <tr>
*       <td>11</td>
*       <td>12</td>
*     </tr>
*     <tr>
*       <td>21</td>
*       <td>22</td>
*     </tr>
*   </tbody>
* </table>
* ```
*
* and the user actions:
*
* 1. Both users have a table with two rows and two columns.
* 2. User A adds a column at the end of the table. This will insert empty table cells to two rows.
* 3. User B adds a row at the end of the table. This will insert a row with two empty table cells.
* 4. Both users will have a table as below:
*
* ```xml
* <table>
*   <tbody>
*     <tr>
*       <td>11</td>
*       <td>12</td>
*       <td>(empty, inserted by A)</td>
*     </tr>
*     <tr>
*       <td>21</td>
*       <td>22</td>
*       <td>(empty, inserted by A)</td>
*     </tr>
*     <tr>
*       <td>(empty, inserted by B)</td>
*       <td>(empty, inserted by B)</td>
*     </tr>
*   </tbody>
* </table>
* ```
*
* The last row is shorter then others so the table post-fixer will add an empty row to the last row:
*
* ```xml
* <table>
*   <tbody>
*     <tr>
*       <td>11</td>
*       <td>12</td>
*       <td>(empty, inserted by A)</td>
*     </tr>
*     <tr>
*       <td>21</td>
*       <td>22</td>
*       <td>(empty, inserted by A)</td>
*     </tr>
*     <tr>
*       <td>(empty, inserted by B)</td>
*       <td>(empty, inserted by B)</td>
*       <td>(empty, inserted by the post-fixer)</td>
*     </tr>
*   </tbody>
* </table>
* ```
*
* Unfortunately undo does not know the nature of the changes and depending on which user applies the post-fixer changes, undoing them
* might lead to a broken table. If User B undoes inserting the column to the table, the undo engine will undo only the operations of
* inserting empty cells to rows from the initial table state (row 1 and 2) but the cell in the post-fixed row will remain:
*
* ```xml
* <table>
*   <tbody>
*     <tr>
*       <td>11</td>
*       <td>12</td>
*     </tr>
*     <tr>
*       <td>21</td>
*       <td>22</td>
*     </tr>
*     <tr>
*       <td>(empty, inserted by B)</td>
*       <td>(empty, inserted by B)</td>
*       <td>(empty, inserted by a post-fixer)</td>
*     </tr>
*   </tbody>
* </table>
* ```
*
* After undo, the table post-fixer will detect that two rows are shorter than others and will fix the table to:
*
* ```xml
* <table>
*   <tbody>
*     <tr>
*       <td>11</td>
*       <td>12</td>
*       <td>(empty, inserted by a post-fixer after undo)</td>
*     </tr>
*     <tr>
*       <td>21</td>
*       <td>22</td>
*       <td>(empty, inserted by a post-fixer after undo)</td>
*     </tr>
*     <tr>
*       <td>(empty, inserted by B)</td>
*       <td>(empty, inserted by B)</td>
*       <td>(empty, inserted by a post-fixer)</td>
*     </tr>
*   </tbody>
* </table>
* ```
*
* @internal
*/
function injectTableLayoutPostFixer(model) {
	model.document.registerPostFixer((writer) => tableLayoutPostFixer(writer, model));
}
/**
* The table layout post-fixer.
*/
function tableLayoutPostFixer(writer, model) {
	const changes = model.document.differ.getChanges();
	let wasFixed = false;
	const analyzedTables = /* @__PURE__ */ new Set();
	for (const entry of changes) {
		let table = null;
		if (entry.type == "insert" && entry.name == "table") table = entry.position.nodeAfter;
		if ((entry.type == "insert" || entry.type == "remove") && (entry.name == "tableRow" || entry.name == "tableCell")) table = entry.position.findAncestor("table");
		if (isTableAttributeEntry(entry)) table = entry.range.start.findAncestor("table");
		if (table && !analyzedTables.has(table)) {
			wasFixed = fixTableCellsRowspan(table, writer) || wasFixed;
			wasFixed = fixTableRowsSizes(table, writer) || wasFixed;
			analyzedTables.add(table);
		}
	}
	return wasFixed;
}
/**
* Fixes the invalid value of the `rowspan` attribute because a table cell cannot vertically extend beyond the table section it belongs to.
*
* @returns Returns `true` if the table was fixed.
*/
function fixTableCellsRowspan(table, writer) {
	let wasFixed = false;
	const cellsToTrim = findCellsToTrim(table);
	if (cellsToTrim.length) {
		wasFixed = true;
		for (const data of cellsToTrim) updateNumericAttribute("rowspan", data.rowspan, data.cell, writer, 1);
	}
	return wasFixed;
}
/**
* Makes all table rows in a table the same size.
*
* @returns Returns `true` if the table was fixed.
*/
function fixTableRowsSizes(table, writer) {
	let wasFixed = false;
	const childrenLengths = getChildrenLengths(table);
	const rowsToRemove = [];
	for (const [rowIndex, size] of childrenLengths.entries()) if (!size && table.getChild(rowIndex).is("element", "tableRow")) rowsToRemove.push(rowIndex);
	if (rowsToRemove.length) {
		wasFixed = true;
		for (const rowIndex of rowsToRemove.reverse()) {
			writer.remove(table.getChild(rowIndex));
			childrenLengths.splice(rowIndex, 1);
		}
	}
	const rowsLengths = childrenLengths.filter((row, rowIndex) => table.getChild(rowIndex).is("element", "tableRow"));
	const tableSize = rowsLengths[0];
	if (!rowsLengths.every((length) => length === tableSize)) {
		const maxColumns = rowsLengths.reduce((prev, current) => current > prev ? current : prev, 0);
		for (const [rowIndex, size] of rowsLengths.entries()) {
			const columnsToInsert = maxColumns - size;
			if (columnsToInsert) {
				for (let i = 0; i < columnsToInsert; i++) createEmptyTableCell(writer, writer.createPositionAt(table.getChild(rowIndex), "end"));
				wasFixed = true;
			}
		}
	}
	return wasFixed;
}
/**
* Searches for table cells that extend beyond the table section to which they belong to. It will return an array of objects
* that stores table cells to be trimmed and the correct value of the `rowspan` attribute to set.
*/
function findCellsToTrim(table) {
	const headingRows = parseInt(table.getAttribute("headingRows") || "0");
	const footerRows = parseInt(table.getAttribute("footerRows") || "0");
	const maxRows = Array.from(table.getChildren()).reduce((count, row) => row.is("element", "tableRow") ? count + 1 : count, 0);
	const footerIndex = maxRows - footerRows;
	const cellsToTrim = [];
	for (const { row, cell, cellHeight } of new TableWalker(table)) {
		if (cellHeight < 2) continue;
		const isInHeader = row < headingRows;
		const isInFooter = row >= footerIndex;
		let rowLimit;
		if (isInHeader) rowLimit = headingRows;
		else if (isInFooter) rowLimit = maxRows;
		else rowLimit = footerIndex;
		if (row + cellHeight > rowLimit) {
			const newRowspan = rowLimit - row;
			cellsToTrim.push({
				cell,
				rowspan: newRowspan
			});
		}
	}
	return cellsToTrim;
}
/**
* Returns an array with lengths of rows assigned to the corresponding row index.
*/
function getChildrenLengths(table) {
	const lengths = new Array(table.childCount).fill(0);
	for (const { rowIndex } of new TableWalker(table, { includeAllSlots: true })) lengths[rowIndex]++;
	return lengths;
}
/**
* Checks if the differ entry for an attribute change is one of the table's attributes.
*/
function isTableAttributeEntry(entry) {
	if (entry.type !== "attribute") return false;
	const key = entry.attributeKey;
	return key === "headingRows" || key === "colspan" || key === "rowspan";
}

/**
* @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
*/
/**
* Injects a table cell post-fixer into the model which inserts a `paragraph` element into empty table cells.
*
* A table cell must contain at least one block element as a child. An empty table cell will have an empty `paragraph` as a child.
*
* ```xml
* <table>
*   <tableRow>
*      <tableCell></tableCell>
*   </tableRow>
* </table>
* ```
*
* Will be fixed to:
*
* ```xml
* <table>
*   <tableRow>
*      <tableCell><paragraph></paragraph></tableCell>
*   </tableRow>
* </table>
* ```
*
* @internal
*/
function injectTableCellParagraphPostFixer(model) {
	model.document.registerPostFixer((writer) => tableCellContentsPostFixer(writer, model));
}
/**
* The table cell contents post-fixer.
*/
function tableCellContentsPostFixer(writer, model) {
	const changes = model.document.differ.getChanges();
	let wasFixed = false;
	for (const entry of changes) {
		if (entry.type == "insert" && entry.name == "table") wasFixed = fixTable(entry.position.nodeAfter, writer) || wasFixed;
		if (entry.type == "insert" && entry.name == "tableRow") wasFixed = fixTableRow(entry.position.nodeAfter, writer) || wasFixed;
		if (entry.type == "insert" && entry.name == "tableCell") wasFixed = fixTableCellContent(entry.position.nodeAfter, writer) || wasFixed;
		if ((entry.type == "remove" || entry.type == "insert") && checkTableCellChange(entry)) wasFixed = fixTableCellContent(entry.position.parent, writer) || wasFixed;
	}
	return wasFixed;
}
/**
* Fixes all table cells in a table.
*/
function fixTable(table, writer) {
	let wasFixed = false;
	for (const row of table.getChildren()) if (row.is("element", "tableRow")) wasFixed = fixTableRow(row, writer) || wasFixed;
	return wasFixed;
}
/**
* Fixes all table cells in a table row.
*/
function fixTableRow(tableRow, writer) {
	let wasFixed = false;
	for (const tableCell of tableRow.getChildren()) wasFixed = fixTableCellContent(tableCell, writer) || wasFixed;
	return wasFixed;
}
/**
* Fixes all table cell content by:
* - Adding a paragraph to a table cell without any child.
* - Wrapping direct $text in a `<paragraph>`.
*/
function fixTableCellContent(tableCell, writer) {
	if (tableCell.childCount == 0) {
		writer.insertElement("paragraph", tableCell);
		return true;
	}
	const textNodes = Array.from(tableCell.getChildren()).filter((child) => child.is("$text"));
	for (const child of textNodes) writer.wrap(writer.createRangeOn(child), "paragraph");
	return !!textNodes.length;
}
/**
* Checks if a differ change should fix the table cell. This happens on:
* - Removing content from the table cell (i.e. `tableCell` can be left empty).
* - Adding a text node directly into a table cell.
*/
function checkTableCellChange(entry) {
	if (!entry.position.parent.is("element", "tableCell")) return false;
	return entry.type == "insert" && entry.name == "$text" || entry.type == "remove";
}

/**
* @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
*/
/**
* Injects a table structure post-fixer into the model.
*
* It checks if the `headingRows` and `footerRows` attributes do not overlap.
* If they overlap, the `footerRows` attribute is corrected.
*
* We prefer `headingRows` over `footerRows` because changing `headingRows` would require updating
* the `tableCellType` attribute of the cells in the row, which is not required when changing `footerRows`.
*
* @param editor The editor instance.
*/
function injectTableStructurePostFixer(editor) {
	const { model } = editor;
	const tableUtils = editor.plugins.get(TableUtils);
	model.document.registerPostFixer((writer) => {
		let changed = false;
		const changes = model.document.differ.getChanges();
		const tables = /* @__PURE__ */ new Set();
		for (const entry of changes) {
			let table = null;
			if (entry.type == "attribute" && (entry.attributeKey == "headingRows" || entry.attributeKey == "footerRows")) table = entry.range.start.nodeAfter;
			else if (entry.type == "insert" && entry.name == "tableRow") table = entry.position.parent;
			else if (entry.type == "remove" && entry.name == "tableRow") table = entry.position.parent;
			if (table && table.is("element", "table")) tables.add(table);
		}
		for (const table of tables) if (fixTableSections(tableUtils, writer, table)) changed = true;
		return changed;
	});
}
/**
* Fixes table sections by ensuring that `headingRows` and `footerRows` do not overlap.
*/
function fixTableSections(tableUtils, writer, table) {
	const headingRows = table.getAttribute("headingRows") || 0;
	const footerRows = table.getAttribute("footerRows") || 0;
	const rows = tableUtils.getRows(table);
	if (headingRows + footerRows > rows) {
		updateNumericAttribute("footerRows", Math.max(0, rows - headingRows), table, writer, 0);
		return true;
	}
	return false;
}

/**
* @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 table headings refresh handler which marks the table cells or rows in the differ to have it re-rendered
* if the headings attribute changed.
*
* Table heading rows and heading columns are represented in the model by a `headingRows` and `headingColumns` attributes.
*
* When table headings attribute changes, all the cells/rows are marked to re-render to change between `<td>` and `<th>`.
*
* @internal
*/
function tableStructureRefreshHandler(model, editing) {
	const differ = model.document.differ;
	const movedRows = /* @__PURE__ */ new Set();
	const rowsToReconvert = /* @__PURE__ */ new Set();
	const cellsToReconvert = /* @__PURE__ */ new Set();
	for (const change of differ.getChanges()) {
		let table;
		if (change.type == "attribute") {
			const element = change.range.start.nodeAfter;
			if (!element || !element.is("element", "table")) continue;
			if (change.attributeKey != "headingRows" && change.attributeKey != "headingColumns" && change.attributeKey != "footerRows") continue;
			table = element;
		} else if (change.name == "tableRow" || change.name == "tableCell") table = change.position.findAncestor("table");
		if (!table) continue;
		if (change.type == "insert" && change.name == "tableRow" && editing.mapper.toViewElement(change.position.nodeAfter)) movedRows.add(change.position.nodeAfter);
		const headingRows = table.getAttribute("headingRows") || 0;
		const headingColumns = table.getAttribute("headingColumns") || 0;
		const tableWalker = new TableWalker(table);
		for (const tableSlot of tableWalker) {
			const viewElement = editing.mapper.toViewElement(tableSlot.cell);
			if (!viewElement || !viewElement.is("element")) continue;
			const expectedElementName = tableSlot.row < headingRows || tableSlot.column < headingColumns ? "th" : "td";
			if (viewElement.name != expectedElementName) {
				cellsToReconvert.add(tableSlot.cell);
				if (movedRows.has(tableSlot.cell.parent)) rowsToReconvert.add(tableSlot.cell.parent);
			}
		}
	}
	for (const item of rowsToReconvert) editing.reconvertItem(item);
	for (const item of cellsToReconvert) editing.reconvertItem(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
*/
/**
* A table cell refresh handler which marks the table cell in the differ to have it re-rendered.
*
* Model `paragraph` inside a table cell can be rendered as `<span>` or `<p>`. It is rendered as `<span>` if this is the only block
* element in that table cell and it does not have any attributes. It is rendered as `<p>` otherwise.
*
* When table cell content changes, for example a second `paragraph` element is added, we need to ensure that the first `paragraph` is
* re-rendered so it changes from `<span>` to `<p>`. The easiest way to do it is to re-render the entire table cell.
*
* @internal
*/
function tableCellRefreshHandler(model, editing) {
	const differ = model.document.differ;
	const cellsToCheck = /* @__PURE__ */ new Set();
	for (const change of differ.getChanges()) {
		const parent = change.type == "attribute" ? change.range.start.parent : change.position.parent;
		if (parent.is("element", "tableCell")) cellsToCheck.add(parent);
	}
	for (const tableCell of cellsToCheck.values()) {
		const paragraphsToRefresh = Array.from(tableCell.getChildren()).filter((child) => shouldRefresh(child, editing.mapper));
		for (const paragraph of paragraphsToRefresh) editing.reconvertItem(paragraph);
	}
}
/**
* Check if given model element needs refreshing.
*/
function shouldRefresh(child, mapper) {
	if (!child.is("element", "paragraph")) return false;
	const viewElement = mapper.toViewElement(child);
	if (!viewElement) return false;
	return isSingleParagraphWithoutAttributes(child) !== viewElement.is("element", "span");
}

/**
* @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 table/tableediting
*/
/**
* The table editing feature.
*/
var TableEditing = class extends Plugin {
	/**
	* Handlers for creating additional slots in the table.
	*/
	_additionalSlots;
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "TableEditing";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [TableUtils];
	}
	/**
	* @inheritDoc
	*/
	constructor(editor) {
		super(editor);
		this._additionalSlots = [];
	}
	/**
	* @inheritDoc
	*/
	init() {
		const editor = this.editor;
		const model = editor.model;
		const schema = model.schema;
		const conversion = editor.conversion;
		const tableUtils = editor.plugins.get(TableUtils);
		editor.config.define("table.enableFooters", false);
		const useFooterElement = !!editor.config.get("table.enableFooters");
		schema.register("table", {
			inheritAllFrom: "$blockObject",
			allowAttributes: [
				"headingRows",
				"headingColumns",
				...useFooterElement ? ["footerRows"] : []
			]
		});
		schema.register("tableRow", {
			allowIn: "table",
			isLimit: true
		});
		schema.register("tableCell", {
			allowContentOf: "$container",
			allowIn: "tableRow",
			allowAttributes: ["colspan", "rowspan"],
			isLimit: true,
			isSelectable: true
		});
		conversion.for("upcast").add(upcastTableFigure());
		conversion.for("upcast").add(upcastTable({ enableFooters: useFooterElement }));
		conversion.for("editingDowncast").elementToStructure({
			model: {
				name: "table",
				attributes: ["headingRows", ...useFooterElement ? ["footerRows"] : []]
			},
			view: downcastTable(tableUtils, {
				asWidget: true,
				additionalSlots: this._additionalSlots
			})
		});
		conversion.for("dataDowncast").elementToStructure({
			model: {
				name: "table",
				attributes: ["headingRows", ...useFooterElement ? ["footerRows"] : []]
			},
			view: downcastTable(tableUtils, { additionalSlots: this._additionalSlots })
		});
		conversion.for("upcast").elementToElement({
			model: "tableRow",
			view: "tr"
		});
		conversion.for("upcast").add(skipEmptyTableRow());
		conversion.for("downcast").elementToElement({
			model: "tableRow",
			view: downcastRow()
		});
		conversion.for("upcast").elementToElement({
			model: "tableCell",
			view: "td"
		});
		conversion.for("upcast").elementToElement({
			model: "tableCell",
			view: "th"
		});
		conversion.for("upcast").add(ensureParagraphInTableCell("td"));
		conversion.for("upcast").add(ensureParagraphInTableCell("th"));
		conversion.for("editingDowncast").elementToElement({
			model: "tableCell",
			view: downcastCell({
				asWidget: true,
				cellTypeEnabled: () => isTableCellTypeEnabled(this.editor)
			})
		});
		conversion.for("dataDowncast").elementToElement({
			model: "tableCell",
			view: downcastCell({ cellTypeEnabled: () => isTableCellTypeEnabled(this.editor) })
		});
		conversion.for("editingDowncast").elementToElement({
			model: "paragraph",
			view: convertParagraphInTableCell({ asWidget: true }),
			converterPriority: "high"
		});
		conversion.for("dataDowncast").elementToElement({
			model: "paragraph",
			view: convertParagraphInTableCell(),
			converterPriority: "high"
		});
		conversion.for("downcast").attributeToAttribute({
			model: "colspan",
			view: "colspan"
		});
		conversion.for("upcast").attributeToAttribute({
			model: {
				key: "colspan",
				value: upcastCellSpan("colspan")
			},
			view: "colspan"
		});
		conversion.for("downcast").attributeToAttribute({
			model: "rowspan",
			view: "rowspan"
		});
		conversion.for("upcast").attributeToAttribute({
			model: {
				key: "rowspan",
				value: upcastCellSpan("rowspan")
			},
			view: "rowspan"
		});
		this._addPlainTableOutputConverters();
		editor.config.define("table.defaultHeadings.rows", 0);
		editor.config.define("table.defaultHeadings.columns", 0);
		editor.config.define("table.defaultFooters", 0);
		editor.config.define("table.showHiddenBorders", true);
		if (editor.config.get("table.showHiddenBorders")) editor.editing.view.change((writer) => {
			for (const root of editor.editing.view.document.roots) writer.addClass("ck-table-show-hidden-borders", root);
		});
		editor.commands.add("insertTable", new InsertTableCommand(editor));
		editor.commands.add("insertTableRowAbove", new InsertRowCommand(editor, { order: "above" }));
		editor.commands.add("insertTableRowBelow", new InsertRowCommand(editor, { order: "below" }));
		editor.commands.add("insertTableColumnLeft", new InsertColumnCommand(editor, { order: "left" }));
		editor.commands.add("insertTableColumnRight", new InsertColumnCommand(editor, { order: "right" }));
		editor.commands.add("removeTableRow", new RemoveRowCommand(editor));
		editor.commands.add("removeTableColumn", new RemoveColumnCommand(editor));
		editor.commands.add("splitTableCellVertically", new SplitCellCommand(editor, { direction: "vertically" }));
		editor.commands.add("splitTableCellHorizontally", new SplitCellCommand(editor, { direction: "horizontally" }));
		editor.commands.add("mergeTableCells", new MergeCellsCommand(editor));
		editor.commands.add("mergeTableCellRight", new MergeCellCommand(editor, { direction: "right" }));
		editor.commands.add("mergeTableCellLeft", new MergeCellCommand(editor, { direction: "left" }));
		editor.commands.add("mergeTableCellDown", new MergeCellCommand(editor, { direction: "down" }));
		editor.commands.add("mergeTableCellUp", new MergeCellCommand(editor, { direction: "up" }));
		editor.commands.add("setTableColumnHeader", new SetHeaderColumnCommand(editor));
		editor.commands.add("setTableRowHeader", new SetHeaderRowCommand(editor));
		if (useFooterElement) editor.commands.add("setTableFooterRow", new SetFooterRowCommand(editor));
		editor.commands.add("selectTableRow", new SelectRowCommand(editor));
		editor.commands.add("selectTableColumn", new SelectColumnCommand(editor));
		injectTableLayoutPostFixer(model);
		injectTableCellParagraphPostFixer(model);
		if (useFooterElement) injectTableStructurePostFixer(editor);
		this.listenTo(model.document, "change:data", () => {
			if (!isTableCellTypeEnabled(editor)) tableStructureRefreshHandler(model, editor.editing);
			tableCellRefreshHandler(model, editor.editing);
		});
	}
	/**
	* Registers downcast handler for the additional table slot.
	*/
	registerAdditionalSlot(slotHandler) {
		this._additionalSlots.push(slotHandler);
	}
	/**
	* Adds converters for plain table output. These converters are used either when the `PlainTableOutput` plugin is loaded
	* or when content is processed by the clipboard pipeline, ensuring that pasted tables are not wrapped in a <figure> element.
	*/
	_addPlainTableOutputConverters() {
		const editor = this.editor;
		editor.conversion.for("dataDowncast").elementToStructure({
			model: "table",
			view: convertPlainTable(editor),
			converterPriority: "high"
		});
		if (editor.plugins.has("TableCaptionEditing")) editor.conversion.for("dataDowncast").elementToElement({
			model: "caption",
			view: convertPlainTableCaption(editor),
			converterPriority: "high"
		});
		if (editor.plugins.has("TablePropertiesEditing")) downcastTableBorderAndBackgroundAttributes(editor);
	}
};
/**
* Returns fixed colspan and rowspan attrbutes values.
*
* @param type colspan or rowspan.
* @returns conversion value function.
*/
function upcastCellSpan(type) {
	return (cell) => {
		const span = parseInt(cell.getAttribute(type));
		if (Number.isNaN(span) || span <= 0) return null;
		return span;
	};
}

/**
* @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 table/ui/inserttableview
*/
/**
* The table size view.
*
* It renders a 10x10 grid to choose the inserted table size.
*
* @internal
*/
var InsertTableView = class extends View {
	/**
	* A collection of table size box items.
	*/
	items;
	/**
	* Listen to `keydown` events fired in this view's main element.
	*/
	keystrokes;
	/**
	* Tracks information about the DOM focus in the grid.
	*/
	focusTracker;
	/**
	* @inheritDoc
	*/
	constructor(locale) {
		super(locale);
		const bind = this.bindTemplate;
		this.items = this._createGridCollection();
		this.keystrokes = new KeystrokeHandler();
		this.focusTracker = new FocusTracker();
		this.set("rows", 0);
		this.set("columns", 0);
		this.bind("label").to(this, "columns", this, "rows", (columns, rows) => `${rows} × ${columns}`);
		this.setTemplate({
			tag: "div",
			attributes: { class: ["ck"] },
			children: [{
				tag: "div",
				attributes: { class: ["ck-insert-table-dropdown__grid"] },
				on: { "mouseover@.ck-insert-table-dropdown-grid-box": bind.to("boxover") },
				children: this.items
			}, {
				tag: "div",
				attributes: {
					class: ["ck", "ck-insert-table-dropdown__label"],
					"aria-hidden": true
				},
				children: [{ text: bind.to("label") }]
			}],
			on: {
				mousedown: bind.to((evt) => {
					evt.preventDefault();
				}),
				click: bind.to(() => {
					this.fire("execute");
				})
			}
		});
		this.on("boxover", (evt, domEvt) => {
			const { row, column } = domEvt.target.dataset;
			this.items.get((parseInt(row, 10) - 1) * 10 + (parseInt(column, 10) - 1)).focus();
		});
		this.focusTracker.on("change:focusedElement", (evt, name, focusedElement) => {
			if (!focusedElement) return;
			const { row, column } = focusedElement.dataset;
			this.set({
				rows: parseInt(row),
				columns: parseInt(column)
			});
		});
		this.on("change:columns", () => this._highlightGridBoxes());
		this.on("change:rows", () => this._highlightGridBoxes());
	}
	render() {
		super.render();
		addKeyboardHandlingForGrid({
			keystrokeHandler: this.keystrokes,
			focusTracker: this.focusTracker,
			gridItems: this.items,
			numberOfColumns: 10,
			uiLanguageDirection: this.locale && this.locale.uiLanguageDirection
		});
		for (const item of this.items) this.focusTracker.add(item.element);
		this.keystrokes.listenTo(this.element);
	}
	/**
	* Resets the rows and columns selection.
	*/
	reset() {
		this.set({
			rows: 1,
			columns: 1
		});
	}
	/**
	* @inheritDoc
	*/
	focus() {
		this.items.get(0).focus();
	}
	/**
	* @inheritDoc
	*/
	focusLast() {
		this.items.get(0).focus();
	}
	/**
	* Highlights grid boxes depending on rows and columns selected.
	*/
	_highlightGridBoxes() {
		const rows = this.rows;
		const columns = this.columns;
		this.items.map((boxView, index) => {
			const itemRow = Math.floor(index / 10);
			const itemColumn = index % 10;
			const isOn = itemRow < rows && itemColumn < columns;
			boxView.set("isOn", isOn);
		});
	}
	/**
	* Creates a new Button for the grid.
	*
	* @param locale The locale instance.
	* @param row Row number.
	* @param column Column number.
	* @param label The grid button label.
	*/
	_createGridButton(locale, row, column, label) {
		const button = new ButtonView(locale);
		button.set({
			label,
			class: "ck-insert-table-dropdown-grid-box"
		});
		button.extendTemplate({ attributes: {
			"data-row": row,
			"data-column": column
		} });
		return button;
	}
	/**
	* @returns A view collection containing boxes to be placed in a table grid.
	*/
	_createGridCollection() {
		const boxes = [];
		for (let index = 0; index < 100; index++) {
			const row = Math.floor(index / 10);
			const column = index % 10;
			const label = `${row + 1} × ${column + 1}`;
			boxes.push(this._createGridButton(this.locale, row + 1, column + 1, label));
		}
		return this.createCollection(boxes);
	}
};

/**
* @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 table/tableui
*/
/**
* The table UI plugin. It introduces:
*
* * The `'insertTable'` dropdown,
* * The `'menuBar:insertTable'` menu bar menu,
* * The `'tableColumn'` dropdown,
* * The `'tableRow'` dropdown,
* * The `'mergeTableCells'` split button.
*
* The `'tableColumn'`, `'tableRow'` and `'mergeTableCells'` dropdowns work best with {@link module:table/tabletoolbar~TableToolbar}.
*/
var TableUI = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "TableUI";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	init() {
		const editor = this.editor;
		const t = this.editor.t;
		const isContentLtr = editor.locale.contentLanguageDirection === "ltr";
		const areTableFootersEnabled = !!editor.config.get("table.enableFooters");
		editor.ui.componentFactory.add("insertTable", (locale) => {
			const command = editor.commands.get("insertTable");
			const dropdownView = createDropdown(locale);
			dropdownView.bind("isEnabled").to(command);
			dropdownView.buttonView.set({
				icon: IconTable,
				label: t("Insert table"),
				tooltip: true
			});
			let insertTableView;
			dropdownView.on("change:isOpen", () => {
				if (insertTableView) return;
				insertTableView = new InsertTableView(locale);
				dropdownView.panelView.children.add(insertTableView);
				insertTableView.delegate("execute").to(dropdownView);
				dropdownView.on("execute", () => {
					editor.execute("insertTable", {
						rows: insertTableView.rows,
						columns: insertTableView.columns
					});
					editor.editing.view.focus();
				});
			});
			return dropdownView;
		});
		editor.ui.componentFactory.add("menuBar:insertTable", (locale) => {
			const command = editor.commands.get("insertTable");
			const menuView = new MenuBarMenuView(locale);
			const insertTableView = new InsertTableView(locale);
			insertTableView.delegate("execute").to(menuView);
			menuView.on("change:isOpen", (event, name, isOpen) => {
				if (!isOpen) insertTableView.reset();
			});
			insertTableView.on("execute", () => {
				editor.execute("insertTable", {
					rows: insertTableView.rows,
					columns: insertTableView.columns
				});
				editor.editing.view.focus();
			});
			menuView.buttonView.set({
				label: t("Table"),
				icon: IconTable
			});
			menuView.panelView.children.add(insertTableView);
			menuView.bind("isEnabled").to(command);
			return menuView;
		});
		editor.ui.componentFactory.add("tableColumn", (locale) => {
			const options = [
				{
					type: "switchbutton",
					model: {
						commandName: "setTableColumnHeader",
						label: t("Header column"),
						bindIsOn: true
					}
				},
				{ type: "separator" },
				{
					type: "button",
					model: {
						commandName: isContentLtr ? "insertTableColumnLeft" : "insertTableColumnRight",
						label: t("Insert column left")
					}
				},
				{
					type: "button",
					model: {
						commandName: isContentLtr ? "insertTableColumnRight" : "insertTableColumnLeft",
						label: t("Insert column right")
					}
				},
				{
					type: "button",
					model: {
						commandName: "removeTableColumn",
						label: t("Delete column")
					}
				},
				{
					type: "button",
					model: {
						commandName: "selectTableColumn",
						label: t("Select column")
					}
				}
			];
			return this._prepareDropdown(t("Column"), IconTableColumn, options, locale);
		});
		editor.ui.componentFactory.add("tableRow", (locale) => {
			const options = [
				{
					type: "switchbutton",
					model: {
						commandName: "setTableRowHeader",
						label: t("Header row"),
						bindIsOn: true
					}
				},
				areTableFootersEnabled && {
					type: "switchbutton",
					model: {
						commandName: "setTableFooterRow",
						label: t("Footer row"),
						bindIsOn: true
					}
				},
				{ type: "separator" },
				{
					type: "button",
					model: {
						commandName: "insertTableRowAbove",
						label: t("Insert row above")
					}
				},
				{
					type: "button",
					model: {
						commandName: "insertTableRowBelow",
						label: t("Insert row below")
					}
				},
				{
					type: "button",
					model: {
						commandName: "removeTableRow",
						label: t("Delete row")
					}
				},
				{
					type: "button",
					model: {
						commandName: "selectTableRow",
						label: t("Select row")
					}
				}
			].filter(Boolean);
			return this._prepareDropdown(t("Row"), IconTableRow, options, locale);
		});
		editor.ui.componentFactory.add("mergeTableCells", (locale) => {
			const options = [
				{
					type: "button",
					model: {
						commandName: "mergeTableCellUp",
						label: t("Merge cell up")
					}
				},
				{
					type: "button",
					model: {
						commandName: isContentLtr ? "mergeTableCellRight" : "mergeTableCellLeft",
						label: t("Merge cell right")
					}
				},
				{
					type: "button",
					model: {
						commandName: "mergeTableCellDown",
						label: t("Merge cell down")
					}
				},
				{
					type: "button",
					model: {
						commandName: isContentLtr ? "mergeTableCellLeft" : "mergeTableCellRight",
						label: t("Merge cell left")
					}
				},
				{ type: "separator" },
				{
					type: "button",
					model: {
						commandName: "splitTableCellVertically",
						label: t("Split cell vertically")
					}
				},
				{
					type: "button",
					model: {
						commandName: "splitTableCellHorizontally",
						label: t("Split cell horizontally")
					}
				}
			];
			return this._prepareMergeSplitButtonDropdown(t("Merge cells"), IconTableMergeCell, options, locale);
		});
	}
	/**
	* Creates a dropdown view from a set of options.
	*
	* @param label The dropdown button label.
	* @param icon An icon for the dropdown button.
	* @param options The list of options for the dropdown.
	*/
	_prepareDropdown(label, icon, options, locale) {
		const editor = this.editor;
		const dropdownView = createDropdown(locale);
		const commands = this._fillDropdownWithListOptions(dropdownView, options);
		dropdownView.buttonView.set({
			label,
			icon,
			tooltip: true
		});
		dropdownView.bind("isEnabled").toMany(commands, "isEnabled", (...areEnabled) => {
			return areEnabled.some((isEnabled) => isEnabled);
		});
		this.listenTo(dropdownView, "execute", (evt) => {
			editor.execute(evt.source.commandName);
			if (!(evt.source instanceof SwitchButtonView)) editor.editing.view.focus();
		});
		return dropdownView;
	}
	/**
	* Creates a dropdown view with a {@link module:ui/dropdown/button/splitbuttonview~SplitButtonView} for
	* merge (and split)–related commands.
	*
	* @param label The dropdown button label.
	* @param icon An icon for the dropdown button.
	* @param options The list of options for the dropdown.
	*/
	_prepareMergeSplitButtonDropdown(label, icon, options, locale) {
		const editor = this.editor;
		const dropdownView = createDropdown(locale, SplitButtonView);
		const mergeCommandName = "mergeTableCells";
		const mergeCommand = editor.commands.get(mergeCommandName);
		const commands = this._fillDropdownWithListOptions(dropdownView, options);
		dropdownView.buttonView.set({
			label,
			icon,
			tooltip: true,
			isEnabled: true
		});
		dropdownView.bind("isEnabled").toMany([mergeCommand, ...commands], "isEnabled", (...areEnabled) => {
			return areEnabled.some((isEnabled) => isEnabled);
		});
		this.listenTo(dropdownView.buttonView, "execute", () => {
			editor.execute(mergeCommandName);
			editor.editing.view.focus();
		});
		this.listenTo(dropdownView, "execute", (evt) => {
			editor.execute(evt.source.commandName);
			editor.editing.view.focus();
		});
		return dropdownView;
	}
	/**
	* Injects a {@link module:ui/list/listview~ListView} into the passed dropdown with buttons
	* which execute editor commands as configured in passed options.
	*
	* @param options The list of options for the dropdown.
	* @returns Commands the list options are interacting with.
	*/
	_fillDropdownWithListOptions(dropdownView, options) {
		const editor = this.editor;
		const commands = [];
		const itemDefinitions = new Collection();
		for (const option of options) addListOption(option, editor, commands, itemDefinitions);
		addListToDropdown(dropdownView, itemDefinitions);
		return commands;
	}
};
/**
* Adds an option to a list view.
*
* @param option A configuration option.
* @param commands The list of commands to update.
* @param itemDefinitions A collection of dropdown items to update with the given option.
*/
function addListOption(option, editor, commands, itemDefinitions) {
	if (option.type === "button" || option.type === "switchbutton") {
		const model = option.model = new UIModel(option.model);
		const { commandName, bindIsOn } = option.model;
		const command = editor.commands.get(commandName);
		commands.push(command);
		model.set({ commandName });
		model.bind("isEnabled").to(command);
		if (bindIsOn) model.bind("isOn").to(command, "value");
		model.set({ withText: true });
	}
	itemDefinitions.add(option);
}

/**
* @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 table/tableselection
*/
/**
* This plugin enables the advanced table cells, rows and columns selection.
* It is loaded automatically by the {@link module:table/table~Table} plugin.
*/
var TableSelection = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "TableSelection";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [TableUtils, TableUtils];
	}
	/**
	* @inheritDoc
	*/
	init() {
		const editor = this.editor;
		const model = editor.model;
		const view = editor.editing.view;
		this.listenTo(model, "deleteContent", (evt, args) => this._handleDeleteContent(evt, args), { priority: "high" });
		this.listenTo(view.document, "insertText", (evt, data) => this._handleInsertTextEvent(evt, data), { priority: "high" });
		this._defineSelectionConverter();
		this._enablePluginDisabling();
	}
	/**
	* Returns the currently selected table cells or `null` if it is not a table cells selection.
	*/
	getSelectedTableCells() {
		const tableUtils = this.editor.plugins.get(TableUtils);
		const selection = this.editor.model.document.selection;
		const selectedCells = tableUtils.getSelectedTableCells(selection);
		if (selectedCells.length == 0) return null;
		return selectedCells;
	}
	/**
	* Returns the selected table fragment as a document fragment.
	*/
	getSelectionAsFragment() {
		const tableUtils = this.editor.plugins.get(TableUtils);
		const selectedCells = this.getSelectedTableCells();
		if (!selectedCells) return null;
		return this.editor.model.change((writer) => {
			const documentFragment = writer.createDocumentFragment();
			const { first: firstColumn, last: lastColumn } = tableUtils.getColumnIndexes(selectedCells);
			const { first: firstRow, last: lastRow } = tableUtils.getRowIndexes(selectedCells);
			const sourceTable = selectedCells[0].findAncestor("table");
			let adjustedLastRow = lastRow;
			let adjustedLastColumn = lastColumn;
			if (tableUtils.isSelectionRectangular(selectedCells)) {
				const dimensions = {
					firstColumn,
					lastColumn,
					firstRow,
					lastRow
				};
				adjustedLastRow = adjustLastRowIndex(sourceTable, dimensions);
				adjustedLastColumn = adjustLastColumnIndex(sourceTable, dimensions);
			}
			const table = cropTableToDimensions(sourceTable, {
				startRow: firstRow,
				startColumn: firstColumn,
				endRow: adjustedLastRow,
				endColumn: adjustedLastColumn
			}, writer);
			writer.insert(table, documentFragment, 0);
			return documentFragment;
		});
	}
	/**
	* Sets the model selection based on given anchor and target cells (can be the same cell).
	* Takes care of setting the backward flag.
	*
	* ```ts
	* const modelRoot = editor.model.document.getRoot();
	* const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
	* const lastCell = modelRoot.getNodeByPath( [ 0, 0, 1 ] );
	*
	* const tableSelection = editor.plugins.get( 'TableSelection' );
	* tableSelection.setCellSelection( firstCell, lastCell );
	* ```
	*/
	setCellSelection(anchorCell, targetCell) {
		const cellsToSelect = this._getCellsToSelect(anchorCell, targetCell);
		this.editor.model.change((writer) => {
			writer.setSelection(cellsToSelect.cells.map((cell) => writer.createRangeOn(cell)), { backward: cellsToSelect.backward });
		});
	}
	/**
	* Returns the focus cell from the current selection.
	*/
	getFocusCell() {
		const element = [...this.editor.model.document.selection.getRanges()].pop().getContainedElement();
		if (element && element.is("element", "tableCell")) return element;
		return null;
	}
	/**
	* Returns the anchor cell from the current selection.
	*/
	getAnchorCell() {
		const selection = this.editor.model.document.selection;
		const element = first(selection.getRanges()).getContainedElement();
		if (element && element.is("element", "tableCell")) return element;
		return null;
	}
	/**
	* Defines a selection converter which marks the selected cells with a specific class.
	*
	* The real DOM selection is put in the last cell. Since the order of ranges is dependent on whether the
	* selection is backward or not, the last cell will usually be close to the "focus" end of the selection
	* (a selection has anchor and focus).
	*
	* The real DOM selection is then hidden with CSS.
	*/
	_defineSelectionConverter() {
		const editor = this.editor;
		const highlighted = /* @__PURE__ */ new Set();
		editor.conversion.for("editingDowncast").add((dispatcher) => dispatcher.on("selection", (evt, data, conversionApi) => {
			const viewWriter = conversionApi.writer;
			clearHighlightedTableCells(viewWriter);
			const selectedCells = this.getSelectedTableCells();
			if (!selectedCells) return;
			for (const tableCell of selectedCells) {
				const viewElement = conversionApi.mapper.toViewElement(tableCell);
				viewWriter.addClass("ck-editor__editable_selected", viewElement);
				highlighted.add(viewElement);
			}
			const lastViewCell = conversionApi.mapper.toViewElement(selectedCells[selectedCells.length - 1]);
			viewWriter.setSelection(lastViewCell, 0);
		}, { priority: "lowest" }));
		function clearHighlightedTableCells(viewWriter) {
			for (const previouslyHighlighted of highlighted) viewWriter.removeClass("ck-editor__editable_selected", previouslyHighlighted);
			highlighted.clear();
		}
	}
	/**
	* Creates a listener that reacts to changes in {@link #isEnabled} and, if the plugin was disabled,
	* it collapses the multi-cell selection to a regular selection placed inside a table cell.
	*
	* This listener helps features that disable the table selection plugin bring the selection
	* to a clear state they can work with (for instance, because they don't support multiple cell selection).
	*/
	_enablePluginDisabling() {
		const editor = this.editor;
		this.on("change:isEnabled", () => {
			if (!this.isEnabled) {
				const selectedCells = this.getSelectedTableCells();
				if (!selectedCells) return;
				editor.model.change((writer) => {
					const position = writer.createPositionAt(selectedCells[0], 0);
					const range = editor.model.schema.getNearestSelectionRange(position);
					writer.setSelection(range);
				});
			}
		});
	}
	/**
	* Overrides the default `model.deleteContent()` behavior over a selected table fragment.
	*
	* @param args Delete content method arguments.
	*/
	_handleDeleteContent(event, args) {
		const tableUtils = this.editor.plugins.get(TableUtils);
		const selection = args[0];
		const options = args[1];
		const model = this.editor.model;
		const isBackward = !options || options.direction == "backward";
		const selectedTableCells = tableUtils.getSelectedTableCells(selection);
		if (!selectedTableCells.length) return;
		event.stop();
		model.change((writer) => {
			const tableCellToSelect = selectedTableCells[isBackward ? selectedTableCells.length - 1 : 0];
			model.change((writer) => {
				for (const tableCell of selectedTableCells) model.deleteContent(writer.createSelection(tableCell, "in"));
			});
			const rangeToSelect = model.schema.getNearestSelectionRange(writer.createPositionAt(tableCellToSelect, 0));
			if (selection.is("documentSelection")) writer.setSelection(rangeToSelect);
			else selection.setTo(rangeToSelect);
		});
	}
	/**
	* This handler makes it possible to remove the content of all selected cells by starting to type.
	* If you take a look at {@link #_defineSelectionConverter} you will find out that despite the multi-cell selection being set
	* in the model, the view selection is collapsed in the last cell (because most browsers are unable to render multi-cell selections;
	* yes, it's a hack).
	*
	* When multiple cells are selected in the model and the user starts to type, the
	* {@link module:engine/view/document~ViewDocument#event:insertText} event carries information provided by the
	* beforeinput DOM  event, that in turn only knows about this collapsed DOM selection in the last cell.
	*
	* As a result, the selected cells have no chance to be cleaned up. To fix this, this listener intercepts
	* the event and injects the custom view selection in the data that translates correctly to the actual state
	* of the multi-cell selection in the model.
	*
	* @param data Insert text event data.
	*/
	_handleInsertTextEvent(evt, data) {
		const editor = this.editor;
		const selectedCells = this.getSelectedTableCells();
		if (!selectedCells) return;
		const view = editor.editing.view;
		const mapper = editor.editing.mapper;
		const viewRanges = selectedCells.map((tableCell) => view.createRangeOn(mapper.toViewElement(tableCell)));
		data.selection = view.createSelection(viewRanges);
		data.preventDefault();
	}
	/**
	* Returns an array of table cells that should be selected based on the
	* given anchor cell and target (focus) cell.
	*
	* The cells are returned in a reverse direction if the selection is backward.
	*/
	_getCellsToSelect(anchorCell, targetCell) {
		const tableUtils = this.editor.plugins.get("TableUtils");
		const startLocation = tableUtils.getCellLocation(anchorCell);
		const endLocation = tableUtils.getCellLocation(targetCell);
		const startRow = Math.min(startLocation.row, endLocation.row);
		const endRow = Math.max(startLocation.row, endLocation.row);
		const startColumn = Math.min(startLocation.column, endLocation.column);
		const endColumnExtraColspan = parseInt(targetCell.getAttribute("colspan") || "1") - 1;
		const endColumn = Math.max(startLocation.column, endLocation.column + endColumnExtraColspan);
		const selectionMap = new Array(endRow - startRow + 1).fill(null).map(() => []);
		const walkerOptions = {
			startRow,
			endRow,
			startColumn,
			endColumn
		};
		for (const { row, cell } of new TableWalker(anchorCell.findAncestor("table"), walkerOptions)) selectionMap[row - startRow].push(cell);
		const flipVertically = endLocation.row < startLocation.row;
		const flipHorizontally = endLocation.column < startLocation.column;
		if (flipVertically) selectionMap.reverse();
		if (flipHorizontally) selectionMap.forEach((row) => row.reverse());
		return {
			cells: selectionMap.flat(),
			backward: flipVertically || flipHorizontally
		};
	}
};

/**
* @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
*/
/**
* This plugin adds support for copying/cutting/pasting fragments of tables.
* It is loaded automatically by the {@link module:table/table~Table} plugin.
*/
var TableClipboard = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "TableClipboard";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [
			ClipboardMarkersUtils,
			ClipboardPipeline,
			TableSelection,
			TableUtils
		];
	}
	/**
	* @inheritDoc
	*/
	init() {
		const viewDocument = this.editor.editing.view.document;
		this.listenTo(viewDocument, "copy", (evt, data) => this._onCopyCut(evt, data));
		this.listenTo(viewDocument, "cut", (evt, data) => this._onCopyCut(evt, data));
		this._listenToContentInsertion();
		this.decorate("_replaceTableSlotCell");
	}
	/**
	* Sets up listening for events from the clipboard pipeline to properly handle
	* table content merging during paste/drop operations.
	*
	* When a user is dragging and dropping a table, we want to insert the entire table into
	* a table cell instead of merging table contents. For paste and other events,
	* the normal table merge behavior is applied.
	*/
	_listenToContentInsertion() {
		const { editor } = this;
		const clipboardPipeline = editor.plugins.get(ClipboardPipeline);
		const tableSelection = editor.plugins.get(TableSelection);
		let isPaste = false;
		clipboardPipeline.on("contentInsertion", (evt, data) => {
			isPaste = data.method === "paste";
		});
		this.listenTo(editor.model, "insertContent", (evt, [content, selectable]) => {
			if (isPaste || tableSelection.getSelectedTableCells() !== null) this._onInsertContent(evt, content, selectable);
		}, { priority: "high" });
		clipboardPipeline.on("contentInsertion", () => {
			isPaste = false;
		}, { priority: "lowest" });
	}
	/**
	* Copies table content to a clipboard on "copy" & "cut" events.
	*
	* @param evt An object containing information about the handled event.
	* @param data Clipboard event data.
	*/
	_onCopyCut(evt, data) {
		const view = this.editor.editing.view;
		const tableSelection = this.editor.plugins.get(TableSelection);
		const clipboardMarkersUtils = this.editor.plugins.get(ClipboardMarkersUtils);
		if (!tableSelection.getSelectedTableCells()) return;
		if (evt.name == "cut" && !this.editor.model.canEditAt(this.editor.model.document.selection)) return;
		data.preventDefault();
		evt.stop();
		this.editor.model.enqueueChange({ isUndoable: evt.name === "cut" }, () => {
			const documentFragment = clipboardMarkersUtils._copySelectedFragmentWithMarkers(evt.name, this.editor.model.document.selection, () => tableSelection.getSelectionAsFragment());
			view.document.fire("clipboardOutput", {
				dataTransfer: data.dataTransfer,
				content: this.editor.data.toView(documentFragment),
				method: evt.name
			});
		});
	}
	/**
	* Overrides default {@link module:engine/model/model~Model#insertContent `model.insertContent()`} method to handle pasting table inside
	* selected table fragment.
	*
	* Depending on selected table fragment:
	* - If a selected table fragment is smaller than paste table it will crop pasted table to match dimensions.
	* - If dimensions are equal it will replace selected table fragment with a pasted table contents.
	*
	* @param content The content to insert.
	* @param selectable The selection into which the content should be inserted.
	* If not provided the current model document selection will be used.
	*/
	_onInsertContent(evt, content, selectable) {
		if (selectable && !selectable.is("documentSelection")) return;
		const model = this.editor.model;
		const tableUtils = this.editor.plugins.get(TableUtils);
		const clipboardMarkersUtils = this.editor.plugins.get(ClipboardMarkersUtils);
		const pastedTable = this.getTableIfOnlyTableInContent(content, model);
		if (!pastedTable) return;
		const selectedTableCells = tableUtils.getSelectionAffectedTableCells(model.document.selection);
		if (!selectedTableCells.length) {
			removeEmptyRowsColumns(pastedTable, tableUtils);
			return;
		}
		evt.stop();
		if (content.is("documentFragment")) clipboardMarkersUtils._pasteMarkersIntoTransformedElement(content.markers, (writer) => this._replaceSelectedCells(pastedTable, selectedTableCells, writer));
		else this.editor.model.change((writer) => {
			this._replaceSelectedCells(pastedTable, selectedTableCells, writer);
		});
	}
	/**
	* Inserts provided `selectedTableCells` into `pastedTable`.
	*/
	_replaceSelectedCells(pastedTable, selectedTableCells, writer) {
		const tableUtils = this.editor.plugins.get(TableUtils);
		const pastedDimensions = {
			width: tableUtils.getColumns(pastedTable),
			height: tableUtils.getRows(pastedTable)
		};
		const selection = prepareTableForPasting(selectedTableCells, pastedDimensions, writer, tableUtils);
		const selectionHeight = selection.lastRow - selection.firstRow + 1;
		const selectionWidth = selection.lastColumn - selection.firstColumn + 1;
		const cropDimensions = {
			startRow: 0,
			startColumn: 0,
			endRow: Math.min(selectionHeight, pastedDimensions.height) - 1,
			endColumn: Math.min(selectionWidth, pastedDimensions.width) - 1
		};
		pastedTable = cropTableToDimensions(pastedTable, cropDimensions, writer);
		const selectedTable = selectedTableCells[0].findAncestor("table");
		const cellsToSelect = this._replaceSelectedCellsWithPasted(pastedTable, pastedDimensions, selectedTable, selection, writer, tableUtils);
		if (this.editor.plugins.get("TableSelection").isEnabled) {
			const selectionRanges = tableUtils.sortRanges(cellsToSelect.map((cell) => writer.createRangeOn(cell)));
			writer.setSelection(selectionRanges);
		} else writer.setSelection(cellsToSelect[0], 0);
		return selectedTable;
	}
	/**
	* Replaces the part of selectedTable with pastedTable.
	*/
	_replaceSelectedCellsWithPasted(pastedTable, pastedDimensions, selectedTable, selection, writer, tableUtils) {
		const { width: pastedWidth, height: pastedHeight } = pastedDimensions;
		const pastedTableLocationMap = createLocationMap(pastedTable, pastedWidth, pastedHeight);
		const selectedTableMap = [...new TableWalker(selectedTable, {
			startRow: selection.firstRow,
			endRow: selection.lastRow,
			startColumn: selection.firstColumn,
			endColumn: selection.lastColumn,
			includeAllSlots: true
		})];
		const cellsToSelect = [];
		let insertPosition;
		for (const tableSlot of selectedTableMap) {
			const { row, column } = tableSlot;
			if (column === selection.firstColumn) insertPosition = tableSlot.getPositionBefore();
			const pastedRow = row - selection.firstRow;
			const pastedColumn = column - selection.firstColumn;
			const pastedCell = pastedTableLocationMap[pastedRow % pastedHeight][pastedColumn % pastedWidth];
			const cellToInsert = pastedCell ? writer.cloneElement(pastedCell) : null;
			const newTableCell = this._replaceTableSlotCell(tableSlot, cellToInsert, insertPosition, writer);
			if (!newTableCell) continue;
			trimTableCellIfNeeded(newTableCell, row, column, selection.lastRow, selection.lastColumn, writer);
			cellsToSelect.push(newTableCell);
			insertPosition = writer.createPositionAfter(newTableCell);
		}
		const headingRows = parseInt(selectedTable.getAttribute("headingRows") || "0");
		const headingColumns = parseInt(selectedTable.getAttribute("headingColumns") || "0");
		const footerRows = parseInt(selectedTable.getAttribute("footerRows") || "0");
		const footerIndex = tableUtils.getRows(selectedTable) - footerRows;
		const areHeadingRowsIntersectingSelection = selection.firstRow < headingRows && headingRows <= selection.lastRow;
		const areHeadingColumnsIntersectingSelection = selection.firstColumn < headingColumns && headingColumns <= selection.lastColumn;
		const areFooterRowsIntersectingSelection = selection.firstRow < footerIndex && footerIndex <= selection.lastRow;
		if (areHeadingRowsIntersectingSelection) {
			const newCells = doHorizontalSplit(selectedTable, headingRows, {
				first: selection.firstColumn,
				last: selection.lastColumn
			}, writer, selection.firstRow);
			cellsToSelect.push(...newCells);
		}
		if (areHeadingColumnsIntersectingSelection) {
			const newCells = doVerticalSplit(selectedTable, headingColumns, {
				first: selection.firstRow,
				last: selection.lastRow
			}, writer);
			cellsToSelect.push(...newCells);
		}
		if (areFooterRowsIntersectingSelection) {
			const newCells = doHorizontalSplit(selectedTable, footerIndex, {
				first: selection.firstColumn,
				last: selection.lastColumn
			}, writer, selection.firstRow);
			cellsToSelect.push(...newCells);
		}
		return cellsToSelect;
	}
	/**
	* Replaces a single table slot.
	*
	* @returns Inserted table cell or null if slot should remain empty.
	* @private
	*/
	_replaceTableSlotCell(tableSlot, cellToInsert, insertPosition, writer) {
		const { cell, isAnchor } = tableSlot;
		if (isAnchor) writer.remove(cell);
		if (!cellToInsert) return null;
		writer.insert(cellToInsert, insertPosition);
		return cellToInsert;
	}
	/**
	* Extracts the table for pasting into a table.
	*
	* @param content The content to insert.
	* @param model The editor model.
	*/
	getTableIfOnlyTableInContent(content, model) {
		if (!content.is("documentFragment") && !content.is("element")) return null;
		if (content.is("element", "table")) return content;
		if (content.childCount == 1 && content.getChild(0).is("element", "table")) return content.getChild(0);
		const contentRange = model.createRangeIn(content);
		for (const element of contentRange.getItems()) if (element.is("element", "table")) {
			const rangeBefore = model.createRange(contentRange.start, model.createPositionBefore(element));
			if (model.hasContent(rangeBefore, { ignoreWhitespaces: true })) return null;
			const rangeAfter = model.createRange(model.createPositionAfter(element), contentRange.end);
			if (model.hasContent(rangeAfter, { ignoreWhitespaces: true })) return null;
			return element;
		}
		return null;
	}
};
/**
* Prepares a table for pasting and returns adjusted selection dimensions.
*/
function prepareTableForPasting(selectedTableCells, pastedDimensions, writer, tableUtils) {
	const selectedTable = selectedTableCells[0].findAncestor("table");
	const columnIndexes = tableUtils.getColumnIndexes(selectedTableCells);
	const rowIndexes = tableUtils.getRowIndexes(selectedTableCells);
	const selection = {
		firstColumn: columnIndexes.first,
		lastColumn: columnIndexes.last,
		firstRow: rowIndexes.first,
		lastRow: rowIndexes.last
	};
	const shouldExpandSelection = selectedTableCells.length === 1;
	if (shouldExpandSelection) {
		selection.lastRow += pastedDimensions.height - 1;
		selection.lastColumn += pastedDimensions.width - 1;
		expandTableSize(selectedTable, selection.lastRow + 1, selection.lastColumn + 1, tableUtils);
	}
	if (shouldExpandSelection || !tableUtils.isSelectionRectangular(selectedTableCells)) splitCellsToRectangularSelection(selectedTable, selection, writer);
	else {
		selection.lastRow = adjustLastRowIndex(selectedTable, selection);
		selection.lastColumn = adjustLastColumnIndex(selectedTable, selection);
	}
	return selection;
}
/**
* Expand table (in place) to expected size.
*/
function expandTableSize(table, expectedHeight, expectedWidth, tableUtils) {
	const tableWidth = tableUtils.getColumns(table);
	const tableHeight = tableUtils.getRows(table);
	if (expectedWidth > tableWidth) tableUtils.insertColumns(table, {
		at: tableWidth,
		columns: expectedWidth - tableWidth
	});
	if (expectedHeight > tableHeight) tableUtils.insertRows(table, {
		at: tableHeight,
		rows: expectedHeight - tableHeight
	});
}
/**
* Returns two-dimensional array that is addressed by [ row ][ column ] that stores cells anchored at given location.
*
* At given row & column location it might be one of:
*
* * cell - cell from pasted table anchored at this location.
* * null - if no cell is anchored at this location.
*
* For instance, from a table below:
*
*   +----+----+----+----+
*   | 00 | 01 | 02 | 03 |
*   +    +----+----+----+
*   |    | 11      | 13 |
*   +----+         +----+
*   | 20 |         | 23 |
*   +----+----+----+----+
*
* The method will return an array (numbers represents cell element):
*
* ```ts
* const map = [
*   [ '00', '01', '02', '03' ],
*   [ null, '11', null, '13' ],
*   [ '20', null, null, '23' ]
* ]
* ```
*
* This allows for a quick access to table at give row & column. For instance to access table cell "13" from pasted table call:
*
* ```ts
* const cell = map[ 1 ][ 3 ]
* ```
*/
function createLocationMap(table, width, height) {
	const map = new Array(height).fill(null).map(() => new Array(width).fill(null));
	for (const { column, row, cell } of new TableWalker(table)) map[row][column] = cell;
	return map;
}
/**
* Make selected cells rectangular by splitting the cells that stand out from a rectangular selection.
*
* In the table below a selection is shown with "::" and slots with anchor cells are named.
*
* +----+----+----+----+----+                    +----+----+----+----+----+
* | 00 | 01 | 02 | 03      |                    | 00 | 01 | 02 | 03      |
* +    +----+    +----+----+                    |    ::::::::::::::::----+
* |    | 11 |    | 13 | 14 |                    |    ::11 |    | 13:: 14 |    <- first row
* +----+----+    +    +----+                    +----::---|    |   ::----+
* | 20 | 21 |    |    | 24 |   select cells:    | 20 ::21 |    |   :: 24 |
* +----+----+    +----+----+     11 -> 33       +----::---|    |---::----+
* | 30      |    | 33 | 34 |                    | 30 ::   |    | 33:: 34 |    <- last row
* +         +    +----+    +                    |    ::::::::::::::::    +
* |         |    | 43 |    |                    |         |    | 43 |    |
* +----+----+----+----+----+                    +----+----+----+----+----+
*                                                      ^          ^
*                                                     first & last columns
*
* Will update table to:
*
*                       +----+----+----+----+----+
*                       | 00 | 01 | 02 | 03      |
*                       +    +----+----+----+----+
*                       |    | 11 |    | 13 | 14 |
*                       +----+----+    +    +----+
*                       | 20 | 21 |    |    | 24 |
*                       +----+----+    +----+----+
*                       | 30 |    |    | 33 | 34 |
*                       +    +----+----+----+    +
*                       |    |    |    | 43 |    |
*                       +----+----+----+----+----+
*
* In th example above:
* - Cell "02" which have `rowspan = 4` must be trimmed at first and at after last row.
* - Cell "03" which have `rowspan = 2` and `colspan = 2` must be trimmed at first column and after last row.
* - Cells "00", "03" & "30" which cannot be cut by this algorithm as they are outside the trimmed area.
* - Cell "13" cannot be cut as it is inside the trimmed area.
*/
function splitCellsToRectangularSelection(table, dimensions, writer) {
	const { firstRow, lastRow, firstColumn, lastColumn } = dimensions;
	const rowIndexes = {
		first: firstRow,
		last: lastRow
	};
	const columnIndexes = {
		first: firstColumn,
		last: lastColumn
	};
	doVerticalSplit(table, firstColumn, rowIndexes, writer);
	doVerticalSplit(table, lastColumn + 1, rowIndexes, writer);
	doHorizontalSplit(table, firstRow, columnIndexes, writer);
	doHorizontalSplit(table, lastRow + 1, columnIndexes, writer, firstRow);
}
function doHorizontalSplit(table, splitRow, limitColumns, writer, startRow = 0) {
	if (splitRow < 1) return;
	return getVerticallyOverlappingCells(table, splitRow, startRow).filter(({ column, cellWidth }) => isAffectedBySelection(column, cellWidth, limitColumns)).map(({ cell }) => splitHorizontally(cell, splitRow, writer));
}
function doVerticalSplit(table, splitColumn, limitRows, writer) {
	if (splitColumn < 1) return;
	return getHorizontallyOverlappingCells(table, splitColumn).filter(({ row, cellHeight }) => isAffectedBySelection(row, cellHeight, limitRows)).map(({ cell, column }) => splitVertically(cell, column, splitColumn, writer));
}
/**
* Checks if cell at given row (column) is affected by a rectangular selection defined by first/last column (row).
*
* The same check is used for row as for column.
*/
function isAffectedBySelection(index, span, limit) {
	const endIndex = index + span - 1;
	const { first, last } = limit;
	return index >= first && index <= last || index < first && endIndex >= first;
}

/**
* @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 table/tablekeyboard
*/
/**
* This plugin enables keyboard navigation for tables.
* It is loaded automatically by the {@link module:table/table~Table} plugin.
*/
var TableKeyboard = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "TableKeyboard";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [TableSelection, TableUtils];
	}
	/**
	* @inheritDoc
	*/
	init() {
		const editor = this.editor;
		const viewDocument = editor.editing.view.document;
		const t = editor.t;
		this.listenTo(viewDocument, "arrowKey", (...args) => this._onArrowKey(...args), { context: "table" });
		this.listenTo(viewDocument, "tab", (...args) => this._handleTabOnSelectedTable(...args), { context: "figure" });
		this.listenTo(viewDocument, "tab", (...args) => this._handleTab(...args), { context: ["th", "td"] });
		editor.accessibility.addKeystrokeInfoGroup({
			id: "table",
			label: t("Keystrokes that can be used in a table cell"),
			keystrokes: [
				{
					label: t("Move the selection to the next cell"),
					keystroke: "Tab"
				},
				{
					label: t("Move the selection to the previous cell"),
					keystroke: "Shift+Tab"
				},
				{
					label: t("Insert a new table row (when in the last cell of a table)"),
					keystroke: "Tab"
				},
				{
					label: t("Navigate through the table"),
					keystroke: [
						["arrowup"],
						["arrowright"],
						["arrowdown"],
						["arrowleft"]
					]
				}
			]
		});
	}
	/**
	* Handles {@link module:engine/view/document~ViewDocument#event:tab tab} events for the <kbd>Tab</kbd> key executed
	* when the table widget is selected.
	*/
	_handleTabOnSelectedTable(bubblingEventInfo, domEventData) {
		const selectedElement = this.editor.model.document.selection.getSelectedElement();
		if (!selectedElement || !selectedElement.is("element", "table")) return;
		domEventData.stopPropagation();
	}
	/**
	* Handles {@link module:engine/view/document~ViewDocument#event:tab tab} events for the <kbd>Tab</kbd> key executed
	* inside table cells.
	*/
	_handleTab(bubblingEventInfo, domEventData) {
		const editor = this.editor;
		const tableUtils = this.editor.plugins.get(TableUtils);
		const tableSelection = this.editor.plugins.get("TableSelection");
		const selection = editor.model.document.selection;
		const isForward = !domEventData.shiftKey;
		let tableCell = tableUtils.getTableCellsContainingSelection(selection)[0];
		if (!tableCell) tableCell = tableSelection.getFocusCell();
		if (!tableCell) return;
		domEventData.stopPropagation();
		const tableRow = tableCell.parent;
		const table = tableRow.parent;
		const currentRowIndex = table.getChildIndex(tableRow);
		const isLastCellInRow = tableRow.getChildIndex(tableCell) === tableRow.childCount - 1;
		const isLastRow = currentRowIndex === tableUtils.getRows(table) - 1;
		if (isForward && isLastRow && isLastCellInRow) editor.execute("insertTableRowBelow");
	}
	/**
	* Handles {@link module:engine/view/document~ViewDocument#event:keydown keydown} events.
	*/
	_onArrowKey(eventInfo, domEventData) {
		const editor = this.editor;
		const keyCode = domEventData.keyCode;
		const direction = getLocalizedArrowKeyCodeDirection(keyCode, editor.locale.contentLanguageDirection);
		if (this._handleArrowKeys(direction, domEventData.shiftKey)) {
			domEventData.preventDefault();
			domEventData.stopPropagation();
			eventInfo.stop();
		}
	}
	/**
	* Handles arrow keys to move the selection around the table.
	*
	* @param direction The direction of the arrow key.
	* @param expandSelection If the current selection should be expanded.
	* @returns Returns `true` if key was handled.
	*/
	_handleArrowKeys(direction, expandSelection) {
		const tableUtils = this.editor.plugins.get(TableUtils);
		const tableSelection = this.editor.plugins.get("TableSelection");
		const model = this.editor.model;
		const selection = model.document.selection;
		const isForward = ["right", "down"].includes(direction);
		const selectedCells = tableUtils.getSelectedTableCells(selection);
		if (selectedCells.length) {
			let focusCell;
			if (expandSelection) focusCell = tableSelection.getFocusCell();
			else focusCell = isForward ? selectedCells[selectedCells.length - 1] : selectedCells[0];
			this._navigateFromCellInDirection(focusCell, direction, expandSelection);
			return true;
		}
		const tableCell = selection.focus.findAncestor("tableCell");
		/* istanbul ignore if: paranoid check -- @preserve */
		if (!tableCell) return false;
		if (!selection.isCollapsed) if (expandSelection) {
			if (selection.isBackward == isForward && !selection.containsEntireContent(tableCell)) return false;
		} else {
			const selectedElement = selection.getSelectedElement();
			if (!selectedElement || !model.schema.isObject(selectedElement)) return false;
		}
		if (this._isSelectionAtCellEdge(selection, tableCell, isForward)) {
			this._navigateFromCellInDirection(tableCell, direction, expandSelection);
			return true;
		}
		return false;
	}
	/**
	* Returns `true` if the selection is at the boundary of a table cell according to the navigation direction.
	*
	* @param selection The current selection.
	* @param tableCell The current table cell element.
	* @param isForward The expected navigation direction.
	*/
	_isSelectionAtCellEdge(selection, tableCell, isForward) {
		const model = this.editor.model;
		const schema = this.editor.model.schema;
		const focus = isForward ? selection.getLastPosition() : selection.getFirstPosition();
		if (!schema.getLimitElement(focus).is("element", "tableCell")) return model.createPositionAt(tableCell, isForward ? "end" : 0).isTouching(focus);
		const probe = model.createSelection(focus);
		model.modifySelection(probe, { direction: isForward ? "forward" : "backward" });
		return focus.isEqual(probe.focus);
	}
	/**
	* Moves the selection from the given table cell in the specified direction.
	*
	* @param focusCell The table cell that is current multi-cell selection focus.
	* @param direction Direction in which selection should move.
	* @param expandSelection If the current selection should be expanded. Default value is false.
	*/
	_navigateFromCellInDirection(focusCell, direction, expandSelection = false) {
		const model = this.editor.model;
		const table = focusCell.findAncestor("table");
		const tableMap = [...new TableWalker(table, { includeAllSlots: true })];
		const { row: lastRow, column: lastColumn } = tableMap[tableMap.length - 1];
		const currentCellInfo = tableMap.find(({ cell }) => cell == focusCell);
		let { row, column } = currentCellInfo;
		switch (direction) {
			case "left":
				column--;
				break;
			case "up":
				row--;
				break;
			case "right":
				column += currentCellInfo.cellWidth;
				break;
			case "down":
				row += currentCellInfo.cellHeight;
				break;
		}
		if (row < 0 || row > lastRow || column < 0 && row <= 0 || column > lastColumn && row >= lastRow) {
			model.change((writer) => {
				writer.setSelection(writer.createRangeOn(table));
			});
			return;
		}
		if (column < 0) {
			column = expandSelection ? 0 : lastColumn;
			row--;
		} else if (column > lastColumn) {
			column = expandSelection ? lastColumn : 0;
			row++;
		}
		const cellToSelect = tableMap.find((cellInfo) => cellInfo.row == row && cellInfo.column == column).cell;
		const isForward = ["right", "down"].includes(direction);
		const tableSelection = this.editor.plugins.get("TableSelection");
		if (expandSelection && tableSelection.isEnabled) {
			const anchorCell = tableSelection.getAnchorCell() || focusCell;
			tableSelection.setCellSelection(anchorCell, cellToSelect);
		} else {
			const positionToSelect = model.createPositionAt(cellToSelect, isForward ? 0 : "end");
			model.change((writer) => {
				writer.setSelection(positionToSelect);
			});
		}
	}
};

/**
* @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 table/tablemouse/mouseeventsobserver
*/
/**
* The mouse selection event observer.
*
* It registers listeners for the following DOM events:
*
* - `'mousemove'`
* - `'mouseleave'`
*
* Note that this observer is disabled by default. To enable this observer, it needs to be added to
* {@link module:engine/view/view~EditingView} using the {@link module:engine/view/view~EditingView#addObserver} method.
*
* The observer is registered by the {@link module:table/tableselection~TableSelection} plugin.
*
* @internal
*/
var MouseEventsObserver = class extends DomEventObserver {
	domEventType = ["mousemove", "mouseleave"];
	/**
	* @inheritDoc
	*/
	onDomEvent(domEvent) {
		this.fire(domEvent.type, 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 table/tablemouse
*/
/**
* This plugin enables a table cells' selection with the mouse.
* It is loaded automatically by the {@link module:table/table~Table} plugin.
*/
var TableMouse = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "TableMouse";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [TableSelection, TableUtils];
	}
	/**
	* @inheritDoc
	*/
	init() {
		this.editor.editing.view.addObserver(MouseEventsObserver);
		this._enableShiftClickSelection();
		this._enableMouseDragSelection();
	}
	/**
	* Enables making cells selection by <kbd>Shift</kbd>+click. Creates a selection from the cell which previously held
	* the selection to the cell which was clicked. It can be the same cell, in which case it selects a single cell.
	*/
	_enableShiftClickSelection() {
		const editor = this.editor;
		const tableUtils = editor.plugins.get(TableUtils);
		let blockSelectionChange = false;
		const tableSelection = editor.plugins.get(TableSelection);
		this.listenTo(editor.editing.view.document, "mousedown", (evt, domEventData) => {
			const selection = editor.model.document.selection;
			if (!this.isEnabled || !tableSelection.isEnabled) return;
			if (!domEventData.domEvent.shiftKey) return;
			const anchorCell = tableSelection.getAnchorCell() || tableUtils.getTableCellsContainingSelection(selection)[0];
			if (!anchorCell) return;
			const targetCell = this._getModelTableCellFromDomEvent(domEventData);
			if (targetCell && haveSameTableParent(anchorCell, targetCell)) {
				blockSelectionChange = true;
				tableSelection.setCellSelection(anchorCell, targetCell);
				domEventData.preventDefault();
			}
		});
		this.listenTo(editor.editing.view.document, "mouseup", () => {
			blockSelectionChange = false;
		});
		this.listenTo(editor.editing.view.document, "selectionChange", (evt) => {
			if (blockSelectionChange) evt.stop();
		}, { priority: "highest" });
	}
	/**
	* Enables making cells selection by dragging.
	*
	* The selection is made only on mousemove. Mouse tracking is started on mousedown.
	* However, the cells selection is enabled only after the mouse cursor left the anchor cell.
	* Thanks to that normal text selection within one cell works just fine. However, you can still select
	* just one cell by leaving the anchor cell and moving back to it.
	*/
	_enableMouseDragSelection() {
		const editor = this.editor;
		let anchorCell, targetCell;
		let beganCellSelection = false;
		let blockSelectionChange = false;
		const tableSelection = editor.plugins.get(TableSelection);
		this.listenTo(editor.editing.view.document, "mousedown", (evt, domEventData) => {
			if (!this.isEnabled || !tableSelection.isEnabled) return;
			if (domEventData.domEvent.shiftKey || domEventData.domEvent.ctrlKey || domEventData.domEvent.altKey) return;
			anchorCell = this._getModelTableCellFromDomEvent(domEventData);
		});
		this.listenTo(editor.editing.view.document, "mousemove", (evt, domEventData) => {
			if (!domEventData.domEvent.buttons) return;
			if (!anchorCell) return;
			const newTargetCell = this._getModelTableCellFromDomEvent(domEventData);
			if (newTargetCell && haveSameTableParent(anchorCell, newTargetCell)) {
				targetCell = newTargetCell;
				if (!beganCellSelection && targetCell != anchorCell) beganCellSelection = true;
			}
			if (!beganCellSelection) return;
			blockSelectionChange = true;
			tableSelection.setCellSelection(anchorCell, targetCell);
			domEventData.preventDefault();
		});
		this.listenTo(editor.editing.view.document, "mouseup", () => {
			beganCellSelection = false;
			blockSelectionChange = false;
			anchorCell = null;
			targetCell = null;
		});
		this.listenTo(editor.editing.view.document, "selectionChange", (evt) => {
			if (blockSelectionChange) evt.stop();
		}, { priority: "highest" });
	}
	/**
	* Returns the model table cell element based on the target element of the passed DOM event.
	*
	* @returns Returns the table cell or `undefined`.
	*/
	_getModelTableCellFromDomEvent(domEventData) {
		const viewTargetElement = domEventData.target;
		const viewPosition = this.editor.editing.view.createPositionAt(viewTargetElement, 0);
		return this.editor.editing.mapper.toModelPosition(viewPosition).parent.findAncestor("tableCell", { includeSelf: true });
	}
};
function haveSameTableParent(cellA, cellB) {
	return cellA.parent.parent == cellB.parent.parent;
}

/**
* @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 table/table
*/
/**
* The table plugin.
*
* For a detailed overview, check the {@glink features/tables/tables Table feature documentation}.
*
* This is a "glue" plugin that loads the following table features:
*
* * {@link module:table/tableediting~TableEditing editing feature},
* * {@link module:table/tableselection~TableSelection selection feature},
* * {@link module:table/tablekeyboard~TableKeyboard keyboard navigation feature},
* * {@link module:table/tablemouse~TableMouse mouse selection feature},
* * {@link module:table/tableclipboard~TableClipboard clipboard feature},
* * {@link module:table/tableui~TableUI UI feature}.
*/
var Table = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [
			TableEditing,
			TableUI,
			TableSelection,
			TableMouse,
			TableKeyboard,
			TableClipboard,
			Widget
		];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "Table";
	}
	/**
	* @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 table/plaintableoutput
*/
/**
* The plain table output feature.
*
* This feature strips the `<figure>` tag from the table data. This is because this tag is not supported
* by most popular email clients and removing it ensures compatibility.
*/
var PlainTableOutput = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "PlainTableOutput";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [Table];
	}
	/**
	* @inheritDoc
	*/
	init() {
		this.editor.conversion.for("upcast").add((dispatcher) => {
			dispatcher.on("element:table", (evt, data, conversionApi) => {
				conversionApi.consumable.consume(data.viewItem, { classes: "table" });
			});
		});
	}
};

/**
* @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
*/
/**
* Depending on the position of the selection either return the selected table or the table higher in the hierarchy.
*
* @internal
*/
function getSelectionAffectedTableWidget(selection) {
	const selectedTable = getSelectedTableWidget(selection);
	if (selectedTable) return selectedTable;
	return getTableWidgetAncestor(selection);
}
/**
* Returns a table widget editing view element if one is selected.
*
* @internal
*/
function getSelectedTableWidget(selection) {
	const viewElement = selection.getSelectedElement();
	if (viewElement && isTableWidget(viewElement)) return viewElement;
	return null;
}
/**
* Returns a table widget editing view element if one is among the selection's ancestors.
*
* @internal
*/
function getTableWidgetAncestor(selection) {
	const selectionPosition = selection.getFirstPosition();
	if (!selectionPosition) return null;
	let parent = selectionPosition.parent;
	while (parent) {
		if (parent.is("element") && isTableWidget(parent)) return parent;
		parent = parent.parent;
	}
	return null;
}
/**
* Checks if a given view element is a table widget.
*
* @internal
*/
function isTableWidget(viewNode) {
	return viewNode.is("element") && !!viewNode.getCustomProperty("table") && isWidget(viewNode);
}

/**
* @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 table/tabletoolbar
*/
/**
* The table toolbar class. It creates toolbars for the table feature and its content (for now only for the table cell content).
*
* The table toolbar shows up when a table widget is selected. Its components (e.g. buttons) are created based on the
* {@link module:table/tableconfig~TableConfig#tableToolbar `table.tableToolbar` configuration option}.
*
* Table content toolbar shows up when the selection is inside the content of a table. It creates its component based on the
* {@link module:table/tableconfig~TableConfig#contentToolbar `table.contentToolbar` configuration option}.
*/
var TableToolbar = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [WidgetToolbarRepository];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "TableToolbar";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	afterInit() {
		const editor = this.editor;
		const t = editor.t;
		const widgetToolbarRepository = editor.plugins.get(WidgetToolbarRepository);
		const tableContentToolbarItems = editor.config.get("table.contentToolbar");
		const tableToolbarItems = editor.config.get("table.tableToolbar");
		if (tableContentToolbarItems) widgetToolbarRepository.register("tableContent", {
			ariaLabel: t("Table toolbar"),
			items: tableContentToolbarItems,
			getRelatedElement: getTableWidgetAncestor
		});
		if (tableToolbarItems) widgetToolbarRepository.register("table", {
			ariaLabel: t("Table toolbar"),
			items: tableToolbarItems,
			getRelatedElement: getSelectedTableWidget
		});
	}
};

/**
* @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 table/ui/colorinputview
*/
/**
* The color input view class. It allows the user to type in a color (hex, rgb, etc.)
* or choose it from the configurable color palette with a preview.
*
* @internal
*/
var ColorInputView = class extends View {
	/**
	* A cached reference to the options passed to the constructor.
	*/
	options;
	/**
	* Tracks information about the DOM focus in the view.
	*/
	focusTracker;
	/**
	* Helps cycling over focusable children in the input view.
	*/
	focusCycler;
	/**
	* A collection of views that can be focused in the view.
	*/
	_focusables;
	/**
	* An instance of the dropdown allowing to select a color from a grid.
	*/
	dropdownView;
	/**
	* An instance of the input allowing the user to type a color value.
	*/
	inputView;
	/**
	* An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
	*/
	keystrokes;
	/**
	* The flag that indicates whether the user is still typing.
	* If set to true, it means that the text input field ({@link #inputView}) still has the focus.
	* So, we should interrupt the user by replacing the input's value.
	*/
	_stillTyping;
	/**
	* Creates an instance of the color input view.
	*
	* @param locale The locale instance.
	* @param options The input options.
	* @param options.colorDefinitions The colors to be displayed in the palette inside the input's dropdown.
	* @param options.columns The number of columns in which the colors will be displayed.
	* @param options.defaultColorValue If specified, the color input view will replace the "Remove color" button with
	* the "Restore default" button. Instead of clearing the input field, the default color value will be set.
	*/
	constructor(locale, options) {
		super(locale);
		this.set("value", "");
		this.set("isReadOnly", false);
		this.set("isFocused", false);
		this.set("isEmpty", true);
		this.options = options;
		this.focusTracker = new FocusTracker();
		this._focusables = new ViewCollection();
		this.dropdownView = this._createDropdownView();
		this.inputView = this._createInputTextView();
		this.keystrokes = new KeystrokeHandler();
		this._stillTyping = false;
		this.focusCycler = new FocusCycler({
			focusables: this._focusables,
			focusTracker: this.focusTracker,
			keystrokeHandler: this.keystrokes,
			actions: {
				focusPrevious: "shift + tab",
				focusNext: "tab"
			}
		});
		this.setTemplate({
			tag: "div",
			attributes: { class: ["ck", "ck-input-color"] },
			children: [this.dropdownView, this.inputView]
		});
		this.on("change:value", (evt, name, inputValue) => this._setInputValue(inputValue));
	}
	/**
	* @inheritDoc
	*/
	render() {
		super.render();
		[this.inputView, this.dropdownView.buttonView].forEach((view) => {
			this.focusTracker.add(view.element);
			this._focusables.add(view);
		});
		this.keystrokes.listenTo(this.element);
	}
	/**
	* Focuses the view.
	*/
	focus(direction) {
		if (direction === -1) this.focusCycler.focusLast();
		else this.focusCycler.focusFirst();
	}
	/**
	* @inheritDoc
	*/
	destroy() {
		super.destroy();
		this.focusTracker.destroy();
		this.keystrokes.destroy();
	}
	/**
	* Creates and configures the {@link #dropdownView}.
	*/
	_createDropdownView() {
		const locale = this.locale;
		const t = locale.t;
		const bind = this.bindTemplate;
		const colorSelector = this._createColorSelector(locale);
		const dropdown = createDropdown(locale);
		const colorPreview = new View();
		colorPreview.setTemplate({
			tag: "span",
			attributes: {
				class: ["ck", "ck-input-color__button__preview"],
				style: { backgroundColor: bind.to("value") }
			},
			children: [{
				tag: "span",
				attributes: { class: [
					"ck",
					"ck-input-color__button__preview__no-color-indicator",
					bind.if("value", "ck-hidden", (value) => value != "")
				] }
			}]
		});
		dropdown.buttonView.extendTemplate({ attributes: { class: "ck-input-color__button" } });
		dropdown.buttonView.children.add(colorPreview);
		dropdown.buttonView.label = t("Color picker");
		dropdown.buttonView.tooltip = true;
		dropdown.panelPosition = locale.uiLanguageDirection === "rtl" ? "se" : "sw";
		dropdown.panelView.children.add(colorSelector);
		dropdown.bind("isEnabled").to(this, "isReadOnly", (value) => !value);
		dropdown.on("change:isOpen", (evt, name, isVisible) => {
			if (isVisible) {
				colorSelector.updateSelectedColors();
				colorSelector.showColorGridsFragment();
			}
		});
		return dropdown;
	}
	/**
	* Creates and configures an instance of {@link module:ui/inputtext/inputtextview~InputTextView}.
	*
	* @returns A configured instance to be set as {@link #inputView}.
	*/
	_createInputTextView() {
		const locale = this.locale;
		const inputView = new InputTextView(locale);
		inputView.extendTemplate({ on: { blur: inputView.bindTemplate.to("blur") } });
		inputView.value = this.value;
		inputView.bind("isReadOnly", "hasError").to(this);
		this.bind("isFocused", "isEmpty").to(inputView);
		inputView.on("input", () => {
			const inputValue = inputView.element.value;
			const mappedColor = this.options.colorDefinitions.find((def) => inputValue === def.label);
			this._stillTyping = true;
			this.value = mappedColor && mappedColor.color || inputValue;
		});
		inputView.on("blur", () => {
			this._stillTyping = false;
			this._setInputValue(inputView.element.value);
		});
		inputView.delegate("input").to(this);
		return inputView;
	}
	/**
	* Creates and configures the panel with "color grid" and "color picker" inside the {@link #dropdownView}.
	*/
	_createColorSelector(locale) {
		const t = locale.t;
		const defaultColor = this.options.defaultColorValue || "";
		const removeColorButtonLabel = defaultColor ? t("Restore default") : t("Remove color");
		const colorSelector = new ColorSelectorView(locale, {
			colors: this.options.colorDefinitions,
			columns: this.options.columns,
			removeButtonLabel: removeColorButtonLabel,
			colorPickerLabel: t("Color picker"),
			colorPickerViewConfig: this.options.colorPickerConfig === false ? false : {
				...this.options.colorPickerConfig,
				hideInput: true
			}
		});
		colorSelector.appendUI();
		colorSelector.on("execute", (evt, data) => {
			if (data.source === "colorPickerSaveButton") {
				this.dropdownView.isOpen = false;
				return;
			}
			this.value = data.value || defaultColor;
			this.fire("input");
			if (data.source !== "colorPicker") this.dropdownView.isOpen = false;
		});
		/**
		* Color is saved before changes in color picker. In case "cancel button" is pressed
		* this color will be applied.
		*/
		let backupColor = this.value;
		colorSelector.on("colorPicker:cancel", () => {
			/**
			* Revert color to previous value before changes in color picker.
			*/
			this.value = backupColor;
			this.fire("input");
			this.dropdownView.isOpen = false;
		});
		colorSelector.colorGridsFragmentView.colorPickerButtonView.on("execute", () => {
			/**
			* Save color value before changes in color picker.
			*/
			backupColor = this.value;
		});
		colorSelector.bind("selectedColor").to(this, "value");
		return colorSelector;
	}
	/**
	* Sets {@link #inputView}'s value property to the color value or color label,
	* if there is one and the user is not typing.
	*
	* Handles cases like:
	*
	* * Someone picks the color in the grid.
	* * The color is set from the plugin level.
	*
	* @param inputValue Color value to be set.
	*/
	_setInputValue(inputValue) {
		if (!this._stillTyping) {
			const normalizedInputValue = normalizeColor(inputValue);
			const mappedColor = this.options.colorDefinitions.find((def) => normalizedInputValue === normalizeColor(def.color));
			if (mappedColor) this.inputView.value = mappedColor.label;
			else this.inputView.value = inputValue || "";
		}
	}
};
/**
* Normalizes color value, by stripping extensive whitespace.
* For example., transforms:
* * `   rgb(  25 50    0 )` to `rgb(25 50 0)`,
* * "\t  rgb(  25 ,  50,0 )		" to `rgb(25 50 0)`.
*
* @param colorString The value to be normalized.
*/
function normalizeColor(colorString) {
	return colorString.replace(/([(,])\s+/g, "$1").replace(/^\s+|\s+(?=[),\s]|$)/g, "").replace(/,|\s/g, " ");
}

/**
* @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 table/utils/ui/table-properties
*/
const isEmpty = (val) => val === "";
/**
* Returns an object containing pairs of CSS border style values and their localized UI
* labels. Used by {@link module:table/tablecellproperties/ui/tablecellpropertiesview~TableCellPropertiesView}
* and {@link module:table/tableproperties/ui/tablepropertiesview~TablePropertiesView}.
*
* @internal
* @param t The "t" function provided by the editor that is used to localize strings.
*/
function getBorderStyleLabels(t) {
	return {
		none: t("None"),
		solid: t("Solid"),
		dotted: t("Dotted"),
		dashed: t("Dashed"),
		double: t("Double"),
		groove: t("Groove"),
		ridge: t("Ridge"),
		inset: t("Inset"),
		outset: t("Outset")
	};
}
/**
* Returns a localized error string that can be displayed next to color (background, border)
* fields that have an invalid value.
*
* @internal
* @param t The "t" function provided by the editor that is used to localize strings.
*/
function getLocalizedColorErrorText(t) {
	return t("The color is invalid. Try \"#FF0000\" or \"rgb(255,0,0)\" or \"red\".");
}
/**
* Returns a localized error string that can be displayed next to length (padding, border width)
* fields that have an invalid value.
*
* @internal
* @param t The "t" function provided by the editor that is used to localize strings.
*/
function getLocalizedLengthErrorText(t) {
	return t("The value is invalid. Try \"10px\" or \"2em\" or simply \"2\".");
}
/**
* Returns `true` when the passed value is an empty string or a valid CSS color expression.
* Otherwise, `false` is returned.
*
* See {@link module:engine/view/styles/utils~isColorStyleValue}.
*
* @internal
*/
function colorFieldValidator(value) {
	value = value.trim().toLowerCase();
	return isEmpty(value) || isColorStyleValue(value);
}
/**
* Returns `true` when the passed value is an empty string, a number without a unit or a valid CSS length expression.
* Otherwise, `false` is returned.
*
* See {@link module:engine/view/styles/utils~isLengthStyleValue}.
* See {@link module:engine/view/styles/utils~isPercentageStyleValue}.
*
* @internal
*/
function lengthFieldValidator(value) {
	value = value.trim();
	return isEmpty(value) || isNumberString(value) || isLengthStyleValue(value) || isPercentageStyleValue(value);
}
/**
* Returns `true` when the passed value is an empty string, a number without a unit or a valid CSS length expression.
* Otherwise, `false` is returned.
*
* See {@link module:engine/view/styles/utils~isLengthStyleValue}.
*
* @internal
*/
function lineWidthFieldValidator(value) {
	value = value.trim();
	return isEmpty(value) || isNumberString(value) || isLengthStyleValue(value);
}
/**
* Generates item definitions for a UI dropdown that allows changing the border style of a table or a table cell.
*
* @internal
* @param defaultStyle The default border.
*/
function getBorderStyleDefinitions(view, defaultStyle) {
	const itemDefinitions = new Collection();
	const styleLabels = getBorderStyleLabels(view.t);
	for (const style in styleLabels) {
		const definition = {
			type: "button",
			model: new UIModel({
				_borderStyleValue: style,
				label: styleLabels[style],
				role: "menuitemradio",
				withText: true
			})
		};
		if (style === "none") definition.model.bind("isOn").to(view, "borderStyle", (value) => {
			if (defaultStyle === "none") return !value;
			return value === style;
		});
		else definition.model.bind("isOn").to(view, "borderStyle", (value) => {
			return value === style;
		});
		itemDefinitions.add(definition);
	}
	return itemDefinitions;
}
/**
* A helper that fills a toolbar with buttons that:
*
* * have some labels,
* * have some icons,
* * set a certain UI view property value upon execution.
*
* @internal
* @param options Configuration options
* @param options.view The view that has the observable property.
* @param options.icons Object with button icons.
* @param options.toolbar The toolbar to fill with buttons.
* @param options.labels Object with button labels.
* @param options.propertyName The name of the observable property in the view.
* @param options.nameToValue A function that maps a button name to a value. By default names are the same as values.
* @param options.defaultValue Default value for the property.
*/
function fillToolbar(options) {
	const { view, icons, toolbar, labels, propertyName, nameToValue, defaultValue } = options;
	for (const name in labels) {
		const button = new ButtonView(view.locale);
		button.set({
			role: "radio",
			isToggleable: true,
			label: labels[name],
			icon: icons[name],
			tooltip: labels[name]
		});
		const buttonValue = nameToValue ? nameToValue(name) : name;
		button.bind("isOn").to(view, propertyName, (value) => {
			let valueToCompare = value;
			if (value === "" && defaultValue) valueToCompare = defaultValue;
			return buttonValue === valueToCompare;
		});
		button.on("execute", () => {
			if (!defaultValue && buttonValue && view[propertyName] === buttonValue) view[propertyName] = void 0;
			else view[propertyName] = buttonValue;
		});
		toolbar.items.add(button);
	}
}
/**
* A default color palette used by various user interfaces related to tables, for instance,
* by {@link module:table/tablecellproperties/tablecellpropertiesui~TableCellPropertiesUI} or
* {@link module:table/tableproperties/tablepropertiesui~TablePropertiesUI}.
*
* The color palette follows the {@link module:table/tableconfig~TableColorConfig table color configuration format}
* and contains the following color definitions:
*
* ```ts
* const defaultColors = [
*   {
*     color: 'hsl(0, 0%, 0%)',
*     label: 'Black'
*   },
*   {
*     color: 'hsl(0, 0%, 30%)',
*     label: 'Dim grey'
*   },
*   {
*     color: 'hsl(0, 0%, 60%)',
*     label: 'Grey'
*   },
*   {
*     color: 'hsl(0, 0%, 90%)',
*     label: 'Light grey'
*   },
*   {
*     color: 'hsl(0, 0%, 100%)',
*     label: 'White',
*     hasBorder: true
*   },
*   {
*     color: 'hsl(0, 75%, 60%)',
*     label: 'Red'
*   },
*   {
*     color: 'hsl(30, 75%, 60%)',
*     label: 'Orange'
*   },
*   {
*     color: 'hsl(60, 75%, 60%)',
*     label: 'Yellow'
*   },
*   {
*     color: 'hsl(90, 75%, 60%)',
*     label: 'Light green'
*   },
*   {
*     color: 'hsl(120, 75%, 60%)',
*     label: 'Green'
*   },
*   {
*     color: 'hsl(150, 75%, 60%)',
*     label: 'Aquamarine'
*   },
*   {
*     color: 'hsl(180, 75%, 60%)',
*     label: 'Turquoise'
*   },
*   {
*     color: 'hsl(210, 75%, 60%)',
*     label: 'Light blue'
*   },
*   {
*     color: 'hsl(240, 75%, 60%)',
*     label: 'Blue'
*   },
*   {
*     color: 'hsl(270, 75%, 60%)',
*     label: 'Purple'
*   }
* ];
* ```
*
* @internal
*/
const defaultColors = [
	{
		color: "hsl(0, 0%, 0%)",
		label: "Black"
	},
	{
		color: "hsl(0, 0%, 30%)",
		label: "Dim grey"
	},
	{
		color: "hsl(0, 0%, 60%)",
		label: "Grey"
	},
	{
		color: "hsl(0, 0%, 90%)",
		label: "Light grey"
	},
	{
		color: "hsl(0, 0%, 100%)",
		label: "White",
		hasBorder: true
	},
	{
		color: "hsl(0, 75%, 60%)",
		label: "Red"
	},
	{
		color: "hsl(30, 75%, 60%)",
		label: "Orange"
	},
	{
		color: "hsl(60, 75%, 60%)",
		label: "Yellow"
	},
	{
		color: "hsl(90, 75%, 60%)",
		label: "Light green"
	},
	{
		color: "hsl(120, 75%, 60%)",
		label: "Green"
	},
	{
		color: "hsl(150, 75%, 60%)",
		label: "Aquamarine"
	},
	{
		color: "hsl(180, 75%, 60%)",
		label: "Turquoise"
	},
	{
		color: "hsl(210, 75%, 60%)",
		label: "Light blue"
	},
	{
		color: "hsl(240, 75%, 60%)",
		label: "Blue"
	},
	{
		color: "hsl(270, 75%, 60%)",
		label: "Purple"
	}
];
/**
* Returns a creator for a color input with a label.
*
* For given options, it returns a function that creates an instance of a
* {@link module:table/ui/colorinputview~ColorInputView color input} logically related to
* a {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView labeled view} in the DOM.
*
* The helper does the following:
*
* * It sets the color input `id` and `ariaDescribedById` attributes.
* * It binds the color input `isReadOnly` to the labeled view.
* * It binds the color input `hasError` to the labeled view.
* * It enables a logic that cleans up the error when the user starts typing in the color input.
*
* Usage:
*
* ```ts
* const colorInputCreator = getLabeledColorInputCreator( {
*   colorConfig: [ ... ],
*   columns: 3,
* } );
*
* const labeledInputView = new LabeledFieldView( locale, colorInputCreator );
* console.log( labeledInputView.view ); // A color input instance.
* ```
*
* @internal
* @param options Color input options.
* @param options.colorConfig The configuration of the color palette displayed in the input's dropdown.
* @param options.columns The configuration of the number of columns the color palette consists of in the input's dropdown.
* @param options.defaultColorValue If specified, the color input view will replace the "Remove color" button with
* the "Restore default" button. Instead of clearing the input field, the default color value will be set.
* @param options.colorPickerConfig The configuration of the color picker. You could disable it or define your output format.
*/
function getLabeledColorInputCreator(options) {
	return (labeledFieldView, viewUid, statusUid) => {
		const colorInputView = new ColorInputView(labeledFieldView.locale, {
			colorDefinitions: colorConfigToColorGridDefinitions(options.colorConfig),
			columns: options.columns,
			defaultColorValue: options.defaultColorValue,
			colorPickerConfig: options.colorPickerConfig
		});
		colorInputView.inputView.set({
			id: viewUid,
			ariaDescribedById: statusUid
		});
		colorInputView.bind("isReadOnly").to(labeledFieldView, "isEnabled", (value) => !value);
		colorInputView.bind("hasError").to(labeledFieldView, "errorText", (value) => !!value);
		colorInputView.on("input", () => {
			labeledFieldView.errorText = null;
		});
		labeledFieldView.bind("isEmpty", "isFocused").to(colorInputView);
		return colorInputView;
	};
}
/**
* A simple helper method to detect number strings.
* I allows full number notation, so omitting 0 is not allowed:
*/
function isNumberString(value) {
	const parsedValue = parseFloat(value);
	return !Number.isNaN(parsedValue) && value === String(parsedValue);
}
function colorConfigToColorGridDefinitions(colorConfig) {
	return colorConfig.map((item) => ({
		color: item.model,
		label: item.label,
		options: { hasBorder: item.hasBorder }
	}));
}

/**
* @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 table/tablecellproperties/ui/tablecellpropertiesview
*/
/**
* The class representing a table cell properties form, allowing users to customize
* certain style aspects of a table cell, for instance, border, padding, text alignment, etc..
*/
var TableCellPropertiesView = class extends View {
	/**
	* Options passed to the view. See {@link #constructor} to learn more.
	*/
	options;
	/**
	* Tracks information about the DOM focus in the form.
	*/
	focusTracker;
	/**
	* An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
	*/
	keystrokes;
	/**
	* A collection of child views in the form.
	*/
	children;
	/**
	* A dropdown that allows selecting the style of the table cell border.
	*/
	borderStyleDropdown;
	/**
	* An input that allows specifying the width of the table cell border.
	*/
	borderWidthInput;
	/**
	* An input that allows specifying the color of the table cell border.
	*/
	borderColorInput;
	/**
	* An input that allows specifying the table cell background color.
	*/
	backgroundInput;
	/**
	* A dropdown that allows selecting the type of the table cell (data or header).
	*/
	cellTypeDropdown;
	/**
	* An input that allows specifying the table cell padding.
	*/
	paddingInput;
	/**
	* An input that allows specifying the table cell width.
	*/
	widthInput;
	/**
	* An input that allows specifying the table cell height.
	*/
	heightInput;
	/**
	* A toolbar with buttons that allow changing the horizontal text alignment in a table cell.
	*/
	horizontalAlignmentToolbar;
	/**
	* A toolbar with buttons that allow changing the vertical text alignment in a table cell.
	*/
	verticalAlignmentToolbar;
	/**
	* The "Save" button view.
	*/
	saveButtonView;
	/**
	* The "Cancel" button view.
	*/
	cancelButtonView;
	/**
	* The "Back" button view.
	*/
	backButtonView;
	/**
	* A collection of views that can be focused in the form.
	*/
	_focusables;
	/**
	* Helps cycling over {@link #_focusables} in the form.
	*/
	_focusCycler;
	/**
	* @param locale The {@link module:core/editor/editor~Editor#locale} instance.
	* @param options Additional configuration of the view.
	* @param options.borderColors A configuration of the border color palette used by the
	* {@link module:table/tablecellproperties/ui/tablecellpropertiesview~TableCellPropertiesView#borderColorInput}.
	* @param options.backgroundColors A configuration of the background color palette used by the
	* {@link module:table/tablecellproperties/ui/tablecellpropertiesview~TableCellPropertiesView#backgroundInput}.
	* @param options.defaultTableCellProperties The default table cell properties.
	*/
	constructor(locale, options) {
		super(locale);
		this.set({
			borderStyle: "",
			borderWidth: "",
			borderColor: "",
			padding: "",
			backgroundColor: "",
			width: "",
			height: "",
			horizontalAlignment: "",
			verticalAlignment: "",
			cellType: ""
		});
		this.options = options;
		const { borderStyleDropdown, borderWidthInput, borderColorInput, borderRowLabel } = this._createBorderFields();
		const { backgroundRowLabel, backgroundInput } = this._createBackgroundFields();
		const { cellTypeRowLabel, cellTypeDropdown } = this._createCellTypeField();
		const { widthInput, operatorLabel, heightInput, dimensionsLabel } = this._createDimensionFields();
		const { horizontalAlignmentToolbar, verticalAlignmentToolbar, alignmentLabel } = this._createAlignmentFields();
		this.focusTracker = new FocusTracker();
		this.keystrokes = new KeystrokeHandler();
		this.children = this.createCollection();
		this.borderStyleDropdown = borderStyleDropdown;
		this.borderWidthInput = borderWidthInput;
		this.borderColorInput = borderColorInput;
		this.backgroundInput = backgroundInput;
		this.cellTypeDropdown = cellTypeDropdown;
		this.paddingInput = this._createPaddingField();
		this.widthInput = widthInput;
		this.heightInput = heightInput;
		this.horizontalAlignmentToolbar = horizontalAlignmentToolbar;
		this.verticalAlignmentToolbar = verticalAlignmentToolbar;
		const { saveButtonView, cancelButtonView } = this._createActionButtons();
		this.saveButtonView = saveButtonView;
		this.cancelButtonView = cancelButtonView;
		this.backButtonView = this._createBackButton();
		this._focusables = new ViewCollection();
		this._focusCycler = new FocusCycler({
			focusables: this._focusables,
			focusTracker: this.focusTracker,
			keystrokeHandler: this.keystrokes,
			actions: {
				focusPrevious: "shift + tab",
				focusNext: "tab"
			}
		});
		const header = new FormHeaderView(locale, { label: this.t("Cell properties") });
		header.children.add(this.backButtonView, 0);
		this.children.add(header);
		this.children.add(new FormRowView(locale, {
			labelView: borderRowLabel,
			children: [
				borderRowLabel,
				borderStyleDropdown,
				borderWidthInput,
				borderColorInput
			],
			class: "ck-table-form__border-row"
		}));
		this.children.add(new FormRowView(locale, { children: [new FormRowView(locale, {
			labelView: cellTypeRowLabel,
			children: [cellTypeRowLabel, cellTypeDropdown],
			class: "ck-table-form__cell-type-row"
		}), new FormRowView(locale, {
			labelView: backgroundRowLabel,
			children: [backgroundRowLabel, backgroundInput],
			class: "ck-table-form__background-row"
		})] }));
		this.children.add(new FormRowView(locale, { children: [new FormRowView(locale, {
			labelView: dimensionsLabel,
			children: [
				dimensionsLabel,
				widthInput,
				operatorLabel,
				heightInput
			],
			class: "ck-table-form__dimensions-row"
		}), new FormRowView(locale, {
			children: [this.paddingInput],
			class: "ck-table-cell-properties-form__padding-row"
		})] }));
		this.children.add(new FormRowView(locale, {
			labelView: alignmentLabel,
			children: [
				alignmentLabel,
				horizontalAlignmentToolbar,
				verticalAlignmentToolbar
			],
			class: "ck-table-cell-properties-form__alignment-row"
		}));
		this.children.add(new FormRowView(locale, {
			children: [this.cancelButtonView, this.saveButtonView],
			class: "ck-table-form__action-row"
		}));
		this.setTemplate({
			tag: "form",
			attributes: {
				class: [
					"ck",
					"ck-form",
					"ck-table-form",
					"ck-table-cell-properties-form"
				],
				tabindex: "-1"
			},
			children: this.children
		});
	}
	/**
	* @inheritDoc
	*/
	render() {
		super.render();
		submitHandler({ view: this });
		[this.borderColorInput, this.backgroundInput].forEach((view) => {
			this._focusCycler.chain(view.fieldView.focusCycler);
		});
		[
			this.borderStyleDropdown,
			this.borderColorInput,
			this.borderWidthInput,
			this.cellTypeDropdown,
			this.backgroundInput,
			this.widthInput,
			this.heightInput,
			this.paddingInput,
			this.horizontalAlignmentToolbar,
			this.verticalAlignmentToolbar,
			this.cancelButtonView,
			this.saveButtonView,
			this.backButtonView
		].forEach((view) => {
			this._focusables.add(view);
			this.focusTracker.add(view.element);
		});
		this.keystrokes.listenTo(this.element);
	}
	/**
	* @inheritDoc
	*/
	destroy() {
		super.destroy();
		this.focusTracker.destroy();
		this.keystrokes.destroy();
	}
	/**
	* Focuses the fist focusable field in the form.
	*/
	focus() {
		this._focusCycler.focusFirst();
	}
	/**
	* Creates the following form fields:
	*
	* * {@link #borderStyleDropdown},
	* * {@link #borderWidthInput},
	* * {@link #borderColorInput}.
	*/
	_createBorderFields() {
		const defaultTableCellProperties = this.options.defaultTableCellProperties;
		const defaultBorder = {
			style: defaultTableCellProperties.borderStyle,
			width: defaultTableCellProperties.borderWidth,
			color: defaultTableCellProperties.borderColor
		};
		const colorInputCreator = getLabeledColorInputCreator({
			colorConfig: this.options.borderColors,
			columns: 5,
			defaultColorValue: defaultBorder.color,
			colorPickerConfig: this.options.colorPickerConfig
		});
		const locale = this.locale;
		const t = this.t;
		const accessibleLabel = t("Style");
		const borderRowLabel = new LabelView(locale);
		borderRowLabel.text = t("Border");
		const styleLabels = getBorderStyleLabels(t);
		const borderStyleDropdown = new LabeledFieldView(locale, createLabeledDropdown);
		borderStyleDropdown.set({
			label: accessibleLabel,
			class: "ck-table-form__border-style"
		});
		borderStyleDropdown.fieldView.buttonView.set({
			ariaLabel: accessibleLabel,
			ariaLabelledBy: void 0,
			isOn: false,
			withText: true,
			tooltip: accessibleLabel
		});
		borderStyleDropdown.fieldView.buttonView.bind("label").to(this, "borderStyle", (value) => {
			return styleLabels[value ? value : "none"];
		});
		borderStyleDropdown.fieldView.on("execute", (evt) => {
			this.borderStyle = evt.source._borderStyleValue;
		});
		borderStyleDropdown.bind("isEmpty").to(this, "borderStyle", (value) => !value);
		addListToDropdown(borderStyleDropdown.fieldView, getBorderStyleDefinitions(this, defaultBorder.style), {
			role: "menu",
			ariaLabel: accessibleLabel
		});
		const borderWidthInput = new LabeledFieldView(locale, createLabeledInputText);
		borderWidthInput.set({
			label: t("Width"),
			class: "ck-table-form__border-width"
		});
		borderWidthInput.fieldView.bind("value").to(this, "borderWidth");
		borderWidthInput.bind("isEnabled").to(this, "borderStyle", isBorderStyleSet$1);
		borderWidthInput.fieldView.on("input", () => {
			this.borderWidth = borderWidthInput.fieldView.element.value;
		});
		const borderColorInput = new LabeledFieldView(locale, colorInputCreator);
		borderColorInput.set({
			label: t("Color"),
			class: "ck-table-form__border-color"
		});
		borderColorInput.fieldView.bind("value").to(this, "borderColor");
		borderColorInput.bind("isEnabled").to(this, "borderStyle", isBorderStyleSet$1);
		borderColorInput.fieldView.on("input", () => {
			this.borderColor = borderColorInput.fieldView.value;
		});
		this.on("change:borderStyle", (evt, name, newValue, oldValue) => {
			if (!isBorderStyleSet$1(newValue)) {
				this.borderColor = "";
				this.borderWidth = "";
			}
			if (!isBorderStyleSet$1(oldValue)) {
				this.borderColor = defaultBorder.color;
				this.borderWidth = defaultBorder.width;
			}
		});
		return {
			borderRowLabel,
			borderStyleDropdown,
			borderColorInput,
			borderWidthInput
		};
	}
	/**
	* Creates the following form fields:
	*
	* * {@link #backgroundInput}.
	*/
	_createBackgroundFields() {
		const locale = this.locale;
		const t = this.t;
		const backgroundRowLabel = new LabelView(locale);
		backgroundRowLabel.text = t("Background");
		const backgroundInput = new LabeledFieldView(locale, getLabeledColorInputCreator({
			colorConfig: this.options.backgroundColors,
			columns: 5,
			defaultColorValue: this.options.defaultTableCellProperties.backgroundColor,
			colorPickerConfig: this.options.colorPickerConfig
		}));
		backgroundInput.set({
			label: t("Color"),
			class: "ck-table-cell-properties-form__background"
		});
		backgroundInput.fieldView.bind("value").to(this, "backgroundColor");
		backgroundInput.fieldView.on("input", () => {
			this.backgroundColor = backgroundInput.fieldView.value;
		});
		return {
			backgroundRowLabel,
			backgroundInput
		};
	}
	/**
	* Create cell type field.
	*
	* * {@link #cellTypeDropdown}.
	*
	* @internal
	*/
	_createCellTypeField() {
		const locale = this.locale;
		const t = this.t;
		const cellTypeRowLabel = new LabelView(locale);
		cellTypeRowLabel.text = t("Cell type");
		const cellTypeLabels = this._cellTypeLabels;
		const cellTypeDropdown = new LabeledFieldView(locale, createLabeledDropdown);
		cellTypeDropdown.set({
			label: t("Cell type"),
			class: "ck-table-cell-properties-form__cell-type"
		});
		cellTypeDropdown.fieldView.buttonView.set({
			ariaLabel: t("Cell type"),
			ariaLabelledBy: void 0,
			isOn: false,
			withText: true,
			tooltip: t("Cell type")
		});
		cellTypeDropdown.fieldView.buttonView.bind("label").to(this, "cellType", (value) => {
			return cellTypeLabels[value || "data"];
		});
		cellTypeDropdown.fieldView.on("execute", (evt) => {
			this.cellType = evt.source._cellTypeValue;
		});
		cellTypeDropdown.bind("isEmpty").to(this, "cellType", (value) => !value);
		addListToDropdown(cellTypeDropdown.fieldView, this._getCellTypeDefinitions(), {
			role: "menu",
			ariaLabel: t("Cell type")
		});
		return {
			cellTypeRowLabel,
			cellTypeDropdown
		};
	}
	/**
	* Creates the following form fields:
	*
	* * {@link #widthInput}.
	* * {@link #heightInput}.
	*/
	_createDimensionFields() {
		const locale = this.locale;
		const t = this.t;
		const dimensionsLabel = new LabelView(locale);
		dimensionsLabel.text = t("Dimensions");
		const widthInput = new LabeledFieldView(locale, createLabeledInputText);
		widthInput.set({
			label: t("Width"),
			class: "ck-table-form__dimensions-row__width"
		});
		widthInput.fieldView.bind("value").to(this, "width");
		widthInput.fieldView.on("input", () => {
			this.width = widthInput.fieldView.element.value;
		});
		const operatorLabel = new View(locale);
		operatorLabel.setTemplate({
			tag: "span",
			attributes: { class: ["ck-table-form__dimension-operator"] },
			children: [{ text: "×" }]
		});
		const heightInput = new LabeledFieldView(locale, createLabeledInputText);
		heightInput.set({
			label: t("Height"),
			class: "ck-table-form__dimensions-row__height"
		});
		heightInput.fieldView.bind("value").to(this, "height");
		heightInput.fieldView.on("input", () => {
			this.height = heightInput.fieldView.element.value;
		});
		return {
			dimensionsLabel,
			widthInput,
			operatorLabel,
			heightInput
		};
	}
	/**
	* Creates the following form fields:
	*
	* * {@link #paddingInput}.
	*/
	_createPaddingField() {
		const locale = this.locale;
		const t = this.t;
		const paddingInput = new LabeledFieldView(locale, createLabeledInputText);
		paddingInput.set({
			label: t("Padding"),
			class: "ck-table-cell-properties-form__padding"
		});
		paddingInput.fieldView.bind("value").to(this, "padding");
		paddingInput.fieldView.on("input", () => {
			this.padding = paddingInput.fieldView.element.value;
		});
		return paddingInput;
	}
	/**
	* Creates the following form fields:
	*
	* * {@link #horizontalAlignmentToolbar},
	* * {@link #verticalAlignmentToolbar}.
	*/
	_createAlignmentFields() {
		const locale = this.locale;
		const t = this.t;
		const alignmentLabel = new LabelView(locale);
		const ALIGNMENT_ICONS = {
			left: IconAlignLeft,
			center: IconAlignCenter,
			right: IconAlignRight,
			justify: IconAlignJustify,
			top: IconAlignTop,
			middle: IconAlignMiddle,
			bottom: IconAlignBottom
		};
		alignmentLabel.text = t("Table cell text alignment");
		const horizontalAlignmentToolbar = new ToolbarView(locale);
		const isContentRTL = locale.contentLanguageDirection === "rtl";
		horizontalAlignmentToolbar.set({
			isCompact: true,
			role: "radiogroup",
			ariaLabel: t("Horizontal text alignment toolbar"),
			class: "ck-table-cell-properties-form__horizontal-alignment-toolbar"
		});
		fillToolbar({
			view: this,
			icons: ALIGNMENT_ICONS,
			toolbar: horizontalAlignmentToolbar,
			labels: this._horizontalAlignmentLabels,
			propertyName: "horizontalAlignment",
			nameToValue: (name) => {
				if (isContentRTL) {
					if (name === "left") return "right";
					else if (name === "right") return "left";
				}
				return name;
			},
			defaultValue: this.options.defaultTableCellProperties.horizontalAlignment
		});
		const verticalAlignmentToolbar = new ToolbarView(locale);
		verticalAlignmentToolbar.set({
			isCompact: true,
			role: "radiogroup",
			ariaLabel: t("Vertical text alignment toolbar"),
			class: "ck-table-cell-properties-form__vertical-alignment-toolbar"
		});
		fillToolbar({
			view: this,
			icons: ALIGNMENT_ICONS,
			toolbar: verticalAlignmentToolbar,
			labels: this._verticalAlignmentLabels,
			propertyName: "verticalAlignment",
			defaultValue: this.options.defaultTableCellProperties.verticalAlignment
		});
		return {
			horizontalAlignmentToolbar,
			verticalAlignmentToolbar,
			alignmentLabel
		};
	}
	/**
	* Creates the following form controls:
	*
	* * {@link #saveButtonView},
	* * {@link #cancelButtonView}.
	*/
	_createActionButtons() {
		const locale = this.locale;
		const t = this.t;
		const saveButtonView = new ButtonView(locale);
		const cancelButtonView = new ButtonView(locale);
		const fieldsThatShouldValidateToSave = [
			this.borderWidthInput,
			this.borderColorInput,
			this.backgroundInput,
			this.paddingInput
		];
		saveButtonView.set({
			label: t("Save"),
			class: "ck-button-action",
			type: "submit",
			withText: true
		});
		saveButtonView.bind("isEnabled").toMany(fieldsThatShouldValidateToSave, "errorText", (...errorTexts) => {
			return errorTexts.every((errorText) => !errorText);
		});
		cancelButtonView.set({
			label: t("Cancel"),
			withText: true
		});
		cancelButtonView.delegate("execute").to(this, "cancel");
		return {
			saveButtonView,
			cancelButtonView
		};
	}
	/**
	* 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 the cell type dropdown definitions.
	*/
	_getCellTypeDefinitions() {
		const itemDefinitions = new Collection();
		const labels = this._cellTypeLabels;
		const types = ["data", "header"];
		if (this.options.showScopedHeaderOptions) types.push("header-column", "header-row");
		for (const type of types) {
			const definition = {
				type: "button",
				model: new UIModel({
					_cellTypeValue: type,
					label: labels[type],
					role: "menuitemradio",
					withText: true
				})
			};
			definition.model.bind("isOn").to(this, "cellType", (value) => value === type);
			itemDefinitions.add(definition);
		}
		return itemDefinitions;
	}
	/**
	* Provides localized labels for {@link #horizontalAlignmentToolbar} buttons.
	*/
	get _horizontalAlignmentLabels() {
		const locale = this.locale;
		const t = this.t;
		const left = t("Align cell text to the left");
		const center = t("Align cell text to the center");
		const right = t("Align cell text to the right");
		const justify = t("Justify cell text");
		if (locale.uiLanguageDirection === "rtl") return {
			right,
			center,
			left,
			justify
		};
		else return {
			left,
			center,
			right,
			justify
		};
	}
	/**
	* Provides localized labels for {@link #verticalAlignmentToolbar} buttons.
	*/
	get _verticalAlignmentLabels() {
		const t = this.t;
		return {
			top: t("Align cell text to the top"),
			middle: t("Align cell text to the middle"),
			bottom: t("Align cell text to the bottom")
		};
	}
	/**
	* Provides localized labels for {@link #cellTypeDropdown}.
	*/
	get _cellTypeLabels() {
		const t = this.t;
		return {
			data: t("Data cell"),
			header: t("Header cell"),
			"header-column": t("Column header"),
			"header-row": t("Row header")
		};
	}
};
function isBorderStyleSet$1(value) {
	return value !== "none";
}

/**
* @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 table/utils/ui/contextualballoon
*/
const BALLOON_POSITIONS = /* #__PURE__ */ (() => [
	BalloonPanelView.defaultPositions.northArrowSouth,
	BalloonPanelView.defaultPositions.northArrowSouthWest,
	BalloonPanelView.defaultPositions.northArrowSouthEast,
	BalloonPanelView.defaultPositions.southArrowNorth,
	BalloonPanelView.defaultPositions.southArrowNorthWest,
	BalloonPanelView.defaultPositions.southArrowNorthEast,
	BalloonPanelView.defaultPositions.viewportStickyNorth
])();
/**
* A helper utility that positions the
* {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon} instance
* with respect to the table in the editor content, if one is selected.
*
* @internal
* @param editor The editor instance.
* @param target Either "cell" or "table". Determines the target the balloon will be attached to.
*/
function repositionContextualBalloon(editor, target) {
	const balloon = editor.plugins.get("ContextualBalloon");
	const selection = editor.editing.view.document.selection;
	let position;
	if (target === "cell") {
		if (getTableWidgetAncestor(selection)) position = getBalloonCellPositionData(editor);
	} else if (getSelectionAffectedTableWidget(selection)) position = getBalloonTablePositionData(editor);
	if (position) 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 table in the editor content.
*
* @param editor The editor instance.
*/
function getBalloonTablePositionData(editor) {
	const selection = editor.model.document.selection;
	const modelTable = getSelectionAffectedTable(selection);
	const viewTable = editor.editing.mapper.toViewElement(modelTable);
	return {
		target: editor.editing.view.domConverter.mapViewToDom(viewTable),
		positions: BALLOON_POSITIONS
	};
}
/**
* Returns the positioning options that control the geometry of the
* {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon} with respect
* to the selected table cell in the editor content.
*
* @param editor The editor instance.
* @internal
*/
function getBalloonCellPositionData(editor) {
	const mapper = editor.editing.mapper;
	const domConverter = editor.editing.view.domConverter;
	const selection = editor.model.document.selection;
	if (selection.rangeCount > 1) return {
		target: () => createBoundingRect(selection.getRanges(), editor),
		positions: BALLOON_POSITIONS
	};
	const modelTableCell = getTableCellAtPosition(selection.getFirstPosition());
	const viewTableCell = mapper.toViewElement(modelTableCell);
	return {
		target: domConverter.mapViewToDom(viewTableCell),
		positions: BALLOON_POSITIONS
	};
}
/**
* Returns the first selected table cell from a multi-cell or in-cell selection.
*
* @param position Document position.
*/
function getTableCellAtPosition(position) {
	return position.nodeAfter && position.nodeAfter.is("element", "tableCell") ? position.nodeAfter : position.findAncestor("tableCell");
}
/**
* Returns bounding rectangle for given model ranges.
*
* @param ranges Model ranges that the bounding rect should be returned for.
* @param editor The editor instance.
*/
function createBoundingRect(ranges, editor) {
	const mapper = editor.editing.mapper;
	const domConverter = editor.editing.view.domConverter;
	const rects = Array.from(ranges).map((range) => {
		const modelTableCell = getTableCellAtPosition(range.start);
		const viewTableCell = mapper.toViewElement(modelTableCell);
		return new Rect(domConverter.mapViewToDom(viewTableCell));
	});
	return Rect.getBoundingRect(rects);
}

/**
* @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 table/tablecellproperties/tablecellpropertiesui
*/
const ERROR_TEXT_TIMEOUT$1 = 500;
const propertyToCommandMap$1 = {
	borderStyle: "tableCellBorderStyle",
	borderColor: "tableCellBorderColor",
	borderWidth: "tableCellBorderWidth",
	height: "tableCellHeight",
	width: "tableCellWidth",
	padding: "tableCellPadding",
	backgroundColor: "tableCellBackgroundColor",
	horizontalAlignment: "tableCellHorizontalAlignment",
	verticalAlignment: "tableCellVerticalAlignment",
	cellType: "tableCellType"
};
/**
* The table cell properties UI plugin. It introduces the `'tableCellProperties'` button
* that opens a form allowing to specify the visual styling of a table cell.
*
* It uses the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon plugin}.
*/
var TableCellPropertiesUI = class extends Plugin {
	/**
	* The default table cell properties.
	*/
	_defaultContentTableCellProperties;
	/**
	* The default layout table cell properties.
	*/
	_defaultLayoutTableCellProperties;
	/**
	* The contextual balloon plugin instance.
	*/
	_balloon;
	/**
	* The cell properties form view displayed inside the balloon.
	*/
	view;
	/**
	* The cell properties form view displayed inside the balloon (content table).
	*/
	_viewWithContentTableDefaults;
	/**
	* The cell properties form view displayed inside the balloon (layout table).
	*/
	_viewWithLayoutTableDefaults;
	/**
	* The batch used to undo all changes made by the form (which are live, as the user types)
	* when "Cancel" was pressed. Each time the view is shown, a new batch is created.
	*/
	_undoStepBatch;
	/**
	* Flag used to indicate whether view is ready to execute update commands
	* (it finished loading initial data).
	*/
	_isReady;
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [ContextualBalloon];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "TableCellPropertiesUI";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	constructor(editor) {
		super(editor);
		editor.config.define("table.tableCellProperties", {
			borderColors: defaultColors,
			backgroundColors: defaultColors
		});
	}
	/**
	* @inheritDoc
	*/
	init() {
		const editor = this.editor;
		const t = editor.t;
		this._defaultContentTableCellProperties = getNormalizedDefaultCellProperties(editor.config.get("table.tableCellProperties.defaultProperties"), {
			includeVerticalAlignmentProperty: true,
			includeHorizontalAlignmentProperty: true,
			includePaddingProperty: true,
			isRightToLeftContent: editor.locale.contentLanguageDirection === "rtl"
		});
		this._defaultLayoutTableCellProperties = getNormalizedDefaultProperties(void 0, {
			includeVerticalAlignmentProperty: true,
			includeHorizontalAlignmentProperty: true,
			isRightToLeftContent: editor.locale.contentLanguageDirection === "rtl"
		});
		this._balloon = editor.plugins.get(ContextualBalloon);
		this.view = null;
		this._isReady = false;
		editor.ui.componentFactory.add("tableCellProperties", (locale) => {
			const view = new ButtonView(locale);
			view.set({
				label: t("Cell properties"),
				icon: IconTableCellProperties,
				tooltip: true
			});
			this.listenTo(view, "execute", () => this._showView());
			const commands = Object.values(propertyToCommandMap$1).map((commandName) => editor.commands.get(commandName)).filter((val) => !!val);
			view.bind("isEnabled").toMany(commands, "isEnabled", (...areEnabled) => areEnabled.some((isCommandEnabled) => isCommandEnabled));
			return view;
		});
	}
	/**
	* @inheritDoc
	*/
	destroy() {
		super.destroy();
		if (this.view) this.view.destroy();
	}
	/**
	* Creates the {@link module:table/tablecellproperties/ui/tablecellpropertiesview~TableCellPropertiesView} instance.
	*
	* @returns The cell properties form view instance.
	*/
	_createPropertiesView(defaultTableCellProperties) {
		const editor = this.editor;
		const config = editor.config.get("table.tableCellProperties");
		const scopedHeaders = !!editor.config.get("table.tableCellProperties.scopedHeaders");
		const borderColorsConfig = normalizeColorOptions(config.borderColors);
		const localizedBorderColors = getLocalizedColorOptions(editor.locale, borderColorsConfig);
		const backgroundColorsConfig = normalizeColorOptions(config.backgroundColors);
		const localizedBackgroundColors = getLocalizedColorOptions(editor.locale, backgroundColorsConfig);
		const hasColorPicker = config.colorPicker !== false;
		const view = new TableCellPropertiesView(editor.locale, {
			borderColors: localizedBorderColors,
			backgroundColors: localizedBackgroundColors,
			defaultTableCellProperties,
			colorPickerConfig: hasColorPicker ? config.colorPicker || {} : false,
			showScopedHeaderOptions: scopedHeaders
		});
		const t = editor.t;
		view.render();
		this.listenTo(view, "submit", () => {
			this._hideView();
		});
		this.listenTo(view, "cancel", () => {
			if (this._undoStepBatch.operations.length) editor.execute("undo", this._undoStepBatch);
			this._hideView();
		});
		view.keystrokes.set("Esc", (data, cancel) => {
			this._hideView();
			cancel();
		});
		clickOutsideHandler({
			emitter: view,
			activator: () => this._isViewInBalloon,
			contextElements: [this._balloon.view.element],
			callback: () => this._hideView()
		});
		const colorErrorText = getLocalizedColorErrorText(t);
		const lengthErrorText = getLocalizedLengthErrorText(t);
		view.on("change:borderStyle", this._getPropertyChangeCallback("tableCellBorderStyle"));
		view.on("change:borderColor", this._getValidatedPropertyChangeCallback({
			viewField: view.borderColorInput,
			commandName: "tableCellBorderColor",
			errorText: colorErrorText,
			validator: colorFieldValidator
		}));
		view.on("change:borderWidth", this._getValidatedPropertyChangeCallback({
			viewField: view.borderWidthInput,
			commandName: "tableCellBorderWidth",
			errorText: lengthErrorText,
			validator: lineWidthFieldValidator
		}));
		view.on("change:padding", this._getValidatedPropertyChangeCallback({
			viewField: view.paddingInput,
			commandName: "tableCellPadding",
			errorText: lengthErrorText,
			validator: lengthFieldValidator
		}));
		view.on("change:width", this._getValidatedPropertyChangeCallback({
			viewField: view.widthInput,
			commandName: "tableCellWidth",
			errorText: lengthErrorText,
			validator: lengthFieldValidator
		}));
		view.on("change:height", this._getValidatedPropertyChangeCallback({
			viewField: view.heightInput,
			commandName: "tableCellHeight",
			errorText: lengthErrorText,
			validator: lengthFieldValidator
		}));
		view.on("change:backgroundColor", this._getValidatedPropertyChangeCallback({
			viewField: view.backgroundInput,
			commandName: "tableCellBackgroundColor",
			errorText: colorErrorText,
			validator: colorFieldValidator
		}));
		view.on("change:horizontalAlignment", this._getPropertyChangeCallback("tableCellHorizontalAlignment"));
		view.on("change:verticalAlignment", this._getPropertyChangeCallback("tableCellVerticalAlignment"));
		const cellTypeCommand = editor.commands.get("tableCellType");
		if (cellTypeCommand) {
			view.cellTypeDropdown.bind("isEnabled").to(cellTypeCommand, "isEnabled");
			view.on("change:cellType", this._getPropertyChangeCallback("tableCellType"));
		}
		return view;
	}
	/**
	* In this method the "editor data -> UI" binding is happening.
	*
	* When executed, this method obtains selected cell property values from various table commands
	* and passes them to the {@link #view}.
	*
	* This way, the UI stays up–to–date with the editor data.
	*/
	_fillViewFormFromCommandValues() {
		const commands = this.editor.commands;
		const borderStyleCommand = commands.get("tableCellBorderStyle");
		Object.entries(propertyToCommandMap$1).flatMap(([property, commandName]) => {
			const effectiveCommandName = property === "width" ? this._getWidthCommandName() : commandName;
			const command = commands.get(effectiveCommandName);
			if (!command) return [];
			const propertyKey = property;
			let defaultValue;
			if (propertyKey === "cellType") defaultValue = "";
			else defaultValue = this.view === this._viewWithContentTableDefaults ? this._defaultContentTableCellProperties[propertyKey] || "" : this._defaultLayoutTableCellProperties[propertyKey] || "";
			return [[property, command.value || defaultValue]];
		}).forEach(([property, value]) => {
			if ((property === "borderColor" || property === "borderWidth") && borderStyleCommand.value === "none") return;
			this.view.set(property, value);
		});
		this._isReady = true;
	}
	/**
	* Shows the {@link #view} in the {@link #_balloon}.
	*
	* **Note**: Each time a view is shown, a new {@link #_undoStepBatch} is created. It contains
	* all changes made to the document when the view is visible, allowing a single undo step
	* for all of them.
	*/
	_showView() {
		const editor = this.editor;
		const viewTable = getSelectionAffectedTableWidget(editor.editing.view.document.selection);
		const modelTable = viewTable && editor.editing.mapper.toModelElement(viewTable);
		const useDefaults = !modelTable || modelTable.getAttribute("tableType") !== "layout";
		if (useDefaults && !this._viewWithContentTableDefaults) this._viewWithContentTableDefaults = this._createPropertiesView(this._defaultContentTableCellProperties);
		else if (!useDefaults && !this._viewWithLayoutTableDefaults) this._viewWithLayoutTableDefaults = this._createPropertiesView(this._defaultLayoutTableCellProperties);
		this.view = useDefaults ? this._viewWithContentTableDefaults : this._viewWithLayoutTableDefaults;
		this.listenTo(editor.ui, "update", () => {
			this._updateView();
		});
		this._fillViewFormFromCommandValues();
		this._balloon.add({
			view: this.view,
			position: getBalloonCellPositionData(editor)
		});
		this._undoStepBatch = editor.model.createBatch();
		this.view.focus();
	}
	/**
	* Removes the {@link #view} from the {@link #_balloon}.
	*/
	_hideView() {
		const editor = this.editor;
		this.stopListening(editor.ui, "update");
		this._isReady = false;
		this.view.saveButtonView.focus();
		this._balloon.remove(this.view);
		this.editor.editing.view.focus();
	}
	/**
	* Repositions the {@link #_balloon} or hides the {@link #view} if a table cell is no longer selected.
	*/
	_updateView() {
		const editor = this.editor;
		const viewDocument = editor.editing.view.document;
		if (!getTableWidgetAncestor(viewDocument.selection)) this._hideView();
		else if (this._isViewVisible) repositionContextualBalloon(editor, "cell");
	}
	/**
	* Returns `true` when the {@link #view} is visible in the {@link #_balloon}.
	*/
	get _isViewVisible() {
		return !!this.view && this._balloon.visibleView === this.view;
	}
	/**
	* Returns `true` when the {@link #view} is in the {@link #_balloon}.
	*/
	get _isViewInBalloon() {
		return !!this.view && this._balloon.hasView(this.view);
	}
	/**
	* Creates a callback that when executed upon the {@link #view view's} property change
	* executes a related editor command with the new property value.
	*
	* @param commandName The default value of the command.
	*/
	_getPropertyChangeCallback(commandName) {
		return (evt, propertyName, newValue) => {
			if (!this._isReady) return;
			this.editor.execute(commandName, {
				value: newValue,
				batch: this._undoStepBatch
			});
		};
	}
	/**
	* Creates a callback that when executed upon the {@link #view view's} property change:
	* * Executes a related editor command with the new property value if the value is valid,
	* * Or sets the error text next to the invalid field, if the value did not pass the validation.
	*/
	_getValidatedPropertyChangeCallback(options) {
		const { commandName, viewField, validator, errorText } = options;
		const setErrorTextDebounced = debounce(() => {
			viewField.errorText = errorText;
		}, ERROR_TEXT_TIMEOUT$1);
		return (evt, propertyName, newValue) => {
			setErrorTextDebounced.cancel();
			if (!this._isReady) return;
			if (validator(newValue)) {
				const executedCommandName = commandName === "tableCellWidth" ? this._getWidthCommandName() : commandName;
				this.editor.execute(executedCommandName, {
					value: newValue,
					batch: this._undoStepBatch
				});
				viewField.errorText = null;
			} else setErrorTextDebounced();
		};
	}
	/**
	* Returns the command that drives the width field. When the table is resized and the selection maps onto a single
	* column, the width field is bound to (and executes) the `'tableColumnWidth'` command, so the width applies to the
	* column instead of being set as a per-cell width that the column group would shadow. Otherwise it falls back to
	* the `'tableCellWidth'` command.
	*/
	_getWidthCommandName() {
		const columnWidthCommand = this.editor.commands.get("tableColumnWidth");
		return columnWidthCommand && columnWidthCommand.isEnabled ? "tableColumnWidth" : "tableCellWidth";
	}
};

/**
* @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 table/tablecellproperties/commands/tablecellpropertycommand
*/
/**
* The table cell attribute command.
*
* The command is a base command for other table cell property commands.
*/
var TableCellPropertyCommand = class extends Command {
	/**
	* The attribute that will be set by the command.
	*/
	attributeName;
	/**
	* The default value for the attribute.
	*
	* @readonly
	*/
	_defaultValue;
	/**
	* The default value for the attribute for the content table.
	*/
	_defaultContentTableValue;
	/**
	* The default value for the attribute for the layout table.
	*/
	_defaultLayoutTableValue;
	/**
	* Creates a new `TableCellPropertyCommand` instance.
	*
	* @param editor An editor in which this command will be used.
	* @param attributeName Table cell attribute name.
	* @param defaultValue The default value of the attribute.
	*/
	constructor(editor, attributeName, defaultValue) {
		super(editor);
		this.attributeName = attributeName;
		this._defaultContentTableValue = defaultValue;
		switch (attributeName) {
			case "tableCellType":
				this._defaultLayoutTableValue = "data";
				break;
			case "tableCellBorderStyle":
				this._defaultLayoutTableValue = "none";
				break;
			case "tableCellHorizontalAlignment":
				this._defaultLayoutTableValue = "left";
				break;
			case "tableCellVerticalAlignment":
				this._defaultLayoutTableValue = "middle";
				break;
			default: this._defaultLayoutTableValue = void 0;
		}
	}
	/**
	* @inheritDoc
	*/
	refresh() {
		const selection = this.editor.model.document.selection;
		const selectedTableCells = this.editor.plugins.get("TableUtils").getSelectionAffectedTableCells(selection);
		const table = getSelectionAffectedTable(selection);
		this._defaultValue = !table || table.getAttribute("tableType") !== "layout" ? this._defaultContentTableValue : this._defaultLayoutTableValue;
		this.isEnabled = !!selectedTableCells.length;
		this.value = this._getSingleValue(selectedTableCells);
	}
	/**
	* Executes the command.
	*
	* @fires execute
	* @param options.value If set, the command will set the attribute on selected table cells.
	* If it is not set, the command will remove the attribute from the selected table cells.
	* @param options.batch Pass the model batch instance to the command to aggregate changes,
	* for example to allow a single undo step for multiple executions.
	*/
	execute(options = {}) {
		const { value, batch } = options;
		const model = this.editor.model;
		const tableCells = this.editor.plugins.get("TableUtils").getSelectionAffectedTableCells(model.document.selection);
		const valueToSet = this._getValueToSet(value);
		model.enqueueChange(batch, (writer) => {
			if (valueToSet) tableCells.forEach((tableCell) => writer.setAttribute(this.attributeName, valueToSet, tableCell));
			else tableCells.forEach((tableCell) => writer.removeAttribute(this.attributeName, tableCell));
			this.fire("afterExecute", {
				writer,
				tableCells,
				valueToSet
			});
		});
	}
	/**
	* Returns the attribute value for a table cell.
	*/
	_getAttribute(tableCell) {
		if (!tableCell) return;
		const value = tableCell.getAttribute(this.attributeName);
		if (value === this._defaultValue) return;
		return value;
	}
	/**
	* Returns the proper model value. It can be used to add a default unit to numeric values.
	*/
	_getValueToSet(value) {
		if (value === this._defaultValue) return;
		return value;
	}
	/**
	* Returns a single value for all selected table cells. If the value is the same for all cells,
	* it will be returned (`undefined` otherwise).
	*/
	_getSingleValue(tableCells) {
		const firstCellValue = this._getAttribute(tableCells[0]);
		return tableCells.every((tableCells) => this._getAttribute(tableCells) === firstCellValue) ? firstCellValue : 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
*/
/**
* The table cell width command.
*
* The command is registered by the {@link module:table/tablecellwidth/tablecellwidthediting~TableCellWidthEditing} as
* the `'tableCellWidth'` editor command.
*
* To change the width of selected cells, execute the command:
*
* ```ts
* editor.execute( 'tableCellWidth', {
*   value: '50px'
* } );
* ```
*
* **Note**: This command adds a default `'px'` unit to numeric values. Executing:
*
* ```ts
* editor.execute( 'tableCellWidth', {
*   value: '50'
* } );
* ```
*
* will set the `width` attribute to `'50px'` in the model.
*/
var TableCellWidthCommand = class extends TableCellPropertyCommand {
	/**
	* Creates a new `TableCellWidthCommand` instance.
	*
	* @param editor An editor in which this command will be used.
	* @param defaultValue The default value of the attribute.
	*/
	constructor(editor, defaultValue) {
		super(editor, "tableCellWidth", defaultValue);
	}
	/**
	* @inheritDoc
	*/
	_getValueToSet(value) {
		value = addDefaultUnitToNumericValue(value, "px");
		if (value === this._defaultValue) return;
		return value;
	}
};

/**
* @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 table/tablecellwidth/tablecellwidthediting
*/
/**
* The table cell width editing feature.
*
* Introduces `tableCellWidth` table cell model attribute alongside with its converters
* and a command.
*/
var TableCellWidthEditing = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "TableCellWidthEditing";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [TableEditing];
	}
	/**
	* @inheritDoc
	*/
	init() {
		const editor = this.editor;
		const defaultTableCellProperties = getNormalizedDefaultCellProperties(editor.config.get("table.tableCellProperties.defaultProperties"));
		enableProperty(editor.model.schema, editor.conversion, {
			modelAttribute: "tableCellWidth",
			styleName: "width",
			attributeName: "width",
			attributeType: "length",
			defaultValue: defaultTableCellProperties.width
		});
		editor.commands.add("tableCellWidth", new TableCellWidthCommand(editor, defaultTableCellProperties.width));
	}
};

/**
* @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 table cell padding command.
*
* The command is registered by the {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
* the `'tableCellPadding'` editor command.
*
* To change the padding of selected cells, execute the command:
*
* ```ts
* editor.execute( 'tableCellPadding', {
*   value: '5px'
* } );
* ```
*
* **Note**: This command adds the default `'px'` unit to numeric values. Executing:
*
* ```ts
* editor.execute( 'tableCellPadding', {
*   value: '5'
* } );
* ```
*
* will set the `padding` attribute to `'5px'` in the model.
*/
var TableCellPaddingCommand = class extends TableCellPropertyCommand {
	/**
	* Creates a new `TableCellPaddingCommand` instance.
	*
	* @param editor An editor in which this command will be used.
	* @param defaultValue The default value of the attribute.
	*/
	constructor(editor, defaultValue) {
		super(editor, "tableCellPadding", defaultValue);
	}
	/**
	* @inheritDoc
	*/
	_getAttribute(tableCell) {
		if (!tableCell) return;
		const value = getSingleValue(tableCell.getAttribute(this.attributeName));
		if (value === this._defaultValue) return;
		return value;
	}
	/**
	* @inheritDoc
	*/
	_getValueToSet(value) {
		const newValue = addDefaultUnitToNumericValue(value, "px");
		if (newValue === this._defaultValue) return;
		return newValue;
	}
};

/**
* @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 table cell height command.
*
* The command is registered by the {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
* the `'tableCellHeight'` editor command.
*
* To change the height of selected cells, execute the command:
*
* ```ts
* editor.execute( 'tableCellHeight', {
*   value: '50px'
* } );
* ```
*
* **Note**: This command adds the default `'px'` unit to numeric values. Executing:
*
* ```ts
* editor.execute( 'tableCellHeight', {
*   value: '50'
* } );
* ```
*
* will set the `height` attribute to `'50px'` in the model.
*/
var TableCellHeightCommand = class extends TableCellPropertyCommand {
	/**
	* Creates a new `TableCellHeightCommand` instance.
	*
	* @param editor An editor in which this command will be used.
	* @param defaultValue The default value of the attribute.
	*/
	constructor(editor, defaultValue) {
		super(editor, "tableCellHeight", defaultValue);
	}
	/**
	* @inheritDoc
	*/
	_getValueToSet(value) {
		const newValue = addDefaultUnitToNumericValue(value, "px");
		if (newValue === this._defaultValue) return;
		return newValue;
	}
};

/**
* @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 table cell background color command.
*
* The command is registered by the {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
* the `'tableCellBackgroundColor'` editor command.
*
* To change the background color of selected cells, execute the command:
*
* ```ts
* editor.execute( 'tableCellBackgroundColor', {
*   value: '#f00'
* } );
* ```
*/
var TableCellBackgroundColorCommand = class extends TableCellPropertyCommand {
	/**
	* Creates a new `TableCellBackgroundColorCommand` instance.
	*
	* @param editor An editor in which this command will be used.
	* @param defaultValue The default value of the attribute.
	*/
	constructor(editor, defaultValue) {
		super(editor, "tableCellBackgroundColor", defaultValue);
	}
};

/**
* @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 table cell vertical alignment command.
*
* The command is registered by the {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
* the `'tableCellVerticalAlignment'` editor command.
*
* To change the vertical text alignment of selected cells, execute the command:
*
* ```ts
* editor.execute( 'tableCellVerticalAlignment', {
*   value: 'top'
* } );
* ```
*
* The following values, corresponding to the
* [`vertical-align` CSS attribute](https://developer.mozilla.org/en-US/docs/Web/CSS/vertical-align), are allowed:
*
* * `'top'`
* * `'bottom'`
*
* The `'middle'` value is the default one so there is no need to set it.
*/
var TableCellVerticalAlignmentCommand = class extends TableCellPropertyCommand {
	/**
	* Creates a new `TableCellVerticalAlignmentCommand` instance.
	*
	* @param editor An editor in which this command will be used.
	* @param defaultValue The default value for the "alignment" attribute.
	*/
	constructor(editor, defaultValue) {
		super(editor, "tableCellVerticalAlignment", defaultValue);
	}
};

/**
* @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 table cell horizontal alignment command.
*
* The command is registered by the {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
* the `'tableCellHorizontalAlignment'` editor command.
*
* To change the horizontal text alignment of selected cells, execute the command:
*
* ```ts
* editor.execute( 'tableCellHorizontalAlignment', {
*  value: 'right'
* } );
* ```
*/
var TableCellHorizontalAlignmentCommand = class extends TableCellPropertyCommand {
	/**
	* Creates a new `TableCellHorizontalAlignmentCommand` instance.
	*
	* @param editor An editor in which this command will be used.
	* @param defaultValue The default value for the "alignment" attribute.
	*/
	constructor(editor, defaultValue) {
		super(editor, "tableCellHorizontalAlignment", defaultValue);
	}
};

/**
* @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 table cell border style command.
*
* The command is registered by the {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
* the `'tableCellBorderStyle'` editor command.
*
* To change the border style of selected cells, execute the command:
*
* ```ts
* editor.execute( 'tableCellBorderStyle', {
*   value: 'dashed'
* } );
* ```
*/
var TableCellBorderStyleCommand = class extends TableCellPropertyCommand {
	/**
	* Creates a new `TableCellBorderStyleCommand` instance.
	*
	* @param editor An editor in which this command will be used.
	* @param defaultValue The default value of the attribute.
	*/
	constructor(editor, defaultValue) {
		super(editor, "tableCellBorderStyle", defaultValue);
	}
	/**
	* @inheritDoc
	*/
	_getAttribute(tableCell) {
		if (!tableCell) return;
		const value = getSingleValue(tableCell.getAttribute(this.attributeName));
		if (value === this._defaultValue) return;
		return value;
	}
};

/**
* @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 table cell border color command.
*
* The command is registered by the {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
* the `'tableCellBorderColor'` editor command.
*
* To change the border color of selected cells, execute the command:
*
* ```ts
* editor.execute( 'tableCellBorderColor', {
*   value: '#f00'
* } );
* ```
*/
var TableCellBorderColorCommand = class extends TableCellPropertyCommand {
	/**
	* Creates a new `TableCellBorderColorCommand` instance.
	*
	* @param editor An editor in which this command will be used.
	* @param defaultValue The default value of the attribute.
	*/
	constructor(editor, defaultValue) {
		super(editor, "tableCellBorderColor", defaultValue);
	}
	/**
	* @inheritDoc
	*/
	_getAttribute(tableCell) {
		if (!tableCell) return;
		const value = getSingleValue(tableCell.getAttribute(this.attributeName));
		if (value === this._defaultValue) return;
		return value;
	}
};

/**
* @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 table cell border width command.
*
* The command is registered by the {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
* the `'tableCellBorderWidth'` editor command.
*
* To change the border width of selected cells, execute the command:
*
* ```ts
* editor.execute( 'tableCellBorderWidth', {
*   value: '5px'
* } );
* ```
*
* **Note**: This command adds the default `'px'` unit to numeric values. Executing:
*
* ```ts
* editor.execute( 'tableCellBorderWidth', {
*   value: '5'
* } );
* ```
*
* will set the `borderWidth` attribute to `'5px'` in the model.
*/
var TableCellBorderWidthCommand = class extends TableCellPropertyCommand {
	/**
	* Creates a new `TableCellBorderWidthCommand` instance.
	*
	* @param editor An editor in which this command will be used.
	* @param defaultValue The default value of the attribute.
	*/
	constructor(editor, defaultValue) {
		super(editor, "tableCellBorderWidth", defaultValue);
	}
	/**
	* @inheritDoc
	*/
	_getAttribute(tableCell) {
		if (!tableCell) return;
		const value = getSingleValue(tableCell.getAttribute(this.attributeName));
		if (value === this._defaultValue) return;
		return value;
	}
	/**
	* @inheritDoc
	*/
	_getValueToSet(value) {
		const newValue = addDefaultUnitToNumericValue(value, "px");
		if (newValue === this._defaultValue) return;
		return newValue;
	}
};

/**
* @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 table cell type command.
*
* The command is registered by the {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
* the `'tableCellType'` editor command.
*
* To change the type of selected cells, execute the command:
*
* ```ts
* editor.execute( 'tableCellType', {
*   value: 'header'
* } );
* ```
*
* The `value` can be either `'header'` or `'data'`.
* It'll return `undefined` if multiple types are selected.
*/
var TableCellTypeCommand = class extends TableCellPropertyCommand {
	/**
	* Creates a new `TableCellTypeCommand` instance.
	*
	* @param editor An editor in which this command will be used.
	*/
	constructor(editor) {
		super(editor, "tableCellType", "data");
		this.on("afterExecute", (_, data) => {
			const { writer, tableCells } = data;
			updateTablesHeadingAttributes(this.editor.plugins.get(TableUtils), writer, groupCellsByTable(tableCells).keys());
		});
	}
	/**
	* @inheritDoc
	*/
	refresh() {
		super.refresh();
		const table = getSelectionAffectedTable(this.editor.model.document.selection);
		if (this.isEnabled && table && table.getAttribute("tableType") === "layout") this.isEnabled = false;
	}
	/**
	* Returns the attribute value for a table cell.
	*/
	_getAttribute(tableCell) {
		return tableCell?.getAttribute(this.attributeName) || "data";
	}
};
/**
* Updates the `headingRows` and `headingColumns` attributes of the given tables
* based on the `tableCellType` of their cells.
*/
function updateTablesHeadingAttributes(tableUtils, writer, tables) {
	let changed = false;
	for (const table of tables) {
		let headingRows = table.getAttribute("headingRows") || 0;
		let headingColumns = table.getAttribute("headingColumns") || 0;
		const footerRows = table.getAttribute("footerRows") || 0;
		const footerIndex = tableUtils.getRows(table) - footerRows;
		const processColumnsFirst = headingColumns > headingRows;
		if (processColumnsFirst) {
			const newHeadingColumns = getAdjustedHeadingSectionSize(tableUtils, table, "column", headingColumns, headingRows);
			if (newHeadingColumns !== headingColumns) {
				tableUtils.setHeadingColumnsCount(writer, table, newHeadingColumns, { updateCellType: false });
				headingColumns = newHeadingColumns;
				changed = true;
			}
		}
		let newHeadingRows = getAdjustedHeadingSectionSize(tableUtils, table, "row", headingRows, headingColumns);
		if (footerRows > 0) newHeadingRows = Math.min(newHeadingRows, footerIndex);
		if (newHeadingRows !== headingRows) {
			tableUtils.setHeadingRowsCount(writer, table, newHeadingRows, { updateCellType: false });
			headingRows = newHeadingRows;
			changed = true;
		}
		if (!processColumnsFirst) {
			const newHeadingColumns = getAdjustedHeadingSectionSize(tableUtils, table, "column", headingColumns, headingRows);
			if (newHeadingColumns !== headingColumns) {
				tableUtils.setHeadingColumnsCount(writer, table, newHeadingColumns, { updateCellType: false });
				changed = true;
			}
		}
	}
	return changed;
}
/**
* Calculates the adjusted size of a heading section (rows or columns).
*
* The algorithm iterates through rows (or columns) to determine if they should be part of the heading section.
* A row/column is included if:
* 1. All its cells are of type 'header'.
* 2. AND it contains at least one header cell that is NOT already covered by the perpendicular heading section.
*
* This check prevents the algorithm from aggressively expanding the heading section when cells are already
* headers due to the other dimension.
*
* Consider a 2x2 table where all cells are headers:
*
* ```
*    C0  C1
*   +---+---+
* R0| H | H |
*   +---+---+
* R1| H | H |
*   +---+---+
* ```
*
* If `headingColumns=2`, both C0 and C1 are heading columns.
* If we want `headingRows=1` (only R0), the algorithm must NOT include R1, even though R1 consists of header cells.
* R1's cells are headers because of C0 and C1.
*
* Without this check, the algorithm would see that R1 is all headers and force `headingRows` to 2.
* This would prevent the user from reducing `headingRows` from 2 to 1 without converting R1 cells to 'data'
* (which would incorrectly break C0 and C1).
*/
function getAdjustedHeadingSectionSize(tableUtils, table, mode, currentSize, perpendicularHeadingSize) {
	const totalRowsOrColumns = mode === "row" ? tableUtils.getRows(table) : tableUtils.getColumns(table);
	let size = currentSize;
	for (let currentIndex = 0; currentIndex < totalRowsOrColumns; currentIndex++) {
		const walker = new TableWalker(table, { [mode]: currentIndex });
		let allCellsAreHeaders = true;
		let hasHeaderOutsidePerpendicularSection = false;
		for (const { cell, row, column } of walker) {
			if (!isTableHeaderCellType(cell.getAttribute("tableCellType"))) {
				allCellsAreHeaders = false;
				break;
			}
			if ((mode === "row" ? column : row) >= perpendicularHeadingSize) hasHeaderOutsidePerpendicularSection = true;
		}
		if (!allCellsAreHeaders) return Math.min(size, currentIndex);
		if (hasHeaderOutsidePerpendicularSection) size = Math.max(size, currentIndex + 1);
	}
	return Math.min(size, totalRowsOrColumns);
}

/**
* @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 table/tablecellproperties/tablecellpropertiesediting
*/
const VALIGN_VALUES_REG_EXP = /^(top|middle|bottom)$/;
const ALIGN_VALUES_REG_EXP = /^(left|center|right|justify)$/;
/**
* The table cell properties editing feature.
*
* Introduces table cell model attributes and their conversion:
*
* - border: `tableCellBorderStyle`, `tableCellBorderColor` and `tableCellBorderWidth`
* - background color: `tableCellBackgroundColor`
* - cell padding: `tableCellPadding`
* - horizontal and vertical alignment: `tableCellHorizontalAlignment`, `tableCellVerticalAlignment`
* - cell width and height: `tableCellWidth`, `tableCellHeight`
*
* It also registers commands used to manipulate the above attributes:
*
* - border: the `'tableCellBorderStyle'`, `'tableCellBorderColor'` and `'tableCellBorderWidth'` commands
* - background color: the `'tableCellBackgroundColor'` command
* - cell padding: the `'tableCellPadding'` command
* - horizontal and vertical alignment: the `'tableCellHorizontalAlignment'` and `'tableCellVerticalAlignment'` commands
* - width and height: the `'tableCellWidth'` and `'tableCellHeight'` commands
*/
var TableCellPropertiesEditing = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "TableCellPropertiesEditing";
	}
	/**
	* @inheritDoc
	* @internal
	*/
	static get licenseFeatureCode() {
		return "TCP";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	static get isPremiumPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [TableEditing, TableCellWidthEditing];
	}
	/**
	* @inheritDoc
	*/
	init() {
		const editor = this.editor;
		const schema = editor.model.schema;
		const conversion = editor.conversion;
		editor.config.define("table.tableCellProperties.defaultProperties", {});
		const defaultTableCellProperties = getNormalizedDefaultCellProperties(editor.config.get("table.tableCellProperties.defaultProperties"), {
			includeVerticalAlignmentProperty: true,
			includeHorizontalAlignmentProperty: true,
			includePaddingProperty: true,
			isRightToLeftContent: editor.locale.contentLanguageDirection === "rtl"
		});
		editor.data.addStyleProcessorRules(addBorderStylesRules);
		enableBorderProperties$1(editor, {
			color: defaultTableCellProperties.borderColor,
			style: defaultTableCellProperties.borderStyle,
			width: defaultTableCellProperties.borderWidth
		});
		editor.commands.add("tableCellBorderStyle", new TableCellBorderStyleCommand(editor, defaultTableCellProperties.borderStyle));
		editor.commands.add("tableCellBorderColor", new TableCellBorderColorCommand(editor, defaultTableCellProperties.borderColor));
		editor.commands.add("tableCellBorderWidth", new TableCellBorderWidthCommand(editor, defaultTableCellProperties.borderWidth));
		enableProperty(schema, conversion, {
			modelAttribute: "tableCellHeight",
			styleName: "height",
			attributeName: "height",
			attributeType: "length",
			defaultValue: defaultTableCellProperties.height
		});
		editor.commands.add("tableCellHeight", new TableCellHeightCommand(editor, defaultTableCellProperties.height));
		editor.data.addStyleProcessorRules(addPaddingStylesRules);
		enableProperty(schema, conversion, {
			modelAttribute: "tableCellPadding",
			styleName: "padding",
			reduceBoxSides: true,
			defaultValue: defaultTableCellProperties.padding
		});
		enableTableCellPaddingAttribute(editor, defaultTableCellProperties.padding);
		editor.commands.add("tableCellPadding", new TableCellPaddingCommand(editor, defaultTableCellProperties.padding));
		editor.data.addStyleProcessorRules(addBackgroundStylesRules);
		enableProperty(schema, conversion, {
			modelAttribute: "tableCellBackgroundColor",
			styleName: "background-color",
			attributeName: "bgcolor",
			attributeType: "color",
			defaultValue: defaultTableCellProperties.backgroundColor
		});
		editor.commands.add("tableCellBackgroundColor", new TableCellBackgroundColorCommand(editor, defaultTableCellProperties.backgroundColor));
		enableHorizontalAlignmentProperty(schema, conversion, defaultTableCellProperties.horizontalAlignment);
		enableLegacyHorizontalAlignmentAttribute(conversion);
		editor.commands.add("tableCellHorizontalAlignment", new TableCellHorizontalAlignmentCommand(editor, defaultTableCellProperties.horizontalAlignment));
		enableVerticalAlignmentProperty(schema, conversion, defaultTableCellProperties.verticalAlignment);
		editor.commands.add("tableCellVerticalAlignment", new TableCellVerticalAlignmentCommand(editor, defaultTableCellProperties.verticalAlignment));
		enableCellTypeProperty(editor);
		editor.commands.add("tableCellType", new TableCellTypeCommand(editor));
	}
};
/**
* Enables the `'tableCellBorderStyle'`, `'tableCellBorderColor'` and `'tableCellBorderWidth'` attributes for table cells.
*
* @param editor The editor instance.
* @param defaultBorder The default border values.
* @param defaultBorder.color The default `tableCellBorderColor` value.
* @param defaultBorder.style The default `tableCellBorderStyle` value.
* @param defaultBorder.width The default `tableCellBorderWidth` value.
*/
function enableBorderProperties$1(editor, defaultBorder) {
	const { conversion } = editor;
	const { schema } = editor.model;
	const modelAttributes = {
		width: "tableCellBorderWidth",
		color: "tableCellBorderColor",
		style: "tableCellBorderStyle"
	};
	schema.extend("tableCell", { allowAttributes: Object.values(modelAttributes) });
	for (const modelAttribute of Object.values(modelAttributes)) schema.setAttributeProperties(modelAttribute, { isFormatting: true });
	upcastBorderStyles(editor, "td", modelAttributes, defaultBorder);
	upcastBorderStyles(editor, "th", modelAttributes, defaultBorder);
	downcastAttributeToStyle(conversion, {
		modelElement: "tableCell",
		modelAttribute: modelAttributes.style,
		styleName: "border-style"
	});
	downcastAttributeToStyle(conversion, {
		modelElement: "tableCell",
		modelAttribute: modelAttributes.color,
		styleName: "border-color"
	});
	downcastAttributeToStyle(conversion, {
		modelElement: "tableCell",
		modelAttribute: modelAttributes.width,
		styleName: "border-width"
	});
}
/**
* Enables the `'tableCellHorizontalAlignment'` attribute for table cells.
*
* @param defaultValue The default horizontal alignment value.
*/
function enableHorizontalAlignmentProperty(schema, conversion, defaultValue) {
	schema.extend("tableCell", { allowAttributes: ["tableCellHorizontalAlignment"] });
	schema.setAttributeProperties("tableCellHorizontalAlignment", { isFormatting: true });
	conversion.for("downcast").attributeToAttribute({
		model: {
			name: "tableCell",
			key: "tableCellHorizontalAlignment"
		},
		view: (alignment) => ({
			key: "style",
			value: { "text-align": alignment }
		})
	});
	conversion.for("upcast").attributeToAttribute({
		view: {
			name: /^(td|th)$/,
			styles: { "text-align": ALIGN_VALUES_REG_EXP }
		},
		model: {
			key: "tableCellHorizontalAlignment",
			value: (viewElement, conversionApi, data) => {
				const localDefaultValue = getDefaultValueAdjusted(defaultValue, "left", data);
				const align = viewElement.getStyle("text-align");
				if (align !== localDefaultValue) return align;
				conversionApi.consumable.consume(viewElement, { styles: "text-align" });
			}
		}
	});
}
/**
* Upcasts legacy `td[align]` property to proper block alignment attributes in child elements.
* If there is no block alignment property supported on the element, then the `alignment` fallback will be used.
*
* See: https://github.com/ckeditor/ckeditor5/issues/20042
*/
function enableLegacyHorizontalAlignmentAttribute(conversion) {
	conversion.for("upcast").add((dispatcher) => {
		const matcher = new Matcher({
			name: /^(td|th)$/,
			attributes: { align: ALIGN_VALUES_REG_EXP }
		});
		dispatcher.on("element", (evt, data, conversionApi) => {
			if (!matcher.match(data.viewItem)) return;
			const modelElement = data.modelRange?.start.nodeAfter;
			/* istanbul ignore if -- @preserve */
			if (!modelElement?.is("element")) return;
			const alignValue = data.viewItem.getAttribute("align");
			if (!conversionApi.consumable.consume(data.viewItem, { attributes: ["align"] })) return;
			for (const child of modelElement.getChildren())
 /* v8 ignore else -- A table cell only contains block elements, so its children are always elements. */
			if (child.is("element")) applyAlignmentToChild(child, alignValue, conversionApi);
		}, { priority: "low" });
	});
	function applyAlignmentToChild(child, alignValue, { schema, writer }) {
		const definition = schema.getDefinition(child);
		/* v8 ignore else -- A converted child element is always registered in the schema, so it always has a definition. */
		if (definition) for (const attrName of definition.allowAttributes) {
			if (child.hasAttribute(attrName)) continue;
			const { blockAlignment } = schema.getAttributeProperties(attrName);
			if (!blockAlignment) continue;
			const mappedValue = (typeof blockAlignment === "function" ? blockAlignment(child) : blockAlignment)[alignValue];
			if (mappedValue && !mappedValue.isDefault) writer.setAttribute(attrName, mappedValue.value, child);
			return;
		}
	}
}
/**
* Enables the `'verticalAlignment'` attribute for table cells.
*
* @param defaultValue The default vertical alignment value.
*/
function enableVerticalAlignmentProperty(schema, conversion, defaultValue) {
	schema.extend("tableCell", { allowAttributes: ["tableCellVerticalAlignment"] });
	schema.setAttributeProperties("tableCellVerticalAlignment", { isFormatting: true });
	conversion.for("downcast").attributeToAttribute({
		model: {
			name: "tableCell",
			key: "tableCellVerticalAlignment"
		},
		view: (alignment) => ({
			key: "style",
			value: { "vertical-align": alignment }
		})
	});
	conversion.for("upcast").attributeToAttribute({
		view: {
			name: /^(td|th)$/,
			styles: { "vertical-align": VALIGN_VALUES_REG_EXP }
		},
		model: {
			key: "tableCellVerticalAlignment",
			value: (viewElement, conversionApi, data) => {
				const localDefaultValue = getDefaultValueAdjusted(defaultValue, "middle", data);
				const align = viewElement.getStyle("vertical-align");
				if (align !== localDefaultValue) return align;
				conversionApi.consumable.consume(viewElement, { styles: "vertical-align" });
			}
		}
	}).attributeToAttribute({
		view: {
			name: /^(td|th)$/,
			attributes: { valign: VALIGN_VALUES_REG_EXP }
		},
		model: {
			key: "tableCellVerticalAlignment",
			value: (viewElement, conversionApi, data) => {
				const localDefaultValue = getDefaultValueAdjusted(defaultValue, "middle", data);
				const valign = viewElement.getAttribute("valign");
				if (valign !== localDefaultValue) return valign;
				conversionApi.consumable.consume(viewElement, { attributes: "valign" });
			}
		}
	});
}
/**
* Enables the `tableCellType` attribute for table cells.
*/
function enableCellTypeProperty(editor) {
	const { model, conversion, editing, config } = editor;
	const { schema } = model;
	config.define("table.tableCellProperties.scopedHeaders", true);
	const scopedHeaders = !!config.get("table.tableCellProperties.scopedHeaders");
	const tableUtils = editor.plugins.get(TableUtils);
	schema.extend("tableCell", { allowAttributes: ["tableCellType"] });
	schema.setAttributeProperties("tableCellType", { isFormatting: true });
	schema.addAttributeCheck((context) => {
		if (Array.from(context).reverse().find((item) => item.name === "table")?.getAttribute("tableType") === "layout") return false;
	}, "tableCellType");
	conversion.for("upcast").add((dispatcher) => {
		dispatcher.on("element:th", (evt, data, conversionApi) => {
			const { writer } = conversionApi;
			const { modelRange } = data;
			const modelElement = modelRange?.start.nodeAfter;
			if (modelElement?.is("element", "tableCell") && !modelElement.hasAttribute("tableCellType")) writer.setAttribute("tableCellType", "header", modelElement);
		});
		dispatcher.on("element:table", (evt, data, conversionApi) => {
			const { writer } = conversionApi;
			const { modelRange } = data;
			const modelElement = modelRange?.start.nodeAfter;
			if (modelElement?.is("element", "table") && modelElement.getAttribute("tableType") === "layout") {
				for (const { cell } of new TableWalker(modelElement)) if (isTableHeaderCellType(cell.getAttribute("tableCellType"))) {
					writer.setAttribute("tableType", "content", modelElement);
					break;
				}
			}
		}, { priority: priorities.low - 1 });
	});
	if (scopedHeaders) {
		conversion.for("downcast").attributeToAttribute({
			model: {
				name: "tableCell",
				key: "tableCellType"
			},
			view: (modelAttributeValue) => {
				switch (modelAttributeValue) {
					case "header-row": return {
						key: "scope",
						value: "row"
					};
					case "header-column": return {
						key: "scope",
						value: "col"
					};
				}
			}
		});
		conversion.for("upcast").add((dispatcher) => {
			dispatcher.on("element:th", (_, data, conversionApi) => {
				const { writer, consumable } = conversionApi;
				const { viewItem, modelRange } = data;
				const modelElement = modelRange?.start.nodeAfter;
				if (!modelElement) return;
				if (modelElement.getAttribute("tableCellType") === "header" && consumable.consume(viewItem, { attributes: ["scope"] })) switch (viewItem.getAttribute("scope")) {
					case "row":
						writer.setAttribute("tableCellType", "header-row", modelElement);
						break;
					case "col":
						writer.setAttribute("tableCellType", "header-column", modelElement);
						break;
				}
			});
		});
	}
	model.document.registerPostFixer((writer) => {
		const changes = model.document.differ.getChanges();
		const tablesToCheck = /* @__PURE__ */ new Set();
		for (const change of changes) {
			if (change.type === "attribute" && (change.attributeKey === "headingRows" || change.attributeKey === "headingColumns")) {
				const table = change.range.start.nodeAfter;
				if (table?.is("element", "table") && table.root.rootName !== "$graveyard") tablesToCheck.add(table);
			}
			if (change.type === "attribute" && change.attributeKey === "tableCellType") {
				const cell = change.range.start.nodeAfter;
				if (cell?.is("element", "tableCell") && cell.root.rootName !== "$graveyard") {
					const table = cell.findAncestor("table");
					/* v8 ignore else -- A table cell always lives inside a table, so it always has a table ancestor here. */
					if (table) tablesToCheck.add(table);
				}
			}
			if (change.type === "insert" && change.position.nodeAfter) {
				for (const { item } of model.createRangeOn(change.position.nodeAfter)) if (item.is("element", "tableCell") && item.getAttribute("tableCellType") && item.root.rootName !== "$graveyard") {
					const table = item.findAncestor("table");
					/* v8 ignore else -- An inserted table cell always lives inside a table, so it always has a table ancestor here. */
					if (table) tablesToCheck.add(table);
				}
			}
		}
		return updateTablesHeadingAttributes(tableUtils, writer, tablesToCheck);
	});
	model.document.on("change:data", () => {
		const { differ } = model.document;
		const cellsToReconvert = /* @__PURE__ */ new Set();
		for (const change of differ.getChanges()) if (change.type === "attribute" && change.attributeKey === "tableCellType") {
			const tableCell = change.range.start.nodeAfter;
			if (tableCell.is("element", "tableCell")) cellsToReconvert.add(tableCell);
		}
		for (const tableCell of cellsToReconvert) {
			const viewElement = editing.mapper.toViewElement(tableCell);
			const expectedElementName = isTableHeaderCellType(tableCell.getAttribute("tableCellType")) ? "th" : "td";
			if (viewElement?.name !== expectedElementName) editing.reconvertItem(tableCell);
		}
	});
}
/**
* Enables the upcasting of the `cellpadding` attribute from table to table cells padding.
*
* @param editor The editor instance.
* @param defaultPadding The default padding value.
*/
function enableTableCellPaddingAttribute(editor, defaultPadding) {
	upcastTableCellPaddingAttribute(editor, "td", defaultPadding);
	upcastTableCellPaddingAttribute(editor, "th", defaultPadding);
}

/**
* @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 table/tablecellproperties
*/
/**
* The table cell properties feature. Enables support for setting properties of table cells (size, border, background, etc.).
*
* Read more in the {@glink features/tables/tables-styling Table and cell styling tools} section.
* See also the {@link module:table/tableproperties~TableProperties} plugin.
*
* This is a "glue" plugin that loads the
* {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing table cell properties editing feature} and
* the {@link module:table/tablecellproperties/tablecellpropertiesui~TableCellPropertiesUI table cell properties UI feature}.
*/
var TableCellProperties = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "TableCellProperties";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [TableCellPropertiesEditing, TableCellPropertiesUI];
	}
};

/**
* @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 table/tablelayout/tablelayoutui
*/
/**
* The table layout UI plugin. It introduces:
*
* * The `'insertTableLayout'` dropdown,
* * The `'menuBar:insertTableLayout'` menu bar menu.
*/
var TableLayoutUI = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "TableLayoutUI";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	init() {
		const editor = this.editor;
		const t = this.editor.t;
		editor.ui.componentFactory.add("insertTableLayout", (locale) => {
			const command = editor.commands.get("insertTableLayout");
			const dropdownView = createDropdown(locale);
			dropdownView.bind("isEnabled").to(command);
			dropdownView.buttonView.set({
				icon: IconTableLayout,
				label: t("Insert table layout"),
				tooltip: true
			});
			let insertTableLayoutView;
			dropdownView.on("change:isOpen", () => {
				if (insertTableLayoutView) return;
				insertTableLayoutView = new InsertTableView(locale);
				dropdownView.panelView.children.add(insertTableLayoutView);
				insertTableLayoutView.delegate("execute").to(dropdownView);
				dropdownView.on("execute", () => {
					editor.execute("insertTableLayout", {
						rows: insertTableLayoutView.rows,
						columns: insertTableLayoutView.columns
					});
					editor.editing.view.focus();
				});
			});
			return dropdownView;
		});
		editor.ui.componentFactory.add("menuBar:insertTableLayout", (locale) => {
			const command = editor.commands.get("insertTableLayout");
			const menuView = new MenuBarMenuView(locale);
			const insertTableLayoutView = new InsertTableView(locale);
			insertTableLayoutView.delegate("execute").to(menuView);
			menuView.on("change:isOpen", (event, name, isOpen) => {
				if (!isOpen) insertTableLayoutView.reset();
			});
			insertTableLayoutView.on("execute", () => {
				editor.execute("insertTableLayout", {
					rows: insertTableLayoutView.rows,
					columns: insertTableLayoutView.columns
				});
				editor.editing.view.focus();
			});
			menuView.buttonView.set({
				label: t("Table layout"),
				icon: IconTableLayout
			});
			menuView.panelView.children.add(insertTableLayoutView);
			menuView.bind("isEnabled").to(command);
			return menuView;
		});
		editor.ui.componentFactory.add("tableType", () => {
			const editor = this.editor;
			const t = editor.t;
			const button = new DropdownButtonView(editor.locale);
			button.set({
				label: t("Table type"),
				icon: IconTableProperties,
				tooltip: true
			});
			return createTableTypeDropdown(editor, button);
		});
	}
	/**
	* @inheritDoc
	*/
	afterInit() {
		const { editor } = this;
		const { ui, plugins } = editor;
		if (!editor.plugins.has("TablePropertiesUI")) return;
		const tablePropertiesUI = plugins.get("TablePropertiesUI");
		ui.componentFactory.add("tableProperties", (locale) => {
			const splitButtonView = new SplitButtonView(locale, tablePropertiesUI._createTablePropertiesButton());
			return createTableTypeDropdown(editor, splitButtonView);
		});
	}
};
/**
* Creates a dropdown for the table type selection.
*
* @param editor The editor instance.
* @param dropdownButton The button view that will be used as the dropdown trigger.
* @returns A dropdown view containing table type options.
*/
function createTableTypeDropdown(editor, dropdownButton) {
	const t = editor.t;
	const locale = editor.locale;
	const tableTypeCommand = editor.commands.get("tableType");
	const dropdownView = createDropdown(locale, dropdownButton);
	addListToDropdown(dropdownView, createTableLayoutTypeDropdownItems(editor), {
		ariaLabel: t("Table type options"),
		role: "menu"
	});
	dropdownButton.tooltip = t("Choose table type");
	dropdownView.on("execute", (evt) => {
		const tableType = evt.source.tableType;
		if (tableType) tableTypeCommand.execute(tableType);
	});
	return dropdownView;
}
/**
* Creates dropdown items for table type selection.
*
* @param editor The editor instance.
* @returns A collection of dropdown items for the table type dropdown.
*/
function createTableLayoutTypeDropdownItems(editor) {
	const t = editor.t;
	const tableTypeCommand = editor.commands.get("tableType");
	const itemDefinitions = new Collection();
	itemDefinitions.add(createTableTypeDropdownItem(tableTypeCommand, "layout", t("Layout table")));
	itemDefinitions.add(createTableTypeDropdownItem(tableTypeCommand, "content", t("Content table")));
	return itemDefinitions;
}
/**
* Creates a dropdown item for a specific table type.
*
* @param tableTypeCommand The table type command.
* @param type The table type value ('layout' or 'content').
* @param label The localized label for the dropdown item.
* @returns The dropdown item definition.
*/
function createTableTypeDropdownItem(tableTypeCommand, type, label) {
	const model = new UIModel({
		label,
		role: "menuitemradio",
		withText: true,
		tableType: type
	});
	model.bind("isEnabled").to(tableTypeCommand, "isEnabled");
	model.bind("isOn").to(tableTypeCommand, "value", (value) => value === type);
	return {
		type: "button",
		model
	};
}

/**
* @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 table/commands/inserttablelayoutcommand
*/
/**
* The insert table layout command.
*
* The command is registered by {@link module:table/tablelayout/tablelayoutediting~TableLayoutEditing}
* as the `'insertTableLayout'` editor command.
*
* To insert a layout table at the current selection, execute the command and specify the dimensions:
*
* ```ts
* editor.execute( 'insertTableLayout', { rows: 20, columns: 5 } );
* ```
*/
var InsertTableLayoutCommand = class extends Command {
	/**
	* @inheritDoc
	*/
	refresh() {
		const model = this.editor.model;
		const selection = model.document.selection;
		const schema = model.schema;
		this.isEnabled = isAllowedInParent(selection, schema);
	}
	/**
	* Executes the command.
	*
	* Inserts a layout table with the given number of rows and columns into the editor.
	*
	* @param options.rows The number of rows to create in the inserted table. Default value is 2.
	* @param options.columns The number of columns to create in the inserted table. Default value is 2.
	* @param options.inheritTextFormattingAttributes Whether every empty cell should inherit the `copyOnEnter` text
	* formatting attributes (e.g. bold, font color) that were uniformly active in the content right before
	* the table, so that whichever cell the user starts typing in first continues that formatting. Defaults
	* to `true`.
	* @fires execute
	*/
	execute(options = {}) {
		const editor = this.editor;
		const model = editor.model;
		const selection = model.document.selection;
		const tableUtils = editor.plugins.get("TableUtils");
		model.change((writer) => {
			const selectionAttributesToCopy = Array.from(_getCopyOnEnterAttributes(model.schema, selection.getAttributes()));
			const normalizedOptions = {
				rows: options.rows || 2,
				columns: options.columns || 2
			};
			const table = tableUtils.createTable(writer, normalizedOptions);
			writer.setAttribute("tableType", "layout", table);
			model.insertObject(table, null, null, { findOptimalPosition: "auto" });
			const singleColumnWidth = `${100 / normalizedOptions.columns}%`;
			const columnWidths = Array(normalizedOptions.columns).fill(singleColumnWidth);
			editor.commands.get("resizeColumnWidths").execute({
				tableWidth: "100%",
				columnWidths,
				table
			});
			writer.setSelection(writer.createPositionAt(table.getNodeByPath([
				0,
				0,
				0
			]), 0));
			if (options.inheritTextFormattingAttributes !== false && selectionAttributesToCopy.length) for (const cellBlock of getEmptyTableCellBlocks(table)) for (const [key, value] of selectionAttributesToCopy) writer.setAttribute(ModelDocumentSelection._getStoreAttributeKey(key), value, cellBlock);
		});
	}
};
/**
* Checks if the table is allowed in the parent.
*/
function isAllowedInParent(selection, schema) {
	const positionParent = selection.getFirstPosition().parent;
	const validParent = positionParent === positionParent.root ? positionParent : positionParent.parent;
	return schema.checkChild(validParent, "table");
}

/**
* @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
*/
/**
* Command used by the {@link module:table/tablecolumnresize~TableColumnResize Table column resize feature} that
* updates the width of the whole table as well as its individual columns.
*/
var TableWidthsCommand = class extends Command {
	/**
	* @inheritDoc
	*/
	refresh() {
		this.isEnabled = true;
	}
	/**
	* Updated the `tableWidth` attribute of the table and the `columnWidth` attribute of the columns of that table.
	*/
	execute(options = {}) {
		const { model, plugins } = this.editor;
		let { table = model.document.selection.getSelectedElement(), columnWidths, tableWidth } = options;
		if (columnWidths) columnWidths = Array.isArray(columnWidths) ? columnWidths : columnWidths.split(",");
		model.change((writer) => {
			if (tableWidth) writer.setAttribute("tableWidth", tableWidth, table);
			else writer.removeAttribute("tableWidth", table);
			const tableColumnGroup = plugins.get("TableColumnResizeEditing").getColumnGroupElement(table);
			if (!columnWidths && !tableColumnGroup) return;
			if (!columnWidths) return writer.remove(tableColumnGroup);
			const widths = normalizeColumnWidths(columnWidths);
			removeCellWidthsFromTable(writer, table);
			if (!tableColumnGroup) {
				const colGroupElement = writer.createElement("tableColumnGroup");
				widths.forEach((columnWidth) => writer.appendElement("tableColumn", { columnWidth }, colGroupElement));
				writer.append(colGroupElement, table);
			} else Array.from(tableColumnGroup.getChildren()).forEach((column, index) => writer.setAttribute("columnWidth", widths[index], column));
		});
	}
};

/**
* @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 table/tablecolumnresize/commands/tablecolumnwidthcommand
*/
/**
* The table column width command.
*
* The command is registered by the {@link module:table/tablecolumnresize/tablecolumnresizeediting~TableColumnResizeEditing}
* as the `'tableColumnWidth'` editor command.
*
* It sets the width of every column covered by the selected cells (keeping the whole table's width mode consistent),
* so the change actually takes effect in a resized table - where a per-cell width would be shadowed by the column
* group. The command is enabled whenever the selection maps onto the columns of a resized, regular table.
*
* ```ts
* editor.execute( 'tableColumnWidth', {
*   value: '150px'
* } );
* ```
*
* **Note**: This command adds a default `'px'` unit to numeric values. Executing:
*
* ```ts
* editor.execute( 'tableColumnWidth', {
*   value: '150'
* } );
* ```
*
* will set the column width to `'150px'`.
*/
var TableColumnWidthCommand = class extends Command {
	/**
	* @inheritDoc
	*/
	refresh() {
		const editor = this.editor;
		const tableUtils = editor.plugins.get("TableUtils");
		const tableColumnResize = editor.plugins.get("TableColumnResizeEditing");
		const tableCells = tableUtils.getSelectionAffectedTableCells(editor.model.document.selection);
		const columns = tableColumnResize.getColumnIndexesForCells(tableCells);
		if (!columns) {
			this.isEnabled = false;
			this.value = null;
			return;
		}
		this.isEnabled = true;
		this.value = tableColumnResize.getTableColumnElements(columns.table)[columns.columnIndexes[0]].getAttribute("columnWidth");
	}
	/**
	* @inheritDoc
	*/
	execute(options = {}) {
		const editor = this.editor;
		const model = editor.model;
		const tableUtils = editor.plugins.get("TableUtils");
		const tableColumnResize = editor.plugins.get("TableColumnResizeEditing");
		const value = addDefaultUnitToNumericValue(options.value, "px");
		const tableCells = tableUtils.getSelectionAffectedTableCells(model.document.selection);
		const columns = tableColumnResize.getColumnIndexesForCells(tableCells);
		if (!value || !columns || Number.isNaN(parseFloat(value))) return;
		model.enqueueChange(options.batch, (writer) => {
			tableColumnResize.applyColumnWidths(writer, columns.table, columns.columnIndexes, value);
			removeCellWidthsFromTable(writer, columns.table);
		});
	}
};

/**
* @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 upcast helper that ensures the number of `<tableColumn>` elements corresponds to the actual number of columns in the table,
* because the input data might have too few or too many <col> elements.
*
* @internal
*/
function upcastColgroupElement(tableUtilsPlugin) {
	return (dispatcher) => dispatcher.on("element:colgroup", (evt, data, conversionApi) => {
		const modelTable = data.modelCursor.findAncestor("table");
		const tableColumnGroup = getColumnGroupElement(modelTable);
		if (!tableColumnGroup) return;
		const columnElements = getTableColumnElements(tableColumnGroup);
		const columnsCount = tableUtilsPlugin.getColumns(modelTable);
		let columnWidths = translateColSpanAttribute(tableColumnGroup, conversionApi.writer);
		columnWidths = Array.from({ length: columnsCount }, (_, index) => columnWidths[index] || "auto");
		if (columnWidths.length != columnElements.length || columnWidths.includes("auto")) updateColumnElements(columnElements, tableColumnGroup, normalizeColumnWidths(columnWidths), conversionApi.writer);
	}, { priority: "low" });
}
/**
* Returns downcast helper for adding `ck-table-resized` class if there is a `<tableColumnGroup>` element inside the table.
*
* @internal
*/
function downcastTableResizedClass() {
	return (dispatcher) => dispatcher.on("insert:table", (evt, data, conversionApi) => {
		const viewWriter = conversionApi.writer;
		const modelTable = data.item;
		const viewElement = conversionApi.mapper.toViewElement(modelTable);
		const viewTable = viewElement.is("element", "table") ? viewElement : Array.from(viewElement.getChildren()).find((viewChild) => viewChild.is("element", "table"));
		if (getColumnGroupElement(modelTable)) viewWriter.addClass("ck-table-resized", viewTable);
		else viewWriter.removeClass("ck-table-resized", viewTable);
	}, { priority: "low" });
}
/**
* Returns a upcast helper that removes the `ck-table-resized` class from the table element.
*
* @internal
*/
function upcastTableResizedClass() {
	return (dispatcher) => {
		dispatcher.on("element:table", (evt, data, conversionApi) => {
			conversionApi.consumable.consume(data.viewItem, { classes: "ck-table-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 table/tablecolumnresize/tablecolumnresizeediting
*/
const toPx = /* #__PURE__ */ toUnit("px");
/**
* The table column resize editing plugin.
*/
var TableColumnResizeEditing = class extends Plugin {
	/**
	* A temporary storage for the required data needed to correctly calculate the widths of the resized columns. This storage is
	* initialized when column resizing begins, and is purged upon completion.
	*/
	_resizingData;
	/**
	* DOM emitter.
	*/
	_domEmitter;
	/**
	* A local reference to the {@link module:table/tableutils~TableUtils} plugin.
	*/
	_tableUtilsPlugin;
	/**
	* Starting mouse position data used to add a threshold to the resizing process.
	*/
	_initialMouseEventData = null;
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [TableEditing, TableUtils];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "TableColumnResizeEditing";
	}
	/**
	* @inheritDoc
	* @internal
	*/
	static get licenseFeatureCode() {
		return "TCR";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	static get isPremiumPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	constructor(editor) {
		super(editor);
		this.set("_isResizingActive", false);
		this.set("_isResizingAllowed", true);
		this._resizingData = null;
		this._domEmitter = new (DomEmitterMixin())();
		this._tableUtilsPlugin = editor.plugins.get("TableUtils");
		this.on("change:_isResizingAllowed", (evt, name, value) => {
			const classAction = value ? "removeClass" : "addClass";
			editor.editing.view.change((writer) => {
				for (const root of editor.editing.view.document.roots) writer[classAction]("ck-column-resize_disabled", editor.editing.view.document.getRoot(root.rootName));
			});
		});
		this.on("change:_isResizingActive", (evt, name, value) => {
			const classAction = value ? "add" : "remove";
			global.document.body.classList[classAction]("ck-table-column-resize__resizing-cursor");
		});
	}
	/**
	* @inheritDoc
	*/
	init() {
		this._extendSchema();
		this._registerPostFixer();
		this._registerConverters();
		this._registerResizingListeners();
		this._registerResizerInserter();
		this.decorate("_setResizingTableWidth");
		this.decorate("_getResizingTableWidth");
		const editor = this.editor;
		const columnResizePlugin = editor.plugins.get("TableColumnResize");
		editor.plugins.get("TableEditing").registerAdditionalSlot({
			filter: (element) => element.is("element", "tableColumnGroup"),
			positionOffset: 0
		});
		const tableWidthsCommand = new TableWidthsCommand(editor);
		editor.commands.add("resizeTableWidth", tableWidthsCommand);
		editor.commands.add("resizeColumnWidths", tableWidthsCommand);
		editor.commands.add("tableColumnWidth", new TableColumnWidthCommand(editor));
		this.bind("_isResizingAllowed").to(editor, "isReadOnly", columnResizePlugin, "isEnabled", tableWidthsCommand, "isEnabled", (isEditorReadOnly, isPluginEnabled, isTableWidthsCommandCommandEnabled) => !isEditorReadOnly && isPluginEnabled && isTableWidthsCommandCommandEnabled);
	}
	/**
	* @inheritDoc
	*/
	afterInit() {
		const editor = this.editor;
		const tableWidthCommand = editor.commands.get("tableWidth");
		if (tableWidthCommand) {
			this.listenTo(tableWidthCommand, "execute", (evt, args) => {
				const options = args[0] || (args[0] = {});
				if (!options.batch) options.batch = editor.model.createBatch();
			}, { priority: "high" });
			this.listenTo(tableWidthCommand, "execute", (evt, args) => {
				const options = args[0];
				const table = getSelectionAffectedTable(editor.model.document.selection);
				editor.model.enqueueChange(options.batch, (writer) => this._reconcileColumnUnits(writer, table));
			}, { priority: "low" });
		}
	}
	/**
	* @inheritDoc
	*/
	destroy() {
		this._domEmitter.stopListening();
		this._isResizingActive = false;
		super.destroy();
	}
	/**
	* The table for which a column resize is currently in progress, or `null` if no resize is active.
	* Only one table can be resized at a time.
	*/
	get resizingTable() {
		return this._resizingData ? this._resizingData.elements.modelTable : null;
	}
	/**
	* Returns a 'tableColumnGroup' element from the 'table'.
	*
	* @param element A 'table' or 'tableColumnGroup' element.
	* @returns A 'tableColumnGroup' element.
	*/
	getColumnGroupElement(element) {
		return getColumnGroupElement(element);
	}
	/**
	* Returns an array of 'tableColumn' elements.
	*
	* @param element A 'table' or 'tableColumnGroup' element.
	* @returns An array of 'tableColumn' elements.
	*/
	getTableColumnElements(element) {
		return getTableColumnElements(element);
	}
	/**
	* Returns an array of table column widths.
	*
	* @param element A 'table' or 'tableColumnGroup' element.
	* @returns An array of table column widths.
	*/
	getTableColumnsWidths(element) {
		return getTableColumnsWidths(element);
	}
	/**
	* Returns the table and the sorted, unique indexes of the columns covered by the given cells (a `colspan` cell
	* covers several columns). Returns `null` when the selection cannot be mapped onto columns - a non-resized table
	* or an irregular column structure.
	*
	* @param cells An array of 'tableCell' model elements.
	*/
	getColumnIndexesForCells(cells) {
		const table = cells.length ? cells[0].findAncestor("table") : null;
		if (!table) return null;
		const columns = getTableColumnElements(table);
		if (!columns.length || columns.length !== this._tableUtilsPlugin.getColumns(table) || columns.some((column) => column.hasAttribute("colSpan"))) return null;
		const columnIndexes = /* @__PURE__ */ new Set();
		for (const cell of cells) {
			const { leftEdge, rightEdge } = getColumnEdgesIndexes(cell, this._tableUtilsPlugin);
			for (let index = leftEdge; index <= rightEdge; index++) columnIndexes.add(index);
		}
		return {
			table,
			columnIndexes: Array.from(columnIndexes).sort((indexA, indexB) => indexA - indexB)
		};
	}
	/**
	* Applies the given width to every column in `columnIndexes`, keeping the whole table's width mode consistent
	* (see {@link module:table/tablecolumnresize/utils~isTableWidthInPixels}).
	*
	* @param writer A model writer instance.
	* @param table A 'table' model element.
	* @param columnIndexes Indexes of the columns the width is applied to.
	* @param value The width to apply. May be expressed in pixels or as a percentage.
	*/
	applyColumnWidths(writer, table, columnIndexes, value) {
		if (isTableWidthInPixels(table)) applyPixelColumnWidths(writer, table, columnIndexes, value);
		else this._applyPercentageColumnWidths(writer, table, columnIndexes, value);
	}
	/**
	* Converts the column widths of a resized table to the unit of the table's own width (`px` or `%`), so a table
	* width change (in the table properties) also switches the columns' unit. It is a no-op when the table is not
	* resized or the columns already use that unit.
	*/
	_reconcileColumnUnits(writer, table) {
		const tableColumnGroup = getColumnGroupElement(table);
		if (!tableColumnGroup) return;
		const columnWidths = getTableColumnsWidths(tableColumnGroup);
		const tableIsPixels = isTableWidthInPixels(table);
		if (tableIsPixels === isColumnWidthsInPixels(columnWidths)) return;
		let reconciledWidths;
		if (tableIsPixels) {
			const tableWidthInPixels = parseFloat(table.getAttribute("tableWidth"));
			reconciledWidths = columnWidths.map((width) => `${toPrecision(parseFloat(width) / 100 * tableWidthInPixels)}px`);
		} else {
			const totalWidth = sumArray(columnWidths.map((width) => parseFloat(width)));
			reconciledWidths = columnWidths.map((width) => `${toPrecision(parseFloat(width) / totalWidth * 100)}%`);
		}
		updateColumnElements(getTableColumnElements(tableColumnGroup), tableColumnGroup, reconciledWidths, writer);
	}
	/**
	* Applies a column width in the percentage mode: the target column gets the (clamped) percentage and the remaining
	* columns are redistributed proportionally so that all the widths keep summing up to 100%.
	*/
	_applyPercentageColumnWidths(writer, table, columnIndexes, value) {
		const columns = getTableColumnElements(table);
		const widths = getTableColumnsWidths(table).map((width) => parseFloat(width));
		if (columnIndexes.includes(columns.length - 1)) {
			this._growTableToColumnWidths(writer, table, columns, widths, columnIndexes, value);
			return;
		}
		const targetPercentage = value.trim().endsWith("%") ? parseFloat(value) : parseFloat(value) / getTableWidthInPixels(table, this.editor) * 100;
		const nextWidths = widths.slice();
		for (const band of getContiguousBands(columnIndexes)) {
			const neighbor = band.end + 1;
			const bandCount = band.indexes.length;
			const available = band.indexes.reduce((sum, index) => sum + widths[index], widths[neighbor]);
			if (available < 5 * (bandCount + 1)) {
				const equalWidth = available / (bandCount + 1);
				for (const index of band.indexes) nextWidths[index] = equalWidth;
				nextWidths[neighbor] = equalWidth;
				continue;
			}
			const target = clamp(targetPercentage, 5, (available - 5) / bandCount);
			for (const index of band.indexes) nextWidths[index] = target;
			nextWidths[neighbor] = available - target * bandCount;
		}
		columns.forEach((columnElement, index) => {
			writer.setAttribute("columnWidth", `${toPrecision(nextWidths[index])}%`, columnElement);
		});
	}
	/**
	* Applies a width to the target columns in the percentage mode when the selection reaches the last column. As
	* there is no next column to balance against, the table itself grows or shrinks (like dragging the last column's
	* right edge): every other column keeps its absolute width, so expressed against the resized table their
	* percentages scale, and the table's own width scales by the inverse.
	*/
	_growTableToColumnWidths(writer, table, columns, widths, columnIndexes, value) {
		const targetColumns = new Set(columnIndexes);
		const targetCount = columnIndexes.length;
		const otherColumnsShare = 100 - columnIndexes.reduce((sum, index) => sum + widths[index], 0);
		if (otherColumnsShare <= 0) {
			const equalWidth = 100 / columns.length;
			columns.forEach((columnElement) => {
				writer.setAttribute("columnWidth", `${toPrecision(equalWidth)}%`, columnElement);
			});
			return;
		}
		let targetPercentage;
		if (value.trim().endsWith("%")) targetPercentage = parseFloat(value);
		else {
			const pixelWidth = parseFloat(value);
			targetPercentage = pixelWidth / (otherColumnsShare / 100 * getTableWidthInPixels(table, this.editor) + targetCount * pixelWidth) * 100;
		}
		const target = clamp(targetPercentage, 5, Math.max(5, (100 - 5 * (columns.length - targetCount)) / targetCount));
		const scale = (100 - target * targetCount) / otherColumnsShare;
		columns.forEach((columnElement, index) => {
			const next = targetColumns.has(index) ? target : widths[index] * scale;
			writer.setAttribute("columnWidth", `${toPrecision(next)}%`, columnElement);
		});
		const tableWidth = parseFloat(table.getAttribute("tableWidth"));
		if (!Number.isNaN(tableWidth)) writer.setAttribute("tableWidth", `${toPrecision(tableWidth / scale)}%`, table);
	}
	/**
	* Applies `width` to whichever element currently represents the table's actual width - by default the
	* widget's `<figure>`. Passing `null` clears it instead of setting anything.
	*
	* @internal
	*/
	_setResizingTableWidth(writer, viewFigure, width) {
		if (width === null) writer.removeStyle("width", viewFigure);
		else writer.setStyle("width", width, viewFigure);
	}
	/**
	* Returns the table's current actual width, read from whichever element holds it - by default the
	* widget's `<figure>`.
	*
	* @internal
	*/
	_getResizingTableWidth(viewFigure) {
		return viewFigure.getStyle("width");
	}
	/**
	* Registers new attributes for a table model element.
	*/
	_extendSchema() {
		const schema = this.editor.model.schema;
		schema.extend("table", { allowAttributes: ["tableWidth"] });
		schema.register("tableColumnGroup", {
			allowIn: "table",
			isLimit: true
		});
		schema.register("tableColumn", {
			allowIn: "tableColumnGroup",
			allowAttributes: ["columnWidth", "colSpan"],
			isLimit: true
		});
		schema.setAttributeProperties("columnWidth", { isFormatting: true });
	}
	/**
	* Registers table column resize post-fixer.
	*
	* It checks if the change from the differ concerns a table-related element or attribute. For detected changes it:
	*  * Adjusts the `columnWidths` attribute to guarantee that the sum of the widths from all columns is 100%.
	*  * Checks if the `columnWidths` attribute gets updated accordingly after columns have been added or removed.
	*/
	_registerPostFixer() {
		const model = this.editor.model;
		model.document.registerPostFixer((writer) => {
			let changed = false;
			for (const table of getChangedResizedTables(model)) {
				const tableColumnGroup = this.getColumnGroupElement(table);
				const columns = this.getTableColumnElements(tableColumnGroup);
				const columnWidths = this.getTableColumnsWidths(tableColumnGroup);
				const isPixelMode = isColumnWidthsInPixels(columnWidths);
				let normalizedWidths = isPixelMode ? normalizePixelColumnWidths(columnWidths, table) : normalizeColumnWidths(columnWidths);
				normalizedWidths = adjustColumnWidths(normalizedWidths, table, this);
				if (isPixelMode && normalizedWidths.length !== columnWidths.length) writer.setAttribute("tableWidth", `${toPrecision(sumArray(normalizedWidths))}px`, table);
				else normalizedWidths = scalePixelColumnsToTableWidth(normalizedWidths, table);
				if (isEqual(columnWidths, normalizedWidths)) continue;
				updateColumnElements(columns, tableColumnGroup, normalizedWidths, writer);
				changed = true;
			}
			return changed;
		});
		/**
		* Adjusts if necessary the `columnWidths` in case if the number of column has changed.
		*
		* @param columnWidths Note: this array **may be modified** by the function.
		* @param table Table to be checked.
		*/
		function adjustColumnWidths(columnWidths, table, plugin) {
			const newTableColumnsCount = plugin._tableUtilsPlugin.getColumns(table);
			if (newTableColumnsCount - columnWidths.length === 0) return columnWidths;
			const isPixelMode = isColumnWidthsInPixels(columnWidths);
			const widths = columnWidths.map((width) => parseFloat(width));
			const cellSet = getAffectedCells(plugin.editor.model.document.differ, table);
			for (const cell of cellSet) {
				const currentColumnsDelta = newTableColumnsCount - widths.length;
				if (currentColumnsDelta === 0) continue;
				const hasMoreColumns = currentColumnsDelta > 0;
				const currentColumnIndex = plugin._tableUtilsPlugin.getCellLocation(cell).column;
				if (hasMoreColumns) {
					const columnWidthsToInsert = createFilledArray(currentColumnsDelta, isPixelMode ? 40 : getColumnMinWidthAsPercentage(table, plugin.editor));
					widths.splice(currentColumnIndex, 0, ...columnWidthsToInsert);
				} else {
					const removedColumnWidths = widths.splice(currentColumnIndex, Math.abs(currentColumnsDelta));
					widths[currentColumnIndex] += sumArray(removedColumnWidths);
				}
			}
			return widths.map((width) => `${width}${isPixelMode ? "px" : "%"}`);
		}
		/**
		* Returns a set of cells that have been changed in a given table.
		*/
		function getAffectedCells(differ, table) {
			const cellSet = /* @__PURE__ */ new Set();
			for (const change of differ.getChanges()) if (change.type == "insert" && change.position.nodeAfter && change.position.nodeAfter.name == "tableCell" && change.position.nodeAfter.getAncestors().includes(table)) cellSet.add(change.position.nodeAfter);
			else if (change.type == "remove") {
				const referenceNode = change.position.nodeBefore || change.position.nodeAfter;
				if (referenceNode.name == "tableCell" && referenceNode.getAncestors().includes(table)) cellSet.add(referenceNode);
			}
			return cellSet;
		}
	}
	/**
	* Registers table column resize converters.
	*/
	_registerConverters() {
		const conversion = this.editor.conversion;
		conversion.for("upcast").attributeToAttribute({
			view: {
				name: /^(figure|table)$/,
				styles: { width: /[\s\S]+/ }
			},
			model: {
				key: "tableWidth",
				value: (viewElement) => {
					if (viewElement.parent.is("element", "figure")) return;
					return viewElement.getStyle("width");
				}
			}
		});
		conversion.for("downcast").attributeToAttribute({
			model: {
				name: "table",
				key: "tableWidth"
			},
			view: (width) => ({
				name: "figure",
				key: "style",
				value: { width }
			})
		});
		conversion.elementToElement({
			model: "tableColumnGroup",
			view: "colgroup"
		});
		conversion.elementToElement({
			model: "tableColumn",
			view: "col"
		});
		conversion.for("downcast").add(downcastTableResizedClass());
		conversion.for("upcast").add(upcastTableResizedClass());
		conversion.for("upcast").add(upcastColgroupElement(this._tableUtilsPlugin));
		conversion.for("upcast").attributeToAttribute({
			view: {
				name: "col",
				styles: { width: /.*/ }
			},
			model: {
				key: "columnWidth",
				value: (viewElement) => {
					const viewColWidth = viewElement.getStyle("width");
					if (!viewColWidth || !viewColWidth.endsWith("%") && !viewColWidth.endsWith("pt") && !(viewColWidth.endsWith("px") && isViewTableWidthInPixels(viewElement))) return "auto";
					return viewColWidth;
				}
			}
		});
		conversion.for("upcast").attributeToAttribute({
			view: {
				name: "col",
				key: "span"
			},
			model: "colSpan"
		});
		conversion.for("downcast").attributeToAttribute({
			model: {
				name: "tableColumn",
				key: "columnWidth"
			},
			view: (width) => ({
				key: "style",
				value: { width }
			})
		});
	}
	/**
	* Registers listeners to handle resizing process.
	*/
	_registerResizingListeners() {
		const editingView = this.editor.editing.view;
		editingView.addObserver(MouseEventsObserver);
		editingView.document.on("mouseover", this._onMouseOverHandler.bind(this), { priority: "high" });
		editingView.document.on("mousedown", this._onMouseDownHandler.bind(this), { priority: "high" });
		editingView.document.on("mouseout", this._onMouseOutHandler.bind(this), { priority: "high" });
		this._domEmitter.listenTo(global.window.document, "mousemove", throttle(this._onMouseMoveHandler.bind(this), 50));
		this._domEmitter.listenTo(global.window.document, "mouseup", this._onMouseUpHandler.bind(this));
	}
	/**
	* Calculate and set `top` and `bottom` styles to the column resizer element to fit the height of the table.
	*
	* @param viewResizer The column resizer element.
	*/
	_recalculateResizerElement(viewResizer) {
		const editor = this.editor;
		const domConverter = editor.editing.view.domConverter;
		const domTable = domConverter.mapViewToDom(viewResizer.findAncestor("table"));
		const domCell = domConverter.mapViewToDom(viewResizer.findAncestor((item) => ["td", "th"].includes(item.name)));
		const rectTable = new Rect(domTable);
		const rectCell = new Rect(domCell);
		const targetTopPosition = toPx(Number((rectTable.top - rectCell.top).toFixed(4)));
		const targetBottomPosition = toPx(Number((rectCell.bottom - rectTable.bottom).toFixed(4)));
		editor.editing.view.change((viewWriter) => {
			viewWriter.setStyle("top", targetTopPosition, viewResizer);
			viewWriter.setStyle("bottom", targetBottomPosition, viewResizer);
		});
	}
	/**
	* Remove `top` and `bottom` styles of the column resizer element.
	*
	* @param viewResizer The column resizer element.
	*/
	_resetResizerStyles(viewResizer) {
		this.editor.editing.view.change((viewWriter) => {
			viewWriter.removeStyle("top", viewResizer);
			viewWriter.removeStyle("bottom", viewResizer);
		});
	}
	/**
	* Handles the `mouseover` event on column resizer element.
	* Recalculates the `top` and `bottom` styles of the column resizer element to fit the height of the table.
	*
	* @param eventInfo An object containing information about the fired event.
	* @param domEventData The data related to the DOM event.
	*/
	_onMouseOverHandler(eventInfo, domEventData) {
		const target = domEventData.target;
		if (!target.hasClass("ck-table-column-resizer")) return;
		if (!this._isResizingAllowed) return;
		this._recalculateResizerElement(target);
	}
	/**
	* Handles the `mouseout` event on column resizer element.
	* When resizing is not active, it resets the `top` and `bottom` styles of the column resizer element.
	*
	* @param eventInfo An object containing information about the fired event.
	* @param domEventData The data related to the DOM event.
	*/
	_onMouseOutHandler(eventInfo, domEventData) {
		const target = domEventData.target;
		if (!target.hasClass("ck-table-column-resizer")) return;
		if (!this._isResizingAllowed) return;
		if (this._isResizingActive) return;
		this._resetResizerStyles(target);
	}
	/**
	* Handles the `mousedown` event on column resizer element:
	*  * calculates the initial column pixel widths,
	*  * inserts the `<colgroup>` element if it is not present in the `<table>`,
	*  * puts the necessary data in the temporary storage,
	*  * applies the attributes to the `<table>` view element.
	*
	* @param eventInfo An object containing information about the fired event.
	* @param domEventData The data related to the DOM event.
	*/
	_onMouseDownHandler(eventInfo, domEventData) {
		const target = domEventData.target;
		if (!target.hasClass("ck-table-column-resizer")) return;
		if (!this._isResizingAllowed) return;
		const editor = this.editor;
		const modelTable = editor.editing.mapper.toModelElement(target.findAncestor("figure"));
		if (!editor.model.canEditAt(modelTable)) return;
		domEventData.preventDefault();
		eventInfo.stop();
		this._initialMouseEventData = domEventData;
	}
	/**
	* Starts the resizing process after the threshold is reached.
	*/
	_startResizingAfterThreshold() {
		const domEventData = this._initialMouseEventData;
		const { target } = domEventData;
		const modelTable = this.editor.editing.mapper.toModelElement(target.findAncestor("figure"));
		const viewTable = target.findAncestor("table");
		const viewFigure = target.findAncestor("figure");
		const isPixelMode = isTableWidthInPixels(modelTable);
		const columnWidthsInPx = _calculateDomColumnWidths(modelTable, this._tableUtilsPlugin, this.editor);
		if (!Array.from(viewTable.getChildren()).find((viewCol) => viewCol.is("element", "colgroup"))) this.editor.editing.view.change((viewWriter) => {
			_insertColgroupElement(viewWriter, columnWidthsInPx, viewTable, isPixelMode);
		});
		this._isResizingActive = true;
		this._resizingData = this._getResizingData(domEventData, columnWidthsInPx);
		this.editor.editing.view.change((writer) => {
			const initialWidth = _applyResizingAttributesToTable(writer, viewTable, this._resizingData);
			this._setResizingTableWidth(writer, viewFigure, initialWidth);
		});
		/**
		* Calculates the DOM columns' widths. It is done by taking the width of the widest cell
		* from each table column (we rely on the  {@link module:table/tablewalker~TableWalker}
		* to determine which column the cell belongs to).
		*
		* @param modelTable A table which columns should be measured.
		* @param tableUtils The Table Utils plugin instance.
		* @param editor The editor instance.
		* @returns Columns' widths expressed in pixels (without unit).
		*/
		function _calculateDomColumnWidths(modelTable, tableUtilsPlugin, editor) {
			const columnWidthsInPx = Array(tableUtilsPlugin.getColumns(modelTable));
			const tableWalker = new TableWalker(modelTable);
			for (const cellSlot of tableWalker) {
				const viewCell = editor.editing.mapper.toViewElement(cellSlot.cell);
				const domCellWidth = getDomCellOuterWidth(editor.editing.view.domConverter.mapViewToDom(viewCell));
				if (!columnWidthsInPx[cellSlot.column] || domCellWidth < columnWidthsInPx[cellSlot.column]) columnWidthsInPx[cellSlot.column] = toPrecision(domCellWidth);
			}
			return columnWidthsInPx;
		}
		/**
		* Creates a `<colgroup>` element with `<col>`s and inserts it into a given view table.
		*
		* @param viewWriter A writer instance.
		* @param columnWidthsInPx Column widths.
		* @param viewTable A table view element.
		*/
		function _insertColgroupElement(viewWriter, columnWidthsInPx, viewTable, isPixelMode) {
			const colgroup = viewWriter.createContainerElement("colgroup");
			for (let i = 0; i < columnWidthsInPx.length; i++) {
				const viewColElement = viewWriter.createEmptyElement("col");
				const columnWidth = isPixelMode ? `${toPrecision(columnWidthsInPx[i])}px` : `${toPrecision(columnWidthsInPx[i] / sumArray(columnWidthsInPx) * 100)}%`;
				viewWriter.setStyle("width", columnWidth, viewColElement);
				viewWriter.insert(viewWriter.createPositionAt(colgroup, "end"), viewColElement);
			}
			viewWriter.insert(viewWriter.createPositionAt(viewTable, 0), colgroup);
		}
		/**
		* Applies the classes to the view table as the resizing begun, and computes the initial live width.
		*
		* @param viewWriter A writer instance.
		* @param viewTable A table containing the clicked resizer.
		* @param resizingData Data related to the resizing.
		* @returns The table's current width as a `%` string, e.g. for seeding {@link #_setResizingTableWidth}.
		*/
		function _applyResizingAttributesToTable(viewWriter, viewTable, resizingData) {
			viewWriter.addClass("ck-table-resized", viewTable);
			viewWriter.addClass("ck-table-column-resizer__active", resizingData.elements.viewResizer);
			if (resizingData.flags.isPixelMode) return `${toPrecision(parseFloat(resizingData.elements.modelTable.getAttribute("tableWidth")))}px`;
			return `${toPrecision(Math.max(resizingData.widths.tableWidth, resizingData.widths.viewFigureWidth) / resizingData.widths.viewFigureParentWidth * 100)}%`;
		}
	}
	/**
	* Handles the `mousemove` event.
	*  * If resizing process is not in progress, it does nothing.
	*  * If resizing is active but not allowed, it stops the resizing process instantly calling the `mousedown` event handler.
	*  * Otherwise it dynamically updates the widths of the resized columns.
	*
	* @param eventInfo An object containing information about the fired event.
	* @param mouseEventData The native DOM event.
	*/
	_onMouseMoveHandler(eventInfo, mouseEventData) {
		if (this._initialMouseEventData) {
			const mouseEvent = this._initialMouseEventData.domEvent;
			if (Math.abs(mouseEventData.clientX - mouseEvent.clientX) >= 3) {
				this._startResizingAfterThreshold();
				this._initialMouseEventData = null;
			} else return;
		}
		if (!this._isResizingActive) return;
		if (!this._isResizingAllowed) {
			this._onMouseUpHandler();
			return;
		}
		const { plugins } = this.editor;
		const { columnPosition, flags: { isRightEdge, isTableCentered, isLtrContent, isPixelMode, isTableWidthWithinContainerAtDragStart, isTableScrollAllowed }, elements: { modelTable, viewFigure, viewLeftColumn, viewRightColumn, viewResizer }, widths: { viewFigureParentWidth, tableWidth, leftColumnWidth, rightColumnWidth } } = this._resizingData;
		const dxLowerBound = -leftColumnWidth + 40;
		const isTableScrollActive = !!(plugins.has("TableScrollEditing") ? plugins.get("TableScrollEditing") : null) && isTableScrollAllowed;
		const containerWidth = getEditableWidth(this.editor, modelTable.root.rootName);
		let dxUpperBound;
		if (isRightEdge) dxUpperBound = isTableScrollActive ? Infinity : viewFigureParentWidth - tableWidth;
		else dxUpperBound = rightColumnWidth - 40;
		const rawDx = mouseEventData.clientX - columnPosition;
		const ltrSign = isLtrContent ? 1 : -1;
		const isCenteredRightEdge = isRightEdge && isTableCentered;
		let dx;
		if (isTableScrollActive && isCenteredRightEdge) {
			const mouseDelta = rawDx * ltrSign;
			let newTableWidth;
			if (isTableWidthWithinContainerAtDragStart) {
				const crossoverPoint = (containerWidth - tableWidth) / 2;
				newTableWidth = mouseDelta <= crossoverPoint ? tableWidth + 2 * mouseDelta : containerWidth + (mouseDelta - crossoverPoint);
			} else {
				const crossoverPoint = containerWidth - tableWidth;
				newTableWidth = mouseDelta >= crossoverPoint ? tableWidth + mouseDelta : containerWidth + 2 * (mouseDelta - crossoverPoint);
			}
			dx = newTableWidth - tableWidth;
		} else dx = rawDx * (ltrSign * (isCenteredRightEdge ? 2 : 1));
		dx = clamp(dx, Math.min(dxLowerBound, 0), Math.max(dxUpperBound, 0));
		if (isTableScrollActive && isRightEdge) dx = clamp(applyContainerWidthResistance(tableWidth + dx, containerWidth) - tableWidth, Math.min(dxLowerBound, 0), Math.max(dxUpperBound, 0));
		if (dx === 0) return;
		this.editor.editing.view.change((writer) => {
			const toWidthValue = (widthInPx, basisInPx) => isPixelMode ? `${toPrecision(widthInPx)}px` : `${toPrecision(widthInPx * 100 / basisInPx)}%`;
			writer.setStyle("width", toWidthValue(leftColumnWidth + dx, tableWidth), viewLeftColumn);
			if (isRightEdge) {
				const tableFigureWidth = isPixelMode ? `${toPrecision(tableWidth + dx)}px` : `${toPrecision((tableWidth + dx) * 100 / viewFigureParentWidth)}%`;
				this._setResizingTableWidth(writer, viewFigure, tableFigureWidth);
			} else writer.setStyle("width", toWidthValue(rightColumnWidth - dx, tableWidth), viewRightColumn);
		});
		this._recalculateResizerElement(viewResizer);
	}
	/**
	* Handles the `mouseup` event.
	*  * If resizing process is not in progress, it does nothing.
	*  * If resizing is active but not allowed, it cancels the resizing process restoring the original widths.
	*  * Otherwise it propagates the changes from view to the model by executing the adequate commands.
	*/
	_onMouseUpHandler() {
		this._initialMouseEventData = null;
		if (!this._isResizingActive) return;
		const { viewResizer, modelTable, viewFigure, viewTable, viewColgroup } = this._resizingData.elements;
		const { isPixelMode } = this._resizingData.flags;
		const editor = this.editor;
		const editingView = editor.editing.view;
		const tableColumnGroup = this.getColumnGroupElement(modelTable);
		const viewColumns = Array.from(viewColgroup.getChildren()).filter((column) => column.is("view:element"));
		const columnWidthsAttributeOld = tableColumnGroup ? this.getTableColumnsWidths(tableColumnGroup) : null;
		const columnWidthsAttributeNew = viewColumns.map((column) => column.getStyle("width"));
		const isColumnWidthsAttributeChanged = !isEqual(columnWidthsAttributeOld, columnWidthsAttributeNew);
		const tableWidthAttributeOld = modelTable.getAttribute("tableWidth");
		const tableWidthAttributeNew = this._getResizingTableWidth(viewFigure);
		const isTableWidthAttributeChanged = tableWidthAttributeOld !== tableWidthAttributeNew;
		if (isColumnWidthsAttributeChanged || isTableWidthAttributeChanged) if (this._isResizingAllowed) {
			const tableWidthInPixels = toPrecision(tableWidthAttributeNew);
			const tableWidthInPixelsBeforeResize = toPrecision(tableWidthAttributeOld);
			const columnWidths = isPixelMode ? columnWidthsAttributeNew.map((width) => typeof width === "string" && !width.endsWith("px") ? `${toPrecision(parseFloat(width) / 100 * tableWidthInPixelsBeforeResize)}px` : width) : columnWidthsAttributeNew;
			editor.execute("resizeTableWidth", {
				table: modelTable,
				tableWidth: isPixelMode ? `${toPrecision(tableWidthInPixels)}px` : `${toPrecision(tableWidthAttributeNew)}%`,
				columnWidths
			});
		} else editingView.change((writer) => {
			if (columnWidthsAttributeOld) for (const viewCol of viewColumns) writer.setStyle("width", columnWidthsAttributeOld.shift(), viewCol);
			else writer.remove(viewColgroup);
			if (isTableWidthAttributeChanged) this._setResizingTableWidth(writer, viewFigure, tableWidthAttributeOld || null);
			if (!columnWidthsAttributeOld && !tableWidthAttributeOld) writer.removeClass("ck-table-resized", viewTable);
		});
		editingView.change((writer) => {
			writer.removeClass("ck-table-column-resizer__active", viewResizer);
		});
		if (!editingView.domConverter.mapViewToDom(viewResizer).matches(":hover")) this._resetResizerStyles(viewResizer);
		this._isResizingActive = false;
		this._resizingData = null;
	}
	/**
	* Retrieves and returns required data needed for the resizing process.
	*
	* @param domEventData The data of the `mousedown` event.
	* @param columnWidths The current widths of the columns.
	* @returns The data needed for the resizing process.
	*/
	_getResizingData(domEventData, columnWidths) {
		const editor = this.editor;
		const columnPosition = domEventData.domEvent.clientX;
		const viewResizer = domEventData.target;
		const viewLeftCell = viewResizer.findAncestor("td") || viewResizer.findAncestor("th");
		const modelLeftCell = editor.editing.mapper.toModelElement(viewLeftCell);
		const modelTable = modelLeftCell.findAncestor("table");
		const leftColumnIndex = getColumnEdgesIndexes(modelLeftCell, this._tableUtilsPlugin).rightEdge;
		const lastColumnIndex = this._tableUtilsPlugin.getColumns(modelTable) - 1;
		let tableAlignment = modelTable.getAttribute("tableAlignment");
		if (modelTable.getAttribute("tableType") !== "layout") {
			tableAlignment ||= editor.config.get("table.tableProperties.defaultProperties.alignment");
			tableAlignment ||= "center";
		}
		const isRightEdge = leftColumnIndex === lastColumnIndex;
		const isLtrContent = editor.locale.contentLanguageDirection !== "rtl";
		const isTableCentered = tableAlignment === "center";
		const isPixelMode = isTableWidthInPixels(modelTable);
		const viewTable = viewLeftCell.findAncestor("table");
		const viewFigure = viewTable.findAncestor("figure");
		const viewColgroup = [...viewTable.getChildren()].find((viewCol) => viewCol.is("element", "colgroup"));
		const viewLeftColumn = viewColgroup.getChild(leftColumnIndex);
		const viewRightColumn = isRightEdge ? void 0 : viewColgroup.getChild(leftColumnIndex + 1);
		const viewFigureParentWidth = getElementWidthInPixels(editor.editing.view.domConverter.mapViewToDom(viewFigure.parent));
		const viewFigureWidth = getElementWidthInPixels(editor.editing.view.domConverter.mapViewToDom(viewFigure));
		const tableWidth = getTableWidthInPixels(modelTable, editor);
		const leftColumnWidth = columnWidths[leftColumnIndex];
		const rightColumnWidth = isRightEdge ? void 0 : columnWidths[leftColumnIndex + 1];
		const isTableWidthWithinContainerAtDragStart = tableWidth <= getEditableWidth(editor, modelTable.root.rootName);
		const tableScrollPlugin = editor.plugins.has("TableScrollEditing") ? editor.plugins.get("TableScrollEditing") : null;
		return {
			columnPosition,
			flags: {
				isRightEdge,
				isTableCentered,
				isLtrContent,
				isPixelMode,
				isTableWidthWithinContainerAtDragStart,
				isTableScrollAllowed: !!tableScrollPlugin && tableScrollPlugin._isTableScrollable(modelTable)
			},
			elements: {
				viewResizer,
				modelTable,
				viewFigure,
				viewTable,
				viewColgroup,
				viewLeftColumn,
				viewRightColumn
			},
			widths: {
				viewFigureWidth,
				viewFigureParentWidth,
				tableWidth,
				leftColumnWidth,
				rightColumnWidth
			}
		};
	}
	/**
	* Registers a listener ensuring that each resizable cell have a resizer handle.
	*/
	_registerResizerInserter() {
		this.editor.conversion.for("editingDowncast").add((dispatcher) => {
			dispatcher.on("insert:tableCell", (evt, data, conversionApi) => {
				const modelElement = data.item;
				const viewElement = conversionApi.mapper.toViewElement(modelElement);
				const viewWriter = conversionApi.writer;
				viewWriter.insert(viewWriter.createPositionAt(viewElement, "end"), viewWriter.createUIElement("div", { class: "ck-table-column-resizer" }));
			}, { priority: "lowest" });
		});
	}
};
/**
* Tells whether the table wrapping the given view `<col>` element is sized in pixels. The width is read from the
* column's own table (or its wrapping `<figure>`), never from an ancestor further up, so a width-less table nested
* inside a wider one is not mistaken for a pixel table.
*/
function isViewTableWidthInPixels(viewColElement) {
	const viewTable = viewColElement.findAncestor((element) => element.is("element", "table"));
	const viewParent = viewTable.parent;
	const width = (viewParent.is("element", "figure") ? viewParent : viewTable).getStyle("width");
	return typeof width === "string" && width.trim().endsWith("px");
}
/**
* In the pixel mode keeps the sum of the column widths equal to the table width. If they differ (for example after a
* manual table width change), the columns are scaled proportionally so that they sum up to the table width. In the
* percentage mode (or when the columns already sum to the table width) it is a no-op.
*/
function scalePixelColumnsToTableWidth(columnWidths, table) {
	if (!isTableWidthInPixels(table) || !isColumnWidthsInPixels(columnWidths)) return columnWidths;
	const tableWidthInPixels = parseFloat(table.getAttribute("tableWidth"));
	const totalWidth = sumArray(columnWidths);
	if (!totalWidth || Math.abs(totalWidth - tableWidthInPixels) < .5) return columnWidths;
	const factor = tableWidthInPixels / totalWidth;
	return columnWidths.map((width) => `${toPrecision(parseFloat(width) * factor)}px`);
}
/**
* Normalizes a pixel-mode column group to concrete pixel widths. It mirrors {@link ~normalizeColumnWidths} (the
* percentage path): percentages are resolved against the table width, and missing (`auto`/`undefined`) columns are
* filled from the width left over in the table - so the column group never keeps `auto` widths or mixes units, and
* downstream arithmetic never sees a non-pixel value.
*/
function normalizePixelColumnWidths(columnWidths, table) {
	const tableWidthInPixels = parseFloat(table.getAttribute("tableWidth"));
	const pixelWidths = columnWidths.map((width) => {
		if (width === "auto" || width === void 0) return null;
		return width.endsWith("%") ? parseFloat(width) / 100 * tableWidthInPixels : parseFloat(width);
	});
	const missingColumns = pixelWidths.filter((width) => width === null).length;
	if (missingColumns) {
		const knownWidth = pixelWidths.reduce((sum, width) => width === null ? sum : sum + width, 0);
		const widthForMissingColumn = Math.max((tableWidthInPixels - knownWidth) / missingColumns, 40);
		return pixelWidths.map((width) => `${toPrecision(width === null ? widthForMissingColumn : width)}px`);
	}
	return pixelWidths.map((width) => `${toPrecision(width)}px`);
}
/**
* Applies a column width in the pixel mode: the target column gets the absolute width and the table's own width
* grows or shrinks to the sum of all column widths.
*/
function applyPixelColumnWidths(writer, table, columnIndexes, value) {
	const columns = getTableColumnElements(table);
	const tableWidthInPixels = parseFloat(table.getAttribute("tableWidth"));
	const toPixels = (width) => width.trim().endsWith("%") ? parseFloat(width) / 100 * tableWidthInPixels : parseFloat(width);
	const targetPixels = Math.max(toPixels(value), 40);
	const widths = getTableColumnsWidths(table).map(toPixels);
	for (const index of columnIndexes) widths[index] = targetPixels;
	const roundedWidths = widths.map((width) => toPrecision(width));
	columns.forEach((columnElement, index) => {
		writer.setAttribute("columnWidth", `${roundedWidths[index]}px`, columnElement);
	});
	writer.setAttribute("tableWidth", `${toPrecision(sumArray(roundedWidths))}px`, table);
}
/**
* Splits a sorted list of column indexes into maximal contiguous bands, for example `[ 0, 1, 3 ]` into `[ 0, 1 ]`
* and `[ 3 ]`.
*/
function getContiguousBands(columnIndexes) {
	const bands = [];
	for (const index of columnIndexes) {
		const lastBand = bands[bands.length - 1];
		if (lastBand && index === lastBand.end + 1) {
			lastBand.indexes.push(index);
			lastBand.end = index;
		} else bands.push({
			indexes: [index],
			end: index
		});
	}
	return bands;
}
/**
* Given the table width a drag would naturally produce, returns the width that should actually be applied
* once snapping and growth resistance around the container's width are taken into account:
*
*  * if the natural width lands close to the container's width (on either side), it's pulled to exactly
*    match it,
*  * if the natural width is past the container's width, it stays pinned at the container's width until the
*    drag has gone far enough beyond it (the "resistance" zone) - past that point it keeps growing 1:1,
*    continuing smoothly from where the resistance was overcome instead of jumping,
*  * shrinking below the container's width is never resisted, only snapped when close.
*
* @internal
*/
function applyContainerWidthResistance(naturalTableWidth, containerWidth) {
	const distance = naturalTableWidth - containerWidth;
	if (distance < 0) return -distance <= 5 ? containerWidth : naturalTableWidth;
	const resistanceZone = 5 + 10;
	return distance <= resistanceZone ? containerWidth : containerWidth + (distance - resistanceZone);
}

/**
* @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 table/tablecolumnresize
*/
/**
* The table column resize feature.
*
* It provides the possibility to set the width of each column in a table using a resize handler.
*/
var TableColumnResize = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [TableColumnResizeEditing, TableCellWidthEditing];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "TableColumnResize";
	}
	/**
	* @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 table/tablelayout/commands/tabletypecommand
*/
/**
* The set table type command.
*
* The command is registered by {@link module:table/tablelayout/tablelayoutediting~TableLayoutEditing}
* as the `'tableType'` editor command.
*
* To set the table type at the current selection, execute the command and specify the table type:
*
* ```ts
* editor.execute( 'tableType', 'layout' );
* ```
*/
var TableTypeCommand = class extends Command {
	/**
	* @inheritDoc
	*/
	refresh() {
		const selection = this.editor.model.document.selection;
		const selectedTable = getSelectionAffectedTable(selection);
		if (selectedTable) {
			this.isEnabled = true;
			this.value = selectedTable.getAttribute("tableType");
		} else {
			this.isEnabled = false;
			this.value = null;
		}
	}
	/**
	* Executes the command.
	*
	* Set table type by the given table type parameter.
	*
	* @param tableType The type of table it should become.
	* @fires execute
	*/
	execute(tableType) {
		const model = this.editor.model;
		const selection = model.document.selection;
		const table = getSelectionAffectedTable(selection);
		if (table.getAttribute("tableType") === tableType) return;
		model.change((writer) => {
			writer.setAttribute("tableType", tableType, table);
			model.schema.removeDisallowedAttributes([table], writer);
			const tableChildren = table.getChildren();
			for (const child of tableChildren) if (!model.schema.checkChild(table, child)) writer.remove(child);
		});
	}
};

/**
* @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 table/tablelayout/tablelayoutediting
*/
const TABLE_TYPES = ["content", "layout"];
/**
* The table layout editing plugin.
*/
var TableLayoutEditing = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "TableLayoutEditing";
	}
	/**
	* @inheritDoc
	* @internal
	*/
	static get licenseFeatureCode() {
		return "TL";
	}
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [TableColumnResize];
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	static get isPremiumPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	init() {
		this._defineSchema();
		this._defineConverters();
		this._defineClipboardPasteHandlers();
		this._registerTableTypeAttributePostfixer();
		this.editor.commands.add("insertTableLayout", new InsertTableLayoutCommand(this.editor));
		this.editor.commands.add("tableType", new TableTypeCommand(this.editor));
	}
	/**
	* Defines the schema for the table layout feature.
	*/
	_defineSchema() {
		const { schema } = this.editor.model;
		schema.extend("table", { allowAttributes: "tableType" });
		schema.addChildCheck(layoutTableCheck, "caption");
		schema.addAttributeCheck(layoutTableCheck, "headingRows");
		schema.addAttributeCheck(layoutTableCheck, "headingColumns");
		schema.addAttributeCheck(layoutTableCheck, "footerRows");
	}
	/**
	* Defines the converters for the table layout feature.
	*/
	_defineConverters() {
		const { editor } = this;
		const { conversion } = editor;
		const preferredExternalTableType = editor.config.get("table.tableLayout.preferredExternalTableType");
		conversion.for("upcast").add(upcastLayoutTable(editor, preferredExternalTableType));
		conversion.for("dataDowncast").add(dataDowncastLayoutTable());
		conversion.for("editingDowncast").attributeToAttribute({
			model: {
				key: "tableType",
				values: ["layout", "content"]
			},
			view: {
				layout: {
					key: "class",
					value: ["layout-table"]
				},
				content: {
					key: "class",
					value: ["content-table"]
				}
			}
		});
	}
	/**
	* Handles the clipboard content insertion events.
	*
	* - If the content is from another editor, do not override the table type.
	* - If the content is from another source, set the table type to 'content'.
	*
	* It handles the scenario when user copies `<table></table>` from Word. We do not want to
	* change the table type to `layout` because it is really `content` table.
	*/
	_defineClipboardPasteHandlers() {
		const { plugins } = this.editor;
		if (!plugins.has("ClipboardPipeline")) return;
		const clipboardPipeline = plugins.get("ClipboardPipeline");
		this.listenTo(clipboardPipeline, "contentInsertion", (evt, data) => {
			if (data.sourceEditorId) return;
			this.editor.model.change((writer) => {
				for (const { item } of writer.createRangeIn(data.content)) if (item.is("element", "table")) writer.setAttribute("tableType", "content", item);
			});
		});
	}
	/**
	* Registers a post-fixer that sets the `tableType` attribute to `content` for inserted "default" tables.
	* Also fixes potential issues with the table structure when the `tableType` attribute has been changed.
	*/
	_registerTableTypeAttributePostfixer() {
		const editor = this.editor;
		editor.model.document.registerPostFixer((writer) => {
			const changes = editor.model.document.differ.getChanges();
			let hasChanged = false;
			for (const entry of changes) {
				if (entry.type == "insert" && entry.name != "$text") {
					const element = entry.position.nodeAfter;
					const range = writer.createRangeOn(element);
					for (const item of range.getItems()) if (item.is("element", "table") && !item.hasAttribute("tableType")) {
						writer.setAttribute("tableType", "content", item);
						hasChanged = true;
					}
				}
				if (entry.type == "attribute" && entry.attributeKey == "tableType") {
					for (const item of entry.range.getItems())
 /* v8 ignore else -- `tableType` is only ever set on a single `table`, so the range holds no other items. */
					if (item.is("element", "table")) {
						editor.model.schema.removeDisallowedAttributes([item], writer);
						const tableChildren = item.getChildren();
						for (const child of tableChildren) if (!editor.model.schema.checkChild(item, child)) {
							writer.remove(child);
							hasChanged = true;
						}
					}
				}
			}
			return hasChanged;
		});
	}
};
/**
* View table element to model table element conversion helper.
*
* This conversion helper overrides the default table converter to meet table layout conditions.
*
* @param editor Editor instance.
* @returns Conversion helper.
*/
function upcastLayoutTable(editor, preferredExternalTableType) {
	return (dispatcher) => {
		dispatcher.on("element:table", (evt, data, conversionApi) => {
			const viewTable = data.viewItem;
			if (!conversionApi.consumable.test(viewTable, { name: true })) return;
			if (resolveTableType(viewTable, preferredExternalTableType) == "content") return;
			const table = conversionApi.writer.createElement("table", { tableType: "layout" });
			if (!conversionApi.safeInsert(table, data.modelCursor)) return;
			conversionApi.consumable.consume(viewTable, { name: true });
			conversionApi.consumable.consume(viewTable, { attributes: ["role"] });
			conversionApi.consumable.consume(viewTable, { classes: ["layout-table"] });
			if (viewTable.getAttribute("border") === "0") conversionApi.consumable.consume(viewTable, { attributes: ["border"] });
			for (const tableChild of viewTable.getChildren()) if (tableChild.is("element")) {
				for (const row of tableChild.getChildren()) if (row.is("element", "tr")) conversionApi.convertItem(row, conversionApi.writer.createPositionAt(table, "end"));
			}
			conversionApi.convertChildren(viewTable, conversionApi.writer.createPositionAt(table, "end"));
			if (table.isEmpty) {
				const row = conversionApi.writer.createElement("tableRow");
				conversionApi.writer.insert(row, conversionApi.writer.createPositionAt(table, "end"));
				createEmptyTableCell(conversionApi.writer, conversionApi.writer.createPositionAt(row, "end"));
			}
			conversionApi.updateConversionResult(table, data);
		}, { priority: "high" });
		dispatcher.on("element:table", (evt, data, conversionApi) => {
			const { viewItem, modelRange } = data;
			if (modelRange) {
				conversionApi.writer.setAttribute("tableType", resolveTableType(viewItem, preferredExternalTableType), modelRange);
				conversionApi.consumable.consume(viewItem, { classes: ["layout-table"] });
				conversionApi.consumable.consume(viewItem, { classes: ["content-table"] });
			}
		}, { priority: "low" });
	};
}
/**
* Model table container element to view table element conversion helper.
*
* @returns Conversion helper.
*/
function dataDowncastLayoutTable() {
	return (dispatcher) => {
		return dispatcher.on("attribute:tableType:table", (evt, data, conversionApi) => {
			const { item, attributeNewValue } = data;
			const { mapper, writer } = conversionApi;
			if (!conversionApi.consumable.test(item, evt.name)) return;
			const table = mapper.toViewElement(item);
			writer.addClass(`${attributeNewValue}-table`, table);
			if (attributeNewValue == "layout") writer.setAttribute("role", "presentation", table);
			conversionApi.consumable.consume(item, evt.name);
		});
	};
}
/**
* Resolves the table type based on the view table element and the preferred external table type.
*/
function resolveTableType(viewTable, preferredExternalTableType) {
	if (viewTable.hasClass("content-table")) return "content";
	if (viewTable.hasClass("layout-table")) return "layout";
	if (preferredExternalTableType && TABLE_TYPES.includes(preferredExternalTableType)) return preferredExternalTableType;
	/**
	* Checks if the table is a content table if any of the following conditions are met:
	* - the `<table>` is wrapped with `<figure>`,
	* - the `<table>` has a `<caption>` element.
	*/
	if (viewTable.parent.is("element", "figure") || Array.from(viewTable.getChildren()).some((child) => child.is("element", "caption"))) return "content";
	return "layout";
}
/**
* Checks if the element is a layout table.
* It is used to disallow attributes or children that is managed by `Schema`.
*/
function layoutTableCheck(context) {
	if (context.endsWith("table") && context.last.getAttribute("tableType") == "layout") return false;
}

/**
* @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 table/tablelayout
*/
/**
* The table plugin.
*
* For a detailed overview, check the {@glink features/tables/layout-tables Layout table feature documentation}.
*/
var TableLayout = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "TableLayout";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [
			TableColumnResize,
			TableLayoutEditing,
			TableLayoutUI
		];
	}
};

/**
* @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 table cell attribute command.
*
* This command is a base command for other table property commands.
*/
var TablePropertyCommand = class extends Command {
	/**
	* The attribute that will be set by the command.
	*/
	attributeName;
	/**
	* The default value for the attribute.
	*
	* @readonly
	*/
	_defaultValue;
	/**
	* The default value for the attribute for the content table.
	*/
	_defaultContentTableValue;
	/**
	* The default value for the attribute for the layout table.
	*/
	_defaultLayoutTableValue;
	/**
	* Creates a new `TablePropertyCommand` instance.
	*
	* @param editor An editor in which this command will be used.
	* @param attributeName Table cell attribute name.
	* @param defaultValue The default value of the attribute.
	*/
	constructor(editor, attributeName, defaultValue) {
		super(editor);
		this.attributeName = attributeName;
		this._defaultContentTableValue = defaultValue;
		this._defaultLayoutTableValue = attributeName === "tableBorderStyle" ? "none" : void 0;
	}
	/**
	* @inheritDoc
	*/
	refresh() {
		const selection = this.editor.model.document.selection;
		const table = getSelectionAffectedTable(selection);
		this._defaultValue = !table || table.getAttribute("tableType") !== "layout" ? this._defaultContentTableValue : this._defaultLayoutTableValue;
		this.isEnabled = !!table;
		this.value = this._getValue(table);
	}
	/**
	* Executes the command.
	*
	* @fires execute
	* @param options.value If set, the command will set the attribute on the selected table.
	* If not set, the command will remove the attribute from the selected table.
	* @param options.batch Pass the model batch instance to the command to aggregate changes,
	* for example, to allow a single undo step for multiple executions.
	*/
	execute(options = {}) {
		const model = this.editor.model;
		const selection = model.document.selection;
		const { value, batch } = options;
		const table = getSelectionAffectedTable(selection);
		const valueToSet = this._getValueToSet(value);
		model.enqueueChange(batch, (writer) => {
			if (valueToSet) writer.setAttribute(this.attributeName, valueToSet, table);
			else writer.removeAttribute(this.attributeName, table);
		});
	}
	/**
	* Returns the attribute value for a table.
	*/
	_getValue(table) {
		if (!table) return;
		const value = table.getAttribute(this.attributeName);
		if (value === this._defaultValue) return;
		return value;
	}
	/**
	* Returns the proper model value. It can be used to add a default unit to numeric values.
	*/
	_getValueToSet(value) {
		if (value === this._defaultValue) return;
		return value;
	}
};

/**
* @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 table background color command.
*
* The command is registered by the {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
* the `'tableBackgroundColor'` editor command.
*
* To change the background color of the selected table, execute the command:
*
* ```ts
* editor.execute( 'tableBackgroundColor', {
*   value: '#f00'
* } );
* ```
*/
var TableBackgroundColorCommand = class extends TablePropertyCommand {
	/**
	* Creates a new `TableBackgroundColorCommand` instance.
	*
	* @param editor An editor in which this command will be used.
	* @param defaultValue The default value of the attribute.
	*/
	constructor(editor, defaultValue) {
		super(editor, "tableBackgroundColor", defaultValue);
	}
};

/**
* @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 table border color command.
*
* The command is registered by the {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
* the `'tableBorderColor'` editor command.
*
* To change the border color of the selected table, execute the command:
*
* ```ts
* editor.execute( 'tableBorderColor', {
*   value: '#f00'
* } );
* ```
*/
var TableBorderColorCommand = class extends TablePropertyCommand {
	/**
	* Creates a new `TableBorderColorCommand` instance.
	*
	* @param editor An editor in which this command will be used.
	* @param defaultValue The default value of the attribute.
	*/
	constructor(editor, defaultValue) {
		super(editor, "tableBorderColor", defaultValue);
	}
	/**
	* @inheritDoc
	*/
	_getValue(table) {
		if (!table) return;
		const value = getSingleValue(table.getAttribute(this.attributeName));
		if (value === this._defaultValue) return;
		return value;
	}
};

/**
* @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 table style border command.
*
* The command is registered by the {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
* the `'tableBorderStyle'` editor command.
*
* To change the border style of the selected table, execute the command:
*
* ```ts
* editor.execute( 'tableBorderStyle', {
*   value: 'dashed'
* } );
* ```
*/
var TableBorderStyleCommand = class extends TablePropertyCommand {
	/**
	* Creates a new `TableBorderStyleCommand` instance.
	*
	* @param editor An editor in which this command will be used.
	* @param defaultValue The default value of the attribute.
	*/
	constructor(editor, defaultValue) {
		super(editor, "tableBorderStyle", defaultValue);
	}
	/**
	* @inheritDoc
	*/
	_getValue(table) {
		if (!table) return;
		const value = getSingleValue(table.getAttribute(this.attributeName));
		if (value === this._defaultValue) return;
		return value;
	}
};

/**
* @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 table width border command.
*
* The command is registered by the {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
* the `'tableBorderWidth'` editor command.
*
* To change the border width of the selected table, execute the command:
*
* ```ts
* editor.execute( 'tableBorderWidth', {
*   value: '5px'
* } );
* ```
*
* **Note**: This command adds the default `'px'` unit to numeric values. Executing:
*
* ```ts
* editor.execute( 'tableBorderWidth', {
*   value: '5'
* } );
* ```
*
* will set the `borderWidth` attribute to `'5px'` in the model.
*/
var TableBorderWidthCommand = class extends TablePropertyCommand {
	/**
	* Creates a new `TableBorderWidthCommand` instance.
	*
	* @param editor An editor in which this command will be used.
	* @param defaultValue The default value of the attribute.
	*/
	constructor(editor, defaultValue) {
		super(editor, "tableBorderWidth", defaultValue);
	}
	/**
	* @inheritDoc
	*/
	_getValue(table) {
		if (!table) return;
		const value = getSingleValue(table.getAttribute(this.attributeName));
		if (value === this._defaultValue) return;
		return value;
	}
	/**
	* @inheritDoc
	*/
	_getValueToSet(value) {
		const newValue = addDefaultUnitToNumericValue(value, "px");
		if (newValue === this._defaultValue) return;
		return newValue;
	}
};

/**
* @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 table/tableproperties/commands/tablewidthcommand
*/
/**
* The table width command.
*
* The command is registered by the {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
* the `'tableWidth'` editor command.
*
* To change the width of the selected table, execute the command:
*
* ```ts
* editor.execute( 'tableWidth', {
*   value: '400px'
* } );
* ```
*
* **Note**: This command adds the default `'px'` unit to numeric values. Executing:
*
* ```ts
* editor.execute( 'tableWidth', {
*   value: '50'
* } );
* ```
*
* will set the `width` attribute to `'50px'` in the model.
*/
var TableWidthCommand = class extends TablePropertyCommand {
	/**
	* Creates a new `TableWidthCommand` instance.
	*
	* @param editor An editor in which this command will be used.
	* @param defaultValue The default value of the attribute.
	*/
	constructor(editor, defaultValue) {
		super(editor, "tableWidth", defaultValue);
	}
	/**
	* @inheritDoc
	*/
	_getValueToSet(value) {
		value = addDefaultUnitToNumericValue(value, "px");
		if (value === this._defaultValue) return;
		return value;
	}
};

/**
* @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 table/tableproperties/commands/tableheightcommand
*/
/**
* The table height command.
*
* The command is registered by the {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
* the `'tableHeight'` editor command.
*
* To change the height of the selected table, execute the command:
*
* ```ts
* editor.execute( 'tableHeight', {
*   value: '500px'
* } );
* ```
*
* **Note**: This command adds the default `'px'` unit to numeric values. Executing:
*
* ```ts
* editor.execute( 'tableHeight', {
*   value: '50'
* } );
* ```
*
* will set the `height` attribute to `'50px'` in the model.
*/
var TableHeightCommand = class extends TablePropertyCommand {
	/**
	* Creates a new `TableHeightCommand` instance.
	*
	* @param editor An editor in which this command will be used.
	* @param defaultValue The default value of the attribute.
	*/
	constructor(editor, defaultValue) {
		super(editor, "tableHeight", defaultValue);
	}
	/**
	* @inheritDoc
	*/
	_getValueToSet(value) {
		value = addDefaultUnitToNumericValue(value, "px");
		if (value === this._defaultValue) return;
		return value;
	}
};

/**
* @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 table alignment command.
*
* The command is registered by the {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
* the `'tableAlignment'` editor command.
*
* To change the alignment of the selected table, execute the command:
*
* ```ts
* editor.execute( 'tableAlignment', {
*   value: 'right'
* } );
* ```
*/
var TableAlignmentCommand = class extends TablePropertyCommand {
	/**
	* Creates a new `TableAlignmentCommand` instance.
	*
	* @param editor An editor in which this command will be used.
	* @param defaultValue The default value for the "alignment" attribute.
	*/
	constructor(editor, defaultValue) {
		super(editor, "tableAlignment", defaultValue);
	}
};

/**
* @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 table/tableproperties/tablepropertiesediting
*/
/**
* The table properties editing feature.
*
* Introduces table's model attributes and their conversion:
*
* - border: `tableBorderStyle`, `tableBorderColor` and `tableBorderWidth`
* - background color: `tableBackgroundColor`
* - horizontal alignment: `tableAlignment`
* - width & height: `tableWidth` & `tableHeight`
*
* It also registers commands used to manipulate the above attributes:
*
* - border: `'tableBorderStyle'`, `'tableBorderColor'` and `'tableBorderWidth'` commands
* - background color: `'tableBackgroundColor'`
* - horizontal alignment: `'tableAlignment'`
* - width & height: `'tableWidth'` & `'tableHeight'`
*/
var TablePropertiesEditing = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "TablePropertiesEditing";
	}
	/**
	* @inheritDoc
	* @internal
	*/
	static get licenseFeatureCode() {
		return "TCP";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	static get isPremiumPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [TableEditing];
	}
	/**
	* @inheritDoc
	*/
	init() {
		const editor = this.editor;
		const schema = editor.model.schema;
		const conversion = editor.conversion;
		editor.config.define("table.tableProperties.defaultProperties", {});
		const defaultTableProperties = getNormalizedDefaultTableProperties(editor.config.get("table.tableProperties.defaultProperties"), { includeAlignmentProperty: true });
		const useInlineStyles = editor.config.get("table.tableProperties.alignment.useInlineStyles") === true;
		editor.data.addStyleProcessorRules(addMarginStylesRules);
		editor.data.addStyleProcessorRules(addBorderStylesRules);
		enableBorderProperties(editor, {
			color: defaultTableProperties.borderColor,
			style: defaultTableProperties.borderStyle,
			width: defaultTableProperties.borderWidth
		});
		editor.commands.add("tableBorderColor", new TableBorderColorCommand(editor, defaultTableProperties.borderColor));
		editor.commands.add("tableBorderStyle", new TableBorderStyleCommand(editor, defaultTableProperties.borderStyle));
		editor.commands.add("tableBorderWidth", new TableBorderWidthCommand(editor, defaultTableProperties.borderWidth));
		enableAlignmentProperty(schema, conversion, defaultTableProperties.alignment, useInlineStyles);
		editor.commands.add("tableAlignment", new TableAlignmentCommand(editor, defaultTableProperties.alignment));
		enableTableToFigureProperty(schema, conversion, {
			modelAttribute: "tableWidth",
			styleName: "width",
			attributeName: "width",
			attributeType: "length",
			defaultValue: defaultTableProperties.width
		});
		editor.commands.add("tableWidth", new TableWidthCommand(editor, defaultTableProperties.width));
		enableTableToFigureProperty(schema, conversion, {
			modelAttribute: "tableHeight",
			styleName: "height",
			attributeName: "height",
			attributeType: "length",
			defaultValue: defaultTableProperties.height
		});
		editor.commands.add("tableHeight", new TableHeightCommand(editor, defaultTableProperties.height));
		editor.data.addStyleProcessorRules(addBackgroundStylesRules);
		enableProperty$1(schema, conversion, {
			modelAttribute: "tableBackgroundColor",
			styleName: "background-color",
			attributeName: "bgcolor",
			attributeType: "color",
			defaultValue: defaultTableProperties.backgroundColor
		});
		editor.commands.add("tableBackgroundColor", new TableBackgroundColorCommand(editor, defaultTableProperties.backgroundColor));
		upcastTableCellPaddingAttribute(editor, "table");
		const viewDoc = editor.editing.view.document;
		this.listenTo(viewDoc, "clipboardOutput", (evt, data) => {
			editor.editing.view.change((writer) => {
				for (const { item } of writer.createRangeIn(data.content)) wrapInDivIfNeeded(item, writer);
				data.dataTransfer.setData("text/html", this.editor.data.htmlProcessor.toData(data.content));
			});
		}, { priority: "lowest" });
	}
};
/**
* Checks whether the view element is a table and if it needs to be wrapped in a div for alignment purposes.
* If so, it wraps it in a div and inserts it into the data content.
*/
function wrapInDivIfNeeded(viewItem, writer) {
	if (!viewItem.is("element", "table")) return;
	const alignAttribute = viewItem.getAttribute("align");
	const floatAttribute = viewItem.getStyle("float");
	const marginLeft = viewItem.getStyle("margin-left");
	const marginRight = viewItem.getStyle("margin-right");
	if (alignAttribute && alignAttribute === "center" || floatAttribute && floatAttribute === "right" && alignAttribute && alignAttribute === "right") {
		insertWrapperWithAlignment(writer, alignAttribute, viewItem);
		return;
	}
	if (floatAttribute === void 0 && marginLeft === "auto" && marginRight === "0") insertWrapperWithAlignment(writer, "right", viewItem);
}
function insertWrapperWithAlignment(writer, align, table) {
	const position = writer.createPositionBefore(table);
	const wrapper = writer.createContainerElement("div", { align }, table);
	writer.insert(position, wrapper);
}
/**
* Enables `tableBorderStyle'`, `tableBorderColor'` and `tableBorderWidth'` attributes for table.
*
* @param defaultBorder The default border values.
* @param defaultBorder.color The default `tableBorderColor` value.
* @param defaultBorder.style The default `tableBorderStyle` value.
* @param defaultBorder.width The default `tableBorderWidth` value.
*/
function enableBorderProperties(editor, defaultBorder) {
	const { conversion } = editor;
	const { schema } = editor.model;
	const modelAttributes = {
		width: "tableBorderWidth",
		color: "tableBorderColor",
		style: "tableBorderStyle"
	};
	schema.extend("table", { allowAttributes: Object.values(modelAttributes) });
	for (const modelAttribute of Object.values(modelAttributes)) schema.setAttributeProperties(modelAttribute, { isFormatting: true });
	upcastBorderStyles(editor, "table", modelAttributes, defaultBorder);
	downcastTableAttribute(conversion, {
		modelAttribute: modelAttributes.color,
		styleName: "border-color"
	});
	downcastTableAttribute(conversion, {
		modelAttribute: modelAttributes.style,
		styleName: "border-style"
	});
	downcastTableAttribute(conversion, {
		modelAttribute: modelAttributes.width,
		styleName: "border-width"
	});
}
/**
* Enables the `'alignment'` attribute for table.
*
* @param defaultValue The default alignment value.
*/
function enableAlignmentProperty(schema, conversion, defaultValue, useInlineStyles) {
	schema.extend("table", { allowAttributes: ["tableAlignment"] });
	schema.setAttributeProperties("tableAlignment", {
		isFormatting: true,
		blockAlignment: (modelElement) => ({
			left: { value: "blockLeft" },
			right: { value: "blockRight" },
			center: {
				value: "center",
				isDefault: modelElement.getAttribute("tableType") !== "layout"
			}
		})
	});
	conversion.for("downcast").attributeToAttribute({
		model: {
			name: "table",
			key: "tableAlignment",
			values: [
				"left",
				"center",
				"right",
				"blockLeft",
				"blockRight"
			]
		},
		view: {
			left: useInlineStyles ? {
				key: "style",
				value: {
					float: "left",
					"margin-right": "var(--ck-content-table-style-spacing, 1.5em)"
				}
			} : {
				key: "class",
				value: DEFAULT_TABLE_ALIGNMENT_OPTIONS.left.className
			},
			right: useInlineStyles ? {
				key: "style",
				value: {
					float: "right",
					"margin-left": "var(--ck-content-table-style-spacing, 1.5em)"
				}
			} : {
				key: "class",
				value: DEFAULT_TABLE_ALIGNMENT_OPTIONS.right.className
			},
			center: useInlineStyles ? {
				key: "style",
				value: {
					"margin-left": "auto",
					"margin-right": "auto"
				}
			} : {
				key: "class",
				value: DEFAULT_TABLE_ALIGNMENT_OPTIONS.center.className
			},
			blockLeft: useInlineStyles ? {
				key: "style",
				value: {
					"margin-left": "0",
					"margin-right": "auto"
				}
			} : {
				key: "class",
				value: DEFAULT_TABLE_ALIGNMENT_OPTIONS.blockLeft.className
			},
			blockRight: useInlineStyles ? {
				key: "style",
				value: {
					"margin-left": "auto",
					"margin-right": "0"
				}
			} : {
				key: "class",
				value: DEFAULT_TABLE_ALIGNMENT_OPTIONS.blockRight.className
			}
		},
		converterPriority: "high"
	});
	/**
	* Enables upcasting of the `tableAlignment` attribute.
	*/
	upcastTableAlignmentConfig.forEach((config) => {
		conversion.for("upcast").attributeToAttribute({
			view: config.view,
			model: {
				key: "tableAlignment",
				value: (viewElement, conversionApi, data) => {
					if (isNonTableFigureElement(viewElement)) return;
					const localDefaultValue = getDefaultValueAdjusted(defaultValue, "", data);
					const align = config.getAlign(viewElement);
					const consumables = config.getConsumables(viewElement);
					conversionApi.consumable.consume(viewElement, consumables);
					if (align !== localDefaultValue) return align;
				}
			}
		});
	});
	conversion.for("upcast").add(upcastTableAlignedDiv(defaultValue));
}
/**
* Returns a function that converts the table view representation:
*
* ```html
* <div align="right"><table>...</table></div>
* <!-- or -->
* <div align="center"><table>...</table></div>
* <!-- or -->
* <div align="left"><table>...</table></div>
* ```
*
* to the model representation:
*
* ```xml
* <table tableAlignment="right|center|left"></table>
* ```
*
* @internal
*/
function upcastTableAlignedDiv(defaultValue) {
	return (dispatcher) => {
		dispatcher.on("element:div", (evt, data, conversionApi) => {
			if (!conversionApi.consumable.test(data.viewItem, {
				name: true,
				attributes: "align"
			})) return;
			const significantChildren = Array.from(data.viewItem.getChildren()).filter((child) => {
				if (child.is("$text") || child.is("$textProxy")) return child.data.trim() !== "";
				return true;
			});
			if (significantChildren.length !== 1 || !significantChildren[0].is("element", "table")) return;
			const [viewTable] = significantChildren;
			if (!conversionApi.consumable.test(viewTable, { name: true })) return;
			conversionApi.consumable.consume(data.viewItem, {
				name: true,
				attributes: "align"
			});
			const conversionResult = conversionApi.convertItem(viewTable, data.modelCursor);
			const modelTable = first(conversionResult.modelRange.getItems());
			if (!modelTable || !modelTable.is("element", "table")) {
				conversionApi.consumable.revert(data.viewItem, {
					name: true,
					attributes: "align"
				});
				if (conversionResult.modelRange && !conversionResult.modelRange.isCollapsed) {
					data.modelRange = conversionResult.modelRange;
					data.modelCursor = conversionResult.modelCursor;
				}
				return;
			}
			const align = convertToTableAlignment(data.viewItem.getAttribute("align"), viewTable.getAttribute("align"), getDefaultValueAdjusted(defaultValue, "", data));
			if (align) conversionApi.writer.setAttribute("tableAlignment", align, modelTable);
			conversionApi.updateConversionResult(modelTable, data);
		});
	};
}
/**
* Converts div `align` and table `align` attributes to the model `tableAlignment` attribute.
*
* @param divAlign The value of the div `align` attribute.
* @param tableAlign The value of the table `align` attribute.
* @param defaultValue The default alignment value.
* @returns The model `tableAlignment` value or `undefined` if no conversion is needed.
*/
function convertToTableAlignment(divAlign, tableAlign, defaultValue) {
	if (divAlign) switch (divAlign) {
		case "right": if (tableAlign === "right") return "right";
		else if (tableAlign === "left") return "left";
		else return "blockRight";
		case "center": return "center";
		case "left": return tableAlign === void 0 ? "blockLeft" : "left";
		default: return defaultValue;
	}
}
/**
* Enables conversion for an attribute for simple view-model mappings.
*
* @param options.defaultValue The default value for the specified `modelAttribute`.
*/
function enableProperty$1(schema, conversion, options) {
	const { modelAttribute } = options;
	schema.extend("table", { allowAttributes: [modelAttribute] });
	schema.setAttributeProperties(modelAttribute, { isFormatting: true });
	upcastStyleToAttribute(conversion, {
		viewElement: "table",
		...options
	});
	downcastTableAttribute(conversion, options);
}
/**
* Enables conversion for an attribute for simple view (figure) to model (table) mappings.
*/
function enableTableToFigureProperty(schema, conversion, options) {
	const { modelAttribute } = options;
	schema.extend("table", { allowAttributes: [modelAttribute] });
	schema.setAttributeProperties(modelAttribute, { isFormatting: true });
	upcastStyleToAttribute(conversion, {
		viewElement: /^(table|figure)$/,
		shouldUpcast: (viewElement) => !(viewElement.name == "table" && viewElement.parent.name == "figure" || viewElement.name == "figure" && !viewElement.hasClass("table")),
		...options
	});
	downcastAttributeToStyle(conversion, {
		modelElement: "table",
		...options
	});
}
/**
* Checks whether a given figure element should be ignored when upcasting table properties.
*/
function isNonTableFigureElement(viewElement) {
	return viewElement.name == "figure" && !viewElement.hasClass("table");
}

/**
* @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 table/tableproperties/ui/tablepropertiesview
*/
/**
* The class representing a table properties form, allowing users to customize
* certain style aspects of a table, for instance, border, background color, alignment, etc..
*/
var TablePropertiesView = class extends View {
	/**
	* Options passed to the view. See {@link #constructor} to learn more.
	*/
	options;
	/**
	* Tracks information about the DOM focus in the form.
	*/
	focusTracker;
	/**
	* An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
	*/
	keystrokes;
	/**
	* A collection of child views in the form.
	*/
	children;
	/**
	* A dropdown that allows selecting the style of the table border.
	*/
	borderStyleDropdown;
	/**
	* An input that allows specifying the width of the table border.
	*/
	borderWidthInput;
	/**
	* An input that allows specifying the color of the table border.
	*/
	borderColorInput;
	/**
	* An input that allows specifying the table background color.
	*/
	backgroundInput;
	/**
	* An input that allows specifying the table width.
	*/
	widthInput;
	/**
	* An input that allows specifying the table height.
	*/
	heightInput;
	/**
	* A toolbar with buttons that allow changing the alignment of an entire table.
	*/
	alignmentToolbar;
	/**
	* The "Save" button view.
	*/
	saveButtonView;
	/**
	* The "Cancel" button view.
	*/
	cancelButtonView;
	/**
	* The Back button view displayed in the header.
	*/
	backButtonView;
	/**
	* A collection of views that can be focused in the form.
	*/
	_focusables;
	/**
	* Helps cycling over {@link #_focusables} in the form.
	*/
	_focusCycler;
	/**
	* @param locale The {@link module:core/editor/editor~Editor#locale} instance.
	* @param options Additional configuration of the view.
	*/
	constructor(locale, options) {
		super(locale);
		this.set({
			borderStyle: "",
			borderWidth: "",
			borderColor: "",
			backgroundColor: "",
			width: "",
			height: "",
			alignment: ""
		});
		this.options = options;
		const { borderStyleDropdown, borderWidthInput, borderColorInput, borderRowLabel } = this._createBorderFields();
		const { backgroundRowLabel, backgroundInput } = this._createBackgroundFields();
		const { widthInput, operatorLabel, heightInput, dimensionsLabel } = this._createDimensionFields();
		const { alignmentToolbar, alignmentLabel } = this._createAlignmentFields();
		this.focusTracker = new FocusTracker();
		this.keystrokes = new KeystrokeHandler();
		this.children = this.createCollection();
		this.borderStyleDropdown = borderStyleDropdown;
		this.borderWidthInput = borderWidthInput;
		this.borderColorInput = borderColorInput;
		this.backgroundInput = backgroundInput;
		this.widthInput = widthInput;
		this.heightInput = heightInput;
		this.alignmentToolbar = alignmentToolbar;
		const { saveButtonView, cancelButtonView } = this._createActionButtons();
		this.saveButtonView = saveButtonView;
		this.cancelButtonView = cancelButtonView;
		this.backButtonView = this._createBackButton();
		this._focusables = new ViewCollection();
		this._focusCycler = new FocusCycler({
			focusables: this._focusables,
			focusTracker: this.focusTracker,
			keystrokeHandler: this.keystrokes,
			actions: {
				focusPrevious: "shift + tab",
				focusNext: "tab"
			}
		});
		const headerView = new FormHeaderView(locale, { label: this.t("Table properties") });
		headerView.children.add(this.backButtonView, 0);
		this.children.add(headerView);
		this.children.add(new FormRowView(locale, {
			labelView: borderRowLabel,
			children: [
				borderRowLabel,
				borderStyleDropdown,
				borderWidthInput,
				borderColorInput
			],
			class: "ck-table-form__border-row"
		}));
		this.children.add(new FormRowView(locale, { children: [new FormRowView(locale, {
			labelView: dimensionsLabel,
			children: [
				dimensionsLabel,
				widthInput,
				operatorLabel,
				heightInput
			],
			class: "ck-table-form__dimensions-row"
		}), new FormRowView(locale, {
			labelView: backgroundRowLabel,
			children: [backgroundRowLabel, backgroundInput],
			class: "ck-table-form__background-row"
		})] }));
		this.children.add(new FormRowView(locale, {
			labelView: alignmentLabel,
			children: [alignmentLabel, alignmentToolbar],
			class: "ck-table-properties-form__alignment-row"
		}));
		this.children.add(new FormRowView(locale, {
			children: [this.cancelButtonView, this.saveButtonView],
			class: "ck-table-form__action-row"
		}));
		this.setTemplate({
			tag: "form",
			attributes: {
				class: [
					"ck",
					"ck-form",
					"ck-table-form",
					"ck-table-properties-form"
				],
				tabindex: "-1"
			},
			children: this.children
		});
	}
	/**
	* @inheritDoc
	*/
	render() {
		super.render();
		submitHandler({ view: this });
		[this.borderColorInput, this.backgroundInput].forEach((view) => {
			this._focusCycler.chain(view.fieldView.focusCycler);
		});
		[
			this.borderStyleDropdown,
			this.borderWidthInput,
			this.borderColorInput,
			this.widthInput,
			this.heightInput,
			this.backgroundInput,
			this.alignmentToolbar,
			this.cancelButtonView,
			this.saveButtonView,
			this.backButtonView
		].forEach((view) => {
			this._focusables.add(view);
			this.focusTracker.add(view.element);
		});
		this.keystrokes.listenTo(this.element);
	}
	/**
	* @inheritDoc
	*/
	destroy() {
		super.destroy();
		this.focusTracker.destroy();
		this.keystrokes.destroy();
	}
	/**
	* Focuses the fist focusable field in the form.
	*/
	focus() {
		this._focusCycler.focusFirst();
	}
	/**
	* Creates the following form fields:
	*
	* * {@link #borderStyleDropdown},
	* * {@link #borderWidthInput},
	* * {@link #borderColorInput}.
	*/
	_createBorderFields() {
		const defaultTableProperties = this.options.defaultTableProperties;
		const defaultBorder = {
			style: defaultTableProperties.borderStyle,
			width: defaultTableProperties.borderWidth,
			color: defaultTableProperties.borderColor
		};
		const colorInputCreator = getLabeledColorInputCreator({
			colorConfig: this.options.borderColors,
			columns: 5,
			defaultColorValue: defaultBorder.color,
			colorPickerConfig: this.options.colorPickerConfig
		});
		const locale = this.locale;
		const t = this.t;
		const accessibleLabel = t("Style");
		const borderRowLabel = new LabelView(locale);
		borderRowLabel.text = t("Border");
		const styleLabels = getBorderStyleLabels(t);
		const borderStyleDropdown = new LabeledFieldView(locale, createLabeledDropdown);
		borderStyleDropdown.set({
			label: accessibleLabel,
			class: "ck-table-form__border-style"
		});
		borderStyleDropdown.fieldView.buttonView.set({
			ariaLabel: accessibleLabel,
			ariaLabelledBy: void 0,
			isOn: false,
			withText: true,
			tooltip: accessibleLabel
		});
		borderStyleDropdown.fieldView.buttonView.bind("label").to(this, "borderStyle", (value) => {
			return styleLabels[value ? value : "none"];
		});
		borderStyleDropdown.fieldView.on("execute", (evt) => {
			this.borderStyle = evt.source._borderStyleValue;
		});
		borderStyleDropdown.bind("isEmpty").to(this, "borderStyle", (value) => !value);
		addListToDropdown(borderStyleDropdown.fieldView, getBorderStyleDefinitions(this, defaultBorder.style), {
			role: "menu",
			ariaLabel: accessibleLabel
		});
		const borderWidthInput = new LabeledFieldView(locale, createLabeledInputText);
		borderWidthInput.set({
			label: t("Width"),
			class: "ck-table-form__border-width"
		});
		borderWidthInput.fieldView.bind("value").to(this, "borderWidth");
		borderWidthInput.bind("isEnabled").to(this, "borderStyle", isBorderStyleSet);
		borderWidthInput.fieldView.on("input", () => {
			this.borderWidth = borderWidthInput.fieldView.element.value;
		});
		const borderColorInput = new LabeledFieldView(locale, colorInputCreator);
		borderColorInput.set({
			label: t("Color"),
			class: "ck-table-form__border-color"
		});
		borderColorInput.fieldView.bind("value").to(this, "borderColor");
		borderColorInput.bind("isEnabled").to(this, "borderStyle", isBorderStyleSet);
		borderColorInput.fieldView.on("input", () => {
			this.borderColor = borderColorInput.fieldView.value;
		});
		this.on("change:borderStyle", (evt, name, newValue, oldValue) => {
			if (!isBorderStyleSet(newValue)) {
				this.borderColor = "";
				this.borderWidth = "";
			}
			if (!isBorderStyleSet(oldValue)) {
				this.borderColor = defaultBorder.color;
				this.borderWidth = defaultBorder.width;
			}
		});
		return {
			borderRowLabel,
			borderStyleDropdown,
			borderColorInput,
			borderWidthInput
		};
	}
	/**
	* Creates the following form fields:
	*
	* * {@link #backgroundInput}.
	*/
	_createBackgroundFields() {
		const locale = this.locale;
		const t = this.t;
		const backgroundRowLabel = new LabelView(locale);
		backgroundRowLabel.text = t("Background");
		const backgroundInput = new LabeledFieldView(locale, getLabeledColorInputCreator({
			colorConfig: this.options.backgroundColors,
			columns: 5,
			defaultColorValue: this.options.defaultTableProperties.backgroundColor,
			colorPickerConfig: this.options.colorPickerConfig
		}));
		backgroundInput.set({
			label: t("Color"),
			class: "ck-table-properties-form__background"
		});
		backgroundInput.fieldView.bind("value").to(this, "backgroundColor");
		backgroundInput.fieldView.on("input", () => {
			this.backgroundColor = backgroundInput.fieldView.value;
		});
		return {
			backgroundRowLabel,
			backgroundInput
		};
	}
	/**
	* Creates the following form fields:
	*
	* * {@link #widthInput},
	* * {@link #heightInput}.
	*/
	_createDimensionFields() {
		const locale = this.locale;
		const t = this.t;
		const dimensionsLabel = new LabelView(locale);
		dimensionsLabel.text = t("Dimensions");
		const widthInput = new LabeledFieldView(locale, createLabeledInputText);
		widthInput.set({
			label: t("Width"),
			class: "ck-table-form__dimensions-row__width"
		});
		widthInput.fieldView.bind("value").to(this, "width");
		widthInput.fieldView.on("input", () => {
			this.width = widthInput.fieldView.element.value;
		});
		const operatorLabel = new View(locale);
		operatorLabel.setTemplate({
			tag: "span",
			attributes: { class: ["ck-table-form__dimension-operator"] },
			children: [{ text: "×" }]
		});
		const heightInput = new LabeledFieldView(locale, createLabeledInputText);
		heightInput.set({
			label: t("Height"),
			class: "ck-table-form__dimensions-row__height"
		});
		heightInput.fieldView.bind("value").to(this, "height");
		heightInput.fieldView.on("input", () => {
			this.height = heightInput.fieldView.element.value;
		});
		return {
			dimensionsLabel,
			widthInput,
			operatorLabel,
			heightInput
		};
	}
	/**
	* Creates the following form fields:
	*
	* * {@link #alignmentToolbar}.
	*/
	_createAlignmentFields() {
		const locale = this.locale;
		const t = this.t;
		const alignmentLabel = new LabelView(locale);
		alignmentLabel.text = t("Table Alignment");
		const alignmentToolbar = new ToolbarView(locale);
		alignmentToolbar.set({
			role: "radiogroup",
			isCompact: true,
			ariaLabel: t("Table alignment toolbar")
		});
		fillToolbar({
			view: this,
			icons: {
				left: IconObjectInlineLeft,
				center: IconObjectCenter,
				right: IconObjectInlineRight,
				blockLeft: IconObjectLeft,
				blockRight: IconObjectRight
			},
			toolbar: alignmentToolbar,
			labels: this._alignmentLabels,
			propertyName: "alignment",
			defaultValue: this.options.defaultTableProperties.alignment
		});
		return {
			alignmentLabel,
			alignmentToolbar
		};
	}
	/**
	* Creates the following form controls:
	*
	* * {@link #saveButtonView},
	* * {@link #cancelButtonView}.
	*/
	_createActionButtons() {
		const locale = this.locale;
		const t = this.t;
		const saveButtonView = new ButtonView(locale);
		const cancelButtonView = new ButtonView(locale);
		const fieldsThatShouldValidateToSave = [
			this.borderWidthInput,
			this.borderColorInput,
			this.backgroundInput,
			this.widthInput,
			this.heightInput
		];
		saveButtonView.set({
			label: t("Save"),
			class: "ck-button-action",
			type: "submit",
			withText: true
		});
		saveButtonView.bind("isEnabled").toMany(fieldsThatShouldValidateToSave, "errorText", (...errorTexts) => {
			return errorTexts.every((errorText) => !errorText);
		});
		cancelButtonView.set({
			label: t("Cancel"),
			withText: true
		});
		cancelButtonView.delegate("execute").to(this, "cancel");
		return {
			saveButtonView,
			cancelButtonView
		};
	}
	/**
	* 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;
	}
	/**
	* Provides localized labels for {@link #alignmentToolbar} buttons.
	*/
	get _alignmentLabels() {
		const locale = this.locale;
		const t = this.t;
		const blockLeft = t("Align table to the left with no text wrapping");
		const blockRight = t("Align table to the right with no text wrapping");
		const left = t("Align table to the left with text wrapping");
		const center = t("Center table with no text wrapping");
		const right = t("Align table to the right with text wrapping");
		if (locale.uiLanguageDirection === "rtl") return {
			right,
			left,
			blockRight,
			center,
			blockLeft
		};
		return {
			blockLeft,
			center,
			blockRight,
			left,
			right
		};
	}
};
function isBorderStyleSet(value) {
	return value !== "none";
}

/**
* @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 table/tableproperties/tablepropertiesui
*/
const ERROR_TEXT_TIMEOUT = 500;
const propertyToCommandMap = {
	borderStyle: "tableBorderStyle",
	borderColor: "tableBorderColor",
	borderWidth: "tableBorderWidth",
	backgroundColor: "tableBackgroundColor",
	width: "tableWidth",
	height: "tableHeight",
	alignment: "tableAlignment"
};
/**
* The table properties UI plugin. It introduces the `'tableProperties'` button
* that opens a form allowing to specify visual styling of an entire table.
*
* It uses the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon plugin}.
*/
var TablePropertiesUI = class extends Plugin {
	/**
	* The default table properties.
	*/
	_defaultContentTableProperties;
	/**
	* The default layout table properties.
	*/
	_defaultLayoutTableProperties;
	/**
	* The contextual balloon plugin instance.
	*/
	_balloon;
	/**
	* The properties form view displayed inside the balloon.
	*/
	view = null;
	/**
	* The properties form view displayed inside the balloon (content table).
	*/
	_viewWithContentTableDefaults = null;
	/**
	* The properties form view displayed inside the balloon (layout table).
	*/
	_viewWithLayoutTableDefaults = null;
	/**
	* The batch used to undo all changes made by the form (which are live, as the user types)
	* when "Cancel" was pressed. Each time the view is shown, a new batch is created.
	*/
	_undoStepBatch;
	/**
	* Flag used to indicate whether view is ready to execute update commands
	* (it finished loading initial data).
	*/
	_isReady;
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [ContextualBalloon];
	}
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "TablePropertiesUI";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	constructor(editor) {
		super(editor);
		editor.config.define("table.tableProperties", {
			borderColors: defaultColors,
			backgroundColors: defaultColors
		});
	}
	/**
	* @inheritDoc
	*/
	init() {
		const editor = this.editor;
		this._defaultContentTableProperties = getNormalizedDefaultTableProperties(editor.config.get("table.tableProperties.defaultProperties"), { includeAlignmentProperty: true });
		this._defaultLayoutTableProperties = getNormalizedDefaultProperties();
		this._balloon = editor.plugins.get(ContextualBalloon);
		editor.ui.componentFactory.add("tableProperties", () => this._createTablePropertiesButton());
	}
	/**
	* Creates the table properties button.
	*
	* @internal
	*/
	_createTablePropertiesButton() {
		const editor = this.editor;
		const t = editor.t;
		const view = new ButtonView(editor.locale);
		view.set({
			label: t("Table properties"),
			icon: IconTableProperties,
			tooltip: true
		});
		this.listenTo(view, "execute", () => this._showView());
		const commands = Object.values(propertyToCommandMap).map((commandName) => editor.commands.get(commandName));
		view.bind("isEnabled").toMany(commands, "isEnabled", (...areEnabled) => areEnabled.some((isCommandEnabled) => isCommandEnabled));
		return view;
	}
	/**
	* @inheritDoc
	*/
	destroy() {
		super.destroy();
		if (this.view) this.view.destroy();
	}
	/**
	* Creates the {@link module:table/tableproperties/ui/tablepropertiesview~TablePropertiesView} instance.
	*
	* @returns The table properties form view instance.
	*/
	_createPropertiesView(defaultTableProperties) {
		const editor = this.editor;
		const config = editor.config.get("table.tableProperties");
		const borderColorsConfig = normalizeColorOptions(config.borderColors);
		const localizedBorderColors = getLocalizedColorOptions(editor.locale, borderColorsConfig);
		const backgroundColorsConfig = normalizeColorOptions(config.backgroundColors);
		const localizedBackgroundColors = getLocalizedColorOptions(editor.locale, backgroundColorsConfig);
		const hasColorPicker = config.colorPicker !== false;
		const view = new TablePropertiesView(editor.locale, {
			borderColors: localizedBorderColors,
			backgroundColors: localizedBackgroundColors,
			defaultTableProperties,
			colorPickerConfig: hasColorPicker ? config.colorPicker || {} : false
		});
		const t = editor.t;
		view.render();
		this.listenTo(view, "submit", () => {
			this._hideView();
		});
		this.listenTo(view, "cancel", () => {
			if (this._undoStepBatch.operations.length) editor.execute("undo", this._undoStepBatch);
			this._hideView();
		});
		view.keystrokes.set("Esc", (data, cancel) => {
			this._hideView();
			cancel();
		});
		clickOutsideHandler({
			emitter: view,
			activator: () => this._isViewInBalloon,
			contextElements: [this._balloon.view.element],
			callback: () => this._hideView()
		});
		const colorErrorText = getLocalizedColorErrorText(t);
		const lengthErrorText = getLocalizedLengthErrorText(t);
		view.on("change:borderStyle", this._getPropertyChangeCallback("tableBorderStyle"));
		view.on("change:borderColor", this._getValidatedPropertyChangeCallback({
			viewField: view.borderColorInput,
			commandName: "tableBorderColor",
			errorText: colorErrorText,
			validator: colorFieldValidator
		}));
		view.on("change:borderWidth", this._getValidatedPropertyChangeCallback({
			viewField: view.borderWidthInput,
			commandName: "tableBorderWidth",
			errorText: lengthErrorText,
			validator: lineWidthFieldValidator
		}));
		view.on("change:backgroundColor", this._getValidatedPropertyChangeCallback({
			viewField: view.backgroundInput,
			commandName: "tableBackgroundColor",
			errorText: colorErrorText,
			validator: colorFieldValidator
		}));
		view.on("change:width", this._getValidatedPropertyChangeCallback({
			viewField: view.widthInput,
			commandName: "tableWidth",
			errorText: lengthErrorText,
			validator: lengthFieldValidator
		}));
		view.on("change:height", this._getValidatedPropertyChangeCallback({
			viewField: view.heightInput,
			commandName: "tableHeight",
			errorText: lengthErrorText,
			validator: lengthFieldValidator
		}));
		view.on("change:alignment", this._getPropertyChangeCallback("tableAlignment"));
		return view;
	}
	/**
	* In this method the "editor data -> UI" binding is happening.
	*
	* When executed, this method obtains selected table property values from various table commands
	* and passes them to the {@link #view}.
	*
	* This way, the UI stays up–to–date with the editor data.
	*/
	_fillViewFormFromCommandValues() {
		const commands = this.editor.commands;
		const borderStyleCommand = commands.get("tableBorderStyle");
		Object.entries(propertyToCommandMap).map(([property, commandName]) => {
			const propertyKey = property;
			const defaultValue = this.view === this._viewWithContentTableDefaults ? this._defaultContentTableProperties[propertyKey] || "" : this._defaultLayoutTableProperties[propertyKey] || "";
			return [propertyKey, commands.get(commandName).value || defaultValue];
		}).forEach(([property, value]) => {
			if ((property === "borderColor" || property === "borderWidth") && borderStyleCommand.value === "none") return;
			this.view.set(property, value);
		});
		this._isReady = true;
	}
	/**
	* Shows the {@link #view} in the {@link #_balloon}.
	*
	* **Note**: Each time a view is shown, the new {@link #_undoStepBatch} is created that contains
	* all changes made to the document when the view is visible, allowing a single undo step
	* for all of them.
	*/
	_showView() {
		const editor = this.editor;
		const viewTable = getSelectionAffectedTableWidget(editor.editing.view.document.selection);
		const modelTable = viewTable && editor.editing.mapper.toModelElement(viewTable);
		const useDefaults = !modelTable || modelTable.getAttribute("tableType") !== "layout";
		if (useDefaults && !this._viewWithContentTableDefaults) this._viewWithContentTableDefaults = this._createPropertiesView(this._defaultContentTableProperties);
		else if (!useDefaults && !this._viewWithLayoutTableDefaults) this._viewWithLayoutTableDefaults = this._createPropertiesView(this._defaultLayoutTableProperties);
		this.view = useDefaults ? this._viewWithContentTableDefaults : this._viewWithLayoutTableDefaults;
		this.listenTo(editor.ui, "update", () => {
			this._updateView();
		});
		this._fillViewFormFromCommandValues();
		this._balloon.add({
			view: this.view,
			position: getBalloonTablePositionData(editor)
		});
		this._undoStepBatch = editor.model.createBatch();
		this.view.focus();
	}
	/**
	* Removes the {@link #view} from the {@link #_balloon}.
	*/
	_hideView() {
		const editor = this.editor;
		this.stopListening(editor.ui, "update");
		this._isReady = false;
		this.view.saveButtonView.focus();
		this._balloon.remove(this.view);
		this.editor.editing.view.focus();
	}
	/**
	* Repositions the {@link #_balloon} or hides the {@link #view} if a table is no longer selected.
	*/
	_updateView() {
		const editor = this.editor;
		const viewDocument = editor.editing.view.document;
		if (!getSelectionAffectedTableWidget(viewDocument.selection)) this._hideView();
		else if (this._isViewVisible) repositionContextualBalloon(editor, "table");
	}
	/**
	* Returns `true` when the {@link #view} is the visible in the {@link #_balloon}.
	*/
	get _isViewVisible() {
		return !!this.view && this._balloon.visibleView === this.view;
	}
	/**
	* Returns `true` when the {@link #view} is in the {@link #_balloon}.
	*/
	get _isViewInBalloon() {
		return !!this.view && this._balloon.hasView(this.view);
	}
	/**
	* Creates a callback that when executed upon {@link #view view's} property change
	* executes a related editor command with the new property value.
	*
	* If new value will be set to the default value, the command will not be executed.
	*
	* @param commandName The command that will be executed.
	*/
	_getPropertyChangeCallback(commandName) {
		return (evt, propertyName, newValue) => {
			if (!this._isReady) return;
			this.editor.execute(commandName, {
				value: newValue,
				batch: this._undoStepBatch
			});
		};
	}
	/**
	* Creates a callback that when executed upon {@link #view view's} property change:
	* * executes a related editor command with the new property value if the value is valid,
	* * or sets the error text next to the invalid field, if the value did not pass the validation.
	*/
	_getValidatedPropertyChangeCallback(options) {
		const { commandName, viewField, validator, errorText } = options;
		const setErrorTextDebounced = debounce(() => {
			viewField.errorText = errorText;
		}, ERROR_TEXT_TIMEOUT);
		return (evt, propertyName, newValue) => {
			setErrorTextDebounced.cancel();
			if (!this._isReady) return;
			if (validator(newValue)) {
				this.editor.execute(commandName, {
					value: newValue,
					batch: this._undoStepBatch
				});
				viewField.errorText = null;
			} else setErrorTextDebounced();
		};
	}
};

/**
* @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 table/tableproperties
*/
/**
* The table properties feature. Enables support for setting properties of tables (size, border, background, etc.).
*
* Read more in the {@glink features/tables/tables-styling Table and cell styling tools} section.
* See also the {@link module:table/tablecellproperties~TableCellProperties} plugin.
*
* This is a "glue" plugin that loads the
* {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing table properties editing feature} and
* the {@link module:table/tableproperties/tablepropertiesui~TablePropertiesUI table properties UI feature}.
*/
var TableProperties = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "TableProperties";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [TablePropertiesEditing, TablePropertiesUI];
	}
};

/**
* @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
*/
/**
* Injects a table caption post-fixer into the model.
*
* The role of the table caption post-fixer is to ensure that the table with caption have the correct structure
* after a {@link module:engine/model/model~Model#change `change()`} block was executed.
*
* The correct structure means that:
*
* * If there are many caption model element, they are merged into one model.
* * A final, merged caption model is placed at the end of the table.
*
* @internal
*/
function injectTableCaptionPostFixer(model) {
	model.document.registerPostFixer((writer) => tableCaptionPostFixer(writer, model));
}
/**
* The table caption post-fixer.
*/
function tableCaptionPostFixer(writer, model) {
	const changes = model.document.differ.getChanges();
	let wasFixed = false;
	for (const entry of changes) {
		if (entry.type != "insert") continue;
		const positionParent = entry.position.parent;
		if (positionParent.is("element", "table") || entry.name == "table") {
			const table = entry.name == "table" ? entry.position.nodeAfter : positionParent;
			const captionsToMerge = Array.from(table.getChildren()).filter((child) => child.is("element", "caption"));
			const firstCaption = captionsToMerge.shift();
			if (!firstCaption) continue;
			for (const caption of captionsToMerge) {
				writer.move(writer.createRangeIn(caption), firstCaption, "end");
				writer.remove(caption);
			}
			if (firstCaption.nextSibling) {
				writer.move(writer.createRangeOn(firstCaption), table, "end");
				wasFixed = true;
			}
			wasFixed = !!captionsToMerge.length || wasFixed;
		}
	}
	return wasFixed;
}

/**
* @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
*/
/**
* Checks if the provided model element is a `table`.
*
* @param modelElement Element to check if it is a table.
* @internal
*/
function isTable(modelElement) {
	return !!modelElement && modelElement.is("element", "table");
}
/**
* Returns the caption model element from a given table element. Returns `null` if no caption is found.
*
* @param tableModelElement Table element in which we will try to find a caption element.
* @internal
*/
function getCaptionFromTableModelElement(tableModelElement) {
	for (const node of tableModelElement.getChildren()) if (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.
*
* @param selection The selection checked for caption presence.
* @internal
*/
function getCaptionFromModelSelection(selection) {
	const tableElement = getSelectionAffectedTable(selection);
	if (!tableElement) return null;
	return getCaptionFromTableModelElement(tableElement);
}
/**
* {@link module:engine/view/matcher~Matcher} pattern. Checks if a given element is a caption.
*
* There are two possible forms of the valid caption:
*  - A `<figcaption>` element inside a `<figure class="table">` element.
*  - A `<caption>` inside a <table>.
*
* @returns Returns the object accepted by {@link module:engine/view/matcher~Matcher} or `null` if the element cannot be matched.
* @internal
*/
function matchTableCaptionViewElement(element) {
	const parent = element.parent;
	if (element.name == "figcaption" && parent && parent.is("element", "figure") && parent.hasClass("table")) return { name: true };
	if (element.name == "caption" && parent && parent.is("element", "table")) 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
*/
/**
* @module table/tablecaption/toggletablecaptioncommand
*/
/**
* The toggle table caption command.
*
* This command is registered by {@link module:table/tablecaption/tablecaptionediting~TableCaptionEditing} as the
* `'toggleTableCaption'` editor command.
*
* Executing this command:
*
* * either adds or removes the table caption of a selected table (depending on whether the caption is present or not),
* * removes the table caption if the selection is anchored in one.
*
* ```ts
* // Toggle the presence of the caption.
* editor.execute( 'toggleTableCaption' );
* ```
*
* **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( 'toggleTableCaption', { focusCaptionOnShow: true } );
* ```
*/
var ToggleTableCaptionCommand = class extends Command {
	/**
	* @inheritDoc
	*/
	refresh() {
		const editor = this.editor;
		const tableElement = getSelectionAffectedTable(editor.model.document.selection);
		this.isEnabled = !!tableElement && editor.model.schema.checkChild(tableElement, "caption");
		if (!this.isEnabled) this.value = false;
		else this.value = !!getCaptionFromTableModelElement(tableElement);
	}
	/**
	* Executes the command.
	*
	* ```ts
	* editor.execute( 'toggleTableCaption' );
	* ```
	*
	* @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({ focusCaptionOnShow = false } = {}) {
		this.editor.model.change((writer) => {
			if (this.value) this._hideTableCaption(writer);
			else this._showTableCaption(writer, focusCaptionOnShow);
		});
	}
	/**
	* Shows the table caption. Also:
	*
	* * it attempts to restore the caption content from the `TableCaptionEditing` caption registry,
	* * it moves the selection to the caption right away, it the `focusCaptionOnShow` option was set.
	*
	* @param focusCaptionOnShow Default focus behavior when showing the caption.
	*/
	_showTableCaption(writer, focusCaptionOnShow) {
		const model = this.editor.model;
		const tableElement = getSelectionAffectedTable(model.document.selection);
		const newCaptionElement = this.editor.plugins.get("TableCaptionEditing")._getSavedCaption(tableElement) || writer.createElement("caption");
		model.insertContent(newCaptionElement, tableElement, "end");
		if (focusCaptionOnShow) writer.setSelection(newCaptionElement, "in");
	}
	/**
	* Hides the caption of a selected table (or an table caption the selection is anchored to).
	*
	* The content of the caption is stored in the `TableCaptionEditing` caption registry to make this
	* a reversible action.
	*/
	_hideTableCaption(writer) {
		const model = this.editor.model;
		const tableElement = getSelectionAffectedTable(model.document.selection);
		const tableCaptionEditing = this.editor.plugins.get("TableCaptionEditing");
		const captionElement = getCaptionFromTableModelElement(tableElement);
		tableCaptionEditing._saveCaption(tableElement, captionElement);
		model.deleteContent(writer.createSelection(captionElement, "on"));
	}
};

/**
* @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 table/tablecaption/tablecaptionediting
*/
/**
* The table caption editing plugin.
*/
var TableCaptionEditing = class extends Plugin {
	/**
	* A map that keeps saved JSONified table captions and table model elements they are
	* associated with.
	*
	* To learn more about this system, see {@link #_saveCaption}.
	*/
	_savedCaptionsMap = /* @__PURE__ */ new WeakMap();
	/**
	* A map that keeps generated ids for table captions to reuse them if the same caption is rendered again.
	*/
	_captionIdsMapping = /* @__PURE__ */ new WeakMap();
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "TableCaptionEditing";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	init() {
		const editor = this.editor;
		const schema = editor.model.schema;
		const view = editor.editing.view;
		const t = editor.t;
		const useCaptionElement = editor.config.get("table.tableCaption.useCaptionElement");
		if (!schema.isRegistered("caption")) schema.register("caption", {
			allowIn: "table",
			allowContentOf: "$block",
			isLimit: true
		});
		else schema.extend("caption", { allowIn: "table" });
		editor.commands.add("toggleTableCaption", new ToggleTableCaptionCommand(this.editor));
		if (useCaptionElement) editor.plugins.get("TableEditing").registerAdditionalSlot({
			filter: (element) => element.is("element", "caption"),
			positionOffset: "end"
		});
		editor.conversion.for("upcast").elementToElement({
			view: matchTableCaptionViewElement,
			model: "caption"
		});
		editor.conversion.for("dataDowncast").elementToElement({
			model: "caption",
			view: (modelElement, { writer }) => {
				if (!isTable(modelElement.parent)) return null;
				return writer.createContainerElement(useCaptionElement ? "caption" : "figcaption");
			}
		});
		editor.conversion.for("editingDowncast").elementToElement({
			model: "caption",
			view: (modelElement, { writer }) => {
				if (!isTable(modelElement.parent)) return null;
				const captionElement = writer.createEditableElement(useCaptionElement ? "caption" : "figcaption");
				writer.setCustomProperty("tableCaption", true, captionElement);
				captionElement.placeholder = t("Enter table caption");
				enableViewPlaceholder({
					view,
					element: captionElement,
					keepOnFocus: true
				});
				return toWidgetEditable(captionElement, writer);
			}
		});
		editor.conversion.for("editingDowncast").add((dispatcher) => {
			dispatcher.on("insert:table", (evt, data, { writer, mapper }) => {
				const modelTable = data.item;
				const viewFigure = mapper.toViewElement(modelTable);
				if (!viewFigure) return;
				const viewTable = Array.from(viewFigure.getChildren()).find((child) => child.is("element", "table"));
				if (!viewTable) return;
				const modelCaption = getCaptionFromTableModelElement(modelTable);
				if (!modelCaption) {
					writer.removeAttribute("aria-labelledby", viewTable);
					return;
				}
				const viewCaption = mapper.toViewElement(modelCaption);
				if (!viewCaption) return;
				let captionId;
				if (viewCaption.hasAttribute("id")) captionId = viewCaption.getAttribute("id");
				else captionId = this._captionIdsMapping.get(modelCaption) ?? `ck-editor__caption_${uid()}`;
				this._captionIdsMapping.set(modelCaption, captionId);
				writer.setAttribute("id", captionId, viewCaption);
				writer.setAttribute("aria-labelledby", captionId, viewTable);
			}, { priority: "low" });
		});
		injectTableCaptionPostFixer(editor.model);
	}
	/**
	* Returns the saved {@link module:engine/model/element~ModelElement#toJSON JSONified} caption
	* of a table model element.
	*
	* See {@link #_saveCaption}.
	*
	* @internal
	* @param tableModelElement The model element the caption should be returned for.
	* @returns The model caption element or `null` if there is none.
	*/
	_getSavedCaption(tableModelElement) {
		const jsonObject = this._savedCaptionsMap.get(tableModelElement);
		return jsonObject ? ModelElement.fromJSON(jsonObject) : null;
	}
	/**
	* Saves a {@link module:engine/model/element~ModelElement#toJSON JSONified} caption for
	* a table element to allow restoring it in the future.
	*
	* A caption is saved every time it gets hidden. The
	* user should be able to restore it on demand.
	*
	* **Note**: The caption cannot be stored in the table 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 tableModelElement The model element the caption is saved for.
	* @param caption The caption model element to be saved.
	*/
	_saveCaption(tableModelElement, caption) {
		this._savedCaptionsMap.set(tableModelElement, caption.toJSON());
	}
};

/**
* @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 table/tablecaption/tablecaptionui
*/
/**
* The table caption UI plugin. It introduces the `'toggleTableCaption'` UI button.
*/
var TableCaptionUI = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "TableCaptionUI";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	init() {
		const editor = this.editor;
		const editingView = editor.editing.view;
		const t = editor.t;
		editor.ui.componentFactory.add("toggleTableCaption", (locale) => {
			const command = editor.commands.get("toggleTableCaption");
			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("toggleTableCaption", { focusCaptionOnShow: true });
				if (command.value) {
					const modelCaptionElement = getCaptionFromModelSelection(editor.model.document.selection);
					const figcaptionElement = editor.editing.mapper.toViewElement(modelCaptionElement);
					if (!figcaptionElement) return;
					editingView.scrollToTheSelection();
					editingView.change((writer) => {
						writer.addClass("table__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 table/tablecaption
*/
/**
* The table caption plugin.
*/
var TableCaption = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "TableCaption";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [TableCaptionEditing, TableCaptionUI];
	}
};

/**
* @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 table/tablescroll/watchers
*/
/**
* Creates a live collection of all `table` model elements present in the document and keeps it
* up to date as tables are inserted, moved, or removed.
*
* @internal
*/
function watchTableModelElements(model) {
	const tables = new Collection();
	model.document.on("change", () => {
		const documentChanges = model.document.differ.getChanges();
		const insertedTables = /* @__PURE__ */ new Set();
		const movedTables = /* @__PURE__ */ new Set();
		for (const change of documentChanges) if (change.type === "insert" && change.name !== "$text" && change.position.nodeAfter) {
			const range = model.createRangeOn(change.position.nodeAfter);
			for (const item of range.getItems()) {
				if (!item.is("element", "table")) continue;
				if (tables.has(item)) movedTables.add(item);
				else insertedTables.add(item);
			}
		}
		for (const table of Array.from(tables)) {
			if (table.root.rootName !== "$graveyard") continue;
			insertedTables.delete(table);
			movedTables.delete(table);
			if (tables.has(table)) tables.remove(table);
		}
		for (const table of movedTables)
 /* v8 ignore else -- @preserve */
		if (tables.has(table)) tables.remove(table);
		if (insertedTables.size || movedTables.size) tables.addMany([...insertedTables, ...movedTables]);
	});
	return tables;
}
/**
* Observes the DOM width of every editing root and calls `onResize` whenever any of them changes width.
* Height-only changes are ignored, since only a root's width can make a table overflow it.
*
* @internal
*/
function watchRootsWidthResize(view, onResize) {
	const { roots } = view.document;
	const observedRoots = /* @__PURE__ */ new Map();
	const lastKnownWidths = /* @__PURE__ */ new Map();
	const attachRoot = (rootName) => {
		if (observedRoots.has(rootName)) return;
		const domRoot = view.getDomRoot(rootName);
		if (!domRoot) return;
		const observer = new ResizeObserver((entries) => {
			for (const entry of entries) {
				const width = entry.contentRect.width;
				if (lastKnownWidths.get(rootName) === width) continue;
				lastKnownWidths.set(rootName, width);
				onResize();
			}
		});
		observer.observe(domRoot);
		observedRoots.set(rootName, observer);
	};
	const detachRoot = (rootName) => {
		const observer = observedRoots.get(rootName);
		if (observer) {
			observer.disconnect();
			observedRoots.delete(rootName);
			lastKnownWidths.delete(rootName);
		}
	};
	const onRootAdd = (evt, viewRoot) => attachRoot(viewRoot.rootName);
	const onRootRemove = (evt, viewRoot) => detachRoot(viewRoot.rootName);
	const attachAllRoots = () => {
		for (const root of roots) attachRoot(root.rootName);
	};
	attachAllRoots();
	roots.on("add", onRootAdd);
	roots.on("remove", onRootRemove);
	view.on("render", attachAllRoots);
	return () => {
		roots.off("add", onRootAdd);
		roots.off("remove", onRootRemove);
		view.off("render", attachAllRoots);
		for (const observer of observedRoots.values()) observer.disconnect();
		observedRoots.clear();
		lastKnownWidths.clear();
	};
}

/**
* @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 table/tablescroll/tablescrollediting
*/
/**
* The table scrolling editing plugin.
*/
var TableScrollEditing = class extends Plugin {
	/**
	* Used to listen to native DOM events.
	*/
	_domEmitter = new (DomEmitterMixin())();
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "TableScrollEditing";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [TableEditing];
	}
	/**
	* @inheritDoc
	*/
	constructor(editor) {
		super(editor);
		editor.config.define("table.tableScroll.tableTypes", ["content"]);
	}
	/**
	* @inheritDoc
	*/
	init() {
		const { editor } = this;
		const tables = watchTableModelElements(editor.model);
		this._watchNewTables(tables);
		this._watchRootEditables(tables);
		this._registerConversion();
		this._watchColumnResize();
		this._watchFiguresScroll();
	}
	/**
	* @inheritDoc
	*/
	destroy() {
		this._domEmitter.stopListening();
		super.destroy();
	}
	/**
	* Whether a given table may overflow its container and become horizontally scrollable, and whether
	* column/table resizing is allowed to grow it past the container's width.
	*
	* @internal
	*/
	_isTableScrollable(table) {
		if (table.parent !== table.root) return false;
		const tableType = table.getAttribute("tableType") || "content";
		return this.editor.config.get("table.tableScroll.tableTypes").includes(tableType);
	}
	/**
	* Determines whether a table overflows its container and applies the corresponding view state:
	* the `ck-table-overflowing` class on the figure, and the actual (possibly container-exceeding)
	* width on the inner `<table>` and a sibling `<figcaption>`, if present.
	*
	* @internal
	*/
	_updateTableScrollOverflowState(table, tableWidthOverride) {
		const { editor } = this;
		const containerWidth = getEditableWidth(editor, table.root.rootName);
		if (containerWidth === null) return;
		const viewFigure = editor.editing.mapper.toViewElement(table);
		if (!viewFigure) return;
		const viewTable = findChildElement(viewFigure, "table");
		if (!viewTable) return;
		const viewFigcaption = findChildElement(viewFigure, "figcaption");
		let tableWidth = null;
		let hasTableWidthSource = false;
		if (tableWidthOverride !== void 0) {
			tableWidth = tableWidthOverride;
			hasTableWidthSource = true;
		} else if (table.hasAttribute("tableWidth")) {
			tableWidth = table.getAttribute("tableWidth");
			hasTableWidthSource = true;
		}
		if (!hasTableWidthSource) return;
		const isOverflowing = !!tableWidth && this._isTableScrollable(table) && isTableWidthOverflowing(tableWidth, containerWidth);
		if (isOverflowing) this._scheduleScrollOffsetSync(viewFigure);
		editor.editing.view.change((writer) => {
			if (isOverflowing) {
				writer.removeStyle("width", viewFigure);
				writer.addClass("ck-table-overflowing", viewFigure);
				writer.setStyle("width", tableWidth, viewTable);
				if (viewFigcaption) writer.setStyle("width", tableWidth, viewFigcaption);
			} else {
				if (tableWidth) writer.setStyle("width", tableWidth, viewFigure);
				else writer.removeStyle("width", viewFigure);
				if (viewFigcaption) writer.removeStyle("width", viewFigcaption);
				writer.removeClass("ck-table-overflowing", viewFigure);
				writer.removeStyle("width", viewTable);
				writer.removeStyle("--ck-table-scroll-offset", viewFigure);
			}
		});
	}
	/**
	* Schedules a re-read of the widget figure's actual `scrollLeft` into `--ck-table-scroll-offset`, once
	* the render reflecting the change that triggered it has actually happened - see the call site for why
	* this can't just read (and force a reflow for) `scrollLeft` immediately instead.
	*/
	_scheduleScrollOffsetSync(viewFigure) {
		const { view } = this.editor.editing;
		view.once("render", () => {
			const domFigure = view.domConverter.mapViewToDom(viewFigure);
			/* v8 ignore else -- @preserve */
			if (domFigure) view.change((writer) => {
				writer.setStyle("--ck-table-scroll-offset", `${domFigure.scrollLeft}px`, viewFigure);
			});
		});
	}
	/**
	* Registers editing-only downcast listeners that keep overflow state in sync.
	*/
	_registerConversion() {
		const { editor } = this;
		editor.conversion.for("editingDowncast").add((dispatcher) => {
			dispatcher.on("attribute:tableWidth:table", (evt, data) => {
				this._updateTableScrollOverflowState(data.item, data.attributeNewValue);
			}, { priority: "low" });
			dispatcher.on("attribute:tableType:table", (evt, data) => {
				this._updateTableScrollOverflowState(data.item);
			}, { priority: "low" });
			dispatcher.on("insert:caption", (evt, data) => {
				const modelTable = data.item.parent;
				if (modelTable?.is("element", "table")) this._updateTableScrollOverflowState(modelTable);
			}, { priority: "lowest" });
			dispatcher.on("insert:table", (evt, data) => {
				this._updateTableScrollOverflowState(data.item);
			}, { priority: "lowest" });
		});
	}
	/**
	* Keeps every overflowing table's `--ck-table-scroll-offset` custom property in sync with its
	* `scrollLeft`, using a single delegated listener instead of one native listener per table.
	*/
	_watchFiguresScroll() {
		const { editor } = this;
		const { view } = editor.editing;
		const onScroll = (evt, domEvent) => {
			const domFigure = domEvent.target;
			if (!domFigure.classList?.contains("ck-table-overflowing")) return;
			const viewFigure = view.domConverter.mapDomToView(domFigure);
			if (!viewFigure) return;
			view.change((writer) => {
				writer.setStyle("--ck-table-scroll-offset", `${domFigure.scrollLeft}px`, viewFigure);
			});
		};
		this._domEmitter.listenTo(global.document, "scroll", onScroll, { useCapture: true });
	}
	/**
	* Listen for column resize events and updates proper view element size.
	*/
	_watchColumnResize() {
		const { editor } = this;
		const { plugins } = editor;
		if (!plugins.has("TableColumnResizeEditing")) return;
		const columnResizeEditing = plugins.get("TableColumnResizeEditing");
		this.listenTo(columnResizeEditing, "_setResizingTableWidth", (evt, [, viewFigure, width]) => {
			evt.stop();
			const modelTable = editor.editing.mapper.toModelElement(viewFigure);
			this._updateTableScrollOverflowState(modelTable, width);
		}, { priority: "high" });
		this.listenTo(columnResizeEditing, "_getResizingTableWidth", (evt, [viewFigure]) => {
			evt.stop();
			evt.return = getWidthHoldingElement(viewFigure).getStyle("width");
		}, { priority: "high" });
	}
	/**
	* Re-evaluates the overflow state of every table whenever the window or an editing root is resized.
	*/
	_watchRootEditables(tables) {
		const { editor } = this;
		const recalculateAll = () => {
			const resizingTable = getCurrentlyResizingTable(editor);
			for (const table of tables) {
				if (table === resizingTable) continue;
				this._updateTableScrollOverflowState(table);
			}
		};
		const throttledRecalculateAll = throttle(recalculateAll, 100);
		const stopWatchingRootsResize = watchRootsWidthResize(editor.editing.view, throttledRecalculateAll);
		editor.ui.view.listenTo(global.window, "resize", throttledRecalculateAll);
		editor.once("ready", recalculateAll);
		this.listenTo(editor, "destroy", () => {
			throttledRecalculateAll.cancel();
			stopWatchingRootsResize();
		});
	}
	/**
	* Evaluates the overflow state of every newly inserted table.
	*/
	_watchNewTables(tables) {
		this.listenTo(tables, "change", (evt, data) => {
			for (const table of data.added) this._updateTableScrollOverflowState(table);
		});
	}
};
/**
* Returns the table for which a column resize is currently in progress, if the
* `TableColumnResizeEditing` plugin is loaded and such a resize is active.
*/
function getCurrentlyResizingTable(editor) {
	const { plugins } = editor;
	if (!plugins.has("TableColumnResizeEditing")) return null;
	return plugins.get("TableColumnResizeEditing").resizingTable;
}
/**
* Checks if given table width overflows container width.
*/
function isTableWidthOverflowing(tableWidth, containerWidth) {
	if (tableWidth.endsWith("%")) return parseFloat(tableWidth) > 100;
	if (tableWidth.endsWith("px")) return parseFloat(tableWidth) > containerWidth;
	return false;
}
/**
* Returns whichever element - the widget's `<figure>` or its inner `<table>` - currently holds the
* table's actual (possibly container-exceeding) width: the `<table>` while overflowing, the `<figure>`
* otherwise.
*/
function getWidthHoldingElement(viewFigure) {
	return viewFigure.hasClass("ck-table-overflowing") ? findChildElement(viewFigure, "table") : viewFigure;
}
/**
* Finds a direct child element of `parent` by its name.
*/
function findChildElement(parent, elementName) {
	return Array.from(parent.getChildren()).find((child) => child.is("element", elementName));
}

/**
* @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 table/tablescroll
*/
var TableScroll = class extends Plugin {
	/**
	* @inheritDoc
	*/
	static get pluginName() {
		return "TableScroll";
	}
	/**
	* @inheritDoc
	*/
	static get isOfficialPlugin() {
		return true;
	}
	/**
	* @inheritDoc
	*/
	static get requires() {
		return [TableScrollEditing];
	}
};

/**
* @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 { InsertColumnCommand, InsertRowCommand, InsertTableCommand, InsertTableLayoutCommand, MergeCellCommand, MergeCellsCommand, PlainTableOutput, RemoveColumnCommand, RemoveRowCommand, SelectColumnCommand, SelectRowCommand, SetFooterRowCommand, SetHeaderColumnCommand, SetHeaderRowCommand, SplitCellCommand, Table, TableAlignmentCommand, TableBackgroundColorCommand, TableBorderColorCommand, TableBorderStyleCommand, TableBorderWidthCommand, TableCaption, TableCaptionEditing, TableCaptionUI, TableCellBackgroundColorCommand, TableCellBorderColorCommand, TableCellBorderStyleCommand, TableCellBorderWidthCommand, TableCellHeightCommand, TableCellHorizontalAlignmentCommand, TableCellPaddingCommand, TableCellProperties, TableCellPropertiesEditing, TableCellPropertiesUI, TableCellPropertiesView, TableCellPropertyCommand, TableCellTypeCommand, TableCellVerticalAlignmentCommand, TableCellWidthCommand, TableCellWidthEditing, TableClipboard, TableColumnResize, TableColumnResizeEditing, TableColumnWidthCommand, TableEditing, TableHeightCommand, TableKeyboard, TableLayout, TableLayoutEditing, TableLayoutUI, TableMouse, TableProperties, TablePropertiesEditing, TablePropertiesUI, TablePropertiesView, TablePropertyCommand, TableScroll, TableScrollEditing, TableSelection, TableSlot, TableToolbar, TableTypeCommand, TableUI, TableUtils, TableWalker, TableWidthCommand, TableWidthsCommand, ToggleTableCaptionCommand, InsertTableView as _InsertTableView, COLUMN_MIN_WIDTH_AS_PERCENTAGE as _TABLE_COLUMN_MIN_WIDTH_AS_PERCENTAGE, COLUMN_MIN_WIDTH_IN_PIXELS as _TABLE_COLUMN_MIN_WIDTH_IN_PIXELS, COLUMN_RESIZE_DISTANCE_THRESHOLD as _TABLE_COLUMN_RESIZE_DISTANCE_THRESHOLD, COLUMN_WIDTH_PRECISION as _TABLE_COLUMN_WIDTH_PRECISION, defaultColors as _TABLE_DEFAULT_COLORS, ColorInputView as _TableColorInputView, MouseEventsObserver as _TableMouseEventsObserver, addDefaultUnitToNumericValue as _addDefaultUnitToNumericValue, adjustLastColumnIndex as _adjustLastTableColumnIndex, adjustLastRowIndex as _adjustLastTableRowIndex, clamp as _clamp, colorFieldValidator as _colorTableFieldValidator, convertParagraphInTableCell as _convertParagraphInTableCell, createEmptyTableCell as _createEmptyTableCell, createFilledArray as _createFilledArray, cropTableToDimensions as _cropTableToDimensions, downcastTable as _downcastTable, downcastTableAttribute as _downcastTableAttribute, downcastAttributeToStyle as _downcastTableAttributeToStyle, downcastCell as _downcastTableCell, downcastTableResizedClass as _downcastTableResizedClass, downcastRow as _downcastTableRow, enableProperty as _enableTableCellProperty, ensureParagraphInTableCell as _ensureParagraphInTableCell, fillToolbar as _fillTableOrCellToolbar, getBalloonCellPositionData as _getBalloonTableCellPositionData, getBalloonTablePositionData as _getBalloonTablePositionData, getBorderStyleLabels as _getBorderTableStyleLabels, getChangedResizedTables as _getChangedResizedTables, getDefaultValueAdjusted as _getDefaultTableValueAdjusted, getDomCellOuterWidth as _getDomTableCellOuterWidth, getElementWidthInPixels as _getElementWidthInPixels, getHorizontallyOverlappingCells as _getHorizontallyOverlappingTableCells, getLabeledColorInputCreator as _getLabeledTableColorInputCreator, getLocalizedColorErrorText as _getLocalizedTableColorErrorText, getLocalizedLengthErrorText as _getLocalizedTableLengthErrorText, getNormalizedDefaultProperties as _getNormalizedDefaultTableBaseProperties, getNormalizedDefaultCellProperties as _getNormalizedDefaultTableCellProperties, getNormalizedDefaultTableProperties as _getNormalizedDefaultTableProperties, getSelectedTableWidget as _getSelectedTableWidget, getSelectionAffectedTable as _getSelectionAffectedTable, getSelectionAffectedTableWidget as _getSelectionAffectedTableWidget, getSingleValue as _getTableBorderBoxSingleValue, getCaptionFromTableModelElement as _getTableCaptionFromModelElement, getCaptionFromModelSelection as _getTableCaptionFromModelSelection, getColumnEdgesIndexes as _getTableColumnEdgesIndexes, getTableColumnElements as _getTableColumnElements, getColumnGroupElement as _getTableColumnGroupElement, getColumnMinWidthAsPercentage as _getTableColumnMinWidthAsPercentage, getTableColumnsWidths as _getTableColumnsWidths, getBorderStyleDefinitions as _getTableOrCellBorderStyleDefinitions, getTableWidgetAncestor as _getTableWidgetAncestor, getTableWidthInPixels as _getTableWidthInPixels, getVerticallyOverlappingCells as _getVerticallyOverlappingTableCells, injectTableCaptionPostFixer as _injectTableCaptionPostFixer, injectTableCellParagraphPostFixer as _injectTableCellParagraphPostFixer, injectTableLayoutPostFixer as _injectTableLayoutPostFixer, isSingleParagraphWithoutAttributes as _isSingleTableParagraphWithoutAttributes, isHeadingColumnCell as _isTableHeadingColumnCell, isTable as _isTableModelElement, lengthFieldValidator as _lengthTableFieldValidator, lineWidthFieldValidator as _lineWidthTableFieldValidator, matchTableCaptionViewElement as _matchTableCaptionViewElement, normalizeColumnWidths as _normalizeTableColumnWidths, removeEmptyColumns as _removeEmptyTableColumns, removeEmptyRows as _removeEmptyTableRows, removeEmptyRowsColumns as _removeEmptyTableRowsColumns, repositionContextualBalloon as _repositionTableContextualBalloon, skipEmptyTableRow as _skipEmptyTableRow, splitHorizontally as _splitTableCellHorizontally, splitVertically as _splitTableCellVertically, sumArray as _sumArray, tableCellRefreshHandler as _tableCellRefreshHandler, tableStructureRefreshHandler as _tableStructureRefreshHandler, toPrecision as _toPrecision, translateColSpanAttribute as _translateTableColspanAttribute, trimTableCellIfNeeded as _trimTableCellIfNeeded, upcastStyleToAttribute as _upcastNormalizedTableStyleToAttribute, upcastTable as _upcastTable, upcastBorderStyles as _upcastTableBorderStyles, upcastColgroupElement as _upcastTableColgroupElement, upcastTableFigure as _upcastTableFigure, updateColumnElements as _updateTableColumnElements, updateNumericAttribute as _updateTableNumericAttribute, isTableHeaderCellType };
//# sourceMappingURL=index.js.map