UNPKG

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