UNPKG

@progress/kendo-ui

Version:

This package is part of the [Kendo UI for jQuery](http://www.telerik.com/kendo-ui) suite.

2,733 lines 108 kB
Object.defineProperties(exports, {
	__esModule: { value: true },
	[Symbol.toStringTag]: { value: "Module" }
});
require("./kendo.data-T0mqWOrQ.js");
const require_valueMapper = require("./valueMapper-D4YTa8cT.js");
//#region ../src/kendo.virtuallist.js
const __meta__ = {
	id: "virtuallist",
	name: "VirtualList",
	category: "framework",
	depends: ["data"],
	hidden: true
};
(function($, undefined) {
	const kendo = window.kendo, ui = kendo.ui, encode = kendo.htmlEncode, Widget = ui.Widget, DataBoundWidget = ui.DataBoundWidget, percentageUnitsRegex = /^\d+(\.\d+)?%$/i, LIST_CONTENT = "k-list-content", TABLE_CONTENT = "k-table-body k-table-scroller", HEADER = "k-list-group-sticky-header", TABLE_HEADER = "k-table-group-sticky-header", LIST_ITEM = "k-list-item", TABLE_ITEM = "k-table-row", HEIGHTCONTAINER = "k-height-container", GROUPITEM = "k-list-item-group-label", GROUP_HEADER_ITEM = "k-list-group-item", LIST_UL = "k-list-ul", TABLE = "k-table", SELECTED = "k-selected", FOCUSED = "k-focus", HOVER = "k-hover", CHANGE = "change", CLICK = "click", LISTBOUND = "listBound", ITEMCHANGE = "itemChange", ACTION = "action", ACTIVATE = "activate", DEACTIVATE = "deactivate", GROUP_ROW_SEL = ".k-table-group-row", VIRTUAL_LIST_NS = ".VirtualList";
	function lastFrom(array) {
		return array[array.length - 1];
	}
	function toArray(value) {
		return value instanceof Array ? value : [value];
	}
	function isPrimitive(dataItem) {
		return typeof dataItem === "string" || typeof dataItem === "number" || typeof dataItem === "boolean";
	}
	function getItemCount(screenHeight, listScreens, itemHeight) {
		return Math.ceil(screenHeight * listScreens / itemHeight);
	}
	function appendChild(parent, className, tagName) {
		const element = document.createElement(tagName || "div");
		if (className) element.className = className;
		parent.appendChild(element);
		return element;
	}
	function getDefaultItemHeight(listSize) {
		const mockList = $("<div class=\"k-list " + listSize + " k-virtual-list\"><div class=\"k-list-content\"><ul class=\"k-list-ul\"><li class=\"k-list-item\"><span class=\"k-list-item-text\">test</span></li></ul></div></div>");
		mockList.css({
			position: "absolute",
			left: "-200000px",
			visibility: "hidden"
		});
		mockList.appendTo(document.body);
		const itemHeight = parseFloat(kendo.getComputedStyles(mockList.find(".k-list-item")[0], ["height"]).height);
		mockList.remove();
		return itemHeight;
	}
	/**
	* Gets the CSS gap value from an existing UL element in the DOM,
	* or creates a temporary one to measure if no UL exists.
	* @param {jQuery|HTMLElement} containerElement - The container element in the DOM (for creating temp UL)
	* @param {jQuery} existingUl - An existing UL element in the DOM (optional)
	* @returns {number} - The gap value in pixels (0 if not found)
	*/
	function getCssGap(containerElement, existingUl) {
		if (existingUl && existingUl.length) {
			let computedStyle = window.getComputedStyle(existingUl[0]);
			let gap = computedStyle.gap || computedStyle.rowGap || "0";
			return parseFloat(gap) || 0;
		}
		const container = containerElement instanceof $ ? containerElement[0] : containerElement;
		if (!container || !container.parentNode) return 0;
		const tempUl = document.createElement("ul");
		tempUl.className = LIST_UL;
		tempUl.style.cssText = "position:absolute;visibility:hidden;left:-9999px;";
		container.appendChild(tempUl);
		const computedStyle = window.getComputedStyle(tempUl);
		const gap = computedStyle.gap || computedStyle.rowGap || "0";
		const gapValue = parseFloat(gap) || 0;
		container.removeChild(tempUl);
		return gapValue;
	}
	function bufferSizes(screenHeight, listScreens, opposite) {
		return {
			down: screenHeight * opposite,
			up: screenHeight * (listScreens - 1 - opposite)
		};
	}
	function listValidator(options, screenHeight) {
		const downThreshold = (options.listScreens - 1 - options.threshold) * screenHeight;
		const upThreshold = options.threshold * screenHeight;
		return function(list, scrollTop, lastScrollTop) {
			if (scrollTop > lastScrollTop) return scrollTop - list.top < downThreshold;
			else return list.top === 0 || scrollTop - list.top > upThreshold;
		};
	}
	function scrollCallback(element, callback) {
		return function(force) {
			return callback(element.scrollTop, force);
		};
	}
	function syncList(reorder) {
		return function(list, force) {
			reorder(list.items, list.index, force);
			return list;
		};
	}
	function position(element, y) {
		element.style.webkitTransform = "translateY(" + y + "px)";
		element.style.transform = "translateY(" + y + "px)";
	}
	/**
	* Calculates the absolute position (in pixels) of a flat index in a grouped list.
	* Takes into account group headers that appear before each group and CSS gap.
	* @param {number} flatIndex - The flat index of an item (0-based, counting only data items)
	* @param {number} itemHeight - Height of each item in pixels
	* @param {Array} groupRanges - Array of group range objects with startIndex and itemCount
	* @param {number} cssGap - CSS gap between items in pixels (default 0)
	* @returns {number} - The absolute pixel position
	*/
	function getGroupedItemPosition(flatIndex, itemHeight, groupRanges, cssGap) {
		let position = 0;
		cssGap = cssGap || 0;
		const effectiveItemHeight = itemHeight + cssGap;
		for (let i = 0; i < groupRanges.length; i++) {
			const { startIndex, itemCount } = groupRanges[i];
			if (flatIndex < startIndex + itemCount) {
				if (i > 0) position += effectiveItemHeight;
				position += (flatIndex - startIndex) * effectiveItemHeight;
				break;
			} else {
				if (i > 0) position += effectiveItemHeight;
				position += itemCount * effectiveItemHeight;
			}
		}
		return position;
	}
	/**
	* Calculates the total height of the grouped list including all group headers and CSS gaps.
	* First group uses the sticky header (not inline), so we subtract one header height.
	* @param {number} totalItems - Total number of data items
	* @param {number} itemHeight - Height of each item
	* @param {number} groupCount - Number of groups
	* @param {number} cssGap - CSS gap between items in pixels (default 0)
	* @returns {number} - Total height in pixels
	*/
	function getGroupedTotalHeight(totalItems, itemHeight, groupCount, cssGap) {
		cssGap = cssGap || 0;
		const totalElements = totalItems + Math.max(0, groupCount - 1);
		const totalGaps = Math.max(0, totalElements - groupCount);
		return totalElements * itemHeight + totalGaps * cssGap;
	}
	/**
	* Builds the group ranges array from the dataSource's group info.
	* For paged/server-side grouping, this builds ranges relative to the current range.
	* @param {Object} dataSource - The Kendo DataSource
	* @param {number} rangeStart - The start index of the current data range (for offset calculation)
	* @param {number} firstGroupIndex - The global index of the first group in this view (0-based, used as fallback)
	* @param {Map} groupValueMap - Optional map of groupValue -> globalGroupIndex for consistent indexing
	* @returns {Array} - Array of { value, startIndex, itemCount, globalGroupIndex }
	*/
	function buildGroupRanges(dataSource, rangeStart, firstGroupIndex, groupValueMap) {
		if (!(dataSource.group() || []).length) return [];
		const view = dataSource.view() || [];
		if (!view.length) return [];
		const ranges = [];
		let currentIndex = rangeStart || 0;
		let nextNewGroupIndex = 0;
		if (groupValueMap && groupValueMap.size > 0) {
			for (const idx of groupValueMap.values()) if (idx >= nextNewGroupIndex) nextNewGroupIndex = idx + 1;
		}
		for (const group of view) {
			const groupItems = group.items || [];
			if (!groupItems.length) continue;
			let groupIndex;
			if (groupValueMap) if (groupValueMap.has(group.value)) groupIndex = groupValueMap.get(group.value);
			else {
				groupIndex = nextNewGroupIndex++;
				groupValueMap.set(group.value, groupIndex);
			}
			else groupIndex = (firstGroupIndex || 0) + ranges.length;
			ranges.push({
				value: group.value,
				startIndex: currentIndex,
				itemCount: groupItems.length,
				globalGroupIndex: groupIndex
			});
			currentIndex += groupItems.length;
		}
		return ranges;
	}
	function map2(callback, templates) {
		return function(arr1, arr2) {
			for (let i = 0, len = arr1.length; i < len; i++) {
				callback(arr1[i], arr2[i], templates);
				if (arr2[i].item) this.trigger(ITEMCHANGE, {
					item: $(arr1[i]),
					data: arr2[i].item,
					ns: kendo.ui
				});
			}
		};
	}
	function reshift(items, diff) {
		let range;
		if (diff > 0) {
			range = items.splice(0, diff);
			items.push(...range);
		} else {
			range = items.splice(diff, -diff);
			items.unshift(...range);
		}
		return range;
	}
	function render(element, data, templates) {
		const that = this;
		const options = that.options;
		const itemTemplate = data.item ? templates.template : templates.placeholderTemplate;
		const hasColumns = options.columns?.length;
		const altRow = data.index % 2 === 1 ? "k-table-alt-row" : "";
		const iconField = options.iconField;
		const descriptionField = options.descriptionField;
		const actionField = options.actionField;
		const dataItem = data.item;
		const hasIcon = iconField && dataItem && dataItem[iconField];
		const hasDescription = descriptionField && dataItem && dataItem[descriptionField];
		const hasAction = actionField && dataItem && dataItem[actionField];
		element = $(element);
		if (data.index === 0 && this.header && data.group) this.header.html(templates.fixedGroupTemplate(data.group));
		element.attr("data-uid", dataItem ? dataItem.uid : "").attr("data-offset-index", data.index);
		if (hasAction) {
			element.attr("data-action", dataItem[actionField]);
			element.addClass("k-list-item-action");
		} else {
			element.removeAttr("data-action");
			element.removeClass("k-list-item-action");
		}
		if (hasColumns && dataItem) {
			if (altRow.length > 0) element.addClass(altRow);
			else element.removeClass("k-table-alt-row");
			const renderedColumns = $(renderColumns(options, dataItem, templates));
			kendo.applyStylesFromKendoAttributes(renderedColumns, ["width", "max-width"]);
			element.empty().append(renderedColumns);
		} else {
			element.find(".k-list-item-group-label").remove();
			const textContainer = element.find(".k-list-item-text");
			textContainer.html(itemTemplate(dataItem || {}));
			let iconWrapper = element.find(".k-list-item-icon-wrapper");
			if (hasIcon) {
				const iconHtml = kendo.ui.icon({
					icon: dataItem[iconField],
					iconClass: "k-list-item-icon",
					attr: { "aria-hidden": "true" }
				});
				if (iconWrapper.length === 0) textContainer.before(`<span class="k-list-item-icon-wrapper" role="presentation">${iconHtml}</span>`);
				else iconWrapper.attr("role", "presentation").html(iconHtml);
			} else iconWrapper.remove();
			let descElement = element.find(".k-list-item-description");
			if (hasDescription) if (descElement.length === 0) textContainer.after(`<span class="k-list-item-description">${encode(dataItem[descriptionField])}</span>`);
			else descElement.html(encode(dataItem[descriptionField]));
			else descElement.remove();
		}
		element.toggleClass(FOCUSED, data.current);
		element.toggleClass(SELECTED, !that.options.checkboxes && data.selected);
		element.attr("aria-selected", data.selected);
		if (that.options.checkboxes) element.find("input.k-checkbox").toggleClass("k-checked", data.selected).prop("checked", data.selected);
		element.toggleClass("k-first", data.newGroup);
		element.toggleClass("k-last", data.isLastGroupedItem);
		element.toggleClass("k-loading-item", !dataItem);
		if (data.index !== 0 && data.newGroup) if (hasColumns) $(`<span class="k-table-td k-table-group-td"><span>${templates.groupTemplate(data.group)}</span></span>`).appendTo(element);
		else $(`<div class="${GROUPITEM}"></div>`).appendTo(element).html(templates.groupTemplate(data.group));
		else if (data.group && hasColumns) element.append($("<span class=\"k-table-td k-table-spacer-td\"></span>"));
		if (data.top !== undefined) element.css("top", data.top + "px");
	}
	function renderColumns(options, dataItem, templates) {
		let item = "";
		for (let i = 0; i < options.columns.length; i++) {
			const currentWidth = options.columns[i].width;
			const currentWidthInt = parseInt(currentWidth, 10);
			let widthStyle = "";
			if (currentWidth) {
				const widthValue = `${currentWidthInt}${percentageUnitsRegex.test(currentWidth) ? "%" : "px"}`;
				widthStyle = `${kendo.attr("style-width")}="${widthValue}" ${kendo.attr("style-max-width")}="${widthValue}"`;
			}
			item += `<span class='k-table-td' ${widthStyle}>`;
			item += templates["column" + i](dataItem);
			item += "</span>";
		}
		return item;
	}
	function mapChangedItems(selected, itemsToMatch) {
		const itemsLength = itemsToMatch.length;
		const selectedLength = selected.length;
		const changed = [];
		const unchanged = [];
		if (selectedLength) for (let i = 0; i < selectedLength; i++) {
			const dataItem = selected[i];
			let found = false;
			for (let j = 0; j < itemsLength; j++) if (dataItem === itemsToMatch[j]) {
				found = true;
				changed.push({
					index: i,
					item: dataItem
				});
				break;
			}
			if (!found) unchanged.push(dataItem);
		}
		return {
			changed,
			unchanged
		};
	}
	function isActivePromise(promise) {
		return promise && promise.state() !== "resolved";
	}
	const VirtualList = DataBoundWidget.extend({
		init: function(element, options) {
			const that = this;
			const contentClasses = options.columns?.length ? TABLE_CONTENT : LIST_CONTENT;
			that.bound(false);
			that._fetching = false;
			Widget.fn.init.call(that, element, options);
			if (!that.options.itemHeight) that.options.itemHeight = getDefaultItemHeight(options.listSize);
			that._cssGap = 0;
			options = that.options;
			that.element.attr("role", "listbox");
			if (that.options.columns?.length) {
				const contentSelector = "." + contentClasses.split(" ").join(".");
				that.content = that.wrapper = that.element.find(contentSelector);
				if (!that.content.length) that.content = that.wrapper = $(`<div unselectable='on' class='${contentClasses}'></div>`).appendTo(that.element);
				const stickyHeader = $(`<div class="${that._getFixedGroupHeaderClass()}"><span class="k-table-th"></span></div>`);
				that.content.before(stickyHeader);
				that.header = stickyHeader.find(".k-table-th");
			} else {
				that.header = $(`<div class='${that._getFixedGroupHeaderClass()}'></div>`).appendTo(that.element);
				that.content = that.wrapper = $(`<div unselectable='on' class='${contentClasses}'></div>`).appendTo(that.element);
			}
			that.element.children(".k-list-footer, .k-table-footer").appendTo(that.element);
			if (options.ariaLabel) this.element.attr("aria-label", options.ariaLabel);
			else if (options.ariaLabelledBy) this.element.attr("aria-labelledby", options.ariaLabelledBy);
			that.element.on("mouseenter.VirtualList", "li:not(.k-loading-item)", function() {
				$(this).addClass(HOVER);
			}).on("mouseleave.VirtualList", "li", function() {
				$(this).removeClass(HOVER);
			});
			that._values = toArray(that.options.value);
			that._selectedDataItems = [];
			that._selectedIndexes = [];
			that._rangesList = {};
			that._promisesList = [];
			that._optionID = kendo.guid();
			that._templates();
			that.setDataSource(options.dataSource);
			that.content.on("scroll.VirtualList", kendo.throttle(function() {
				that._renderItems();
				that._triggerListBound();
			}, options.delay));
			that._selectable();
			DataBoundWidget.fn.endInit.call(that);
		},
		options: {
			name: "VirtualList",
			autoBind: true,
			delay: 100,
			height: null,
			listScreens: 4,
			threshold: .5,
			itemHeight: null,
			oppositeBuffer: 1,
			type: "flat",
			selectable: false,
			value: [],
			dataValueField: null,
			template: (data) => encode(data),
			placeholderTemplate: () => "loading...",
			groupTemplate: (data) => encode(data),
			fixedGroupTemplate: (data) => encode(data),
			fixedGroupHeader: true,
			mapValueTo: "index",
			valueMapper: null,
			ariaLabel: null,
			ariaLabelledBy: null,
			id: null,
			iconField: null,
			descriptionField: null,
			groupIconField: null,
			actionField: null,
			checkboxes: false
		},
		events: [
			CHANGE,
			CLICK,
			LISTBOUND,
			ITEMCHANGE,
			ACTIVATE,
			DEACTIVATE,
			ACTION
		],
		_isTableVariant: function() {
			return !!this.options.columns?.length;
		},
		_getItemClass: function() {
			return this._isTableVariant() ? TABLE_ITEM : LIST_ITEM;
		},
		_getItemHeightStyle: function() {
			return this.options.itemHeight + "px";
		},
		_getHeaderHeight: function() {
			return this.header?.is(":visible") ? this.header.outerHeight() : 0;
		},
		_toggleHeaderVisibility: function(show) {
			(this.header.closest(GROUP_ROW_SEL).length ? this.header.closest(GROUP_ROW_SEL) : this.header)[show ? "show" : "hide"]();
		},
		_getHeaderTextSelector: function() {
			return this._isTableVariant() ? ".k-table-th" : ".k-list-item-text";
		},
		_getGroupHeaderClass: function() {
			return this._isTableVariant() ? "k-table-group-row" : GROUP_HEADER_ITEM;
		},
		_getFixedGroupHeaderClass: function() {
			return this._isTableVariant() ? TABLE_HEADER : HEADER;
		},
		setOptions: function(options) {
			Widget.fn.setOptions.call(this, options);
			if (this._selectProxy && this.options.selectable === false) this.element.off(CLICK, "." + this._getItemClass(), this._selectProxy);
			else if (!this._selectProxy && this.options.selectable) this._selectable();
			this._templates();
			this.refresh();
		},
		items: function() {
			return $(this._items);
		},
		/**
		* Returns the UL elements in the virtual list.
		* For flat (non-grouped) lists, returns the single UL element.
		* For grouped lists, returns all currently rendered group UL elements.
		* @returns {jQuery} - jQuery collection of UL elements
		*/
		ulElements: function() {
			const that = this;
			if (that.options.type === "flat") return that.ul || $();
			else return that.content.find(".k-list-ul");
		},
		destroy: function() {
			this.wrapper.off(VIRTUAL_LIST_NS);
			this.dataSource.unbind(CHANGE, this._refreshHandler);
			Widget.fn.destroy.call(this);
		},
		setDataSource: function(source) {
			const that = this;
			let dataSource = source || {};
			dataSource = Array.isArray(dataSource) ? { data: dataSource } : dataSource;
			dataSource = kendo.data.DataSource.create(dataSource);
			if (that.dataSource) {
				that.dataSource.unbind(CHANGE, that._refreshHandler);
				that._clean();
				that.bound(false);
				that._deferValueSet = true;
				const value = that.value();
				that.value([]);
				that.mute(function() {
					that.value(value);
				});
			} else that._refreshHandler = that.refresh.bind(that);
			that.dataSource = dataSource.bind(CHANGE, that._refreshHandler);
			that.setDSFilter(dataSource.filter());
			if (dataSource.view().length !== 0) that.refresh();
			else if (that.options.autoBind) dataSource.fetch();
		},
		skip: function() {
			return this.dataSource.currentRangeStart();
		},
		_triggerListBound: function() {
			const that = this;
			const skip = that.skip();
			if (that.bound() && !that._selectingValue && that._skip !== skip) {
				that._skip = skip;
				that.trigger(LISTBOUND);
			}
		},
		_getValues: function(dataItems) {
			const getter = this._valueGetter;
			return $.map(dataItems, (dataItem) => getter(dataItem));
		},
		_highlightSelectedItems: function() {
			const hasCheckboxes = this.options.checkboxes;
			for (let i = 0; i < this._selectedDataItems.length; i++) {
				const item = this._getElementByDataItem(this._selectedDataItems[i]);
				if (item.length) if (hasCheckboxes) {
					item.find("input.k-checkbox").toggleClass("k-checked", true).prop("checked", true);
					item.attr("aria-selected", true);
				} else item.addClass(SELECTED);
			}
		},
		_focusSelectedInFilteredData: function() {
			const that = this;
			const selectedDataItems = that._selectedDataItems;
			const valueGetter = that._valueGetter;
			let focusIndex = 0;
			if (selectedDataItems.length) {
				const data = that.dataSource.flatView();
				const selectedValue = valueGetter(selectedDataItems[0]);
				for (let i = 0; i < data.length; i++) {
					const item = that.options.type === "group" ? data[i].item : data[i];
					if (item && valueGetter(item) === selectedValue) {
						focusIndex = i;
						break;
					}
				}
			}
			that.focus(focusIndex);
		},
		refresh: function(e) {
			const that = this;
			const action = e?.action;
			const isItemChange = action === "itemchange";
			const filtered = this.isFiltered();
			if (that._mute) return;
			that._deferValueSet = false;
			if (!that._fetching) {
				if (filtered) that._focusSelectedInFilteredData();
				that._createList();
				if (!action && that._values.length && !filtered && !that.options.skipUpdateOnBind && !that._emptySearch) {
					that._selectingValue = true;
					that.bound(true);
					that.value(that._values, true).done(function() {
						that._selectingValue = false;
						that._triggerListBound();
					});
				} else {
					that.bound(true);
					that._highlightSelectedItems();
					that._triggerListBound();
				}
			} else {
				if (that._renderItems) that._renderItems(true);
				that._triggerListBound();
			}
			if (isItemChange || action === "remove") {
				const result = mapChangedItems(that._selectedDataItems, e.items);
				if (result.changed.length) if (isItemChange) that.trigger("selectedItemChange", { items: result.changed });
				else that.value(that._getValues(result.unchanged));
			}
			that._fetching = false;
		},
		removeAt: function(position) {
			const value = this._values.splice(position, 1)[0];
			return {
				position,
				dataItem: this._removeSelectedDataItem(value)
			};
		},
		_removeSelectedDataItem: function(value) {
			const that = this;
			const valueGetter = that._valueGetter;
			for (const idx in that._selectedDataItems) if (valueGetter(that._selectedDataItems[idx]) === value) {
				that._selectedIndexes.splice(idx, 1);
				return that._selectedDataItems.splice(idx, 1)[0];
			}
		},
		setValue: function(value) {
			this._values = toArray(value);
		},
		value: function(value, _forcePrefetch) {
			const that = this;
			if (value === undefined) return that._values.slice();
			if (value === null) value = [];
			value = toArray(value);
			if (!that._valueDeferred || that._valueDeferred.state() === "resolved") that._valueDeferred = $.Deferred();
			if (that.options.selectable === "multiple" && that.select().length && value.length || !value.length) that.select(-1);
			that._values = value;
			if (that.bound() && !that._mute && !that._deferValueSet || _forcePrefetch) that._prefetchByValue(value);
			return that._valueDeferred;
		},
		_checkValuesOrder: function(value) {
			if (this._removedAddedIndexes?.length === value.length) {
				const newValue = this._removedAddedIndexes.slice();
				this._removedAddedIndexes = null;
				return newValue;
			}
			return value;
		},
		_prefetchByValue: function(value) {
			const that = this;
			const dataView = that._dataView;
			const valueGetter = that._valueGetter;
			const mapValueTo = that.options.mapValueTo;
			const forSelection = [];
			for (let i = 0; i < value.length; i++) for (let idx = 0; idx < dataView.length; idx++) {
				const item = dataView[idx].item;
				if (item) {
					if (isPrimitive(item) ? value[i] === item : value[i] === valueGetter(item)) forSelection.push(dataView[idx].index);
				}
			}
			if (forSelection.length === value.length) {
				that._values = [];
				that.select(forSelection);
				return;
			}
			if (typeof that.options.valueMapper === "function") {
				const callback = mapValueTo === "index" ? that.mapValueToIndex : that.mapValueToDataItem;
				that.options.valueMapper(require_valueMapper.valueMapperOptions(this.options, value, callback.bind(that)));
			} else if (!that.value()[0]) that.select([-1]);
			else {
				that._selectingValue = false;
				that._triggerListBound();
			}
		},
		mapValueToIndex: function(indexes) {
			if (indexes === undefined || indexes === -1 || indexes === null) indexes = [];
			else indexes = toArray(indexes);
			if (!indexes.length) indexes = [-1];
			else {
				const removed = this._deselect([]).removed;
				if (removed.length) this._triggerChange(removed, []);
			}
			this.select(indexes);
		},
		mapValueToDataItem: function(dataItems) {
			if (dataItems === undefined || dataItems === null) dataItems = [];
			else dataItems = toArray(dataItems);
			if (!dataItems.length) this.select([-1]);
			else {
				const removed = $.map(this._selectedDataItems, (item, index) => ({
					index,
					dataItem: item
				}));
				const added = $.map(dataItems, (item, index) => ({
					index,
					dataItem: item
				}));
				this._selectedDataItems = dataItems;
				this._selectedIndexes = [];
				for (let i = 0; i < this._selectedDataItems.length; i++) {
					const item = this._getElementByDataItem(this._selectedDataItems[i]);
					this._selectedIndexes.push(this._getIndecies(item)[0]);
					if (this.options.checkboxes) {
						item.find("input.k-checkbox").toggleClass("k-checked", true).prop("checked", true);
						item.attr("aria-selected", true);
					} else item.addClass(SELECTED);
				}
				this._triggerChange(removed, added);
				if (this._valueDeferred) this._valueDeferred.resolve();
			}
		},
		deferredRange: function(index) {
			const dataSource = this.dataSource;
			const take = this.itemCount;
			const ranges = this._rangesList;
			const result = $.Deferred();
			const defs = [];
			const low = Math.floor(index / take) * take;
			const high = Math.ceil(index / take) * take;
			const pages = high === low ? [high] : [low, high];
			$.each(pages, (_, skip) => {
				const end = skip + take;
				const existingRange = ranges[skip];
				let deferred;
				if (!existingRange || existingRange.end !== end) {
					deferred = $.Deferred();
					ranges[skip] = {
						end,
						deferred
					};
					dataSource._multiplePrefetch(skip, take, () => {
						deferred.resolve();
					});
				} else deferred = existingRange.deferred;
				defs.push(deferred);
			});
			$.when(...defs).done(() => {
				result.resolve();
			});
			return result;
		},
		prefetch: function(indexes) {
			const that = this;
			const take = this.itemCount;
			const isEmptyList = !that._promisesList.length;
			if (!isActivePromise(that._activeDeferred)) {
				that._activeDeferred = $.Deferred();
				that._promisesList = [];
			}
			$.each(indexes, (_, index) => {
				that._promisesList.push(that.deferredRange(that._getSkip(index, take)));
			});
			if (isEmptyList) $.when(...that._promisesList).done(() => {
				that._promisesList = [];
				that._activeDeferred.resolve();
			});
			return that._activeDeferred;
		},
		_findDataItem: function(view, index) {
			if (this.options.type === "group") for (let i = 0; i < view.length; i++) {
				const group = view[i].items;
				if (group.length <= index) index = index - group.length;
				else return group[index];
			}
			return view[index];
		},
		_getRange: function(skip, take) {
			return this.dataSource._findRange(skip, Math.min(skip + take, this.dataSource.total()));
		},
		dataItemByIndex: function(index) {
			const that = this;
			const take = that.itemCount;
			const skip = that._getSkip(index, take);
			let view = this._getRange(skip, take);
			if (!that._getRange(skip, take).length) return null;
			if (that.options.type === "group") {
				kendo.ui.progress($(that.wrapper), true);
				that.mute(function() {
					that.dataSource.range(skip, take, () => {
						kendo.ui.progress($(that.wrapper), false);
					});
					view = that.dataSource.view();
				});
			}
			return that._findDataItem(view, [index - skip]);
		},
		selectedDataItems: function() {
			return this._selectedDataItems.slice();
		},
		scrollWith: function(value) {
			this.content.scrollTop(this.content.scrollTop() + value);
		},
		scrollTo: function(y) {
			this.content.scrollTop(y);
		},
		scrollToIndex: function(index) {
			const position = this._getElementPosition ? this._getElementPosition(index) : index * (this.options.itemHeight + this._cssGap);
			this.scrollTo(position);
		},
		focus: function(candidate) {
			const that = this;
			const itemHeight = that.options.itemHeight;
			const id = that._optionID;
			let element;
			let index;
			let current;
			let triggerEvent = true;
			if (candidate === undefined) {
				current = that.content.find(".k-focus");
				return current.length ? current : null;
			}
			if (typeof candidate === "function") {
				const data = that.dataSource.flatView();
				for (let idx = 0; idx < data.length; idx++) if (candidate(data[idx])) {
					candidate = idx;
					break;
				}
			}
			if (candidate instanceof Array) candidate = lastFrom(candidate);
			if (isNaN(candidate)) {
				element = $(candidate);
				index = parseInt($(element).attr("data-offset-index"), 10);
			} else {
				index = candidate;
				element = that._getElementByIndex(index);
			}
			if (index === -1) {
				that.content.find(".k-focus").removeClass(FOCUSED);
				that._focusedIndex = undefined;
				return;
			}
			if (element.length) {
				if (element.hasClass(FOCUSED)) triggerEvent = false;
				const previousIndex = that._focusedIndex;
				if (previousIndex !== undefined) {
					current = that._getElementByIndex(previousIndex);
					current.removeClass(FOCUSED).removeAttr("id");
					if (triggerEvent) that.trigger(DEACTIVATE);
				}
				that._focusedIndex = index;
				element.addClass(FOCUSED).attr("id", id);
				let elementPosition = that._getRenderedElementPosition(element);
				if (elementPosition === 0 && index > 0) elementPosition = that._getElementPosition(index);
				const headerHeight = that._getHeaderHeight();
				const goingUp = previousIndex !== undefined && index < previousIndex;
				const goingDown = previousIndex !== undefined && index > previousIndex;
				const scrollTop = this.content.scrollTop();
				const visibleTop = scrollTop + headerHeight;
				const screenEnd = scrollTop + this._screenHeight;
				const elementBottom = elementPosition + itemHeight;
				let position;
				if (elementPosition < visibleTop && elementBottom > scrollTop) position = "top";
				else if (elementPosition === screenEnd || elementPosition < screenEnd && screenEnd < elementBottom) position = "bottom";
				else if (elementPosition >= visibleTop && elementBottom <= screenEnd) position = "inScreen";
				else position = "outScreen";
				if (position === "top") that.scrollTo(Math.max(0, elementPosition - headerHeight));
				else if (position === "bottom") that.scrollTo(elementPosition + itemHeight - that._screenHeight);
				else if (position === "outScreen") if (goingUp) that.scrollTo(Math.max(0, elementPosition - headerHeight));
				else if (goingDown) that.scrollTo(elementPosition + itemHeight - that._screenHeight);
				else that.scrollTo(Math.max(0, elementPosition - headerHeight));
				if (triggerEvent) that.trigger(ACTIVATE);
			} else {
				const previousIndex = that._focusedIndex;
				if (previousIndex !== undefined) {
					current = that._getElementByIndex(previousIndex);
					current.removeClass(FOCUSED).removeAttr("id");
				}
				that._focusedIndex = index;
				that.items().removeClass(FOCUSED);
				const elementPosition = that._getElementPosition(index);
				const headerHeight = that._getHeaderHeight();
				const goingUp = previousIndex !== undefined && index < previousIndex;
				const goingDown = previousIndex !== undefined && index > previousIndex;
				if (goingUp) that.scrollTo(Math.max(0, elementPosition - headerHeight));
				else if (goingDown) that.scrollTo(elementPosition + itemHeight - that._screenHeight);
				else that.scrollTo(Math.max(0, elementPosition - headerHeight));
			}
		},
		focusIndex: function() {
			return this._focusedIndex;
		},
		focusFirst: function() {
			this.scrollTo(0);
			this.focus(0);
		},
		focusLast: function() {
			const lastIndex = this.dataSource.total();
			this.scrollTo(this.heightContainer.offsetHeight);
			this.focus(lastIndex - 1);
		},
		focusPrev: function() {
			let index = this._focusedIndex;
			if (!isNaN(index) && index > 0) {
				index -= 1;
				this.focus(index);
				if (this.focus()?.hasClass("k-loading-item")) {
					index += 1;
					this.focus(index);
				}
				return index;
			} else {
				index = this.dataSource.total() - 1;
				this.focus(index);
				return index;
			}
		},
		focusNext: function() {
			let index = this._focusedIndex;
			const lastIndex = this.dataSource.total() - 1;
			if (!isNaN(index) && index < lastIndex) {
				index += 1;
				this.focus(index);
				if (this.focus()?.hasClass("k-loading-item")) {
					index -= 1;
					this.focus(index);
				}
				return index;
			} else {
				index = 0;
				this.focus(index);
				return index;
			}
		},
		_triggerChange: function(removed = [], added = []) {
			if (removed.length || added.length) this.trigger(CHANGE, {
				removed,
				added
			});
		},
		select: function(candidate) {
			const that = this;
			const singleSelection = that.options.selectable !== "multiple";
			let prefetchStarted = isActivePromise(that._activeDeferred);
			const filtered = this.isFiltered();
			if (candidate === undefined) return that._selectedIndexes.slice();
			if (!that._selectDeferred || that._selectDeferred.state() === "resolved") that._selectDeferred = $.Deferred();
			let indices = that._getIndecies(candidate);
			const isAlreadySelected = singleSelection && !filtered && lastFrom(indices) === lastFrom(this._selectedIndexes);
			let removed = that._deselectCurrentValues(indices);
			if (removed.length || !indices.length || isAlreadySelected) {
				that._triggerChange(removed);
				if (that._valueDeferred) that._valueDeferred.resolve().promise();
				return that._selectDeferred.resolve().promise();
			}
			if (indices.length === 1 && indices[0] === -1) indices = [];
			const initialIndices = indices;
			const result = that._deselect(indices);
			removed = result.removed;
			indices = result.indices;
			if (singleSelection) {
				prefetchStarted = false;
				if (indices.length) indices = [lastFrom(indices)];
			}
			const done = function() {
				const added = that._select(indices);
				if (!that._preventFocus && (initialIndices.length === indices.length || singleSelection)) that.focus(indices);
				that._triggerChange(removed, added);
				if (that._valueDeferred) that._valueDeferred.resolve();
				that._selectDeferred.resolve();
			};
			const deferred = that.prefetch(indices);
			if (!prefetchStarted) if (deferred) deferred.done(done);
			else done();
			return that._selectDeferred.promise();
		},
		bound: function(bound) {
			if (bound === undefined) return this._listCreated;
			this._listCreated = bound;
		},
		mute: function(callback) {
			this._mute = true;
			callback();
			this._mute = false;
		},
		setDSFilter: function(filter) {
			this._lastDSFilter = $.extend({}, filter);
		},
		isFiltered: function() {
			if (!this._lastDSFilter) this.setDSFilter(this.dataSource.filter());
			return !kendo.data.Query.compareFilters(this.dataSource.filter(), this._lastDSFilter);
		},
		skipUpdate: $.noop,
		_getElementByIndex: function(index) {
			return this.items().filter((idx, element) => index === parseInt($(element).attr("data-offset-index"), 10));
		},
		_getElementByDataItem: function(dataItem) {
			const dataView = this._dataView;
			const valueGetter = this._valueGetter;
			let element;
			for (let i = 0; i < dataView.length; i++) if (dataView[i].item && isPrimitive(dataView[i].item) ? dataView[i].item === dataItem : dataView[i].item && dataItem && valueGetter(dataView[i].item) == valueGetter(dataItem)) {
				element = dataView[i];
				break;
			}
			return element ? this._getElementByIndex(element.index) : $();
		},
		_clean: function() {
			this.result = undefined;
			this._lastScrollTop = undefined;
			this._skip = undefined;
			$(this.heightContainer).remove();
			this.heightContainer = undefined;
			if (this.content) this.content.empty();
			this._flatUl = null;
			this.ul = null;
			if (this._groupUlCache) for (const key in this._groupUlCache) {
				const cached = this._groupUlCache[key];
				if (cached?.ul?.parentNode) cached.ul.parentNode.removeChild(cached.ul);
			}
			this._groupContainers = [];
			this._groupUlCache = {};
			this._groupRanges = null;
			this._items = [];
			this._groupValueMap = null;
		},
		_height: function() {
			const hasData = !!this.dataSource.view().length;
			let height = this.options.height;
			const effectiveItemHeight = this.options.itemHeight + (this._cssGap || 0);
			const total = this.dataSource.total();
			if (!hasData) height = 0;
			else if (height / effectiveItemHeight > total) height = total * effectiveItemHeight;
			return height;
		},
		setScreenHeight: function() {
			const height = this._height();
			this.content.height(height);
			this._screenHeight = height;
		},
		screenHeight: function() {
			return this._screenHeight;
		},
		/**
		* Gets the actual DOM position of a rendered element relative to the scroll container.
		* This is more accurate than _getElementPosition for rendered items because it uses
		* the actual DOM layout rather than calculated positions.
		* @param {jQuery} element - The rendered element
		* @returns {number} - The pixel position relative to the scroll container's content
		*/
		_getRenderedElementPosition: function(element) {
			if (!element || !element.length) return 0;
			const el = element[0];
			const parentUl = element.closest("ul")[0];
			if (!parentUl) return el.offsetTop;
			const match = (parentUl.style.transform || parentUl.style.webkitTransform || "").match(/translateY\((-?\d+(?:\.\d+)?)px\)/);
			return (match ? parseFloat(match[1]) : 0) + el.offsetTop;
		},
		_getElementLocation: function(index) {
			const scrollTop = this.content.scrollTop();
			const screenHeight = this._screenHeight;
			const itemHeight = this.options.itemHeight;
			const yPosition = this._getElementPosition(index);
			const yDownPostion = yPosition + itemHeight;
			const screenEnd = scrollTop + screenHeight;
			const visibleTop = scrollTop + this._getHeaderHeight();
			if (yPosition < visibleTop && yDownPostion > scrollTop) return "top";
			else if (yPosition === screenEnd || yPosition < screenEnd && screenEnd < yDownPostion) return "bottom";
			else if (yPosition >= visibleTop && yDownPostion <= screenEnd) return "inScreen";
			else return "outScreen";
		},
		_getElementPosition: function(index) {
			const itemHeight = this.options.itemHeight;
			const groupRanges = this._groupRanges;
			const cssGap = this._cssGap || 0;
			if (groupRanges && groupRanges.length) {
				const lastRange = groupRanges[groupRanges.length - 1];
				const lastKnownIndex = lastRange.startIndex + lastRange.itemCount - 1;
				if (index <= lastKnownIndex) return getGroupedItemPosition(index, itemHeight, groupRanges, cssGap);
				const knownPosition = getGroupedItemPosition(lastKnownIndex, itemHeight, groupRanges, cssGap);
				const itemsBeyond = index - lastKnownIndex;
				const avgItemsPerGroup = lastKnownIndex / groupRanges.length;
				const estimatedNewHeaders = Math.floor(itemsBeyond / avgItemsPerGroup);
				return knownPosition + itemsBeyond * (itemHeight + cssGap) + estimatedNewHeaders * itemHeight;
			}
			return index * (itemHeight + cssGap);
		},
		_templates: function() {
			const options = this.options;
			const templates = {
				template: options.template,
				placeholderTemplate: options.placeholderTemplate,
				groupTemplate: options.groupTemplate,
				fixedGroupTemplate: options.fixedGroupTemplate
			};
			if (options.columns) options.columns.forEach((column, i) => {
				const templateText = column.field ? column.field.toString() : "text";
				const templateFunc = (data) => encode(kendo.getter(templateText)(data));
				templates["column" + i] = column.template || templateFunc;
			});
			for (const key in templates) if (typeof templates[key] !== "function") templates[key] = kendo.template(templates[key] || "");
			this.templates = templates;
		},
		_generateItems: function(element, count) {
			const items = [];
			const itemHeight = this._getItemHeightStyle();
			const itemClass = this._getItemClass();
			const hasCheckboxes = this.options.checkboxes;
			while (count-- > 0) {
				const item = document.createElement("li");
				item.tabIndex = -1;
				item.className = itemClass;
				item.setAttribute("role", "option");
				item.style.height = itemHeight;
				item.style.minHeight = itemHeight;
				item.style.position = "absolute";
				item.style.width = "100%";
				if (hasCheckboxes) item.appendChild(this._createCheckboxWrap());
				const text = document.createElement("span");
				text.className = "k-list-item-text";
				item.appendChild(text);
				element.appendChild(item);
				items.push(item);
			}
			return items;
		},
		/**
		* Generates a unique ID for a group header element.
		* @param {number} groupIndex - The global group index
		* @param {string} groupValue - The group value
		* @returns {string} - A unique ID for the group header
		*/
		_generateGroupId: function(groupIndex, groupValue) {
			const safeValue = (groupValue === null || groupValue === undefined ? "" : String(groupValue)).replace(/\s+/g, "-").replace(/[^a-zA-Z0-9-_]/g, "");
			return this._optionID + "-group-" + groupIndex + "-" + safeValue;
		},
		/**
		* Creates a group UL container with group header (except first group)
		* Following Angular implementation pattern with role="group" and aria-labelledby
		* @param {boolean} hasHeader - Whether to include a group header li
		* @param {string} groupValue - The group value for the header
		* @param {number} groupIndex - The global group index (for generating unique IDs)
		* @returns {Object} - { ul, header, items: [], groupId }
		*/
		_createGroupUl: function(hasHeader, groupValue, groupIndex, firstDataItem) {
			const that = this;
			const options = that.options;
			const itemHeight = that._getItemHeightStyle();
			const ul = document.createElement("ul");
			const groupId = that._generateGroupId(groupIndex, groupValue);
			const groupIconField = options.groupIconField;
			ul.className = that._isTableVariant() ? `${LIST_UL} ${TABLE}` : LIST_UL;
			ul.style.position = "absolute";
			ul.style.width = "100%";
			ul.style.top = "0";
			ul.style.left = "0";
			if (options.id) ul.id = `${options.id}-group-${groupIndex}`;
			ul.setAttribute("role", "group");
			ul.setAttribute("aria-labelledby", groupId);
			if (options.ariaLive) ul.setAttribute("aria-live", options.ariaLive);
			const groupContainer = {
				ul,
				header: null,
				items: [],
				groupValue,
				hasHeader,
				groupId
			};
			if (hasHeader) {
				const header = document.createElement("li");
				header.className = that._getGroupHeaderClass();
				header.setAttribute("role", "presentation");
				header.setAttribute("id", groupId);
				header.style.height = itemHeight;
				header.style.minHeight = itemHeight;
				header.style.position = "relative";
				header.style.transform = "none";
				header.style.overflow = "hidden";
				if (groupIconField && firstDataItem && firstDataItem[groupIconField]) {
					const iconWrapper = document.createElement("span");
					iconWrapper.className = "k-list-item-icon-wrapper";
					iconWrapper.setAttribute("role", "presentation");
					const iconHtml = kendo.ui.icon({
						icon: firstDataItem[groupIconField],
						iconClass: "k-list-group-icon",
						attr: { "aria-hidden": "true" }
					});
					$(iconWrapper).append(iconHtml);
					$(header).append(iconWrapper);
				}
				const headerText = document.createElement("span");
				headerText.className = that._isTableVariant() ? "k-table-th" : "k-list-item-text";
				header.appendChild(headerText);
				ul.appendChild(header);
				groupContainer.header = header;
			}
			return groupContainer;
		},
		/**
		* Adds items to a group UL
		* @param {Object} groupContainer - The group container object
		* @param {number} count - Number of items to add
		*/
		_addGroupItems: function(groupContainer, count) {
			const that = this;
			const itemHeight = that._getItemHeightStyle();
			const hasColumns = that._isTableVariant();
			const itemClass = that._getItemClass();
			const hasCheckboxes = !hasColumns && that.options.checkboxes;
			for (let i = 0; i < count; i++) {
				const item = document.createElement("li");
				item.tabIndex = -1;
				item.className = itemClass;
				item.setAttribute("role", "option");
				item.style.height = itemHeight;
				item.style.minHeight = itemHeight;
				item.style.position = "relative";
				item.style.transform = "none";
				if (!hasColumns) {
					if (hasCheckboxes) item.appendChild(that._createCheckboxWrap());
					const text = document.createElement("span");
					text.className = "k-list-item-text";
					item.appendChild(text);
				}
				groupContainer.ul.appendChild(item);
				groupContainer.items.push(item);
			}
		},
		/**
		* Generates grouped UL structure based on visible groups
		* @param {HTMLElement} container - The container element
		* @param {Array} visibleGroups - Array of visible group info
		* @param {number} itemsPerGroup - Max items to render per group
		* @returns {Object} - { groupContainers: [...], allItems: [...] }
		*/
		_generateGroupedItems: function(container, visibleGroups, itemsPerGroup) {
			const that = this;
			const groupContainers = [];
			const allItems = [];
			container.innerHTML = "";
			for (const groupInfo of visibleGroups) {
				const groupContainer = that._createGroupUl(groupInfo.hasHeader, groupInfo.groupValue, groupInfo.groupIndex);
				const itemCount = Math.min(groupInfo.itemCount, itemsPerGroup);
				that._addGroupItems(groupContainer, itemCount);
				position(groupContainer.ul, groupInfo.top);
				groupContainer.ul.setAttribute("data-group-index", groupInfo.groupIndex);
				groupContainer.ul.setAttribute("data-start-index", groupInfo.startIndex);
				groupContainer.startIndex = groupInfo.startIndex;
				groupContainer.itemCount = itemCount;
				groupContainer.top = groupInfo.top;
				groupContainer.groupIndex = groupInfo.groupIndex;
				container.appendChild(groupContainer.ul);
				groupContainers.push(groupContainer);
				allItems.push(...groupContainer.items);
			}
			return {
				groupContainers,
				allItems
			};
		},
		_saveInitialRanges: function() {
			const ranges = this.dataSource._ranges;
			const deferred = $.Deferred();
			deferred.resolve();
			this._rangesList = {};
			for (let i = 0; i < ranges.length; i++) this._rangesList[ranges[i].start] = {
				end: ranges[i].end,
				deferred
			};
		},
		_createList: function() {
			const that = this;
			const content = that.content.get(0);
			const options = that.options;
			const dataSource = that.dataSource;
			if (that.bound()) that._clean();
			that._saveInitialRanges();
			that._buildValueGetter();
			that.setScreenHeight();
			that.itemCount = getItemCount(that._screenHeight, options.listScreens, options.itemHeight);
			if (that.itemCount > dataSource.total()) that.itemCount = dataSource.total();
			that.options.type = (dataSource.group() || []).length ? "group" : "flat";
			if (that.options.type === "flat") {
				const ul = document.createElement("ul");
				ul.className = that._isTableVariant() ? `${LIST_UL} ${TABLE}` : LIST_UL;
				ul.style.position = "relative";
				that.content.get(0).appendChild(ul);
				that._flatUl = ul;
				if (options.id) ul.id = options.id;
				if (options.ariaLive) ul.setAttribute("aria-live", options.ariaLive);
				that.ul = $(ul);
				that._cssGap = that._isTableVariant() ? 0 : getCssGap(that.content, that.ul);
				that._items = that._generateItems(ul, that.itemCount);
				const totalHeight = dataSource.total() * (options.itemHeight + that._cssGap);
				that._setHeight(totalHeight);
				that._toggleHeaderVisibility(false);
				that.getter = that._getter(() => {
					that._renderItems(true);
				});
				that._onScroll = (scrollTop, force) => {
					const getList = that._listItems(that.getter);
					return that._fixedHeader(scrollTop, getList(scrollTop, force));
				};
				that._renderItems = that._whenChanged(scrollCallback(content, that._onScroll), syncList(that._reorderList(that._items, render.bind(that))));
			} else {
				that._toggleHeaderVisibility(options.fixedGroupHeader !== false);
				that._groupValueMap = /* @__PURE__ */ new Map();
				that._cssGap = getCssGap(that.content, null);
				that._groupRanges = buildGroupRanges(dataSource, 0, 0, that._groupValueMap);
				let estimatedTotalGroups = options.groupCount;
				const isServerOperations = dataSource.options.serverGrouping === true;
				if (!estimatedTotalGroups && that._groupRanges.length > 0) if (isServerOperations) estimatedTotalGroups = that._groupRanges.length;
				else {
					const groupDescriptor = dataSource.group();
					const allData = dataSource.data();
					if (allData?.length && groupDescriptor?.length) estimatedTotalGroups = new kendo.data.Query(allData).group(groupDescriptor).data.length;
					else estimatedTotalGroups = that._groupRanges.length;
				}
				that._totalGroupCount = estimatedTotalGroups || that._groupRanges.length;
				that._initialGroupRanges = that._groupRanges.slice();
				const groupCount = that._totalGroupCount;
				const totalHeight = getGroupedTotalHeight(dataSource.options.serverGrouping === true ? dataSource.total() : dataSource.data().length || dataSource.total(), options.itemHeight, groupCount, that._cssGap);
				that._setHeight(totalHeight);
				that._initGroupedRendering();
			}
			that._renderItems();
			that._calculateGroupPadding(that._screenHeight);
			that._calculateColumnsHeaderPadding();
		},
		_initGroupedRendering: function() {
			const that = this;
			const content = that.content.get(0);
			that._currentPageStart = 0;
			that._renderedPageStart = null;
			that._groupUls = [];
			that._items = [];
			that.getter = that._groupedGetter(() => {
				that._renderItems(true);
			});
			that._onScroll = (scrollTop, force) => that._handleGroupedScroll(scrollTop, force);
			that._renderItems = that._whenChangedGrouped(scrollCallback(content, that._onScroll), (list, force) => {
				that._renderGroupedPage(list, force);
				return list;
			});
		},
		/**
		* Handles scroll events for grouped list.
		* Determines which page of data should be displayed based on scroll position.
		*/
		_handleGroupedScroll: function(scrollTop, force) {
			const that = this;
			const itemHeight = that.options.itemHeight;
			const dataSource = that.dataSource;
			const pageSize = that.itemCount;
			const total = dataSource.total();
			const screenHeight = that._screenHeight || 300;
			const totalGroupCount = that._totalGroupCount || 1;
			if (!(dataSource.options.serverGrouping === true)) return that._buildGroupedPageData(0, scrollTop);
			const totalHeight = total * itemHeight + Math.max(0, totalGroupCount - 1) * itemHeight;
			let pageStart = that._scrollPositionToPageInfo(scrollTop).pageStart;
			const maxPageStart = Math.max(0, total - pageSize);
			if (scrollTop + screenHeight >= totalHeight - screenHeight * 2 && pageStart < maxPageStart) pageStart = maxPageStart;
			let constrainedPageStart = Math.min(Math.max(0, pageStart), maxPageStart);
			if (that._currentPageStart !== constrainedPageStart || force) {
				if (!dataSource.inRange(constrainedPageStart, pageSize)) {
					if (that._pageDeferred) that._pageDeferred.reject();
					that._pageDeferred = that.deferredRange(constrainedPageStart);
					that._pageDeferred.then(() => {
						that._pageDeferred = null;
						that.mute(() => {
							dataSource.range(constrainedPageStart, pageSize);
						});
						const firstGroupIdx = that._estimateFirstGroupIndex(constrainedPageStart);
						that._groupRanges = buildGroupRanges(dataSource, constrainedPageStart, firstGroupIdx, that._groupValueMap);
						that._updateGroupCountAndHeight();
						that._currentPageStart = constrainedPageStart;
						that._renderItems(true);
					});
					return that._buildPlaceholderPageData(constrainedPageStart, scrollTop);
				}
				if (dataSource.currentRangeStart() !== constrainedPageStart) {
					that.mute(() => {
						dataSource.range(constrainedPageStart, pageSize);
					});
					that._groupRanges = buildGroupRanges(dataSource, constrainedPageStart, that._estimateFirstGroupIndex(constrainedPageStart), that._groupValueMap);
					that._updateGroupCountAndHeight();
					that._currentPageStart = constrainedPageStart;
					const list = that._buildGroupedPageData(constrainedPageStart, scrollTop);
					that._renderGroupedPage(list, true, scrollTop);
					return list;
				}
				that._currentPageStart = constrainedPageStart;
			}
			return that._buildGroupedPageData(constrainedPageStart, scrollTop);
		},
		/**
		* Calculates aggregate info about the known initial ranges.
		* @returns {Object} - { knownPosition, lastKnownIndex, lastKnownGroupIndex, avgGroupHeight, avgItemsPerGroup }
		*/
		_getKnownRangeInfo: function(initialRanges, itemHeight) {
			let knownPosition = 0;
			for (let i = 0; i < initialRanges.length; i++) {
				const range = initialRanges[i];
				knownPosition += ((i > 0 ? 1 : 0) + range.itemCount) * itemHeight;
			}
			const lastRange = initialRanges[initialRanges.length - 1];
			const lastKnownIndex = lastRange.startIndex + lastRange.itemCount - 1;
			const lastKnownGroupIndex = initialRanges.length - 1;
			const avgGroupHeight = knownPosition / initialRanges.length;
			const avgItemsPerGroup = (lastKnownIndex + 1) / initialRanges.length;
			return {
				knownPosition,
				lastKnownIndex,
				lastKnownGroupIndex,
				avgGroupHeight,
				avgItemsPerGroup
			};
		},
		/**
		* Estimates viewport indices when scroll position is beyond known ranges.
		*/
		_estimateViewportIndicesBeyondKnown: function(scrollTop, viewportEnd, rangeInfo, total) {
			const { knownPosition, lastKnownGroupIndex, avgGroupHeight, avgItemsPerGroup } = rangeInfo;
			const positionBeyondKnown = scrollTop - knownPosition;
			const groupsBeyondKnown = Math.floor(positionBeyondKnown / avgGroupHeight);
			const targetGroupIndex = lastKnownGroupIndex + 1 + groupsBeyondKnown;
			let viewportStartIndex = Math.floor(targetGroupIndex * avgItemsPerGroup);
			viewportStartIndex = Math.min(viewportStartIndex, total - 1);
			const endPositionBeyondKnown = viewportEnd - knownPosition;
			const endGroupsBeyondKnown = Math.ceil(endPositionBeyondKnown / avgGroupHeight);
			const endGroupIndex = lastKnownGroupIndex + 1 + endGroupsBeyondKnown;
			let viewportEndIndex = Math.floor(endGroupIndex * avgItemsPerGroup);
			viewportEndIndex = Math.min(viewportEndIndex, total - 1);
			return {
				viewportStartIndex,
				viewportEndIndex,
				targetGroupIndex
			};
		},
		/**
		* Finds viewport indices within known ranges using precise calculation.
		*/
		_findViewportIndicesInKnownRanges: function(scrollTop, viewportEnd, initialRanges, itemHeight, rangeInfo, total) {
			const { knownPosition, lastKnownGroupIndex, avgGroupHeight, avgItemsPerGroup } = rangeInfo;
			let viewportStartIndex = 0;
			let viewportEndIndex = 0;
			let targetGroupIndex = 0;
			let position = 0;
			for (let i = 0; i < initialRanges.length; i++) {
				const range = initialRanges[i];
				const headerSlots = i > 0 ? 1 : 0;
				const groupHeight = (headerSlots + range.itemCount) * itemHeight;
				const groupEnd = position + groupHeight;
				if (scrollTop >= position && scrollTop < groupEnd) {
					const posInGroup = scrollTop - position;
					const slotsIntoGroup = Math.floor(posInGroup / itemHeight);
					const itemsIntoGroup = Math.max(0, slotsIntoGroup - headerSlots);
					viewportStartIndex = range.startIndex + Math.min(itemsIntoGroup, range.itemCount - 1);
					targetGroupIndex = i;
				}
				if (viewportEnd >= position && viewportEnd < groupEnd) {
					const posInGroup = viewportEnd - position;
					const slotsIntoGroup = Math.ceil(posInGroup / itemHeight);
					const itemsIntoGroup = Math.max(0, slotsIntoGroup - headerSlots);
					viewportEndIndex = range.startIndex + Math.min(itemsIntoGroup, range.itemCount - 1);
				}
				position += groupHeight;
			}
			if (viewportEnd >= knownPosition && viewportEndIndex === 0) {
				const endPositionBeyondKnown = viewportEnd - knownPosition;
				const endGroupsBeyondKnown = Math.ceil(endPositionBeyondKnown / avgGroupHeight);
				const endGroupIndex = lastKnownGroupIndex + 1 + endGroupsBeyondKnown;
				viewportEndIndex = Math.floor(endGroupIndex * avgItemsPerGroup);
				viewportEndIndex = Math.min(viewportEndIndex, total - 1);
			}
			return {
				viewportStartIndex,
				viewportEndIndex,
				targetGroupIndex
			};
		},
		/**
		* Calculates page start that centers the page around the viewport.
		*/
		_calculateCenteredPageStart: function(viewportStartIndex, viewportEndIndex, pageSize, total) {
			const viewportItemCount = viewportEndIndex - viewportStartIndex + 1;
			const bufferItems = Math.floor((pageSize - viewportItemCount) / 2);
			const maxPageStart = Math.max(0, total - pageSize);
			let pageStart = Math.max(0, viewportStartIndex - bufferItems);
			pageStart = Math.min(pageStart, maxPageStart);
			if (viewportEndIndex > pageStart + pageSize - 1) {
				pageStart = Math.min(maxPageStart, viewportEndIndex - pageSize + 1);
				pageStart = Math.max(0, pageStart);
			}
			return pageStart;
		},
		/**
		* Converts a scroll position to the required data page start.
		* Returns a pageStart that ensures the visible groups have data loaded.
		*/
		_scrollPositionToPageInfo: function(scrollTop) {
			const that = this;
			const itemHeight = that.options.itemHeight;
			const pageSize = that.itemCount;
			const initialRanges = that._initialGroupRanges;
			const screenHeight = that._screenHeight || 300;
			const total = that.dataSource.total();
			if (!initialRanges || !initialRanges.length) {
				const itemIndex = Math.floor(scrollTop / itemHeight);
				const maxPageStart = Math.max(0, total - pageSize);
				let pageStart = Math.max(0, itemIndex - Math.floor(pageSize / 2));
				pageStart = Math.min(pageStart, maxPageStart);
				return {
					pageStart,
					groupIndex: 0
				};
			}
			const rangeInfo = that._getKnownRangeInfo(initialRanges, itemHeight);
			const viewportEnd = scrollTop + screenHeight;
			let result;
			if (scrollTop >= rangeInfo.knownPosition) result = that._estimateViewportIndicesBeyondKnown(scrollTop, viewportEnd, rangeInfo, total);
			else result = that._findViewportIndicesInKnownRanges(scrollTop, viewportEnd, initialRanges, itemHeight, rangeInfo, total);
			let { viewportStartIndex, viewportEndIndex, targetGroupIndex } = result;
			if (viewportEndIndex === 0 || viewportEndIndex < viewportStartIndex) viewportEndIndex = Math.min(viewportStartIndex + Math.ceil(screenHeight / itemHeight), total - 1);
			return {
				pageStart: that._calculateCenteredPageStart(viewportStartIndex, viewportEndIndex, pageSize, total),
				groupIndex: targetGroupIndex
			};
		},
		/**
		* Updates the total group count and height container based on discovered groups.
		* Call this after buildGroupRanges to update height if new groups were discovered
		* or if we've reached the end of data and can determine the exact group count.
		*/
		_updateGroupCountAndHeight: function() {
			const that = this;
			const options = that.options;
			const dataSource = that.dataSource;
			if (!that._groupValueMap || options.type !== "group") return;
			const discoveredGroupCount = that._groupValueMap.size;
			const previousGroupCount = that._totalGroupCount || 0;
			const total = dataSource.total();
			const pageSize = that.itemCount;
			const isAtEnd = (dataSource.currentRangeStart() || 0) + pageSize >= total;
			const itemHeight = that.options.itemHeight;
			if (discoveredGroupCount > previousGroupCount || isAtEnd && discoveredGroupCount !== previousGroupCount) {
				that._totalGroupCount = discoveredGroupCount;
				const totalHeight = getGroupedTotalHeight(total, itemHeight, discoveredGroupCount, that._cssGap);
				that._setHeight(totalHeight);
			}
		},
		/**
		* Estimates which group index corresponds to a given data startIndex.
		*/
		_estimateFirstGroupIndex: function(startIndex) {
			const initialRanges = this._initialGroupRanges;
			const totalGroups = this._totalGroupCount || 1;
			const avgGroupSize = this.dataSource.total() / totalGroups;
			if (initialRanges && initialRanges.length) {
				const lastRange = initialRanges[initialRanges.length - 1];
				if (startIndex < lastRange.startIndex + lastRange.itemCount) for (let i = 0; i < initialRanges.length; i++) {
					const range = initialRanges[i];
					if (startIndex < range.startIndex + range.itemCount) return i;
				}
			}
			return Math.floor(startIndex / avgGroupSize);
		},
		/**
		* Builds placeholder data for the loading area.
		* Creates placeholder items to fill the visible viewport while data is loading.
		*/
		_buildPlaceholderPageData: function(pageStart, scrollTop) {
			const that = this;
			const itemHeight = that.options.itemHeight;
			const screenHeight = that._screenHeight || 400;
			const visibleItemCount = Math.ceil(screenHeight / itemHeight) + 2;
			const startIndex = Math.floor(scrollTop / itemHeight);
			const placeholderTop = startIndex * itemHeight;
			const items = [];
			for (let i = 0; i < visibleItemCount; i++) {
				const itemIndex = startIndex + i;
				items.push({
					item: null,
					group: "Loading...",
					newGroup: i === 0,
					selected: false,
					current: false,
					index: itemIndex,
					top: itemIndex * itemHeight,
					trueGlobalIndex: itemIndex,
					globalGroupIndex: -1,
					localIndex: i,
					viewIndex: 0,
					isPlaceholder: true
				});
			}
			return {
				pageStart,
				pageGroups: [{
					viewIndex: 0,
					groupValue: "Loading...",
					startIndex: 0,
					actualGlobalStartIndex: startIndex,
					globalGroupIndex: -1,
					itemCount: visibleItemCount,
					fullGroupItemCount: visibleItemCount,
					hasHeader: false,
					isPartialGroup: false,
					top: placeholderTop,
					height: visibleItemCount * itemHeight,
					itemsBeforeLoad: 0,
					isPlaceholder: true
				}],
				items,
				isPlaceholder: true
			};
		},
		/**
		* Builds group info objects for all groups in client-side grouping.
		*/
		_buildAllClientGroupInfos: function(view, itemHeight, cssGap) {
			const that = this;
			const allGroupInfos = [];
			let globalItemIndex = 0;
			let cumulativeHeight = 0;
			for (let groupIdx = 0; groupIdx < view.length; groupIdx++) {
				const viewGroup = view[groupIdx];
				const groupValue = viewGroup.value;
				const groupItems = viewGroup.items || [];
				const groupItemCount = groupItems.length;
				if (groupItemCount === 0) continue;
				const hasHeader = groupIdx > 0 || that.options.fixedGroupHeader === false;
				const groupTop = cumulativeHeight;
				const elementCount = groupItemCount + (hasHeader ? 1 : 0);
				const gapsInGroup = Math.max(0, elementCount - 1) * cssGap;
				const visualHeight = elementCount * itemHeight + gapsInGroup;
				const groupBottom = groupTop + visualHeight;
				allGroupInfos.push({
					viewIndex: groupIdx,
					groupValue,
					startIndex: globalItemIndex,
					actualGlobalStartIndex: globalItemIndex,
					globalGroupIndex: groupIdx,
					itemCount: groupItemCount,
					fullGroupItemCount: groupItemCount,
					hasHeader,
					isPartialGroup: false,
					top: groupTop,
					bottom: groupBottom,
					height: visualHeight,
					itemsBeforeLoad: 0,
					groupItems
				});
				cumulativeHeight += visualHeight;
				globalItemIndex += groupItemCount;
			}
			return allGroupInfos;
		},
		/**
		* Filters visible groups and builds mapped items for client-side grouping.
		*/
		_filterVisibleGroupsAndBuildItems: function(allGroupInfos, scrollTop, screenHeight, bufferSize) {
			const that = this;
			const visibleTop = Math.max(0, scrollTop - bufferSize);
			const visibleBottom = scrollTop + screenHeight + bufferSize;
			const pageGroups = [];
			const items = [];
			for (const groupInfo of allGroupInfos) {
				if (groupInfo.bottom >= visibleTop && groupInfo.top <= visibleBottom) {
					pageGroups.push(groupInfo);
					const groupItems = groupInfo.groupItems || [];
					for (let j = 0; j < groupInfo.itemCount; j++) {
						const dataItem = groupItems[j];
						if (!dataItem) continue;
						const itemIndex = groupInfo.startIndex + j;
						const mappedItem = that._itemMapper({
							item: dataItem,
							group: groupInfo.groupValue
						}, itemIndex, that._values.slice());
						mappedItem.trueGlobalIndex = itemIndex;
						mappedItem.globalGroupIndex = groupInfo.globalGroupIndex;
						mappedItem.localIndex = j;
						mappedItem.viewIndex = groupInfo.viewIndex;
						items.push(mappedItem);
					}
				}
				delete groupInfo.groupItems;
			}
			return {
				pageGroups,
				items
			};
		},
		/**
		* Builds page data for client-side grouping.
		*/
		_buildClientGroupedPageData: function(pageStart, scrollTop) {
			const that = this;
			const dataSource = that.dataSource;
			const itemHeight = that.options.itemHeight;
			const cssGap = that._cssGap || 0;
			const groupDescriptor = dataSource.group();
			const allData = dataSource.data();
			if (!allData || !allData.length || !groupDescriptor || !groupDescriptor.length) return {
				pageStart,
				pageGroups: [],
				items: []
			};
			const view = new kendo.data.Query(allData).group(groupDescriptor).data;
			if (!view || !view.length) return {
				pageStart,
				pageGroups: [],
				items: []
			};
			const screenHeight = that._screenHeight || 400;
			const bufferSize = screenHeight;
			const allGroupInfos = that._buildAllClientGroupInfos(view, itemHeight, cssGap);
			const { pageGroups, items } = that._filterVisibleGroupsAndBuildItems(allGroupInfos, scrollTop, screenHeight, bufferSize);
			that._updateFixedHeaderClientGrouped(scrollTop, pageGroups, itemHeight);
			return {
				pageStart,
				pageGroups,
				items
			};
		},
		/**
		* Calculates the header offset for a section based on previous sections.
		*/
		_calculateSectionHeaderOffset: function(viewIdx, groupRanges, view, pageStart, itemHeight) {
			const that = this;
			let headerOffset = 0;
			for (let i = 0; i < viewIdx; i++) {
				const prevRange = groupRanges[i];
				const prevViewGroup = view[i];
				if (prevRange && prevViewGroup) {
					const prevItemsBeforeLoad = Math.max(prevRange.startIndex, pageStart) - prevRange.startIndex;
					if ((i > 0 || that.options.fixedGroupHeader === false) && prevItemsBeforeLoad === 0) headerOffset += itemHeight;
				}
			}
			return headerOffset;
		},
		/**
		* Builds a single server group section info and its items.
		*/
		_buildServerGroupSection: function(viewIdx, viewGroup, groupRange, pageStart, pageEnd, itemHeight, headerOffset) {
			const that = this;
			const groupStartIndex = groupRange.startIndex;
			const groupEndIndex = groupRange.startIndex + groupRange.itemCount;
			if (groupEndIndex <= pageStart || groupStartIndex >= pageEnd) return null;
			const firstItemInPage = Math.max(groupStartIndex, pageStart);
			const itemsInPage = Math.min(groupEndIndex, pageEnd) - firstItemInPage;
			const itemOffset = firstItemInPage - groupStartIndex;
			const groupItems = (viewGroup.items || []).slice(itemOffset, itemOffset + itemsInPage);
			const groupItemCount = groupItems.length;
			if (groupItemCount === 0) return null;
			const globalStartIndex = firstItemInPage;
			const globalGroupIndex = groupRange.globalGroupIndex;
			const sectionTop = globalStartIndex * itemHeight;
			const itemsBeforeLoad = globalStartIndex - groupRange.startIndex;
			const isPartialGroup = itemsBeforeLoad > 0;
			const hasHeader = (viewIdx > 0 || that.options.fixedGroupHeader === false) && !isPartialGroup;
			const groupTop = sectionTop + headerOffset;
			const groupHeight = (hasHeader ? itemHeight : 0) + groupItemCount * itemHeight;
			const sectionItems = [];
			for (let j = 0; j < groupItemCount; j++) {
				const dataItem = groupItems[j];
				if (!dataItem) continue;
				const globalIndex = globalStartIndex + j;
				const mappedItem = that._itemMapper({
					item: dataItem,
					group: viewGroup.value
				}, globalIndex, that._values.slice());
				mappedItem.viewIndex = viewIdx;
				mappedItem.globalGroupIndex = globalGroupIndex;
				mappedItem.localIndex = j;
				sectionItems.push(mappedItem);
			}
			return {
				groupInfo: {
					viewIndex: viewIdx,
					groupValue: viewGroup.value,
					startIndex: globalStartIndex,
					actualGlobalStartIndex: groupRange.startIndex,
					globalGroupIndex,
					sectionIndex: viewIdx,
					itemCount: groupItemCount,
					fullGroupItemCount: groupRange.itemCount,
					hasHeader,
					isPartialGroup,
					top: groupTop,
					height: groupHeight,
					itemsBeforeLoad
				},
				sectionItems
			};
		},
		/**
		* Builds page data for server-side grouping.
		*/
		_buildServerGroupedPageData: function(pageStart, scrollTop) {
			const that = this;
			const view = that.dataSource.view();
			const groupRanges = that._groupRanges || [];
			const itemHeight = that.options.itemHeight;
			const pageEnd = pageStart + that.itemCount;
			const pageGroups = [];
			const items = [];
			for (let viewIdx = 0; viewIdx < view.length; viewIdx++) {
				const viewGroup = view[viewIdx];
				const groupRange = groupRanges[viewIdx];
				if (!groupRange) continue;
				const headerOffset = that._calculateSectionHeaderOffset(viewIdx, groupRanges, view, pageStart, itemHeight);
				const result = that._buildServerGroupSection(viewIdx, viewGroup, groupRange, pageStart, pageEnd, itemHeight, headerOffset);
				if (result) {
					pageGroups.push(result.groupInfo);
					items.push(...result.sectionItems);
				}
			}
			that._updateFixedHeader(scrollTop, pageGroups);
			return {
				pageStart,
				pageGroups,
				items
			};
		},
		/**
		* Builds the data structure for the current page.
		* Delegates to client or server grouping handlers.
		*/
		_buildGroupedPageData: function(pageStart, scrollTop) {
			const that = this;
			if (!that.dataSource.options.serverGrouping) return that._buildClientGroupedPageData(pageStart, scrollTop);
			return that._buildServerGroupedPageData(pageStart, scrollTop);
		},
		/**
		* Calculates the absolute pixel position for a group's UL.
		* Position = (items before * itemHeight) + (headers before * itemHeight)
		*/
		_calculateGroupTopPosition: function(startIndex, globalGroupIndex, itemHeight) {
			const headersBefore = Math.max(0, globalGroupIndex - 1);
			return startIndex * itemHeight + headersBefore * itemHeight;
		},
		/**
		* Updates the sticky header based on current scroll position.
		*/
		_updateFixedHeader: function(scrollTop, pageGroups) {
			const that = this;
			if (!pageGroups.length) return;
			let currentGroup = null;
			for (let i = pageGroups.length - 1; i >= 0; i--) {
				const group = pageGroups[i];
				if (scrollTop >= group.top) {
					currentGroup = group;
					break;
				}
			}
			if (!currentGroup) currentGroup = pageGroups[0];
			if (that.currentVisibleGroup !== currentGroup.groupValue) {
				that.header.html(that.templates.fixedGroupTemplate(currentGroup.groupValue));
				that.currentVisibleGroup = currentGroup.groupValue;
				that._currentVisibleGroupIndex = currentGroup.globalGroupIndex;
			}
		},
		/**
		* Updates the sticky header for client-side grouped lists based on scroll position.
		* Uses the pageGroups array to find which group is currently visible.
		* Also hides inline group headers that match the sticky header to avoid duplication.
		*/
		_updateFixedHeaderClientGrouped: function(scrollTop, pageGroups, itemHeight) {
			const that = this;
			if (!pageGroups.length) return;
			let currentGroup = null;
			for (let i = pageGroups.length - 1; i >= 0; i--) {
				const group = pageGroups[i];
				if (scrollTop >= group.top) {
					currentGroup = group;
					break;
				}
			}
			if (!currentGroup) currentGroup = pageGroups[0];
			if (that.currentVisibleGroup !== currentGroup.groupValue) {
				that.header.html(that.templates.fixedGroupTemplate(currentGroup.groupValue));
				that.currentVisibleGroup = currentGroup.groupValue;
				that._currentVisibleGroupIndex = currentGroup.globalGroupIndex;
			}
		},
		_removeOrphanUls: function(content) {
			const that = this;
			const trackedUlSet = new Set(that._groupUls);
			const domUls = content.querySelectorAll("ul[data-group-index]");
			for (const domUl of domUls) {
				if (parseInt(domUl.getAttribute("data-group-index"), 10) === -1 || trackedUlSet.has(domUl)) continue;
				if (domUl.parentNode) domUl.parentNode.removeChild(domUl);
			}
		},
		_buildExistingUlMap: function() {
			const that = this;
			const existingMap = /* @__PURE__ */ new Map();
			const cleanedGroupUls = [];
			for (const ul of that._groupUls) {
				const groupIndex = parseInt(ul.getAttribute("data-group-index"), 10);
				const sectionIndex = parseInt(ul.getAttribute("data-section-index") || "0", 10);
				const groupKey = groupIndex + "_" + sectionIndex;
				if (!isNaN(groupIndex)) if (existingMap.has(groupKey)) {
					if (ul.parentNode) ul.parentNode.removeChild(ul);
				} else {
					existingMap.set(groupKey, ul);
					cleanedGroupUls.push(ul);
				}
			}
			that._groupUls = cleanedGroupUls;
			return existingMap;
		},
		_processExistingUl: function(existingUl, groupInfo, listItems, force, keptUls, newUls, existingMap) {
			const that = this;
			const groupIndex = groupInfo.globalGroupIndex;
			const sectionIndex = groupInfo.sectionIndex !== undefined ? groupInfo.sectionIndex : 0;
			const groupKey = groupIndex + "_" + sectionIndex;
			const headerSelector = that._isTableVariant() ? ".k-table-group-row" : ".k-list-group-item";
			if (!!existingUl.querySelector(headerSelector) !== groupInfo.hasHeader) {
				if (existingUl.parentNode) existingUl.parentNode.removeChild(existingUl);
				existingMap.delete(groupKey);
				const newUl = that._createGroupUlElement(groupInfo, listItems);
				if (sectionIndex > 0) newUl.setAttribute("data-section-index", sectionIndex);
				newUls.push({
					ul: newUl,
					groupIndex,
					sectionIndex,
					groupKey
				});
				return;
			}
			const cachedStartIndex = parseInt(existingUl.getAttribute("data-start-index"), 10);
			const cachedItemCount = existingUl.querySelectorAll("li[data-offset-index]").length;
			const startIndexChanged = cachedStartIndex !== groupInfo.startIndex;
			const itemCountChanged = cachedItemCount !== groupInfo.itemCount;
			if (startIndexChanged || itemCountChanged || force) {
				that._updateGroupUl(existingUl, groupInfo, listItems);
				if (startIndexChanged) existingUl.setAttribute("data-start-index", groupInfo.startIndex);
			}
			if (existingUl._cachedTop !== groupInfo.top) {
				position(existingUl, groupInfo.top);
				existingUl._cachedTop = groupInfo.top;
			}
			keptUls.push(existingUl);
			existingMap.delete(groupKey);
			const items = existingUl.querySelectorAll("li[data-offset-index]");
			that._items.push(...Array.from(items));
		},
		_insertNewUlsInOrder: function(content, newUls) {
			for (const { ul: newUl, sectionIndex: newSectionIndex } of newUls) {
				let insertBefore = null;
				const existingUls = content.querySelectorAll("ul[data-group-index]");
				for (const existingUl of existingUls) if (parseInt(existingUl.getAttribute("data-section-index") || "0", 10) > newSectionIndex) {
					insertBefore = existingUl;
					break;
				}
				if (insertBefore) content.insertBefore(newUl, insertBefore);
				else content.appendChild(newUl);
			}
		},
		_renderGroupedPage: function(list, force, targetScrollTop) {
			const that = this;
			const content = that.content.get(0);
			const pageGroups = list.pageGroups || [];
			const pageStart = list.pageStart;
			const scrollTop = typeof targetScrollTop === "number" ? targetScrollTop : content.scrollTop;
			if (list.isPlaceholder) {
				that._renderPlaceholderPage(list, scrollTop);
				return;
			}
			if (that._placeholderUl && that._placeholderUl.parentNode) {
				that._placeholderUl.parentNode.removeChild(that._placeholderUl);
				that._placeholderUl = null;
			}
			that._lastRenderScrollTop = scrollTop;
			that._removeOrphanUls(content);
			const existingMap = that._buildExistingUlMap();
			const keptUls = [];
			const newUls = [];
			that._items = [];
			for (const groupInfo of pageGroups) {
				const groupIndex = groupInfo.globalGroupIndex;
				const sectionIndex = groupInfo.sectionIndex !== undefined ? groupInfo.sectionIndex : 0;
				const groupKey = groupIndex + "_" + sectionIndex;
				const existingUl = existingMap.get(groupKey);
				if (existingUl) that._processExistingUl(existingUl, groupInfo, list.items, force, keptUls, newUls, existingMap);
				else {
					const newUl = that._createGroupUlElement(groupInfo, list.items);
					if (sectionIndex > 0) newUl.setAttribute("data-section-index", sectionIndex);
					newUls.push({
						ul: newUl,
						groupIndex,
						sectionIndex,
						groupKey
					});
				}
			}
			for (const ul of existingMap.values()) if (ul.parentNode) ul.parentNode.removeChild(ul);
			that._insertNewUlsInOrder(content, newUls);
			that._groupUls = keptUls.concat(newUls.map((n) => n.ul));
			that._renderedPageStart = pageStart;
			content.scrollTop = scrollTop;
			that._dataView = list.items;
		},
		_calculateCoveredRange: function(visibleTop, visibleBottom, screenHeight) {
			const that = this;
			let coveredTop = Infinity;
			let coveredBottom = 0;
			for (const ul of that._groupUls) {
				if (!ul || !ul.parentNode) continue;
				const yMatch = ul.style.transform.match(/translateY\((\d+)px\)/);
				const ulTop = yMatch ? parseFloat(yMatch[1]) : 0;
				const ulBottom = ulTop + ul.offsetHeight;
				if (ulBottom >= visibleTop - screenHeight && ulTop <= visibleBottom + screenHeight) {
					coveredTop = Math.min(coveredTop, ulTop);
					coveredBottom = Math.max(coveredBottom, ulBottom);
				}
			}
			return {
				coveredTop,
				coveredBottom
			};
		},
		_calculatePlaceholderPosition: function(visibleTop, visibleBottom, coveredTop, coveredBottom, screenHeight, totalContentHeight) {
			let placeholderTop = 0;
			let placeholderHeight = 0;
			if (coveredTop === Infinity) {
				placeholderTop = Math.max(0, visibleTop);
				placeholderHeight = screenHeight;
			} else if (visibleTop < coveredTop) {
				placeholderTop = Math.max(0, visibleTop);
				placeholderHeight = coveredTop - placeholderTop;
			} else if (visibleBottom > coveredBottom && coveredBottom < totalContentHeight) {
				placeholderTop = coveredBottom;
				placeholderHeight = Math.min(visibleBottom, totalContentHeight) - coveredBottom;
			}
			return {
				placeholderTop,
				placeholderHeight
			};
		},
		_ensurePlaceholderUl: function(content) {
			const that = this;
			if (!that._placeholderUl) {
				const ul = document.createElement("ul");
				ul.className = (that._isTableVariant() ? `${LIST_UL} ${TABLE}` : LIST_UL) + " k-loading-placeholder";
				ul.style.position = "absolute";
				ul.style.width = "100%";
				ul.style.top = "0";
				ul.style.left = "0";
				ul.setAttribute("role", "group");
				ul.setAttribute("data-group-index", "-1");
				if (that.options.ariaLive) ul.setAttribute("aria-live", that.options.ariaLive);
				that._placeholderUl = ul;
				content.appendChild(ul);
			}
			return that._placeholderUl;
		},
		_updatePlaceholderItems: function(ul, placeholderItemCount) {
			const that = this;
			const templates = that.templates;
			let currentCount = ul.querySelectorAll("li.k-list-item").length;
			while (currentCount < placeholderItemCount) {
				const li = that._createItemLi();
				li.classList.add("k-loading-item");
				const textSpan = li.querySelector(".k-list-item-text");
				if (textSpan) textSpan.innerHTML = templates.placeholderTemplate({});
				ul.appendChild(li);
				currentCount++;
			}
			while (ul.children.length > placeholderItemCount) ul.removeChild(ul.lastChild);
		},
		_renderPlaceholderPage: function(list, scrollTop) {
			const that = this;
			const content = that.content.get(0);
			const itemHeight = that.options.itemHeight;
			const screenHeight = that._screenHeight || 400;
			const totalContentHeight = getGroupedTotalHeight(that.dataSource.total(), itemHeight, that._totalGroupCount || 1, that._cssGap);
			const visibleTop = scrollTop;
			const visibleBottom = Math.min(scrollTop + screenHeight, totalContentHeight);
			const { coveredTop, coveredBottom } = that._calculateCoveredRange(visibleTop, visibleBottom, screenHeight);
			const { placeholderTop, placeholderHeight } = that._calculatePlaceholderPosition(visibleTop, visibleBottom, coveredTop, coveredBottom, screenHeight, totalContentHeight);
			if (placeholderHeight <= 0) {
				if (that._placeholderUl && that._placeholderUl.parentNode) that._placeholderUl.style.display = "none";
				return;
			}
			const placeholderItemCount = Math.ceil(placeholderHeight / itemHeight) + 1;
			const ul = that._ensurePlaceholderUl(content);
			ul.style.display = "";
			position(ul, placeholderTop);
			that._updatePlaceholderItems(ul, placeholderItemCount);
			that.header.html(that.templates.fixedGroupTemplate("Loading..."));
		},
		/**
		* Creates a group UL element with all its items (does not add to DOM).
		*/
		_createGroupUlElement: function(groupInfo, allItems) {
			const that = this;
			const templates = that.templates;
			let groupItems;
			if (groupInfo.sectionIndex !== undefined) groupItems = allItems.filter((item) => item.viewIndex === groupInfo.sectionIndex);
			else groupItems = allItems.filter((item) => item.globalGroupIndex === groupInfo.globalGroupIndex);
			const firstDataItem = groupItems.length > 0 ? groupItems[0].item : null;
			const groupContainer = that._createGroupUl(groupInfo.hasHeader, groupInfo.groupValue, groupInfo.globalGroupIndex, firstDataItem);
			if (groupContainer.header) {
				const headerText = groupContainer.header.querySelector(that._getHeaderTextSelector());
				if (headerText) headerText.innerHTML = templates.groupTemplate(groupInfo.groupValue);
			}
			that._addGroupItems(groupContainer, groupItems.length);
			for (let i = 0; i < groupItems.length; i++) that._renderGroupItem(groupContainer.items[i], groupItems[i], templates);
			position(groupContainer.ul, groupInfo.top);
			groupContainer.ul.setAttribute("data-group-index", groupInfo.globalGroupIndex);
			groupContainer.ul.setAttribute("data-start-index", groupInfo.startIndex);
			groupContainer.ul._cachedTop = groupInfo.top;
			that._items.push(...groupContainer.items);
			return groupContainer.ul;
		},
		/**
		* Updates an existing group UL with new items.
		*/
		_updateGroupUl: function(ul, groupInfo, allItems) {
			const that = this;
			const templates = that.templates;
			const globalGroupIndex = groupInfo.globalGroupIndex;
			let groupItems;
			if (groupInfo.sectionIndex !== undefined) groupItems = allItems.filter((item) => item.viewIndex === groupInfo.sectionIndex);
			else groupItems = allItems.filter((item) => item.globalGroupIndex === globalGroupIndex);
			const existingItems = Array.from(ul.querySelectorAll("li[data-offset-index]"));
			while (existingItems.length > groupItems.length) {
				const li = existingItems.pop();
				if (li && li.parentNode) li.parentNode.removeChild(li);
			}
			for (let i = 0; i < groupItems.length; i++) {
				const itemData = groupItems[i];
				let li = existingItems[i];
				if (!li) {
					li = that._createItemLi();
					ul.appendChild(li);
				}
				that._renderGroupItem(li, itemData, templates);
			}
		},
		_createCheckboxWrap: function() {
			const checkboxWrap = document.createElement("span");
			checkboxWrap.className = "k-checkbox-wrap";
			checkboxWrap.style.pointerEvents = "none";
			const checkbox = document.createElement("input");
			checkbox.className = "k-checkbox";
			checkbox.type = "checkbox";
			checkbox.setAttribute("tabindex", "-1");
			checkbox.setAttribute("aria-hidden", "true");
			checkboxWrap.appendChild(checkbox);
			return checkboxWrap;
		},
		/**
		* Creates a single item LI element.
		*/
		_createItemLi: function() {
			const that = this;
			const itemHeight = that._getItemHeightStyle();
			const hasColumns = that._isTableVariant();
			const itemClass = that._getItemClass();
			const li = document.createElement("li");
			li.setAttribute("tabindex", "-1");
			li.setAttribute("role", "option");
			li.className = itemClass;
			li.style.height = itemHeight;
			li.style.minHeight = itemHeight;
			li.style.position = "relative";
			li.style.transform = "none";
			if (!hasColumns) {
				if (that.options.checkboxes) li.appendChild(that._createCheckboxWrap());
				const textSpan = document.createElement("span");
				textSpan.className = "k-list-item-text";
				li.appendChild(textSpan);
			}
			return li;
		},
		/**
		* Removes all group ULs from the DOM.
		*/
		_clearAllGroupUls: function() {
			const that = this;
			for (const ul of that._groupUls) if (ul && ul.parentNode) ul.parentNode.removeChild(ul);
			that._groupUls = [];
			that._items = [];
		},
		/**
		* Creates and renders a single group UL with all its items.
		*/
		_createAndRenderGroupUl: function(groupInfo, allItems) {
			const that = this;
			const content = that.content.get(0);
			const templates = that.templates;
			const groupItems = allItems.filter((item) => item.globalGroupIndex === groupInfo.globalGroupIndex);
			const groupContainer = that._createGroupUl(groupInfo.hasHeader, groupInfo.groupValue, groupInfo.globalGroupIndex);
			if (groupContainer.header) {
				const headerText = groupContainer.header.querySelector(that._getHeaderTextSelector());
				if (headerText) headerText.innerHTML = templates.groupTemplate(groupInfo.groupValue);
			}
			that._addGroupItems(groupContainer, groupItems.length);
			for (let i = 0; i < groupItems.length; i++) that._renderGroupItem(groupContainer.items[i], groupItems[i], templates);
			position(groupContainer.ul, groupInfo.top);
			groupContainer.ul.setAttribute("data-group-index", groupInfo.globalGroupIndex);
			groupContainer.ul.setAttribute("data-start-index", groupInfo.startIndex);
			groupContainer.ul._cachedTop = groupInfo.top;
			content.appendChild(groupContainer.ul);
			that._groupUls.push(groupContainer.ul);
			that._items.push(...groupContainer.items);
		},
		/**
		* PAGE-BASED: Renders or updates a single group UL.
		* Creates new UL if not cached, or updates existing one.
		*/
		_renderGroupUlPaged: function(groupInfo, allItems) {
			const that = this;
			const content = that.content.get(0);
			const templates = that.templates;
			const viewIndex = groupInfo.viewIndex;
			const groupItems = allItems.filter((item) => item.viewIndex === viewIndex);
			const cached = that._groupUlCache[viewIndex];
			if (!cached) {
				const groupContainer = that._createGroupUl(groupInfo.hasHeader, groupInfo.groupValue, groupInfo.viewIndex);
				if (groupContainer.header) {
					const headerText = groupContainer.header.querySelector(that._getHeaderTextSelector());
					if (headerText) headerText.innerHTML = templates.groupTemplate(groupInfo.groupValue);
				}
				that._addGroupItems(groupContainer, groupItems.length);
				for (let i = 0; i < groupItems.length; i++) that._renderGroupItem(groupContainer.items[i], groupItems[i], templates);
				position(groupContainer.ul, groupInfo.top);
				groupContainer.ul.setAttribute("data-view-index", viewIndex);
				groupContainer.ul.setAttribute("data-start-index", groupInfo.startIndex);
				groupContainer.startIndex = groupInfo.startIndex;
				groupContainer.itemCount = groupItems.length;
				groupContainer.top = groupInfo.top;
				groupContainer.viewIndex = viewIndex;
				content.appendChild(groupContainer.ul);
				that._groupUlCache[viewIndex] = groupContainer;
				that._items.push(...groupContainer.items);
			} else {
				const newTop = groupInfo.top;
				position(cached.ul, newTop);
				cached.top = newTop;
				for (let k = 0; k < groupItems.length; k++) {
					const li = cached.items[k];
					if (li) {
						const itemData = groupItems[k];
						that._renderGroupItem(li, itemData, templates);
						li.style.display = "";
					}
				}
			}
		},
		/**
		* Reorders group ULs in the DOM to match the expected order.
		*/
		_reorderGroupUls: function(content, pageGroups) {
			const currentUls = Array.from(content.querySelectorAll(".k-list-ul"));
			let needsReorder = false;
			let lastIndex = -1;
			for (const ul of currentUls) {
				const idx = parseInt(ul.getAttribute("data-view-index"), 10);
				if (idx < lastIndex) {
					needsReorder = true;
					break;
				}
				lastIndex = idx;
			}
			if (needsReorder) {
				const sortedUls = currentUls.sort((a, b) => parseInt(a.getAttribute("data-view-index"), 10) - parseInt(b.getAttribute("data-view-index"), 10));
				for (const ul of sortedUls) content.appendChild(ul);
			}
		},
		/**
		* Finds the first visible item based on scroll position
		*/
		_findFirstVisibleItem: function(items, scrollTop, pageGroups) {
			const itemHeight = this.options.itemHeight;
			if (pageGroups) for (let i = 0; i < pageGroups.length; i++) {
				const group = pageGroups[i];
				if (scrollTop < group.top + (group.hasHeader ? itemHeight : 0) + group.itemCount * itemHeight) return items.filter((item) => item.viewIndex === group.viewIndex)[0] || items[0];
			}
			return items[0];
		},
		/**
		* Renders a single item in a group
		*/
		_renderGroupItem: function(element, data, templates) {
			const that = this;
			const options = that.options;
			const $element = $(element);
			const itemTemplate = data.item ? templates.template : templates.placeholderTemplate;
			const hasColumns = options.columns?.length;
			const dataItem = data.item;
			$element.attr("data-uid", dataItem ? dataItem.uid : "").attr("data-offset-index", data.index);
			if (hasColumns && dataItem) {
				const renderedColumns = $(renderColumns(options, dataItem, templates));
				kendo.applyStylesFromKendoAttributes(renderedColumns, ["width", "max-width"]);
				$element.empty().append(renderedColumns);
				const altRow = data.index % 2 === 1;
				$element.toggleClass("k-table-alt-row", altRow);
			} else if (hasColumns && !dataItem) {
				let placeholderHtml = "";
				for (let i = 0; i < options.columns.length; i++) {
					const currentWidth = options.columns[i].width;
					const currentWidthInt = parseInt(currentWidth, 10);
					let widthStyle = "";
					if (currentWidth) {
						const widthValue = currentWidthInt + "px";
						widthStyle = `${kendo.attr("style-width")}="${widthValue}" ${kendo.attr("style-max-width")}="${widthValue}"`;
					}
					placeholderHtml += `<span class='k-table-td' ${widthStyle}></span>`;
				}
				const renderedPlaceholder = $(placeholderHtml);
				kendo.applyStylesFromKendoAttributes(renderedPlaceholder, ["width", "max-width"]);
				$element.empty().append(renderedPlaceholder);
			} else $element.find(".k-list-item-text").html(itemTemplate(dataItem || {}));
			$element.toggleClass(FOCUSED, data.current);
			$element.toggleClass(SELECTED, !that.options.checkboxes && data.selected);
			$element.attr("aria-selected", data.selected);
			if (that.options.checkboxes) $element.find("input.k-checkbox").toggleClass("k-checked", data.selected).prop("checked", data.selected);
			$element.toggleClass("k-loading-item", !dataItem);
			element.style.display = "";
		},
		/**
		* Creates a grouped data getter
		*/
		_groupedGetter: function(callback) {
			const that = this;
			const dataSource = that.dataSource;
			const pageSize = that.itemCount;
			let lastRequestedRange = null;
			let lastRangeStart = dataSource.skip();
			if (dataSource.pageSize() < pageSize) that.mute(() => {
				dataSource.pageSize(pageSize);
			});
			return function(index, rangeStart) {
				if (!dataSource.inRange(rangeStart, pageSize)) {
					if (lastRequestedRange !== rangeStart) {
						lastRequestedRange = rangeStart;
						lastRangeStart = rangeStart;
						if (that._getterDeferred) that._getterDeferred.reject();
						that._getterDeferred = that.deferredRange(rangeStart);
						that._getterDeferred.then(() => {
							that._getterDeferred = null;
							const firstGroupIdx = that._estimateFirstGroupIndex(rangeStart);
							that._groupRanges = buildGroupRanges(dataSource, rangeStart, firstGroupIdx, that._groupValueMap);
							that._updateGroupCountAndHeight();
							callback();
						});
					}
					return null;
				} else {
					if (lastRangeStart !== rangeStart) {
						that.mute(() => {
							dataSource.range(rangeStart, pageSize);
							lastRangeStart = rangeStart;
						});
						const firstGroupIdx = that._estimateFirstGroupIndex(rangeStart);
						that._groupRanges = buildGroupRanges(dataSource, rangeStart, firstGroupIdx, that._groupValueMap);
						that._updateGroupCountAndHeight();
					}
					const view = dataSource.view();
					const flatIndex = index - rangeStart;
					let currentIndex = 0;
					for (const group of view) {
						const groupItems = group.items || [];
						if (flatIndex < currentIndex + groupItems.length) return {
							item: groupItems[flatIndex - currentIndex],
							group: group.value
						};
						currentIndex += groupItems.length;
					}
					return null;
				}
			};
		},
		_setHeight: function(height) {
			let heightContainer = this.heightContainer;
			let currentHeight;
			if (!heightContainer) heightContainer = this.heightContainer = appendChild(this.content[0], HEIGHTCONTAINER);
			else currentHeight = heightContainer.offsetHeight;
			if (height !== currentHeight) {
				heightContainer.innerHTML = "";
				while (height > 0) {
					const padHeight = Math.min(height, 25e4);
					appendChild(heightContainer).style.height = padHeight + "px";
					height -= padHeight;
				}
			}
		},
		_getter: function() {
			const dataSource = this.dataSource;
			const type = this.options.type;
			const pageSize = this.itemCount;
			const flatGroups = {};
			let lastRequestedRange = null;
			let lastRangeStart = dataSource.skip();
			if (dataSource.pageSize() < pageSize) this.mute(function() {
				dataSource.pageSize(pageSize);
			});
			return function(index, rangeStart) {
				const that = this;
				if (!dataSource.inRange(rangeStart, pageSize)) {
					if (lastRequestedRange !== rangeStart) {
						lastRequestedRange = rangeStart;
						lastRangeStart = rangeStart;
						if (that._getterDeferred) that._getterDeferred.reject();
						that._getterDeferred = that.deferredRange(rangeStart);
						that._getterDeferred.then(() => {
							const firstItemIndex = that._indexConstraint(that.content[0].scrollTop);
							that._getterDeferred = null;
							if (rangeStart <= firstItemIndex && firstItemIndex <= rangeStart + pageSize) {
								that._fetching = true;
								dataSource.range(rangeStart, pageSize);
							}
						});
					}
					return null;
				} else {
					if (lastRangeStart !== rangeStart) this.mute(() => {
						dataSource.range(rangeStart, pageSize);
						lastRangeStart = rangeStart;
					});
					let result;
					if (type === "group") {
						if (!flatGroups[rangeStart]) {
							const flatGroup = flatGroups[rangeStart] = [];
							const groups = dataSource.view();
							for (const group of groups) for (const item of group.items) flatGroup.push({
								item,
								group: group.value
							});
						}
						result = flatGroups[rangeStart][index - rangeStart];
					} else result = dataSource.view()[index - rangeStart];
					return result;
				}
			};
		},
		_fixedHeader: function(scrollTop, list) {
			const group = this.currentVisibleGroup;
			const itemHeight = this.options.itemHeight;
			const firstVisibleDataItem = (list.items || [])[Math.floor((scrollTop - list.top) / itemHeight)];
			if (firstVisibleDataItem?.item) {
				const firstVisibleGroup = firstVisibleDataItem.group;
				if (firstVisibleGroup !== group) {
					const fixedGroupText = firstVisibleGroup || "";
					this.header.html(this.templates.fixedGroupTemplate(fixedGroupText));
					this.currentVisibleGroup = firstVisibleGroup;
				}
			}
			return list;
		},
		_itemMapper: function(item, index, value) {
			const listType = this.options.type;
			const itemHeight = this.options.itemHeight;
			const cssGap = this._cssGap || 0;
			const currentIndex = this._focusedIndex;
			const valueGetter = this._valueGetter;
			let selected = false;
			let current = false;
			let newGroup = false;
			let group = null;
			if (listType === "group") {
				if (item) {
					newGroup = index === 0 || this._currentGroup !== false && this._currentGroup !== item.group;
					this._currentGroup = item.group;
				}
				group = item ? item.group : null;
				item = item ? item.item : null;
			}
			if (this.options.mapValueTo === "dataItem" && this._selectedDataItems.length && item) {
				for (let i = 0; i < this._selectedDataItems.length; i++) if (valueGetter(this._selectedDataItems[i]) === valueGetter(item)) {
					selected = true;
					break;
				}
			} else if (!this.isFiltered() && value.length && item) {
				for (let j = 0; j < value.length; j++) if (isPrimitive(item) ? value[j] === item : value[j] === valueGetter(item)) {
					value.splice(j, 1);
					selected = true;
					break;
				}
			}
			if (currentIndex === index) current = true;
			return {
				item: item ? item : null,
				group,
				newGroup,
				selected,
				current,
				index,
				top: index * (itemHeight + cssGap)
			};
		},
		_range: function(index) {
			const itemCount = this.itemCount;
			const value = this._values.slice();
			const items = [];
			this._view = {};
			this._currentGroup = false;
			for (let i = index, length = index + itemCount; i < length; i++) {
				const item = this._itemMapper(this.getter(i, index), i, value);
				if (items[items.length - 1]) items[items.length - 1].isLastGroupedItem = item.newGroup;
				items.push(item);
				this._view[item.index] = item;
			}
			this._dataView = items;
			return items;
		},
		_getDataItemsCollection: function(scrollTop, lastScrollTop) {
			const items = this._range(this._listIndex(scrollTop, lastScrollTop));
			return {
				index: items.length ? items[0].index : 0,
				top: items.length ? items[0].top : 0,
				items
			};
		},
		_listItems: function() {
			const screenHeight = this._screenHeight;
			const options = this.options;
			const theValidator = listValidator(options, screenHeight);
			return function(value, force) {
				let result = this.result;
				const lastScrollTop = this._lastScrollTop;
				if (force || !result || !theValidator(result, value, lastScrollTop)) result = this._getDataItemsCollection(value, lastScrollTop);
				this._lastScrollTop = value;
				this.result = result;
				return result;
			}.bind(this);
		},
		_whenChanged: function(getter, callback) {
			let current;
			return (force) => {
				const theNew = getter(force);
				if (theNew !== current) {
					current = theNew;
					callback(theNew, force);
				}
			};
		},
		/**
		* Smart change detection for grouped mode.
		* Only triggers callback when the page content actually changes.
		*/
		_whenChangedGrouped: function(getter, callback) {
			let currentPageStart = null;
			let currentGroupKeys = null;
			return (force) => {
				const theNew = getter(force);
				if (!theNew) return;
				const newPageStart = theNew.pageStart;
				const newGroupKeys = (theNew.pageGroups || []).map((g) => `${g.globalGroupIndex}:${g.startIndex}:${g.itemCount}`).join(",");
				if (force || currentPageStart !== newPageStart || currentGroupKeys !== newGroupKeys) {
					currentPageStart = newPageStart;
					currentGroupKeys = newGroupKeys;
					callback(theNew, force);
				}
			};
		},
		_reorderList: function(list, reorder) {
			const that = this;
			const length = list.length;
			let currentOffset = -Infinity;
			reorder = map2(reorder, this.templates).bind(this);
			return function(list2, offset, force) {
				const diff = offset - currentOffset;
				let range, range2;
				if (force || Math.abs(diff) >= length) {
					range = list;
					range2 = list2;
				} else {
					range = reshift(list, diff);
					range2 = diff > 0 ? list2.slice(-diff) : list2.slice(0, -diff);
				}
				reorder(range, range2, that.bound());
				currentOffset = offset;
			};
		},
		_bufferSizes: function() {
			const options = this.options;
			return bufferSizes(this._screenHeight, options.listScreens, options.oppositeBuffer);
		},
		_indexConstraint: function(position) {
			const itemCount = this.itemCount;
			const effectiveItemHeight = this.options.itemHeight + (this._cssGap || 0);
			const total = this.dataSource.total();
			return Math.min(Math.max(total - itemCount, 0), Math.max(0, Math.floor(position / effectiveItemHeight)));
		},
		_listIndex: function(scrollTop, lastScrollTop) {
			const buffers = this._bufferSizes();
			const position = scrollTop - (scrollTop > lastScrollTop ? buffers.down : buffers.up);
			return this._indexConstraint(position);
		},
		_selectable: function() {
			if (this.options.selectable) {
				this._selectProxy = this._clickHandler.bind(this);
				this.element.on("click.VirtualList", "." + this._getItemClass(), this._selectProxy);
			}
		},
		getElementIndex: function(element) {
			if (!(element instanceof jQuery)) return undefined;
			return parseInt(element.attr("data-offset-index"), 10);
		},
		_getIndecies: function(candidate) {
			let result = [];
			if (typeof candidate === "function") {
				const data = this.dataSource.flatView();
				for (let idx = 0; idx < data.length; idx++) if (candidate(data[idx])) {
					result.push(idx);
					break;
				}
			}
			if (typeof candidate === "number") result.push(candidate);
			const elementIndex = this.getElementIndex(candidate);
			if (!isNaN(elementIndex)) result.push(elementIndex);
			if (candidate instanceof Array) result = candidate;
			return result;
		},
		_deselect: function(indices) {
			const selectedIndexes = this._selectedIndexes;
			const selectedDataItems = this._selectedDataItems;
			const selectable = this.options.selectable;
			const valueGetter = this._valueGetter;
			const removed = [];
			let position = 0;
			let removedindexesCounter = 0;
			indices = indices.slice();
			if (selectable === true || !indices.length) {
				for (let idx = 0; idx < selectedIndexes.length; idx++) {
					let element;
					if (selectedIndexes[idx] !== undefined) element = this._getElementByIndex(selectedIndexes[idx]);
					else if (selectedDataItems[idx]) element = this._getElementByDataItem(selectedDataItems[idx]);
					if (element) {
						element.removeClass(SELECTED);
						if (this.options.checkboxes) {
							element.removeAttr("aria-selected");
							element.find("input.k-checkbox").toggleClass("k-checked", false).prop("checked", false);
						}
					}
					removed.push({
						index: selectedIndexes[idx],
						position: idx,
						dataItem: selectedDataItems[idx]
					});
				}
				this._values = [];
				this._selectedDataItems = [];
				this._selectedIndexes = [];
			} else if (selectable === "multiple") {
				let i = 0;
				while (i < indices.length) {
					let result = null;
					position = $.inArray(indices[i], selectedIndexes);
					const dataItem = this.dataItemByIndex(indices[i]);
					if (position === -1 && dataItem) {
						for (let j = 0; j < selectedDataItems.length; j++) if (isPrimitive(dataItem) ? selectedDataItems[j] === dataItem : valueGetter(selectedDataItems[j]) === valueGetter(dataItem)) {
							const item = this._getElementByIndex(indices[i]);
							result = this._deselectSingleItem(item, j, indices[i], removedindexesCounter);
						}
					} else {
						const selectedIndex = selectedIndexes[position];
						if (selectedIndex !== undefined) {
							const item = this._getElementByIndex(selectedIndex);
							result = this._deselectSingleItem(item, position, selectedIndex, removedindexesCounter);
						}
					}
					if (result) {
						indices.splice(i, 1);
						removed.push(result);
						removedindexesCounter++;
						i--;
					}
					i++;
				}
			}
			return {
				indices,
				removed
			};
		},
		_deselectSingleItem: function(item, position, selectedIndex, removedindexesCounter) {
			const isSelectedInDOM = this.options.checkboxes ? item.attr("aria-selected") === "true" : item.hasClass(SELECTED);
			if (item.length > 0 && !isSelectedInDOM) return;
			if (item.length) {
				item.removeClass(SELECTED);
				if (this.options.checkboxes) {
					item.removeAttr("aria-selected");
					item.find("input.k-checkbox").toggleClass("k-checked", false).prop("checked", false);
				}
			}
			this._values.splice(position, 1);
			this._selectedIndexes.splice(position, 1);
			const dataItem = this._selectedDataItems.splice(position, 1)[0];
			return {
				index: selectedIndex,
				position: position + removedindexesCounter,
				dataItem
			};
		},
		_deselectCurrentValues: function(indices) {
			const items = this._items || [];
			const values = this._values;
			const removed = [];
			if (this.options.selectable !== "multiple" || !this.isFiltered()) return [];
			if (indices[0] === -1) {
				$(items).removeClass(SELECTED);
				const removedItems = $.map(this._selectedDataItems.slice(0), (dataItem, idx) => ({
					dataItem,
					position: idx
				}));
				this._selectedIndexes = [];
				this._selectedDataItems = [];
				this._values = [];
				return removedItems;
			}
			for (let idx = 0; idx < indices.length; idx++) {
				let position = -1;
				const index = indices[idx];
				let value;
				if (this.dataItemByIndex(index)) value = this._valueGetter(this.dataItemByIndex(index));
				for (let j = 0; j < values.length; j++) if (value == values[j]) {
					position = j;
					break;
				}
				if (position > -1) {
					removed.push(this.removeAt(position));
					$(items[index]).removeClass(SELECTED);
				}
			}
			return removed;
		},
		_getSkip: function(index, take) {
			return ((index < take ? 1 : Math.floor(index / take) + 1) - 1) * take;
		},
		_select: function(indexes) {
			const that = this;
			const singleSelection = this.options.selectable !== "multiple";
			const dataSource = this.dataSource;
			const take = this.itemCount;
			const valueGetter = this._valueGetter;
			const added = [];
			if (singleSelection) {
				that._selectedIndexes = [];
				that._selectedDataItems = [];
				that._values = [];
			}
			const oldSkip = dataSource.skip();
			$.each(indexes, (_, index) => {
				const skip = that._getSkip(index, take);
				that.mute(() => {
					dataSource.range(skip, take);
					const dataItem = that._findDataItem(dataSource.view(), [index - skip]);
					that._selectedIndexes.push(index);
					that._selectedDataItems.push(dataItem);
					that._values.push(isPrimitive(dataItem) ? dataItem : valueGetter(dataItem));
					added.push({
						index,
						dataItem
					});
					const selEl = that._getElementByIndex(index);
					if (that.options.checkboxes) {
						selEl.find("input.k-checkbox").toggleClass("k-checked", true).prop("checked", true);
						selEl.attr("aria-selected", true);
					} else selEl.addClass(SELECTED);
					dataSource.range(oldSkip, take);
				});
			});
			that._values = that._checkValuesOrder(that._values);
			return added;
		},
		_clickHandler: function(e) {
			const that = this;
			const item = $(e.currentTarget);
			const actionField = that.options.actionField;
			if (!e.isDefaultPrevented() && item.attr("data-uid")) {
				if (actionField) {
					const dataItem = that.dataItemByIndex(that.getElementIndex(item));
					if (dataItem && dataItem[actionField]) {
						if (!that.trigger("action", {
							item,
							dataItem,
							action: dataItem[actionField]
						})) return;
					}
				}
				this.trigger(CLICK, { item });
			}
		},
		_buildValueGetter: function() {
			this._valueGetter = kendo.getter(this.options.dataValueField);
		},
		_calculateGroupPadding: function(height) {
			const firstItem = this.items().first();
			const groupHeader = this.header;
			let padding = 0;
			if (groupHeader[0] && groupHeader[0].style.display !== "none") {
				if (height !== "auto") padding = kendo.support.scrollbar();
				padding += parseFloat(firstItem.css("border-right-width"), 10) + parseFloat(firstItem.children(".k-group").css("right"), 10);
				groupHeader.css("padding-right", padding);
			}
		},
		_calculateColumnsHeaderPadding: function() {
			if (this._isTableVariant()) {
				const isRtl = kendo.support.isRtl(this.wrapper);
				const scrollbar = kendo.support.scrollbar();
				const columnsHeader = this.content.parent().parent().find(".k-table-header");
				const total = this.dataSource.total();
				columnsHeader.css(isRtl ? "padding-left" : "padding-right", total ? scrollbar : 0);
			}
		}
	});
	kendo.ui.VirtualList = VirtualList;
	kendo.ui.plugin(VirtualList);
})(window.kendo.jQuery);
var kendo_virtuallist_default = kendo;
//#endregion
exports.__meta__ = __meta__;
exports.default = kendo_virtuallist_default;