@syncfusion/ej2-lists
Version:
The listview control allows you to select an item or multiple items from a list-like interface and represents the data in interactive hierarchical structure across different layouts or views.
908 lines (863 loc) • 51.5 kB
text/typescript
import { ListView, ItemCreatedArgs, classNames, Fields, UISelectedItem, SelectEventArgs } from './list-view';
import { EventHandler, append, isNullOrUndefined, detach, compile, formatUnit, select } from '@syncfusion/ej2-base';
import { ListBase } from '../common/list-base';
import { DataManager } from '@syncfusion/ej2-data';
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* ElementContext
*/
export interface ElementContext extends HTMLElement {
context: { [key: string]: string | object };
}
const listElementCount: number = 1.5;
const windowElementCount: number = 3;
export class Virtualization {
constructor(instance: ListView) {
this.listViewInstance = instance;
}
private listViewInstance: any;
private templateData: DataSource;
private topElementHeight: number;
private bottomElementHeight: number;
public listItemHeight: number;
private domItemCount: number;
private expectedDomItemCount: number;
private scrollPosition: number;
private onVirtualScroll: EventListener;
private updateUl: EventListener;
private checkListWrapper: Node;
private iconCssWrapper: Node;
public uiFirstIndex: number;
private uiLastIndex: number;
private totalHeight: number;
private topElement: HTMLElement;
private bottomElement: HTMLElement;
private activeIndex: number;
private uiIndices: { [key: string]: number[] };
private listDiff: number;
private elementDifference: number = 0;
/**
* For internal use only.
*
* @private
*/
public isNgTemplate(): boolean {
return !isNullOrUndefined(this.listViewInstance.templateRef) && typeof this.listViewInstance.templateRef !== 'string';
}
/**
* Checks if the platform is a Vue and its template property is a function type.
*
* @returns {boolean} indicating the result of the check
*/
private isVueFunctionTemplate(): boolean {
return this.listViewInstance.isVue && typeof this.listViewInstance.template === 'function';
}
/**
* For internal use only.
*
* @private
*/
public uiVirtualization(): void {
this.wireScrollEvent(false);
const curViewDS: { [key: string]: object; }[] = this.listViewInstance.curViewDS as { [key: string]: object; }[];
const isRendered: boolean = this.listViewInstance.isRendered;
const firstIndex: number = isRendered && !isNullOrUndefined(this.uiFirstIndex) && this.uiLastIndex <= Object.keys(curViewDS).length
? this.uiFirstIndex : 0;
const firstDs: { [key: string]: object }[] = curViewDS.slice(firstIndex, firstIndex + 1);
this.listViewInstance.ulElement = this.listViewInstance.curUL = ListBase.createList(
this.listViewInstance.createElement, firstDs as { [key: string]: object; }[],
this.listViewInstance.listBaseOption, null, this.listViewInstance);
this.listViewInstance.contentContainer = this.listViewInstance.createElement('div', { className: classNames.container });
this.listViewInstance.element.appendChild(this.listViewInstance.contentContainer);
this.listViewInstance.contentContainer.appendChild(this.listViewInstance.ulElement);
this.listItemHeight = this.listViewInstance.ulElement.firstElementChild.getBoundingClientRect().height;
this.expectedDomItemCount = this.ValidateItemCount(10000);
this.updateDOMItemCount();
const lastIndex: number = isRendered && !isNullOrUndefined(this.uiLastIndex) && this.listDiff !== 0
? this.uiLastIndex : this.domItemCount - 1;
this.uiFirstIndex = firstIndex;
this.uiLastIndex = lastIndex;
const otherDs: { [key: string]: object; }[] = curViewDS.slice(firstIndex + 1, lastIndex + 1);
const listItems: HTMLElement[] = ListBase.createListItemFromJson(
this.listViewInstance.createElement, otherDs as { [key: string]: object; }[],
this.listViewInstance.listBaseOption, null, null, this.listViewInstance);
append(listItems, this.listViewInstance.ulElement);
this.listViewInstance.liCollection = <HTMLElement[] & NodeListOf<HTMLLIElement>>
this.listViewInstance.curUL.querySelectorAll('li');
this.topElement = this.listViewInstance.createElement('div');
this.listViewInstance.ulElement.insertBefore(this.topElement, this.listViewInstance.ulElement.firstElementChild);
this.bottomElement = this.listViewInstance.createElement('div');
this.listViewInstance.ulElement.insertBefore(this.bottomElement, null);
this.totalHeight = (Object.keys(curViewDS).length * this.listItemHeight) - (this.domItemCount * this.listItemHeight);
this.topElement.style.height = isRendered ? this.topElementHeight + 'px' : '0px';
this.bottomElement.style.height = isRendered ? (this.totalHeight - this.topElementHeight) + 'px' : this.totalHeight + 'px';
this.topElementHeight = isRendered ? this.topElementHeight : 0;
this.bottomElementHeight = isRendered ? (this.totalHeight - this.topElementHeight) : this.totalHeight;
this.listDiff = isRendered && Object.keys(curViewDS).length !== this.domItemCount ? this.listDiff : 0;
if (isRendered) {
this.listViewInstance.element.scrollTop = this.listViewInstance.previousScrollTop;
}
this.uiIndicesInitialization();
}
private wireScrollEvent(destroy: boolean): void {
if (!destroy) {
if (this.listViewInstance.isWindow) {
this.onVirtualScroll = this.onVirtualUiScroll.bind(this);
window.addEventListener('scroll', this.onVirtualScroll);
} else {
EventHandler.add(this.listViewInstance.element, 'scroll', this.onVirtualUiScroll, this);
}
} else {
if (this.listViewInstance.isWindow === true) {
window.removeEventListener('scroll', this.onVirtualScroll);
window.removeEventListener('scroll', this.updateUl);
}
else {
EventHandler.remove(this.listViewInstance.element, 'scroll', this.onVirtualUiScroll);
}
}
}
private ValidateItemCount(dataSourceLength: number): number {
const height: number = parseFloat(formatUnit(this.listViewInstance.height));
let itemCount: number;
if (this.listViewInstance.isWindow) {
itemCount = Math.round((window.innerHeight / this.listItemHeight) * windowElementCount);
} else {
if (typeof this.listViewInstance.height === 'string' && this.listViewInstance.height.indexOf('%') !== -1) {
itemCount = Math.round(
(this.listViewInstance.element.getBoundingClientRect().height / this.listItemHeight) * listElementCount);
} else {
itemCount = Math.round((height / this.listItemHeight) * listElementCount);
}
}
if (itemCount > dataSourceLength) {
itemCount = dataSourceLength;
}
return itemCount;
}
public updateDOMItemCount(): void {
this.domItemCount = this.ValidateItemCount(Object.keys(this.listViewInstance.curViewDS).length);
}
private uiIndicesInitialization(): void {
this.uiIndices = { 'activeIndices': [], 'disabledItemIndices': [], 'hiddenItemIndices': [] };
const data: DataSource[] = this.listViewInstance.curViewDS as DataSource[];
for (let i: number = 0; i < data.length; i++) {
if (this.listViewInstance.showCheckBox && data[i as number][this.listViewInstance.fields.isChecked]) {
this.uiIndices.activeIndices.push(i);
}
if (!isNullOrUndefined((data[parseInt(i.toString(), 10)] as {[key: string]: object; })[this.listViewInstance.fields.enabled]) &&
!data[i as number][this.listViewInstance.fields.enabled]) {
(this.uiIndices.disabledItemIndices.push(i)) as number;
}
}
if (this.isNgTemplate()) {
const items: ElementContext[] = this.listViewInstance.element.querySelectorAll('.' + classNames.listItem);
for (let index: number = 0; index < items.length; index++) {
items[index as number].context = this.listViewInstance.viewContainerRef.get(index).context;
}
}
}
public refreshItemHeight(): void {
if (this.listViewInstance.curViewDS.length) {
const curViewDS: { [key: string]: object; }[] = this.listViewInstance.curViewDS as { [key: string]: object; }[];
this.listItemHeight = (this.topElement.nextSibling as HTMLElement).getBoundingClientRect().height;
this.totalHeight = (Object.keys(curViewDS).length * this.listItemHeight) - (this.domItemCount * this.listItemHeight);
this.bottomElementHeight = this.totalHeight;
this.bottomElement.style.height = this.totalHeight + 'px';
}
}
private getscrollerHeight(startingHeight: number): number {
return this.listViewInstance.isWindow ? (((pageYOffset - startingHeight) <= 0) ? 0 :
(pageYOffset - startingHeight)) : ((this.listViewInstance.element.scrollTop - startingHeight) <= 0) ? 0 :
(this.listViewInstance.element.scrollTop - startingHeight);
}
private onVirtualUiScroll(): void {
let startingHeight: number;
const curViewDS: { [key: string]: object; }[] = this.listViewInstance.curViewDS as { [key: string]: object; }[];
this.listItemHeight = select('.e-list-item', this.listViewInstance.element).getBoundingClientRect().height;
this.totalHeight = (Object.keys(curViewDS).length * this.listItemHeight) - (this.domItemCount * this.listItemHeight);
if (this.listViewInstance.isWindow) {
startingHeight = this.listViewInstance.ulElement.getBoundingClientRect().top -
document.documentElement.getBoundingClientRect().top;
} else {
startingHeight = this.listViewInstance.headerEle ? this.listViewInstance.headerEle.getBoundingClientRect().height : 0;
}
this.scrollPosition = isNullOrUndefined(this.scrollPosition) ? 0 : this.scrollPosition;
const scroll: number = this.getscrollerHeight(startingHeight);
this.topElementHeight = this.listItemHeight * Math.floor(scroll / this.listItemHeight);
this.bottomElementHeight = this.totalHeight - this.topElementHeight;
[this.topElementHeight, this.bottomElementHeight] = scroll <= this.totalHeight ?
[this.topElementHeight, this.bottomElementHeight] : [this.totalHeight, 0];
if (this.topElementHeight !== parseFloat(this.topElement.style.height)) {
this.topElement.style.height = this.topElementHeight + 'px';
this.bottomElement.style.height = this.bottomElementHeight + 'px';
if (scroll > this.scrollPosition) {
const listDiff: number = Math.round(((this.topElementHeight / this.listItemHeight) - this.listDiff));
if (listDiff > (this.expectedDomItemCount + 5)) {
this.onLongScroll(listDiff, true);
} else {
this.onNormalScroll(listDiff, true);
}
} else {
const listDiff: number = Math.round((this.listDiff - (this.topElementHeight / this.listItemHeight)));
if (listDiff > (this.expectedDomItemCount + 5)) {
this.onLongScroll(listDiff, false);
} else {
this.onNormalScroll(listDiff, false);
}
}
}
this.listDiff = Math.round(this.topElementHeight / this.listItemHeight);
if (typeof this.listViewInstance.onUIScrolled === 'function') {
this.listViewInstance.onUIScrolled();
}
this.scrollPosition = scroll;
}
private onLongScroll(listDiff: number, isScrollingDown: boolean): void {
let index: number = isScrollingDown ? (this.uiFirstIndex + listDiff) : (this.uiFirstIndex - listDiff);
const elements: HTMLElement[] = this.listViewInstance.ulElement.querySelectorAll('li');
for (let i: number = 0; i < elements.length; i++) {
this.updateUI(elements[i as number], index);
index++;
}
this.uiLastIndex = isScrollingDown ? (this.uiLastIndex + listDiff) : (this.uiLastIndex - listDiff);
this.uiFirstIndex = isScrollingDown ? (this.uiFirstIndex + listDiff) : (this.uiFirstIndex - listDiff);
}
private onNormalScroll(listDiff: number, isScrollingDown: boolean): void {
if (isScrollingDown) {
for (let i: number = 0; i < listDiff; i++) {
const index: number = ++this.uiLastIndex;
this.updateUI(this.topElement.nextElementSibling as HTMLElement, index, this.bottomElement);
this.uiFirstIndex++;
}
} else {
for (let i: number = 0; i < listDiff; i++) {
const index: number = --this.uiFirstIndex;
const target: HTMLElement = this.topElement.nextSibling as HTMLElement;
this.updateUI(this.bottomElement.previousElementSibling as HTMLElement, index, target);
this.uiLastIndex--;
}
}
}
private updateUiContent(element: HTMLElement, index: number): void {
const curViewDs: { [key: string]: Object; }[] = this.listViewInstance.curViewDS as DataSource[];
if (typeof (this.listViewInstance.dataSource as string[])[0] === 'string' ||
typeof (this.listViewInstance.dataSource as number[])[0] === 'number') {
element.dataset.uid = ListBase.generateId();
element.getElementsByClassName(classNames.listItemText)[0].innerHTML =
(this.listViewInstance.curViewDS as string[] | number[])[index as number].toString();
} else {
element.dataset.uid = (curViewDs[parseInt(index.toString(), 10)][this.listViewInstance.fields.id]) as string ?
(curViewDs[parseInt(index.toString(), 10)][this.listViewInstance.fields.id]) as string : (ListBase.generateId() as string);
element.getElementsByClassName(classNames.listItemText)[0].innerHTML =
(curViewDs[parseInt(index.toString(), 10)][this.listViewInstance.fields.text]) as string;
}
if (this.listViewInstance.showIcon) {
if (element.querySelector('.' + classNames.listIcon)) {
detach(element.querySelector('.' + classNames.listIcon));
}
if ((this.listViewInstance.curViewDS[index as number] as { [key: string]: object; })[this.listViewInstance.fields.iconCss]) {
const textContent: Element = element.querySelector('.' + classNames.textContent);
const curViewDS: { [key: string]: object; } =
this.listViewInstance.curViewDS[index as number] as { [key: string]: object; };
const iconCss: string = curViewDS[this.listViewInstance.fields.iconCss].toString();
const target: Element = this.listViewInstance.createElement('div', {
className: classNames.listIcon + ' ' + iconCss
});
textContent.insertBefore(target, element.querySelector('.' + classNames.listItemText));
}
}
if (this.listViewInstance.showCheckBox && this.listViewInstance.fields.groupBy) {
if (!this.checkListWrapper) {
this.checkListWrapper = this.listViewInstance.curUL.querySelector('.' + classNames.checkboxWrapper).cloneNode(true);
}
const textContent: Element = element.querySelector('.' + classNames.textContent);
if ((this.listViewInstance.curViewDS[index as number] as { [key: string]: object; }).isHeader) {
if (element.querySelector('.' + classNames.checkboxWrapper)) {
element.classList.remove(classNames.checklist);
textContent.classList.remove(classNames.checkbox);
detach(element.querySelector('.' + classNames.checkboxWrapper));
}
} else {
if (!element.querySelector('.' + classNames.checkboxWrapper)) {
element.classList.add(classNames.checklist);
textContent.classList.add(classNames.checkbox);
if (this.listViewInstance.checkBoxPosition === 'Left') {
textContent.classList.add('e-checkbox-left');
}
else {
textContent.classList.add('e-checkbox-right');
}
textContent.append(this.checkListWrapper.cloneNode(true));
}
}
}
}
private changeElementAttributes(element: Element, index: number): void {
element.classList.remove(classNames.disable);
if (this.uiIndices.disabledItemIndices.length && this.uiIndices.disabledItemIndices.indexOf(index) !== -1) {
element.classList.add(classNames.disable);
}
(element as HTMLElement).style.display = '';
if (this.uiIndices.hiddenItemIndices.length && this.uiIndices.hiddenItemIndices.indexOf(index) !== -1) {
(element as HTMLElement).style.display = 'none';
}
if (this.listViewInstance.showCheckBox) {
const checklistElement: Element = element.querySelector('.' + classNames.checkboxWrapper);
element.classList.remove(classNames.selected);
element.classList.remove(classNames.focused);
if (checklistElement) {
checklistElement.removeAttribute('aria-checked');
checklistElement.firstElementChild.classList.remove(classNames.checked);
}
if (this.uiIndices.activeIndices.length && this.uiIndices.activeIndices.indexOf(index) !== -1 &&
!this.listViewInstance.curUL.querySelector(classNames.selected)) {
element.classList.add(classNames.selected);
checklistElement.firstElementChild.classList.add(classNames.checked);
checklistElement.setAttribute('aria-checked', 'true');
if (this.activeIndex === index) {
element.classList.add(classNames.focused);
}
}
} else {
element.classList.remove(classNames.selected);
element.removeAttribute('aria-selected');
if (!isNullOrUndefined(this.activeIndex) && this.activeIndex === index &&
!this.listViewInstance.curUL.querySelector(classNames.selected)) {
element.classList.add(classNames.selected);
element.setAttribute('aria-selected', 'true');
}
}
if (this.listViewInstance.fields.groupBy) {
if ((this.listViewInstance.curViewDS as { [key: string]: object; }[])[index as number].isHeader) {
if (element.classList.contains(classNames.listItem)) {
element.classList.remove(classNames.listItem);
element.setAttribute('role', 'group');
element.classList.add(classNames.groupListItem);
}
} else {
if (element.classList.contains(classNames.groupListItem)) {
element.classList.remove(classNames.groupListItem);
element.setAttribute('role', 'listitem');
element.classList.add(classNames.listItem);
}
}
}
}
private findDSAndIndexFromId(
ds: DataSource[] | string[] | number[], fields: Fields | Element): DataSource {
const resultJSON: { [key: string]: object | number } = {};
fields = this.listViewInstance.getElementUID(fields);
if (!isNullOrUndefined(fields)) {
(ds as { [key: string]: object; }[]).some((data: { [key: string]: object; }, index: number) => {
if (((fields as { [key: string]: object; })[this.listViewInstance.fields.id] &&
(fields as { [key: string]: object; })[this.listViewInstance.fields.id]
=== (data[this.listViewInstance.fields.id] && data[this.listViewInstance.fields.id] as object) || fields === data)) {
resultJSON.index = index;
resultJSON.data = data;
return true;
} else {
return false;
}
});
}
return resultJSON as DataSource;
}
public getSelectedItems(): UISelectedItem {
if (!isNullOrUndefined(this.activeIndex) || (this.listViewInstance.showCheckBox && this.uiIndices.activeIndices.length)) {
const dataCollection: string[] | { [key: string]: object; }[] = [];
const textCollection: string[] = [];
if (typeof (this.listViewInstance.dataSource as string[])[0] === 'string' ||
typeof (this.listViewInstance.dataSource as number[])[0] === 'number') {
const curViewDS: string[] | number[] = this.listViewInstance.curViewDS as string[] | number[];
if (this.listViewInstance.showCheckBox) {
const indices: number[] = this.uiIndices.activeIndices;
for (let i: number = 0; i < indices.length; i++) {
(dataCollection as string[]).push((curViewDS as string[])[indices[i as number]]);
}
return {
text: dataCollection as string[],
data: dataCollection as string | number | string[] | number[] | {
[key: string]: object;
} | {
[key: string]: object;
}[],
index: this.uiIndices.activeIndices.map((index: number) =>
(this.listViewInstance.dataSource as string[]).indexOf((curViewDS as string[])[index as number]))
};
} else {
return {
text: curViewDS[this.activeIndex],
data: curViewDS[this.activeIndex],
index: (this.listViewInstance.dataSource as string[]).indexOf((curViewDS as string[])[this.activeIndex])
};
}
} else {
const curViewDS: { [key: string]: object | string }[] = this.listViewInstance.curViewDS as { [key: string]: object; }[];
const text: string = this.listViewInstance.fields.text;
if (this.listViewInstance.showCheckBox) {
const indexArray: number[] = this.uiIndices.activeIndices;
for (let i: number = 0; i < indexArray.length; i++) {
textCollection.push((curViewDS[indexArray[i as number]] as { [key: string]: string; })[`${text}`]);
(dataCollection as { [key: string]: Object; }[]).push(
curViewDS[indexArray[parseInt(i.toString(), 10)]] as DataSource);
}
const dataSource: { [key: string]: Object; }[] =
this.listViewInstance.dataSource instanceof DataManager
? curViewDS : this.listViewInstance.dataSource;
return {
text: textCollection,
data: dataCollection as string | number | number[] | string[] | { [key: string]: object; } |
{ [key: string]: object; }[],
index: this.uiIndices.activeIndices.map((index: number) =>
dataSource.indexOf(curViewDS[index as number] as DataSource))
};
} else {
const dataSource: { [key: string]: Object; }[] =
this.listViewInstance.dataSource instanceof DataManager
? curViewDS : this.listViewInstance.dataSource;
return {
text: curViewDS[this.activeIndex][this.listViewInstance.fields.text] as string,
data: curViewDS[this.activeIndex] as string | number | number[] | string[] | { [key: string]: object; } |
{ [key: string]: object; }[],
index: dataSource.indexOf(curViewDS[this.activeIndex])
};
}
}
} else {
return undefined;
}
}
public selectItem(obj: Fields | HTMLElement | Element): void {
const resutJSON: { [key: string]: object | number } = this.findDSAndIndexFromId(this.listViewInstance.curViewDS, obj);
if (Object.keys(resutJSON).length) {
const isSelected: boolean = this.activeIndex === resutJSON.index;
let isChecked: boolean;
this.activeIndex = resutJSON.index as number;
if (this.listViewInstance.showCheckBox) {
if (this.uiIndices.activeIndices.indexOf(resutJSON.index as number) === -1) {
isChecked = true;
this.uiIndices.activeIndices.push(resutJSON.index as number);
} else {
isChecked = false;
this.uiIndices.activeIndices.splice(this.uiIndices.activeIndices.indexOf(resutJSON.index as number), 1);
}
if (this.listViewInstance.curUL.querySelector('.' + classNames.focused)) {
this.listViewInstance.curUL.querySelector('.' + classNames.focused).classList.remove(classNames.focused);
}
}
if (this.listViewInstance.getLiFromObjOrElement(obj)) {
if (this.listViewInstance.showCheckBox) {
this.listViewInstance.setCheckboxLI(this.listViewInstance.getLiFromObjOrElement(obj));
} else {
this.listViewInstance.setSelectLI(this.listViewInstance.getLiFromObjOrElement(obj));
}
} else {
let eventArgs: { [key: string]: Object; };
if (typeof (this.listViewInstance.dataSource as string[])[0] === 'string' ||
typeof (this.listViewInstance.dataSource as number[])[0] === 'number') {
eventArgs = {
text: (this.listViewInstance.curViewDS as number[] | string[])[this.activeIndex],
data: (this.listViewInstance.curViewDS as number[] | string[])[this.activeIndex],
index: this.activeIndex
};
} else {
const curViewDS: { [key: string]: object; }[] = this.listViewInstance.curViewDS as { [key: string]: object; }[];
eventArgs = {
text: curViewDS[this.activeIndex][this.listViewInstance.fields.text],
data: curViewDS[this.activeIndex],
index: this.activeIndex
};
}
if (this.listViewInstance.showCheckBox) {
this.listViewInstance.trigger('select', eventArgs, (observedArgs: SelectEventArgs) => {
if (observedArgs.cancel) {
if (!isChecked) {
eventArgs.isChecked = isChecked;
this.uiIndices.activeIndices.push(resutJSON.index as number);
} else {
eventArgs.isChecked = !isChecked;
this.uiIndices.activeIndices.splice(this.uiIndices.activeIndices.indexOf(resutJSON.index as number), 1);
}
}
});
} else if (!isSelected) {
this.listViewInstance.removeSelect();
this.listViewInstance.trigger('select', eventArgs, (observedArgs: SelectEventArgs) => {
if (observedArgs.cancel) {
this.activeIndex = undefined;
}
});
}
}
} else if (isNullOrUndefined(obj) && !this.listViewInstance.showCheckBox) {
this.listViewInstance.removeSelect();
this.activeIndex = undefined;
}
}
public enableItem(obj: Fields | HTMLElement | Element): void {
const resutJSON: { [key: string]: object | number } = this.findDSAndIndexFromId(this.listViewInstance.curViewDS, obj);
if (Object.keys(resutJSON).length) {
this.uiIndices.disabledItemIndices.splice(this.uiIndices.disabledItemIndices.indexOf(resutJSON.index as number), 1);
}
}
public disableItem(obj: Fields | HTMLElement | Element): void {
const resutJSON: { [key: string]: object | number } = this.findDSAndIndexFromId(this.listViewInstance.curViewDS, obj);
if (Object.keys(resutJSON).length && this.uiIndices.disabledItemIndices.indexOf(resutJSON.index as number) === -1) {
this.uiIndices.disabledItemIndices.push(resutJSON.index as number);
}
}
public showItem(obj: Fields | HTMLElement | Element): void {
const resutJSON: { [key: string]: object | number } = this.findDSAndIndexFromId(this.listViewInstance.curViewDS, obj);
if (Object.keys(resutJSON).length) {
this.uiIndices.hiddenItemIndices.splice(this.uiIndices.hiddenItemIndices.indexOf(resutJSON.index as number), 1);
}
}
public hideItem(obj: Fields | HTMLElement | Element): void {
const resutJSON: { [key: string]: object | number } = this.findDSAndIndexFromId(this.listViewInstance.curViewDS, obj);
if (Object.keys(resutJSON).length && this.uiIndices.hiddenItemIndices.indexOf(resutJSON.index as number) === -1) {
this.uiIndices.hiddenItemIndices.push(resutJSON.index as number);
}
}
public removeItem(obj: HTMLElement | Element | Fields): void {
let dataSource: DataSource;
const curViewDS: DataSource[] = this.listViewInstance.curViewDS;
const resutJSON: { [key: string]: object | number } = this.findDSAndIndexFromId(curViewDS, obj);
if (Object.keys(resutJSON).length) {
dataSource = resutJSON.data as DataSource;
if (curViewDS[(resutJSON.index as number) - 1] &&
curViewDS[(resutJSON.index as number) - 1].isHeader &&
((curViewDS[(resutJSON.index as number) - 1])
.items as { [key: string]: object; }[]).length === 1) {
this.removeUiItem((resutJSON.index as number) - 1);
this.removeUiItem((resutJSON.index as number) - 1);
} else {
this.removeUiItem((resutJSON.index as number));
}
}
const listDataSource: DataSource[] = this.listViewInstance.dataSource instanceof DataManager
? this.listViewInstance.localData : this.listViewInstance.dataSource;
const index: number = listDataSource.indexOf(dataSource);
if (index !== -1) {
listDataSource.splice(index, 1);
this.listViewInstance.setViewDataSource(listDataSource);
}
// recollect all the list item into collection
this.listViewInstance.liCollection =
<HTMLElement[] & NodeListOf<HTMLLIElement>>this.listViewInstance.curUL.querySelectorAll('li');
}
// eslint-disable-next-line
public setCheckboxLI(li: HTMLElement | Element, e?: MouseEvent | KeyboardEvent | FocusEvent): void {
const index: number = Array.prototype.indexOf.call(this.listViewInstance.curUL.querySelectorAll('li'), li) + this.uiFirstIndex;
this.activeIndex = Array.prototype.indexOf.call(this.listViewInstance.curUL.querySelectorAll('li'), li) + this.uiFirstIndex;
if (li.classList.contains(classNames.selected)) {
if (this.uiIndices.activeIndices.indexOf(index) === -1) {
this.uiIndices.activeIndices.push(index);
}
} else {
this.uiIndices.activeIndices.splice(this.uiIndices.activeIndices.indexOf(index), 1);
}
}
// eslint-disable-next-line
public setSelectLI(li: HTMLElement | Element, e?: MouseEvent | KeyboardEvent | FocusEvent): void {
this.activeIndex = Array.prototype.indexOf.call(this.listViewInstance.curUL.querySelectorAll('li'), li) + this.uiFirstIndex;
}
public checkedItem(checked: boolean): void {
if (checked) {
this.uiIndices.activeIndices = [];
this.activeIndex = undefined;
const data: DataSource[] = this.listViewInstance.curViewDS as DataSource[];
for (let index: number = 0; index < data.length; index++) {
if (!data[index as number].isHeader) {
this.uiIndices.activeIndices.push(index);
}
}
} else {
this.activeIndex = undefined;
this.uiIndices.activeIndices = [];
}
}
private addUiItem(index: number): void {
// virtually new add list item based on the scollbar position
// if the scroll bar is at the top, just pretend the new item has been added since no UI
// change is required for the item that has been added at last but when scroll bar is at the bottom
// just detach top and inject into bottom to mimic new item is added
const curViewDs: DataSource[] = this.listViewInstance.curViewDS as DataSource[];
this.changeUiIndices(index, true);
if (this.activeIndex && this.activeIndex >= index) {
this.activeIndex++;
}
if (this.listViewInstance.showCheckBox &&
(curViewDs[index as number] as { [key: string]: object; })[this.listViewInstance.fields.isChecked]) {
this.uiIndices.activeIndices.push(index);
}
if (!parseFloat(this.bottomElement.style.height) && !parseFloat(this.topElement.style.height)) {
this.bottomElement.style.height = parseFloat(this.bottomElement.style.height) + this.listItemHeight + 'px';
}
if (parseFloat(this.bottomElement.style.height)) {
const liItem: HTMLElement = this.listViewInstance.curUL.lastElementChild.previousSibling as HTMLElement;
const target: HTMLElement = this.listViewInstance.getLiFromObjOrElement(curViewDs[index + 1]) ||
this.listViewInstance.getLiFromObjOrElement(curViewDs[index + 2]);
if (target) {
this.bottomElement.style.height = parseFloat(this.bottomElement.style.height) + this.listItemHeight + 'px';
this.updateUI(liItem, index, target);
}
} else {
const liItem: HTMLElement = this.listViewInstance.curUL.firstElementChild.nextSibling as HTMLElement;
let target: HTMLElement;
if ((Object.keys(this.listViewInstance.curViewDS).length - 1) === index) {
target = this.listViewInstance.curUL.lastElementChild as HTMLElement;
} else {
target = this.listViewInstance.getLiFromObjOrElement(curViewDs[index + 1]) ||
this.listViewInstance.getLiFromObjOrElement(curViewDs[index + 2]);
}
this.topElement.style.height = parseFloat(this.topElement.style.height) + this.listItemHeight + 'px';
this.uiFirstIndex++;
this.uiLastIndex++;
if (target) {
this.updateUI(liItem, index, target);
if (this.listViewInstance.isWindow === true) {
window.scrollTo(0, (pageYOffset + this.listItemHeight));
}
else {
this.listViewInstance.element.scrollTop += this.listItemHeight;
}
}
}
this.totalHeight += this.listItemHeight;
this.listDiff = Math.round(parseFloat(this.topElement.style.height) / this.listItemHeight);
}
private removeUiItem(index: number): void {
this.totalHeight -= this.listItemHeight;
const curViewDS: DataSource = (this.listViewInstance.curViewDS as { [key: string]: object; }[])[index as number];
const liItem: HTMLElement = this.listViewInstance.getLiFromObjOrElement(curViewDS);
(this.listViewInstance.curViewDS as { [key: string]: object; }[]).splice(index, 1);
if (this.activeIndex && this.activeIndex >= index) {
this.activeIndex--;
}
if (liItem) {
if (this.domItemCount > Object.keys(this.listViewInstance.curViewDS).length) {
detach(liItem);
this.domItemCount--;
this.uiLastIndex--;
this.totalHeight = 0;
} else {
if (liItem.classList.contains(classNames.disable)) {
liItem.classList.remove(classNames.disable);
this.uiIndices.disabledItemIndices.splice(this.uiIndices.disabledItemIndices.indexOf(index), 1);
}
if (liItem.style.display === 'none') {
liItem.style.display = '';
this.uiIndices.hiddenItemIndices.splice(this.uiIndices.hiddenItemIndices.indexOf(index), 1);
}
if (this.listViewInstance.showCheckBox && liItem.classList.contains(classNames.selected)) {
this.listViewInstance.removeSelect();
this.uiIndices.activeIndices.splice(this.uiIndices.activeIndices.indexOf(index), 1);
const checklistElement: Element = liItem.querySelector('.' + classNames.checkboxWrapper);
checklistElement.removeAttribute('aria-checked');
checklistElement.firstElementChild.classList.remove(classNames.checked);
if (liItem.classList.contains(classNames.focused)) {
liItem.classList.remove(classNames.focused);
this.activeIndex = undefined;
}
} else if (liItem.classList.contains(classNames.selected)) {
this.listViewInstance.removeSelect();
this.activeIndex = undefined;
}
if (!parseFloat(this.bottomElement.style.height) && !parseFloat(this.topElement.style.height)) {
this.updateUI(liItem, this.uiLastIndex, this.bottomElement);
} else if (parseFloat(this.bottomElement.style.height)) {
this.bottomElement.style.height = parseFloat(this.bottomElement.style.height) - this.listItemHeight + 'px';
this.updateUI(liItem, this.uiLastIndex, this.bottomElement);
} else {
this.topElement.style.height = parseFloat(this.topElement.style.height) - this.listItemHeight + 'px';
this.updateUI(liItem, (this.uiFirstIndex - 1), this.topElement.nextSibling as HTMLElement);
this.uiLastIndex--;
this.uiFirstIndex--;
}
}
}
this.changeUiIndices(index, false);
this.listDiff = Math.round(parseFloat(this.topElement.style.height) / this.listItemHeight);
}
private changeUiIndices(index: number, increment: boolean): void {
const keys: string[] = Object.keys(this.uiIndices);
for (let ind: number = 0; ind < keys.length; ind++) {
this.uiIndices[keys[ind as number]] = this.uiIndices[keys[ind as number]].map((i: number) => {
if (i >= index) {
return increment ? ++i : --i;
} else {
return i;
}
});
}
}
public addItem(data: DataSource[], fields: Fields, dataSource: DataSource[], index: number): void {
for (let i: number = 0; i < data.length; i++) {
const currentItem: { [key: string]: object; } = data[i as number];
// push the given data to main data array
dataSource = this.listViewInstance.addItemAtIndex(index, dataSource, currentItem);
// recalculate all the group data or other datasource related things
this.listViewInstance.setViewDataSource(dataSource);
// render list items for first time due to no datasource present earlier
if (!this.domItemCount) {
// fresh rendering for first time
if ((this.listViewInstance.template || this.listViewInstance.groupTemplate) && !this.isNgTemplate()) {
this.listViewInstance.listBaseOption.template = null;
this.listViewInstance.listBaseOption.groupTemplate = null;
this.listViewInstance.listBaseOption.itemCreated = this.createUIItem.bind(this);
}
this.uiVirtualization();
// when expected expected DOM count doesn't meet the condition we need to create and inject new item into DOM
} else if (this.domItemCount < this.expectedDomItemCount) {
const ds: DataSource = this.listViewInstance.findItemFromDS(dataSource, fields);
if (ds instanceof Array) {
if (this.listViewInstance.ulElement) {
let index: number = (this.listViewInstance.curViewDS as DataSource[]).indexOf(currentItem);
// inject new list item into DOM
this.createAndInjectNewItem(currentItem, index);
// check for group header item
const curViewDS: DataSource =
(this.listViewInstance.curViewDS as DataSource[])[index - 1];
if (curViewDS && curViewDS.isHeader && (curViewDS.items as DataSource[]).length === 1) {
// target group item index in datasource
--index;
// inject new group header into DOM for previously created list item
this.createAndInjectNewItem(curViewDS, index);
}
}
// recollect all the list item into collection
this.listViewInstance.liCollection =
<HTMLElement[] & NodeListOf<HTMLLIElement>>this.listViewInstance.curUL.querySelectorAll('li');
}
} else {
const index: number = (this.listViewInstance.curViewDS as { [key: string]: object; }[]).indexOf(currentItem);
// virtually new add list item based on the scollbar position
this.addUiItem(index);
// check for group header item needs to be added
const curViewDS: DataSource = (this.listViewInstance.curViewDS as DataSource[])[index - 1];
if (curViewDS && curViewDS.isHeader && (curViewDS.items as DataSource[]).length === 1) {
this.addUiItem(index - 1);
}
}
}
}
private createAndInjectNewItem(itemData: DataSource, index: number): void {
// generate li item for given datasource
let target: HTMLElement;
const li: HTMLElement[] = ListBase.createListItemFromJson(
this.listViewInstance.createElement,
[itemData] as {[key: string]: object}[],
this.listViewInstance.listBaseOption,
null,
null,
this.listViewInstance);
// check for target element whether to insert before last item or group item
if ((Object.keys(this.listViewInstance.curViewDS).length - 1) === index) {
target = this.listViewInstance.curUL.lastElementChild as HTMLElement;
} else {
// target group header's first child item to append its header
target = this.listViewInstance.getLiFromObjOrElement(this.listViewInstance.curViewDS[index + 1]) ||
this.listViewInstance.getLiFromObjOrElement(this.listViewInstance.curViewDS[index + 2]);
}
if (this.listViewInstance.fields.groupBy
&& this.listViewInstance.curViewDS[index + 1]
&& this.listViewInstance.curViewDS[index + 1].isHeader) {
const targetEle: HTMLElement = this.listViewInstance.getLiFromObjOrElement(this.listViewInstance.curViewDS[index - 1]);
if (targetEle) {
target = targetEle.nextElementSibling as HTMLElement;
}
}
// insert before the target element
this.listViewInstance.ulElement.insertBefore(li[0], target);
// increment internal DOM count, last index count for new element
this.domItemCount++;
if (this.bottomElementHeight <= 0) {
this.uiLastIndex++;
}
// recalculate the current item height, to avoid jumpy scroller
this.refreshItemHeight();
}
public createUIItem(args: ItemCreatedArgs): void {
if (!args.item.classList.contains('e-list-group-item')) {
this.templateData = args.curData.isHeader ? (args.curData as { [key: string]: object[]; }).items[0] as DataSource :
args.curData;
if (this.listViewInstance.showCheckBox) {
(this.listViewInstance as any).renderCheckbox(args);
if ((!isNullOrUndefined(this.listViewInstance.virtualCheckBox)) &&
(!isNullOrUndefined(this.listViewInstance.virtualCheckBox.outerHTML))) {
const div: HTMLElement = document.createElement('div');
const commonTemplate: string = '<div class="e-text-content" role="presentation"> ' +
'<span class="e-list-text"> ${' + this.listViewInstance.fields.text + '} </span></div>';
const templateFunction: Function = compile(this.listViewInstance.template || commonTemplate, this.listViewInstance);
const nodes: NodeList = templateFunction(this.templateData, this.listViewInstance);
if (this.listViewInstance.template && this.listViewInstance.isReact) {
this.listViewInstance.renderReactTemplates();
}
[].slice.call(nodes).forEach((ele: HTMLElement): void => {
div.appendChild(ele);
});
if (div.children && div.children[0]) {
div.children[0].classList.add('e-checkbox');
if (this.listViewInstance.checkBoxPosition === 'Left') {
div.children[0].classList.add('e-checkbox-left');
}
else {
div.children[0].classList.add('e-checkbox-right');
}
if (this.listViewInstance.checkBoxPosition === 'Left') {
div.children[0].insertBefore(
this.listViewInstance.virtualCheckBox,
(div.childNodes[0] as HTMLElement).children[0]
);
} else {
div.children[0].appendChild(this.listViewInstance.virtualCheckBox);
}
while (args.item.lastChild) {
args.item.removeChild(args.item.lastChild);
}
[].slice.call(div.children).forEach((ele: HTMLElement): void => {
args.item.appendChild(ele);
});
}
}
}
}
}
public reRenderUiVirtualization(): void {
this.wireScrollEvent(true);
if (this.listViewInstance.contentContainer) {
detach(this.listViewInstance.contentContainer);
}
this.listViewInstance.preRender();
// resetting the dom count to 0, to avoid edge case of dataSource suddenly becoming zero
// and then manually adding item using addItem API
this.domItemCount = 0;
this.listViewInstance.header();
this.listViewInstance.setLocalData();
}
private updateUI(element: HTMLElement, index: number, targetElement?: HTMLElement): void {
const onChange: Function = this.isNgTemplate() ? this.onNgChange : this.onChange;
if (this.listViewInstance.template || this.listViewInstance.groupTemplate) {
const curViewDS: DataSource = (this.listViewInstance.curViewDS as DataSource[])[index as number];
element.dataset.uid = (curViewDS[this.listViewInstance.fields.id]) as any ?
(curViewDS[this.listViewInstance.fields.id]) as any : ListBase.generateId() as any;
onChange(curViewDS, element, this);
} else {
this.updateUiContent(element, index);
}
this.changeElementAttributes(element, index);
if (targetElement) {
this.listViewInstance.ulElement.insertBefore(element, targetElement);
}
}
/**
* Handles the UI change in vue for the list view.
*
* @param {DataSource} newData - The new data source for the list view.
* @param {ElementContext} listElement - The HTML element context for the list view.
* @param {Virtualization} virtualThis - The virtualization context for the list view.
* @returns {void}
*/
private onChange(newData: DataSource, listElement: ElementContext, virtualThis: Virtualization): void {
const liItem: HTMLElement[] = ListBase.createListItemFromJson(virtualThis.listViewInstance.