@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.
1,241 lines (1,164 loc) • 114 kB
text/typescript
import { Virtualization } from './virtualization';
import { merge, formatUnit, isNullOrUndefined, append, detach, ModuleDeclaration, extend } from '@syncfusion/ej2-base';
import { attributes, addClass, removeClass, prepend, closest, remove } from '@syncfusion/ej2-base';
import { Component, EventHandler, BaseEventArgs, Property, Complex, Event } from '@syncfusion/ej2-base';
import { NotifyPropertyChanges, INotifyPropertyChanged, ChildProperty } from '@syncfusion/ej2-base';
import { KeyboardEventArgs, EmitType, compile, SanitizeHtmlHelper } from '@syncfusion/ej2-base';
import { Animation, AnimationOptions, Effect, rippleEffect, Touch, SwipeEventArgs, animationMode } from '@syncfusion/ej2-base';
import { DataManager, Query } from '@syncfusion/ej2-data';
import { createCheckBox } from '@syncfusion/ej2-buttons';
import { ListBase, ListBaseOptions, SortOrder, getFieldValues, FieldsMapping } from '../common/list-base';
import { ListViewModel, FieldSettingsModel } from './list-view-model';
/* eslint-disable @typescript-eslint/no-explicit-any */
// Effect Configuration Effect[] = [fromViewBackward,fromViewForward,toViewBackward,toviewForward];
const effectsConfig: { [key: string]: Effect[] } = {
'None': [],
'SlideLeft': ['SlideRightOut', 'SlideLeftOut', 'SlideLeftIn', 'SlideRightIn'],
'SlideDown': ['SlideTopOut', 'SlideBottomOut', 'SlideBottomIn', 'SlideTopIn'],
'Zoom': ['FadeOut', 'FadeZoomOut', 'FadeZoomIn', 'FadeIn'],
'Fade': ['FadeOut', 'FadeOut', 'FadeIn', 'FadeIn']
};
const effectsRTLConfig: { [key: string]: Effect[] } = {
'None': [],
'SlideLeft': ['SlideLeftOut', 'SlideRightOut', 'SlideRightIn', 'SlideLeftIn'],
'SlideDown': ['SlideBottomOut', 'SlideTopOut', 'SlideTopIn', 'SlideBottomIn'],
'Zoom': ['FadeZoomOut', 'FadeOut', 'FadeIn', 'FadeZoomIn'],
'Fade': ['FadeOut', 'FadeOut', 'FadeIn', 'FadeIn']
};
// don't use space in classnames.
export const classNames: ClassNames = {
root: 'e-listview',
hover: 'e-hover',
selected: 'e-active',
focused: 'e-focused',
parentItem: 'e-list-parent',
listItem: 'e-list-item',
listIcon: 'e-list-icon',
textContent: 'e-text-content',
listItemText: 'e-list-text',
groupListItem: 'e-list-group-item',
hasChild: 'e-has-child',
view: 'e-view',
header: 'e-list-header',
headerText: 'e-headertext',
headerTemplateText: 'e-headertemplate-text',
text: 'e-text',
disable: 'e-disabled',
container: 'e-list-container',
icon: 'e-icons',
backIcon: 'e-icon-back',
backButton: 'e-back-button',
checkboxWrapper: 'e-checkbox-wrapper',
checkbox: 'e-checkbox',
checked: 'e-check',
checklist: 'e-checklist',
checkboxIcon: 'e-frame',
checkboxRight: 'e-checkbox-right',
checkboxLeft: 'e-checkbox-left',
listviewCheckbox: 'e-listview-checkbox',
itemCheckList: 'e-checklist',
virtualElementContainer: 'e-list-virtualcontainer'
};
const LISTVIEW_TEMPLATE_PROPERTY: string = 'Template';
const LISTVIEW_GROUPTEMPLATE_PROPERTY: string = 'GroupTemplate';
const LISTVIEW_HEADERTEMPLATE_PROPERTY: string = 'HeaderTemplate';
const swipeVelocity: number = 0.5;
/**
* An interface that holds options of fields.
*/
export interface Fields {
/**
* Specifies the id field mapped in dataSource.
*/
id?: string | number;
/**
* The `text` property is used to map the text value from the data source for each list item.
*/
text?: string | number;
/**
* It is used to map the custom field values of list item from the dataSource.
*/
[key: string]: Object | string | number | undefined;
}
/**
* Represents the field settings of the ListView.
*/
export class FieldSettings extends ChildProperty<FieldSettings> {
/**
* Specifies the id field mapped in dataSource.
*/
@Property('id')
public id: string;
/**
* The `text` property is used to map the text value from the data source for each list item.
*/
@Property('text')
public text: string;
/**
* The `isChecked` property is used to check whether the list items are in checked state or not.
*/
@Property('isChecked')
public isChecked: string;
/**
* The `isVisible` property is used to check whether the list items are in visible state or not.
*/
@Property('isVisible')
public isVisible: string;
/**
* Specifies the enabled state of the ListView component.
* And, we can disable the component using this property by setting its value as false.
*/
@Property('enabled')
public enabled: string;
/**
* The `iconCss` is used to customize the icon to the list items dynamically.
* We can add a specific image to the icons using `iconCss` property.
*/
@Property('iconCss')
public iconCss: string;
/**
* The `child` property is used for nested navigation of listed items.
*/
@Property('child')
public child: string;
/**
* The `tooltip` is used to display the information about the target element while hovering on list items.
*/
@Property('tooltip')
public tooltip: string;
/**
* The `groupBy` property is used to wraps the ListView elements into a group.
*/
@Property('groupBy')
public groupBy: string;
/**
* The `sortBy` property used to enable the sorting of list items to be ascending or descending order.
*/
@Property('text')
public sortBy: string;
/**
* The `htmlAttributes` allows additional attributes such as id, class, etc., and
* accepts n number of attributes in a key-value pair format.
*/
@Property('htmlAttributes')
public htmlAttributes: string;
/**
* Specifies the `tableName` used to fetch data from a specific table in the server.
*/
@Property('tableName')
public tableName: string;
}
/**
* An interface that holds animation settings.
*/
export interface AnimationSettings {
/**
* It is used to specify the effect which is shown in sub list transform.
*/
effect?: ListViewEffect;
/**
* It is used to specify the time duration of transform object.
*/
duration?: number;
/**
* It is used to specify the easing effect applied while transform
*/
easing?: string;
}
/**
* An enum type that denotes the effects of the ListView. Available options are as follows None, SlideLeft, SlideDown, Zoom, Fade;
* ```props
* None :- No animation is applied when items are added or removed from the ListView.
* SlideLeft :- Items slide in from the left when added and slide out to the left when removed.
* SlideDown :- Items slide in from the top when added and slide out to the top when removed.
* Zoom :- Items zoom in or out when added or removed.
* Fade :- Items fade in or out when added or removed.
* ```
*/
export type ListViewEffect = 'None' | 'SlideLeft' | 'SlideDown' | 'Zoom' | 'Fade';
/**
* An enum type that denotes the position of checkbox of the ListView. Available options are as follows Left and Right;
* ```props
* Left :- The checkbox is positioned on the left side of the ListView item.
* Right :- The checkbox is positioned on the right side of the ListView item.
* ```
*/
export type checkBoxPosition = 'Left' | 'Right';
/**
* Represents the EJ2 ListView control.
* ```html
* <div id="listview">
* <ul>
* <li>Favorite</li>
* <li>Documents</li>
* <li>Downloads</li>
* </ul>
* </div>
* ```
* ```typescript
* var listviewObject = new ListView({});
* listviewObject.appendTo("#listview");
* ```
*/
@NotifyPropertyChanges
export class ListView extends Component<HTMLElement> implements INotifyPropertyChanged {
private ulElement: HTMLElement;
private selectedLI: Element;
private onUIScrolled: Function;
private curUL: HTMLElement;
private curDSLevel: string[];
private curViewDS: DataSource[] | string[] | number[];
private curDSJSON: DataSource;
public localData: DataSource[];
private liCollection: HTMLElement[];
private headerEle: HTMLElement;
private contentContainer: HTMLElement;
private touchModule: Touch;
private listBaseOption: ListBaseOptions;
public virtualizationModule: Virtualization;
private animateOptions: AnimationOptions;
private rippleFn: Function;
private isNestedList: boolean;
private currentLiElements: HTMLElement[];
private selectedData: string[] | string;
private selectedId: string[];
private isWindow: boolean;
private selectedItems: SelectedItem;
private aniObj: Animation;
private LISTVIEW_TEMPLATE_ID: string;
private LISTVIEW_GROUPTEMPLATE_ID: string;
private LISTVIEW_HEADERTEMPLATE_ID: string;
private liElement: Element;
private virtualCheckBox: Element | string;
private liElementHeight: number;
private previousSelectedItems: string[] = [];
private hiddenItems: string[] = [];
private enabledItems: string[] = [];
private disabledItems: string[] = [];
private isOffline: boolean;
private previousScrollTop: number;
/**
* The `cssClass` property is used to add a user-preferred class name in the root element of the ListView,
* using which we can customize the component (both CSS and functionality customization)
*
* {% codeBlock src='listview/cssClass/index.md' %}{% endcodeBlock %}
*
* @default ''
*/
@Property('')
public cssClass: string;
/**
* If `enableVirtualization` set to true, which will increase the ListView performance, while loading a large amount of data.
*
* {% codeBlock src='listview/enableVirtualization/index.md' %}{% endcodeBlock %}
*
* @default false
*/
@Property(false)
public enableVirtualization: boolean;
/**
* The `htmlAttributes` allows additional attributes such as id, class, etc., and
* accepts n number of attributes in a key-value pair format.
*
* {% codeBlock src='listview/htmlAttributes/index.md' %}{% endcodeBlock %}
*
* @default {}
*/
@Property({})
public htmlAttributes: { [key: string]: string; };
/**
* If `enable` set to true, the list items are enabled.
* And, we can disable the component using this property by setting its value as false.
*
* {% codeBlock src='listview/enable/index.md' %}{% endcodeBlock %}
*
* @default true
*/
@Property(true)
public enable: boolean;
/* es-lint disable */
/**
* The `dataSource` provides the data to render the ListView component which is mapped with the fields of ListView.
*
* @isGenericType true
*
* {% codeBlock src='listview/dataSource/index.md' %}{% endcodeBlock %}
*
* @default []
*/
@Property([])
public dataSource: { [key: string]: Object }[] | string[] | number[] | DataManager;
/* es-lint enable */
/**
* The `query` is used to fetch the specific data from dataSource by using where and select keywords.
*
* {% codeBlock src='listview/query/index.md' %}{% endcodeBlock %}
*
* @default null
*/
@Property()
public query: Query;
/**
* The `fields` is used to map keys from the dataSource which extracts the appropriate data from the dataSource
* with specified mapped with the column fields to render the ListView.
*
* {% codeBlock src='listview/fields/index.md' %}{% endcodeBlock %}
*
* @default defaultMappedFields
*/
@Complex<FieldSettingsModel>(ListBase.defaultMappedFields, FieldSettings)
public fields: FieldSettingsModel;
/**
* The `animation` property provides an option to apply the different
* animations on the ListView component.
*
* {% codeBlock src='listview/animation/index.md' %}{% endcodeBlock %}
*
*
* @default { effect: 'SlideLeft', duration: 400, easing: 'ease' }
*/
@Property<AnimationSettings>({ effect: 'SlideLeft', duration: 400, easing: 'ease' })
public animation: AnimationSettings;
/**
* The `sortOrder` is used to sort the data source. The available type of sort orders are,
* * `None` - The data source is not sorting.
* * `Ascending` - The data source is sorting with ascending order.
* * `Descending` - The data source is sorting with descending order.
*
* {% codeBlock src='listview/sortOrder/index.md' %}{% endcodeBlock %}
*
* @default 'None'
*/
@Property<SortOrder>('None')
public sortOrder: SortOrder;
/**
* If `showIcon` set to true, which will show or hide the icon of the list item.
*
* {% codeBlock src='listview/showIcon/index.md' %}{% endcodeBlock %}
*
* @default false
*/
@Property<boolean>(false)
public showIcon: boolean;
/**
* If `showCheckBox` set to true, which will show or hide the checkbox.
*
* {% codeBlock src='listview/showCheckBox/index.md' %}{% endcodeBlock %}
*
*
* @default false
*/
@Property<boolean>(false)
public showCheckBox: boolean;
/**
* The `checkBoxPosition` is used to set the position of check box in a list item.
* By default, the `checkBoxPosition` is Left, which will appear before the text content in a list item.
*
* {% codeBlock src='listview/checkBoxPosition/index.md' %}{% endcodeBlock %}
*
* @default 'Left'
*/
@Property<string>('Left')
public checkBoxPosition: checkBoxPosition;
/**
* The `headerTitle` is used to set the title of the ListView component.
*
* {% codeBlock src='listview/headerTitle/index.md' %}{% endcodeBlock %}
*
*
* @default ""
*/
@Property<string>('')
public headerTitle: string;
/**
* If `showHeader` set to true, which will show or hide the header of the ListView component.
*
* {% codeBlock src='listview/showHeader/index.md' %}{% endcodeBlock %}
*
* @default false
*/
@Property<boolean>(false)
public showHeader: boolean;
/**
* Specifies whether to display or remove the untrusted HTML values in the ListView component.
* If 'enableHtmlSanitizer' set to true, the component will sanitize any suspected untrusted strings and scripts before rendering them.
*
* {% codeBlock src='listview/enableHtmlSanitizer/index.md' %}{% endcodeBlock %}
*
* @default true
*/
@Property(true)
public enableHtmlSanitizer: boolean;
/**
* Defines the height of the ListView component which accepts both string and number values.
*
* {% codeBlock src='listview/height/index.md' %}{% endcodeBlock %}
*
* @default ''
*/
@Property('')
public height: number | string;
/**
* Defines the width of the ListView component which accepts both string and number values.
*
* {% codeBlock src='listview/width/index.md' %}{% endcodeBlock %}
*
* @default ''
*/
@Property('')
public width: number | string;
/**
* The ListView component supports to customize the content of each list items with the help of `template` property.
*
* {% codeBlock src='listview/template/index.md' %}{% endcodeBlock %}
*
* @default null
* @angularType string | object
* @reactType string | function | JSX.Element
* @vueType string | function
* @aspType string
*/
@Property(null)
public template: string | Function;
/**
* The ListView has an option to custom design the ListView header title with the help of `headerTemplate` property.
*
* {% codeBlock src="listview/headerTemplate/index.md" %}{% endcodeBlock %}
*
* @default null
* @angularType string | object
* @reactType string | function | JSX.Element
* @vueType string | function
* @aspType string
*/
@Property(null)
public headerTemplate: string | Function;
/**
* The ListView has an option to custom design the group header title with the help of `groupTemplate` property.
*
* {% codeBlock src="listview/groupTemplate/index.md" %}{% endcodeBlock %}
*
* @default null
* @angularType string | object
* @reactType string | function | JSX.Element
* @vueType string | function
* @aspType string
*/
@Property(null)
public groupTemplate: string | Function;
/**
* Triggers when we select the list item in the component.
*
* @event 'object'
*/
@Event()
public select: EmitType<SelectEventArgs>;
/**
* Triggers when every ListView action starts.
*
* @event 'object'
*/
@Event()
public actionBegin: EmitType<object>;
/**
* Triggers when every ListView actions completed.
*
* @event 'object'
*/
/* es-lint disable */
@Event()
public actionComplete: EmitType<MouseEvent>;
/* es-lint enable */
/**
* Triggers, when the data fetch request from the remote server, fails.
*
* @event 'object'
*
*/
@Event()
public actionFailure: EmitType<MouseEvent>;
/**
* Triggers when scrollbar of the ListView component reaches to the top or bottom.
*
* @event 'object'
*/
@Event()
public scroll: EmitType<ScrolledEventArgs>;
/**
* Constructor for creating the widget
*
* @param options
*
* @param element
*/
constructor(options?: ListViewModel, element?: string | HTMLElement) {
super(options, <HTMLElement | string>element);
}
/**
* @param newProp
*
* @param oldProp
*
* @private
*/
public onPropertyChanged(newProp: ListViewModel, oldProp: ListViewModel): void {
for (const prop of Object.keys(newProp)) {
switch (prop) {
case 'htmlAttributes':
this.setHTMLAttribute();
break;
case 'cssClass':
this.setCSSClass(oldProp.cssClass);
break;
case 'enable':
this.setEnable();
break;
case 'width':
case 'height':
this.setSize();
break;
case 'enableRtl':
this.setEnableRTL();
break;
case 'fields':
this.listBaseOption.fields = (this.fields as FieldSettingsModel & { properties: object }).properties;
if (this.enableVirtualization) {
this.virtualizationModule.reRenderUiVirtualization();
} else {
this.reRender();
}
break;
case 'headerTitle':
if (!this.curDSLevel.length) {
this.header(this.headerTitle, false, 'header');
}
break;
case 'query':
if (this.enableVirtualization) {
this.virtualizationModule.reRenderUiVirtualization();
} else {
this.reRender();
}
break;
case 'showHeader':
this.header(this.headerTitle, false, 'header');
break;
case 'enableVirtualization':
if (!isNullOrUndefined(this.contentContainer)) {
detach(this.contentContainer);
}
this.refresh();
break;
case 'showCheckBox':
case 'checkBoxPosition':
if (this.enableVirtualization) {
this.virtualizationModule.reRenderUiVirtualization();
} else {
this.setCheckbox();
}
break;
case 'dataSource':
this.previousScrollTop = this.element.scrollTop;
if (this.enableVirtualization) {
this.virtualizationModule.reRenderUiVirtualization();
} else {
this.reRender();
}
break;
case 'sortOrder':
case 'template':
if (!this.enableVirtualization) {
this.refresh();
}
break;
case 'showIcon':
if (this.enableVirtualization) {
this.virtualizationModule.reRenderUiVirtualization();
} else {
this.listBaseOption.showIcon = this.showIcon;
this.curViewDS = this.getSubDS();
this.resetCurrentList();
}
break;
default:
break;
}
}
}
// Model Changes
private setHTMLAttribute(): void {
if (!isNullOrUndefined(this.htmlAttributes) && Object.keys(this.htmlAttributes).length) {
attributes(this.element, this.htmlAttributes);
}
}
private setCSSClass(oldCSSClass?: string): void {
if (this.cssClass) {
addClass([this.element], this.cssClass.split(' ').filter((css: string) => css));
}
if (oldCSSClass) {
removeClass([this.element], oldCSSClass.split(' ').filter((css: string) => css));
}
}
private setSize(): void {
this.element.style.height = formatUnit(this.height);
this.element.style.width = formatUnit(this.width);
this.isWindow = this.element.clientHeight ? false : true;
}
private setEnable(): void {
this.enableElement(this.element, this.enable);
}
private setEnableRTL(): void {
if (this.enableRtl) {
this.element.classList.add('e-rtl');
} else {
this.element.classList.remove('e-rtl');
}
}
private enableElement(element: HTMLElement, isEnabled?: boolean): void {
if (isEnabled) {
element.classList.remove(classNames.disable);
} else {
element.classList.add(classNames.disable);
}
}
// Support Component Functions
private header(text?: string, showBack?: boolean, prop?: string): void {
if (this.headerEle === undefined && this.showHeader) {
this.headerEle = this.createElement('div', { className: classNames.header });
const innerHeaderEle: HTMLElement = this.createElement('span', { className: classNames.headerText });
if (this.enableHtmlSanitizer) {
this.setProperties({ headerTitle: SanitizeHtmlHelper.sanitize(this.headerTitle) }, true);
innerHeaderEle.innerText = this.headerTitle;
} else {
innerHeaderEle.innerHTML = this.headerTitle;
}
const textEle: HTMLElement = this.createElement('div', { className: classNames.text, innerHTML: innerHeaderEle.outerHTML });
const hedBackButton: HTMLElement = this.createElement('div', {
className: classNames.icon + ' ' + classNames.backIcon + ' ' + classNames.backButton,
attrs: { style: 'display:none;' }
});
this.headerEle.appendChild(hedBackButton);
this.headerEle.appendChild(textEle);
if (this.headerTemplate) {
const compiledString: Function = compile(this.headerTemplate);
const headerTemplateEle: HTMLElement = this.createElement('div', { className: classNames.headerTemplateText });
const compiledElement: HTMLElement[] =
compiledString({}, this, prop, this.LISTVIEW_HEADERTEMPLATE_ID, null, null, this.headerEle);
if (compiledElement) {
append(compiledElement, headerTemplateEle);
}
append([headerTemplateEle], this.headerEle);
if ((this as any).isReact) {
this.renderReactTemplates();
}
}
if (this.headerTemplate && this.headerTitle) {
textEle.classList.add('header');
}
this.element.classList.add('e-has-header');
prepend([this.headerEle], this.element);
} else if (this.headerEle) {
if (this.showHeader) {
this.headerEle.style.display = '';
const textEle: Element = this.headerEle.querySelector('.' + classNames.headerText);
const hedBackButton: Element = this.headerEle.querySelector('.' + classNames.backIcon);
if (this.enableHtmlSanitizer) {
text = SanitizeHtmlHelper.sanitize(text);
}
textEle.innerHTML = text;
if (this.headerTemplate && showBack) {
textEle.parentElement.classList.remove('header');
this.headerEle.querySelector('.' + classNames.headerTemplateText).classList.add('nested-header');
}
if (this.headerTemplate && !showBack) {
textEle.parentElement.classList.add('header');
this.headerEle.querySelector('.' + classNames.headerTemplateText).classList.remove('nested-header');
this.headerEle.querySelector('.' + classNames.headerTemplateText).classList.add('header');
}
if (showBack === true) {
(hedBackButton as HTMLElement).style.display = '';
} else {
(hedBackButton as HTMLElement).style.display = 'none';
}
} else {
this.headerEle.style.display = 'none';
}
}
}
// Animation Related Functions
private switchView(fromView: HTMLElement, toView: HTMLElement, reverse?: boolean): void {
if (fromView && toView) {
const fPos: string = fromView.style.position;
const overflow: string = (this.element.style.overflow !== 'hidden') ? this.element.style.overflow : '';
fromView.style.position = 'absolute';
fromView.classList.add('e-view');
let anim: Effect[];
let duration: number = this.animation.duration;
if (this.animation.effect) {
anim = (this.enableRtl ? effectsRTLConfig[this.animation.effect] : effectsConfig[this.animation.effect]);
} else {
const slideLeft: string = 'SlideLeft';
anim = effectsConfig[`${slideLeft}`];
reverse = this.enableRtl;
duration = 0;
}
this.element.style.overflow = 'hidden';
this.aniObj.animate(fromView, {
name: (reverse === true ? anim[0] : anim[1]),
duration: (duration === 0 && animationMode === 'Enable') ? 400 : duration,
timingFunction: this.animation.easing,
end: (): void => {
fromView.style.display = 'none';
this.element.style.overflow = overflow;
fromView.style.position = fPos;
fromView.classList.remove('e-view');
}
});
toView.style.display = '';
this.aniObj.animate(toView, {
name: (reverse === true ? anim[2] : anim[3]),
duration: (duration === 0 && animationMode === 'Enable') ? 400 : duration,
timingFunction: this.animation.easing,
end: (): void => {
this.trigger('actionComplete');
}
});
this.curUL = toView;
}
}
protected preRender(): void {
if (this.template) {
try {
if (typeof this.template !== 'function' && document.querySelectorAll(this.template).length) {
this.setProperties({ template: document.querySelector(this.template).innerHTML.trim() }, true);
}
} catch (e) {
compile(this.template);
}
}
this.listBaseOption = {
template: this.template,
headerTemplate: this.headerTemplate,
groupTemplate: this.groupTemplate, expandCollapse: true, listClass: '',
ariaAttributes: {
itemRole: 'listitem', listRole: 'list', itemText: '',
groupItemRole: 'presentation', wrapperRole: 'presentation'
},
fields: ((this.fields as FieldSettingsModel & { properties: Object }).properties) as FieldsMapping,
sortOrder: this.sortOrder,
showIcon: this.showIcon,
itemCreated: this.renderCheckbox.bind(this),
templateID: `${this.element.id}${LISTVIEW_TEMPLATE_PROPERTY}`,
groupTemplateID: `${this.element.id}${LISTVIEW_GROUPTEMPLATE_PROPERTY}`,
enableHtmlSanitizer: this.enableHtmlSanitizer
};
this.initialization();
}
private initialization(): void {
this.curDSLevel = [];
this.animateOptions = {};
this.curViewDS = [];
this.currentLiElements = [];
this.isNestedList = false;
this.selectedData = [];
this.selectedId = this.enablePersistence ? this.selectedId : [];
this.LISTVIEW_TEMPLATE_ID = `${this.element.id}${LISTVIEW_TEMPLATE_PROPERTY}`;
this.LISTVIEW_GROUPTEMPLATE_ID = `${this.element.id}${LISTVIEW_GROUPTEMPLATE_PROPERTY}`;
this.LISTVIEW_HEADERTEMPLATE_ID = `${this.element.id}${LISTVIEW_HEADERTEMPLATE_PROPERTY}`;
this.aniObj = new Animation(this.animateOptions);
this.removeElement(this.curUL);
this.removeElement(this.ulElement);
this.removeElement(this.headerEle);
this.removeElement(this.contentContainer);
this.curUL = this.ulElement = this.liCollection = this.headerEle = this.contentContainer = undefined;
}
private renderCheckbox(args: ItemCreatedArgs): void {
if (args.item.classList.contains(classNames.hasChild)) {
this.isNestedList = true;
}
if (this.showCheckBox && args.item.classList.contains(classNames.listItem)) {
let fieldData: DataSource;
const checkboxElement: Element = createCheckBox(this.createElement, false, {
checked: false, enableRtl: this.enableRtl,
cssClass: classNames.listviewCheckbox
});
checkboxElement.setAttribute('role', 'checkbox');
const frameElement: Element = checkboxElement.querySelector('.' + classNames.checkboxIcon);
args.item.classList.add(classNames.itemCheckList);
args.item.firstElementChild.classList.add(classNames.checkbox);
if (typeof (this.dataSource as string[])[0] !== 'string' && typeof (this.dataSource as number[])[0] !== 'number') {
fieldData = <DataSource>getFieldValues(args.curData, this.listBaseOption.fields);
if (this.enablePersistence && !isNullOrUndefined(this.selectedId)) {
const index: number = this.selectedId.findIndex(
(e: string) => e === fieldData[this.listBaseOption.fields.id].toString()
);
if (index !== -1) {
this.checkInternally(args, checkboxElement);
}
}
else if (<object>fieldData[this.listBaseOption.fields.isChecked]) {
this.checkInternally(args, checkboxElement);
}
} else if (((typeof (this.dataSource as string[])[0] === 'string' ||
typeof (this.dataSource as number[])[0] === 'number') && this.selectedData.indexOf(args.text) !== -1)) {
this.checkInternally(args, checkboxElement);
}
checkboxElement.setAttribute('aria-checked', frameElement.classList.contains(classNames.checked) ? 'true' : 'false');
checkboxElement.setAttribute('aria-label', args.text);
if (this.checkBoxPosition === 'Left') {
checkboxElement.classList.add(classNames.checkboxLeft);
args.item.firstElementChild.classList.add(classNames.checkboxLeft);
args.item.firstElementChild.insertBefore(checkboxElement, args.item.firstElementChild.childNodes[0]);
} else {
checkboxElement.classList.add(classNames.checkboxRight);
args.item.firstElementChild.classList.add(classNames.checkboxRight);
args.item.firstElementChild.appendChild(checkboxElement);
}
this.currentLiElements.push(args.item);
if (this.checkBoxPosition === 'Left') {
this.virtualCheckBox = args.item.firstElementChild.children[0];
}
else {
this.virtualCheckBox = args.item.firstElementChild.lastElementChild;
}
}
}
private checkInternally(args: ItemCreatedArgs, checkboxElement: Element): void {
args.item.classList.add(classNames.selected);
checkboxElement.querySelector('.' + classNames.checkboxIcon).classList.add(classNames.checked);
checkboxElement.setAttribute('aria-checked', 'true');
}
/**
* Checks the specific list item by passing the unchecked fields as an argument to this method.
*
* @param {Fields | HTMLElement | Element} item - It accepts Fields or HTML list element as an argument.
*/
public checkItem(item: Fields | HTMLElement | Element): void {
this.toggleCheckBase(item, true);
}
private toggleCheckBase(item: Fields | Element | HTMLElement, checked: boolean): void {
if (this.showCheckBox) {
let liElement: Element = item as Element;
if (item instanceof Object && (item as Object).constructor !== HTMLLIElement) {
liElement = this.getLiFromObjOrElement(item);
}
if (!isNullOrUndefined(liElement)) {
const checkboxIcon: Element = liElement.querySelector('.' + classNames.checkboxIcon);
if (checked === true) {
liElement.classList.add(classNames.selected);
}
else {
liElement.classList.remove(classNames.selected);
}
if (checked === true) {
checkboxIcon.classList.add(classNames.checked);
}
else {
checkboxIcon.classList.remove(classNames.checked);
}
checkboxIcon.parentElement.setAttribute('aria-checked', checked ? 'true' : 'false');
}
this.setSelectedItemData(liElement);
this.updateSelectedId();
}
}
/**
* Uncheck the specific list item by passing the checked fields as an argument to this method.
*
* @param {Fields | HTMLElement | Element} item - It accepts Fields or HTML list element as an argument.
*/
public uncheckItem(item: Fields | HTMLElement | Element): void {
this.toggleCheckBase(item, false);
}
/**
* Checks all the unchecked items in the ListView.
*/
public checkAllItems(): void {
this.toggleAllCheckBase(true);
}
/**
* Uncheck all the checked items in ListView.
*/
public uncheckAllItems(): void {
this.toggleAllCheckBase(false);
}
private toggleAllCheckBase(checked: boolean): void {
if (this.showCheckBox) {
for (let i: number = 0; i < this.liCollection.length; i++) {
const checkIcon: Element = this.liCollection[i as number].querySelector('.' + classNames.checkboxIcon);
if (checkIcon) {
if (checked) {
if (!checkIcon.classList.contains(classNames.checked)) {
this.checkItem(this.liCollection[i as number]);
}
} else {
if (checkIcon.classList.contains(classNames.checked)) {
this.uncheckItem(this.liCollection[i as number]);
}
}
}
}
if (this.enableVirtualization) {
this.virtualizationModule.checkedItem(checked);
}
this.updateSelectedId();
}
}
private setCheckbox(): void {
if (this.showCheckBox) {
const liCollection: HTMLElement[] = Array.prototype.slice.call(this.element.querySelectorAll('.' + classNames.listItem));
const args: ItemCreatedArgs = {
item: undefined, curData: undefined, dataSource: undefined, fields: undefined,
options: undefined, text: ''
};
for (let i: number = 0; i < liCollection.length; i++) {
const element: HTMLElement = liCollection[i as number];
args.item = element;
args.curData = this.getItemData(element) as { [key: string]: Object };
if (element.querySelector('.' + classNames.checkboxWrapper)) {
this.removeElement(element.querySelector('.' + classNames.checkboxWrapper));
}
this.renderCheckbox(args);
if (args.item.classList.contains(classNames.selected)) {
this.checkInternally(args, args.item.querySelector('.' + classNames.checkboxWrapper));
}
}
} else {
const liCollection: HTMLElement[] = Array.prototype.slice.call(this.element.querySelectorAll('.' + classNames.itemCheckList));
for (let i: number = 0; i < liCollection.length; i++) {
const element: HTMLElement = liCollection[i as number];
element.classList.remove(classNames.selected);
element.firstElementChild.classList.remove(classNames.checkbox);
this.removeElement(element.querySelector('.' + classNames.checkboxWrapper));
}
if (this.selectedItems) {
this.selectedItems.item.classList.add(classNames.selected);
}
}
}
/**
* Refresh the height of the list item only on enabling the virtualization property.
*/
public refreshItemHeight(): void {
if (this.virtualizationModule) {
this.virtualizationModule.refreshItemHeight();
}
}
private handleCheckboxState(
li: Element, checkIcon: Element, checkboxElement: Element,
isCheckedBefore: boolean, isFocusedBefore: boolean, eventArgs: Object,
isSetCheckboxLI: boolean, textAreaFocus?: HTMLTextAreaElement | HTMLInputElement
): void {
this.trigger('select', eventArgs, (observedArgs: SelectEventArgs) => {
if (observedArgs.cancel) {
if (isSetCheckboxLI ? isCheckedBefore : !isCheckedBefore) {
checkIcon.classList.add(classNames.checked);
li.classList.add(classNames.selected);
} else {
checkIcon.classList.remove(classNames.checked);
li.classList.remove(classNames.selected);
}
checkboxElement.setAttribute('aria-checked', isSetCheckboxLI ? (isCheckedBefore ? 'true' : 'false') : (isCheckedBefore ? 'false' : 'true'));
merge(eventArgs, { isChecked: checkIcon.classList.contains(classNames.checked) });
if (isFocusedBefore) {
li.classList.remove(classNames.focused);
if (textAreaFocus) {
textAreaFocus.classList.remove('e-focused');
}
}
}
});
}
targetElement: Element;
private clickHandler(e: MouseEvent): void {
if (Array.isArray(this.dataSource) && this.dataSource.length === 0){
return;
}
const target: Element = <Element>e.target;
this.targetElement = target;
const classList: DOMTokenList = target.classList;
let closestElement: HTMLElement;
if (classList.contains(classNames.backIcon) || classList.contains(classNames.headerText)) {
if (this.showCheckBox && this.curDSLevel[this.curDSLevel.length - 1]) {
this.uncheckAllItems();
}
this.back();
} else {
let li: HTMLElement = <HTMLElement>closest(target.parentNode, '.' + classNames.listItem);
if (li === null) { li = <HTMLElement>target; }
this.removeFocus();
if (this.enable && this.showCheckBox && this.isValidLI(li)) {
if ((e.target as HTMLElement).classList.contains(classNames.checkboxIcon)) {
li.classList.add(classNames.focused);
if (isNullOrUndefined(li.querySelector('.' + classNames.checked))) {
const args: ItemCreatedArgs = {
curData: undefined, dataSource: undefined, fields: undefined, options: undefined,
text: undefined, item: li
};
this.checkInternally(args, args.item.querySelector('.' + classNames.checkboxWrapper));
} else {
this.uncheckItem(li);
li.classList.add(classNames.focused);
}
if (this.enableVirtualization) {
this.virtualizationModule.setCheckboxLI(li, e);
}
if (e) {
const eventArgs: Object = this.selectEventData(li, e);
const checkIcon: Element = li.querySelector('.' + classNames.checkboxIcon);
merge(eventArgs, { isChecked: checkIcon.classList.contains(classNames.checked) });
const checkboxElement: Element = li.querySelector('.' + classNames.checkboxWrapper);
const isCheckedBefore: boolean = checkIcon.classList.contains(classNames.checked);
const isFocusedBefore: boolean = li.classList.contains(classNames.focused);
this.handleCheckboxState(li, checkIcon, checkboxElement, isCheckedBefore, isFocusedBefore, eventArgs, false);
}
} else if (li.classList.contains(classNames.hasChild)) {
this.removeHover();
this.removeSelect();
this.removeSelect(li);
this.setSelectLI(li, e);
li.classList.remove(classNames.selected);
} else {
this.setCheckboxLI(li, e);
if ((target.nodeName === 'INPUT') || (target.nodeName === 'TEXTAREA')) {
target.classList.add('e-focused');
this.targetElement = target;
}
}
} else {
this.setSelectLI(li, e);
if ((target.nodeName === 'INPUT') || (target.nodeName === 'TEXTAREA')) {
target.classList.add('e-focused');
this.targetElement = target;
}
}
closestElement = closest((e.target as HTMLElement), 'li') as HTMLElement;
if (!isNullOrUndefined(closestElement)) {
if (closestElement.classList.contains('e-has-child') &&
!(e.target as HTMLElement).parentElement.classList.contains('e-listview-checkbox')) {
closestElement.classList.add(classNames.disable);
}
}
}
this.updateSelectedId();
}
private removeElement(element: HTMLElement | Element): HTMLElement | Element {
return element && element.parentNode && element.parentNode.removeChild(element);
}
private hoverHandler(e: MouseEvent): void {
const curLi: HTMLElement = <HTMLElement>closest((<Element>e.target).parentNode, '.' + classNames.listItem);
this.setHoverLI(curLi);
}
private leaveHandler(): void {
this.removeHover();
}
private homeKeyHandler(e: KeyboardEventArgs, end?: boolean): void {
e.preventDefault();
if (Object.keys(this.dataSource).length && this.curUL) {
const li: Element[] = <NodeListOf<Element> & Element[]>this.curUL.querySelectorAll('.' + classNames.listItem);
const focusedElement: Element = this.curUL.querySelector('.' + classNames.focused) ||
this.curUL.querySelector('.' + classNames.selected);
if (focusedElement) {
focusedElement.classList.remove(classNames.focused);
if (!this.showCheckBox) {
focusedElement.classList.remove(classNames.selected);
}
}
const index: number = !end ? 0 : li.length - 1;
if (li[index as number].classList.contains(classNames.hasChild) || this.showCheckBox) {
li[index as number].classList.add(classNames.focused);
} else {
this.setSelectLI(li[index as number], e);
}
if (li[index as number]) {
this.element.setAttribute('aria-activedescendant', (<HTMLElement>li[index as number]).id.toString());
} else {
this.element.removeAttribute('aria-activedescendant');
}
}
}
private onArrowKeyDown(e: KeyboardEventArgs, prev: boolean): Element {
let siblingLI: Element;
let li: Element;
const hasChild: boolean = !isNullOrUndefined(this.curUL.querySelector('.' + classNames.hasChild)) ? true : false;
if (hasChild || this.showCheckBox) {
li = this.curUL.querySelector('.' + classNames.focused) || this.curUL.querySelector('.' + classNames.selected);
siblingLI = ListBase.getSiblingLI(this.curUL.querySelectorAll('.' + classNames.listItem), li, prev);
if (!isNullOrUndefined(siblingLI)) {
if (li) {
li.classList.remove(classNames.focused);
if (!this.showCheckBox) {
li.classList.remove(classNames.selected);
}
}
if (siblingLI.classList.contains(classNames.hasChild) || this.showCheckBox) {
siblingLI.classList.add(classNames.focused);
} else {
this.setSelectLI(siblingLI, e);
}
}
} else {
li = this.curUL.querySelector('.' + classNames.selected);
siblingLI = ListBase.getSiblingLI(this.curUL.querySelectorAll('.' + classNames.listItem), li, prev);
this.setSelectLI(siblingLI, e);
}
if (siblingLI) {
this.element.setAttribute('aria-activedescendant', (<HTMLElement>siblingLI).id.toString());
} else {
this.element.removeAttribute('aria-activedescendant');
}
return siblingLI;
}
private arrowKeyHandler(e: KeyboardEventArgs, prev?: boolean): void {
e.preventDefault();
if (Object.keys(this.dataSource).length && this.curUL) {
const siblingLI: Element = this.onArrowKeyDown(e, prev);
const elementTop: number = this.element.getBoundingClientRect().top;
const elementHeight: number = this.element.getBoundingClientRect().height;
const firstItemBounds: ClientRect = this.curUL.querySelector('.' + classNames.listItem).getBoundingClientRect();
let heightDiff: number;
let groupItemBounds: ClientRect;
if (this.fields.groupBy) {
groupItemBounds = this.curUL.querySelector('.' + classNames.groupListItem).getBoundingClientRect();
}
if (siblingLI) {
const siblingTop: number = siblingLI.getBoundingClientRect().top;
const siblingHeight: number = siblingLI.getBoundingClientRect().height;
if (!prev) {
const height: number = this.isWindow ? window.innerHeight : elementHeight;
heightDiff = this.isWindow ? (siblingTop + siblingHeight) :
((siblingTop - elementTop) + siblingHeight);
if (heightDiff > height) {
if (this.isWindow === true) {
window.scroll(0, pageYOffset + (heightDiff - height));
}
else {
this.element.scrollTop = this.element.scrollTop + (heightDiff - height);
}
}
} else {
heightDiff = this.isWindow ? siblingTop : (siblingTop - elementTop);
if (heightDiff < 0) {
if (this.isWindow === true) {
window.scroll(0, pageYOffset + heightDiff);
}
else {
this.element.scrollTop = this.element.scrollTop + heightDiff;
}
}
}
} else if (this.enableVirtualization && prev && this.virtualizationModule.uiFirstIndex) {
this.onUIScrolled = () => {
this.onArrowKeyDown(e, prev);
this.onUIScrolled = undefined;
};
heightDiff = this.virtualizationModule.listItemHeight;
if (this.isWindow === true) {
window.scroll(0, pageYOffset - heightDiff);
}
else {
this.element.scrollTop = this.element.scrollTop - heightDiff;
}
} else if (prev) {
if (this.showHeader && this.headerEle) {
const topHeight: number = groupIt