UNPKG

@progress/kendo-ui

Version:

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

1,465 lines (1,464 loc) 106 kB
import { t as valueMapperOptions } from "./valueMapper-mu5kAYRd.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 group = groupRanges[i]; const { startIndex, itemCount } = group; 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 inlineHeaders = Math.max(0, groupCount - 1); const totalElements = totalItems + inlineHeaders; 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) { const groups = dataSource.group() || []; if (!groups.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("." + GROUPITEM).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, 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" + VIRTUAL_LIST_NS, "li:not(.k-loading-item)", function() { $(this).addClass(HOVER); }).on("mouseleave" + VIRTUAL_LIST_NS, "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" + VIRTUAL_LIST_NS, kendo.throttle(function() { that._renderItems(); that._triggerListBound(); }, options.delay)); that._selectable(); }, 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 }, 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) { const target = this.header.closest(GROUP_ROW_SEL).length ? this.header.closest(GROUP_ROW_SEL) : this.header; target[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); }, ulElements: function() { const that = this; if (that.options.type === "flat") { return that.ul || $(); } else { return that.content.find("." + 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() { for (let i = 0; i < this._selectedDataItems.length; i++) { const item = this._getElementByDataItem(this._selectedDataItems[i]); if (item.length) { 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(); } const shouldClear = that.options.selectable === "multiple" && that.select().length && value.length; if (shouldClear || !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) { const match = isPrimitive(item) ? value[i] === item : value[i] === valueGetter(item); if (match) { 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(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]); 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("." + FOCUSED); 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("." + FOCUSED).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); const current = this.focus(); if (current?.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); const current = this.focus(); if (current?.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 (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++) { const match = dataView[i].item && isPrimitive(dataView[i].item) ? dataView[i].item === dataItem : dataView[i].item && dataItem && valueGetter(dataView[i].item) == valueGetter(dataItem); if (match) { 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 itemHeight = this.options.itemHeight; const cssGap = this._cssGap || 0; const effectiveItemHeight = itemHeight + cssGap; 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; }, _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 transform = parentUl.style.transform || parentUl.style.webkitTransform || ""; const match = transform.match(/translateY\((-?\d+(?:\.\d+)?)px\)/); const ulTranslateY = match ? parseFloat(match[1]) : 0; return ulTranslateY + 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 headerHeight = this._getHeaderHeight(); const visibleTop = scrollTop + headerHeight; 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(); while (count-- > 0) { const text = document.createElement("span"); text.className = "k-list-item-text"; 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%"; item.appendChild(text); element.appendChild(item); items.push(item); } return items; }, _generateGroupId: function(groupIndex, groupValue) { const stringValue = groupValue === null || groupValue === undefined ? "" : String(groupValue); const safeValue = stringValue.replace(/\s+/g, "-").replace(/[^a-zA-Z0-9-_]/g, ""); return this._optionID + "-group-" + groupIndex + "-" + safeValue; }, _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; }, _addGroupItems: function(groupContainer, count) { const that = this; const itemHeight = that._getItemHeightStyle(); const hasColumns = that._isTableVariant(); const itemClass = that._getItemClass(); 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) { const text = document.createElement("span"); text.className = "k-list-item-text"; item.appendChild(text); } groupContainer.ul.appendChild(item); groupContainer.items.push(item); } }, _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 totalItems = dataSource.total(); const effectiveItemHeight = options.itemHeight + that._cssGap; const totalHeight = totalItems * effectiveItemHeight; 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 = 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) { const fullGrouped = new kendo.data.Query(allData).group(groupDescriptor).data; estimatedTotalGroups = fullGrouped.length; } else { estimatedTotalGroups = that._groupRanges.length; } } } that._totalGroupCount = estimatedTotalGroups || that._groupRanges.length; that._initialGroupRanges = that._groupRanges.slice(); const groupCount = that._totalGroupCount; const isServerGrouping = dataSource.options.serverGrouping === true; const itemCount = isServerGrouping ? dataSource.total() : dataSource.data().length || dataSource.total(); const totalHeight = getGroupedTotalHeight(itemCount, 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; }); }, _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; const isServerOperations = dataSource.options.serverGrouping === true; if (!isServerOperations) { return that._buildGroupedPageData(0, scrollTop); } const totalHeight = total * itemHeight + Math.max(0, totalGroupCount - 1) * itemHeight; const pag