@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
JavaScript
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