@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,038 lines (1,001 loc) • 61.5 kB
text/typescript
/* eslint-disable no-inner-declarations */
import { extend, merge, isNullOrUndefined, getValue, SanitizeHtmlHelper } from '@syncfusion/ej2-base';
import { attributes, prepend, isVisible, append, addClass } from '@syncfusion/ej2-base';
import { compile } from '@syncfusion/ej2-base';
import { DataManager, Query } from '@syncfusion/ej2-data';
export let cssClass: ClassList = {
li: 'e-list-item',
ul: 'e-list-parent e-ul',
group: 'e-list-group-item',
icon: 'e-list-icon',
text: 'e-list-text',
check: 'e-list-check',
checked: 'e-checked',
selected: 'e-selected',
expanded: 'e-expanded',
textContent: 'e-text-content',
hasChild: 'e-has-child',
level: 'e-level',
url: 'e-list-url',
collapsible: 'e-icon-collapsible',
disabled: 'e-disabled',
image: 'e-list-img',
iconWrapper: 'e-icon-wrapper',
anchorWrap: 'e-anchor-wrap',
navigable: 'e-navigable'
};
/**
* An interface that holds item class list
*/
export interface ClassList {
li: string;
ul: string;
group: string;
icon: string;
text: string;
check: string;
checked: string;
selected: string;
expanded: string;
textContent: string;
hasChild: string;
level: string;
url: string;
collapsible: string;
disabled: string;
image: string;
iconWrapper: string;
anchorWrap: string;
navigable: string;
}
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/**
* Sorting Order
*/
export type SortOrder = 'None' | 'Ascending' | 'Descending';
/**
* Base List Generator
*/
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace ListBase {
/**
*
* Default mapped fields.
*/
export const defaultMappedFields: FieldsMapping = {
id: 'id',
text: 'text',
url: 'url',
value: 'value',
isChecked: 'isChecked',
enabled: 'enabled',
expanded: 'expanded',
selected: 'selected',
iconCss: 'iconCss',
child: 'child',
isVisible: 'isVisible',
hasChildren: 'hasChildren',
tooltip: 'tooltip',
htmlAttributes: 'htmlAttributes',
urlAttributes: 'urlAttributes',
imageAttributes: 'imageAttributes',
imageUrl: 'imageUrl',
groupBy: null,
sortBy: null
};
const defaultAriaAttributes: AriaAttributesMapping = {
level: 1,
listRole: 'presentation',
itemRole: 'presentation',
groupItemRole: 'group',
itemText: 'list-item',
wrapperRole: 'presentation'
};
const defaultListBaseOptions: ListBaseOptions = {
showCheckBox: false,
showIcon: false,
enableHtmlSanitizer: false,
expandCollapse: false,
fields: defaultMappedFields,
ariaAttributes: defaultAriaAttributes,
listClass: '',
itemClass: '',
processSubChild: false,
sortOrder: 'None',
template: null,
groupTemplate: null,
headerTemplate: null,
expandIconClass: 'e-icon-collapsible',
moduleName: 'list',
expandIconPosition: 'Right',
itemNavigable: false
};
/**
* Function helps to created and return the UL Li element based on your data.
*
* @param {createElementParams} createElement - Specifies an array of JSON data.
*
* @param {{Object}[]} dataSource - Specifies an array of JSON data.
*
* @param {ListBaseOptions} [options] - Specifies the list options that need to provide.
*
* @param {boolean} [isSingleLevel] - Specifies the list options that need to provide.
*
* @param {any} [componentInstance] - Specifies the list options that need to provide.
*
* @returns {createElement} createListFromJson - Specifies the list options that need to provide.
*/
export function createList(
createElement: createElementParams, dataSource: { [key: string]: Object }[] | string[] | number[],
options?: ListBaseOptions, isSingleLevel?: boolean, componentInstance?: any):
HTMLElement {
const curOpt: ListBaseOptions = extend({}, defaultListBaseOptions, options);
const ariaAttributes: AriaAttributesMapping = extend({}, defaultAriaAttributes, curOpt.ariaAttributes);
const type: string = typeofData(dataSource).typeof as string;
if (type === 'string' || type === 'number') {
return createListFromArray(createElement, <string[] | number[]>dataSource, isSingleLevel, options, componentInstance);
} else {
return createListFromJson(
createElement,
<{ [key: string]: Object }[]>dataSource,
options,
ariaAttributes.level,
isSingleLevel,
componentInstance
);
}
}
/**
* Function helps to created an element list based on string array input .
*
* @param {createElementParams} createElement - Specifies an array of JSON data.
*
* @param {{Object}[]} dataSource - Specifies an array of JSON data.
*
* @param {boolean} [isSingleLevel] - Specifies the list options that need to provide.
*
* @param {ListBaseOptions} [options] - Specifies the list options that need to provide.
*
* @param {any} [componentInstance] - Specifies the list options that need to provide.
*
* @returns {createElement} generateUL - returns the list options that need to provide.
*/
export function createListFromArray(
createElement: createElementParams, dataSource: string[] | number[],
isSingleLevel?: boolean, options?: ListBaseOptions, componentInstance?: any): HTMLElement {
const subChild: HTMLElement[] = createListItemFromArray(createElement, dataSource, isSingleLevel, options, componentInstance);
return generateUL(createElement, subChild, null, options);
}
/**
* Function helps to created an element list based on string array input .
*
* @param {createElementParams} createElement - Specifies an array of JSON data.
*
* @param {{Object}[]} dataSource - Specifies an array of JSON data.
*
* @param {boolean} [isSingleLevel] - Specifies the list options that need to provide.
*
* @param {ListBaseOptions} [options] - Specifies the list options that need to provide.
*
* @param {any} [componentInstance] - Specifies the list options that need to provide.
*
* @returns {HTMLElement[]} subChild - returns the list options that need to provide.
*/
export function createListItemFromArray(
createElement: createElementParams, dataSource: string[] | number[],
isSingleLevel?: boolean, options?: ListBaseOptions, componentInstance?: any): HTMLElement[] {
const subChild: HTMLElement[] = [];
const curOpt: ListBaseOptions = extend({}, defaultListBaseOptions, options);
cssClass = getModuleClass(curOpt.moduleName);
const id: string = generateId(); // generate id for drop-down-list option.
for (let i: number = 0; i < dataSource.length; i++) {
if (isNullOrUndefined(dataSource[i as number])) {
continue;
}
let li: HTMLElement;
if (curOpt.itemCreating && typeof curOpt.itemCreating === 'function') {
const curData: object = {
dataSource: dataSource,
curData: dataSource[i as number],
text: dataSource[i as number],
options: curOpt
};
curOpt.itemCreating(curData);
}
if (isSingleLevel) {
li = generateSingleLevelLI(createElement, dataSource[i as number], undefined, null, null, [], null, id, i, options);
} else {
li = generateLI(createElement, dataSource[i as number], undefined, null, null, options, componentInstance);
}
if (curOpt.itemCreated && typeof curOpt.itemCreated === 'function') {
const curData: Object = {
dataSource: dataSource,
curData: dataSource[i as number],
text: dataSource[i as number],
item: li,
options: curOpt
};
curOpt.itemCreated(curData);
}
subChild.push(li);
}
return subChild;
}
/**
* Function helps to created an element list based on array of JSON input .
*
* @param {createElementParams} createElement - Specifies an array of JSON data.
*
* @param {{Object}[]} dataSource - Specifies an array of JSON data.
*
* @param {ListBaseOptions} [options] - Specifies the list options that need to provide.
*
* @param {number} [level] - Specifies the list options that need to provide.
*
* @param {boolean} [isSingleLevel] - Specifies the list options that need to provide.
*
* @param {any} [componentInstance] - Specifies the list options that need to provide.
*
* @returns {HTMLElement[]} child - returns the list options that need to provide.
*/
export function createListItemFromJson(
createElement: createElementParams, dataSource: { [key: string]: Object }[],
options?: ListBaseOptions, level?: number, isSingleLevel?: boolean, componentInstance?: any): HTMLElement[] {
const curOpt: ListBaseOptions = extend({}, defaultListBaseOptions, options);
cssClass = getModuleClass(curOpt.moduleName);
const fields: FieldsMapping = (componentInstance &&
(componentInstance.getModuleName() === 'listview' || componentInstance.getModuleName() === 'multiselect'))
? curOpt.fields : extend({}, defaultMappedFields, curOpt.fields);
const ariaAttributes: AriaAttributesMapping = extend({}, defaultAriaAttributes, curOpt.ariaAttributes);
let id: string;
let checkboxElement: HTMLElement[] = [];
if (level) {
ariaAttributes.level = level;
}
const child: HTMLElement[] = [];
let li: HTMLElement;
let anchorElement: HTMLElement;
if (dataSource && dataSource.length && !isNullOrUndefined(typeofData(dataSource).item) &&
!Object.prototype.hasOwnProperty.call(typeofData(dataSource).item, fields.id)) {
id = generateId(); // generate id for drop-down-list option.
}
for (let i: number = 0; i < dataSource.length; i++) {
let fieldData: { [key: string]: Object } = <{ [key: string]: Object }>getFieldValues(dataSource[i as number], fields);
if (isNullOrUndefined(dataSource[i as number])) { continue; }
if (curOpt.itemCreating && typeof curOpt.itemCreating === 'function') {
const curData: { [key: string]: object | string } = {
dataSource: dataSource,
curData: dataSource[i as number],
text: fieldData[fields.text],
options: curOpt,
fields: fields
};
curOpt.itemCreating(curData);
}
const curItem: { [key: string]: Object } = dataSource[i as number];
if (curOpt.itemCreating && typeof curOpt.itemCreating === 'function') {
fieldData = <{ [key: string]: Object }>getFieldValues(dataSource[i as number], fields);
}
if (Object.prototype.hasOwnProperty.call(fieldData, fields.id) && !isNullOrUndefined(fieldData[fields.id])) {
id = <string>fieldData[fields.id];
}
const innerEle: HTMLElement[] = [];
if (curOpt.showCheckBox) {
if (curOpt.itemNavigable && (fieldData[fields.url] || fieldData[fields.urlAttributes])) {
checkboxElement.push(createElement('input', { className: cssClass.check, attrs: { type: 'checkbox' } }));
} else {
innerEle.push(createElement('input', { className: cssClass.check, attrs: { type: 'checkbox' } }));
}
}
if (isSingleLevel === true) {
if (curOpt.showIcon && Object.prototype.hasOwnProperty.call(fieldData, fields.iconCss)
&& !isNullOrUndefined(fieldData[fields.iconCss])) {
innerEle.push(createElement('span', { className: cssClass.icon + ' ' + <string>fieldData[fields.iconCss] }));
}
li = generateSingleLevelLI(
createElement,
curItem,
fieldData,
fields,
curOpt.itemClass,
innerEle,
(Object.prototype.hasOwnProperty.call(curItem, 'isHeader') &&
(curItem as { isHeader: Object } & { [key: string]: Object }).isHeader) ? true : false,
id,
i, options);
anchorElement = li.querySelector('.' + cssClass.anchorWrap);
if (Object.prototype.hasOwnProperty.call(fieldData, fields.tooltip)) {
let tooltipText: string = <string>fieldData[fields.tooltip];
if (options && options.enableHtmlSanitizer) {
tooltipText = SanitizeHtmlHelper.sanitize(tooltipText);
} else {
let tooltipTextElement: HTMLElement = createElement('span', { innerHTML: tooltipText });
tooltipText = tooltipTextElement.innerText;
tooltipTextElement = null;
}
li.setAttribute('title', tooltipText);
}
if (curOpt.itemNavigable && checkboxElement.length) {
prepend(checkboxElement, li.firstElementChild);
}
} else {
li = generateLI(createElement, curItem, fieldData, fields, curOpt.itemClass, options, componentInstance);
li.classList.add(cssClass.level + '-' + ariaAttributes.level);
li.setAttribute('aria-level', ariaAttributes.level.toString());
if (ariaAttributes.groupItemRole === 'presentation' || ariaAttributes.itemRole === 'presentation') {
li.removeAttribute('aria-level');
}
anchorElement = li.querySelector('.' + cssClass.anchorWrap);
if (Object.prototype.hasOwnProperty.call(fieldData, fields.tooltip)) {
let tooltipText: string = <string>fieldData[fields.tooltip];
if (options && options.enableHtmlSanitizer) {
tooltipText = SanitizeHtmlHelper.sanitize(tooltipText);
} else {
let tooltipTextElement: HTMLElement = createElement('span', { innerHTML: tooltipText });
tooltipText = tooltipTextElement.innerText;
tooltipTextElement = null;
}
li.setAttribute('title', tooltipText);
}
if (Object.prototype.hasOwnProperty.call(fieldData, fields.htmlAttributes) && fieldData[fields.htmlAttributes]) {
const htmlAttributes: { [key: string]: string } = <{ [key: string]: string }>fieldData[fields.htmlAttributes];
// Check if 'class' attribute is present and not an empty string
if ('class' in htmlAttributes && typeof htmlAttributes['class'] === 'string' && htmlAttributes['class'].trim() === '') {
delete htmlAttributes['class'];
}
setAttribute(li, htmlAttributes);
}
if (Object.prototype.hasOwnProperty.call(fieldData, fields.enabled) && fieldData[fields.enabled] === false) {
li.classList.add(cssClass.disabled);
}
if (Object.prototype.hasOwnProperty.call(fieldData, fields.isVisible) && fieldData[fields.isVisible] === false) {
li.style.display = 'none';
}
if (Object.prototype.hasOwnProperty.call(fieldData, fields.imageUrl) && !isNullOrUndefined(fieldData[fields.imageUrl])
&& !curOpt.template) {
const attr: { [key: string]: string } = { src: <string>fieldData[fields.imageUrl], alt: !isNullOrUndefined(<string>fieldData.name) ? ('Displaying ' + <string>fieldData.name + ' Image') : 'Displaying Image'};
merge(attr, fieldData[fields.imageAttributes]);
const imageElemnt: HTMLElement = createElement('img', { className: cssClass.image, attrs: attr });
if (anchorElement) {
anchorElement.insertAdjacentElement('afterbegin', imageElemnt);
} else {
prepend([imageElemnt], li.firstElementChild);
}
}
if (curOpt.showIcon && Object.prototype.hasOwnProperty.call(fieldData, fields.iconCss) &&
!isNullOrUndefined(fieldData[fields.iconCss]) && !curOpt.template) {
const iconElement: HTMLElement = createElement('div', { className: cssClass.icon + ' ' + <string>fieldData[fields.iconCss] });
if (anchorElement) {
anchorElement.insertAdjacentElement('afterbegin', iconElement);
} else {
prepend([iconElement], li.firstElementChild);
}
}
if (innerEle.length) {
prepend(innerEle, li.firstElementChild);
}
if (curOpt.itemNavigable && checkboxElement.length) {
prepend(checkboxElement, li.firstElementChild);
}
processSubChild(createElement, fieldData, fields, dataSource, curOpt, li, ariaAttributes.level);
}
if (anchorElement) {
addClass([li], [cssClass.navigable]);
}
if (curOpt.itemCreated && typeof curOpt.itemCreated === 'function') {
const curData: { [key: string]: object | string } = {
dataSource: dataSource,
curData: dataSource[i as number],
text: fieldData[fields.text],
item: li,
options: curOpt,
fields: fields
};
curOpt.itemCreated(curData);
}
checkboxElement = [];
child.push(li);
}
return child;
}
/**
* Function helps to created an element list based on array of JSON input .
*
* @param {createElementParams} createElement - Specifies an array of JSON data.
*
* @param {{Object}[]} dataSource - Specifies an array of JSON data.
*
* @param {ListBaseOptions} [options] - Specifies the list options that need to provide.
*
* @param {number} [level] - Specifies the list options that need to provide.
*
* @param {boolean} [isSingleLevel] - Specifies the list options that need to provide.
*
* @param {any} [componentInstance] - Specifies the list options that need to provide.
*
* @returns {createElement} generateUL - Specifies the list options that need to provide.
*/
export function createListFromJson(
createElement: createElementParams, dataSource: { [key: string]: Object }[],
options?: ListBaseOptions, level?: number, isSingleLevel?: boolean, componentInstance?: any): HTMLElement {
const curOpt: ListBaseOptions = extend({}, defaultListBaseOptions, options);
const li: HTMLElement[] = createListItemFromJson(createElement, dataSource, options, level, isSingleLevel, componentInstance);
return generateUL(createElement, li, curOpt.listClass, options);
}
/**
* Return the next or previous visible element.
*
* @param {Element[]|NodeList} elementArray - An element array to find next or previous element.
* @param {Element} element - An element to find next or previous after this element.
* @param {boolean} [isPrevious] - Specify when the need get previous element from array.
* @returns {Element|undefined} The next or previous visible element, or undefined if the element array is empty.
*/
export function getSiblingLI(elementArray: Element[] | NodeList, element: Element, isPrevious?: boolean): Element {
cssClass = getModuleClass(defaultListBaseOptions.moduleName);
if (!elementArray || !elementArray.length) { return void 0; }
let siblingLI: Element;
let liIndex: number;
const liCollections: Element[] = Array.prototype.slice.call(elementArray);
if (element) {
liIndex = indexOf(element, liCollections);
} else {
liIndex = (isPrevious === true ? liCollections.length : -1);
}
siblingLI = liCollections[liIndex + (isPrevious === true ? -1 : 1)];
while (siblingLI && (!isVisible(siblingLI) || siblingLI.classList.contains(cssClass.disabled))) {
liIndex = liIndex + (isPrevious === true ? -1 : 1);
siblingLI = liCollections[liIndex as number];
}
return siblingLI;
}
/**
* Return the index of the li element
*
* @param {Element} item - An element to find next or previous after this element.
* @param {Element[]} elementArray - An element array to find index of given li.
* @returns {number} - The index of the item in the element array, or undefined if either parameter is false.
*/
export function indexOf(item: Element, elementArray: Element[] | NodeList): number {
if (!elementArray || !item) { return void 0; } else {
let liCollections: Element[] = <Element[]>elementArray;
liCollections = Array.prototype.slice.call(elementArray);
return liCollections.indexOf(item);
}
}
/**
* Returns the grouped data from given dataSource.
*
* @param {{Object}[]} dataSource - The JSON data which is necessary to process.
* @param {FieldsMapping} fields - Fields that are mapped from the data source.
* @param {SortOrder} [sortOrder='None'] - Specifies final result sort order. Defaults to 'None'.
* @returns {Object[]} - The grouped data.
*/
export function groupDataSource(
dataSource: { [key: string]: Object }[],
fields: FieldsMapping,
sortOrder: SortOrder = 'None'): { [key: string]: Object }[] {
const curFields: FieldsMapping = extend({}, defaultMappedFields, fields);
let cusQuery: Query = new Query().group(curFields.groupBy);
// need to remove once sorting issues fixed in DataManager
cusQuery = addSorting(sortOrder, 'key', cusQuery);
const ds: { [key: string]: Object }[] = getDataSource(dataSource, cusQuery);
dataSource = [];
for (let j: number = 0; j < ds.length; j++) {
const itemObj: { [key: string]: Object }[]
= (ds[j as number] as { items: { [key: string]: Object }[] } & { [key: string]: Object }).items;
const grpItem: { [key: string]: Object } = {};
const hdr: string = 'isHeader';
grpItem[curFields.text] = (ds[j as number] as { key: string } & { [key: string]: Object }).key;
grpItem[`${hdr}`] = true;
let newtext: string = curFields.text;
if (newtext === 'id') {
newtext = 'text';
grpItem[`${newtext}`] = ds[j as number].key;
}
grpItem._id = 'group-list-item-' + ((ds[j as number] as { [key: string]: Object }).key ?
(ds[j as number] as { [key: string]: Object }).key.toString().trim() : 'undefined');
grpItem.items = itemObj;
dataSource.push(grpItem);
for (let k: number = 0; k < itemObj.length; k++) {
dataSource.push(itemObj[k as number]);
}
}
return dataSource;
}
/**
* Returns a sorted query object.
*
* @param {SortOrder} sortOrder - Specifies that sort order.
* @param {string} sortBy - Specifies sortBy fields.
* @param {Query} query - Pass if any existing query.
* @returns {Query} - The updated query object with sorting applied.
*/
export function addSorting(sortOrder: SortOrder, sortBy: string, query: Query = new Query()): Query {
if (sortOrder === 'Ascending') {
query.sortBy(sortBy, 'ascending', true);
} else if (sortOrder === 'Descending') {
query.sortBy(sortBy, 'descending', true);
} else {
for (let i: number = 0; i < query.queries.length; i++) {
if (query.queries[i as number].fn === 'onSortBy') {
query.queries.splice(i, 1);
}
}
}
return query;
}
/**
* Return an array of JSON Data that processed based on queries.
*
* @param {{Object}[]} dataSource - Specifies local JSON data source.
*
* @param {Query} query - Specifies query that need to process.
*
* @returns {Object[]} - An array of objects representing the retrieved data.
*/
export function getDataSource(dataSource: { [key: string]: Object }[], query: Query): { [key: string]: Object }[] {
return <{ [key: string]: Object }[]>new DataManager(<{ [key: string]: Object }[]>dataSource)
.executeLocal(query);
}
/**
* Created JSON data based the UL and LI element
*
* @param {HTMLElement|Element} element - UL element that need to convert as a JSON
* @param {ListBaseOptions} [options] - Specifies ListBase option for fields.
* @returns {Object[]} - An array of objects representing the JSON data.
*/
export function createJsonFromElement(
element: HTMLElement | Element, options?: ListBaseOptions): { [key: string]: Object }[] {
const curOpt: ListBaseOptions = extend({}, defaultListBaseOptions, options);
const fields: FieldsMapping = extend({}, defaultMappedFields, curOpt.fields);
const curEle: HTMLElement = <HTMLElement>element.cloneNode(true);
const jsonAr: { [key: string]: {} }[] = [];
curEle.classList.add('json-parent');
const childs: HTMLElement[] = <HTMLElement[] & NodeListOf<HTMLElement>>curEle.querySelectorAll('.json-parent>li');
curEle.classList.remove('json-parent');
for (let i: number = 0; i < childs.length; i++) {
const li: HTMLElement = childs[i as number];
const anchor: HTMLElement = li.querySelector('a');
const ul: Element = li.querySelector('ul');
const json: { [key: string]: {} } = {};
const childNodes: NodeList = anchor ? anchor.childNodes : li.childNodes;
const keys: string[] = Object.keys(childNodes);
for (let i: number = 0; i < childNodes.length; i++) {
if (!(childNodes[Number(keys[i as number])]).hasChildNodes()) {
json[fields.text] = childNodes[Number(keys[i as number])].textContent;
}
}
let attributes: { [key: string]: string } = getAllAttributes(li);
if (attributes.id) {
json[fields.id] = attributes.id;
delete attributes.id;
} else {
json[fields.id] = generateId();
}
if (Object.keys(attributes).length) {
json[fields.htmlAttributes] = attributes;
}
if (anchor) {
attributes = getAllAttributes(anchor);
if (Object.keys(attributes).length) {
json[fields.urlAttributes] = attributes;
}
}
if (ul) {
json[fields.child] = createJsonFromElement(ul, options);
}
jsonAr.push(json);
}
return jsonAr;
}
/**
* Determines the type of data in an array of objects, strings, or numbers.
*
* @param {Object[] | string[] | number[]} data - The array containing objects, strings, or numbers.
* @returns {{typeof: (string | null), item: (Object | string | number)}} - An object containing the type of data and the corresponding item.
*/
function typeofData(data: { [key: string]: Object }[] | string[] | number[]): { [key: string]: Object } {
let match: { [key: string]: Object } = <{ [key: string]: Object }>{ typeof: null, item: null };
for (let i: number = 0; i < data.length; i++) {
if (!isNullOrUndefined(data[i as number])) {
return match = { typeof: typeof data[i as number], item: data[i as number] };
}
}
return match;
}
/**
* Sets attributes on an HTML element.
*
* @param {HTMLElement} element - The HTML element to set attributes on.
* @param {Object.<string, string>} elementAttributes - An object containing attribute names and their corresponding values.
* @returns {void}
*/
function setAttribute(element: HTMLElement, elementAttributes: { [key: string]: string }): void {
const attr: { [key: string]: string } = {};
merge(attr, elementAttributes);
if (attr.class) {
addClass([element], attr.class.split(' '));
delete attr.class;
}
attributes(element, attr);
}
/**
* Retrieves all attributes of an HTML element.
*
* @param {HTMLElement} element - The HTML element to retrieve attributes from.
* @returns {Object.<string, string>} - An object containing attribute names as keys and their corresponding values as values.
*/
function getAllAttributes(element: HTMLElement): { [key: string]: string } {
const attributes: { [key: string]: string } = {};
const attr: NamedNodeMap = element.attributes;
for (let index: number = 0; index < attr.length; index++) {
attributes[attr[index as number].nodeName] = attr[index as number].nodeValue;
}
return attributes;
}
/**
* Created UL element from content template.
*
* @param {createElementParams} createElement - Specifies an array of JSON data.
* @param {string} template - that need to convert and generate li element.
* @param {{Object}[]} dataSource - Specifies local JSON data source.
* @param {FieldsMapping} [fields] - Specifies fields for mapping the dataSource.
* @param {ListBaseOptions} [options] - Specifies ListBase option for fields.
* @param {any} [componentInstance] - Specifies component instance.
* @returns {HTMLElement} - The generated LI element.
*/
export function renderContentTemplate(
createElement: createElementParams, template: string | Function, dataSource: { [key: string]: Object }[] | string[] | number[],
fields?: FieldsMapping, options?: ListBaseOptions, componentInstance?: any): HTMLElement {
cssClass = getModuleClass(defaultListBaseOptions.moduleName);
const ulElement: HTMLElement = createElement('ul', { className: cssClass.ul, attrs: { role: 'presentation' } });
const curOpt: ListBaseOptions = extend({}, defaultListBaseOptions, options);
const curFields: FieldsMapping = extend({}, defaultMappedFields, fields);
const compiledString: Function = compileTemplate(template);
const liCollection: HTMLElement[] = [];
let value: string;
const id: string = generateId(); // generate id for drop-down-list option.
for (let i: number = 0; i < dataSource.length; i++) {
let fieldData: { [key: string]: Object } = <{ [key: string]: Object }>getFieldValues(dataSource[i as number], curFields);
const curItem: { [key: string]: Object } | string | number = dataSource[i as number];
const isHeader: Object = (curItem as { isHeader: Object } & { [key: string]: Object }).isHeader;
if (typeof dataSource[i as number] === 'string' || typeof dataSource[i as number] === 'number') {
value = curItem as string;
} else {
value = fieldData[curFields.value] as string;
}
if (curOpt.itemCreating && typeof curOpt.itemCreating === 'function') {
const curData: object = {
dataSource: dataSource,
curData: curItem,
text: value,
options: curOpt,
fields: curFields
};
curOpt.itemCreating(curData);
}
if (curOpt.itemCreating && typeof curOpt.itemCreating === 'function') {
fieldData = <{ [key: string]: Object }>getFieldValues(dataSource[i as number], curFields);
if (typeof dataSource[i as number] === 'string' || typeof dataSource[i as number] === 'number') {
value = curItem as string;
} else {
value = fieldData[curFields.value] as string;
}
}
const li: HTMLElement = createElement('li', {
id: id + '-' + i,
className: isHeader ? cssClass.group : cssClass.li, attrs: { role: 'presentation' }
});
if (isHeader) {
if (typeof dataSource[i as number] === 'string' || typeof dataSource[i as number] === 'number') {
li.innerText = curItem as string;
} else {
li.innerText = fieldData[curFields.text] as string;
}
} else {
const currentID: string = isHeader ? curOpt.groupTemplateID : curOpt.templateID;
if (isHeader) {
if (componentInstance && componentInstance.getModuleName() !== 'listview') {
const compiledElement: HTMLElement[] = compiledString(
curItem,
componentInstance,
'headerTemplate',
currentID,
!!curOpt.isStringTemplate,
null,
li);
if (compiledElement) {
append(compiledElement, li);
}
} else {
append(compiledString(curItem, componentInstance, 'headerTemplate', currentID, !!curOpt.isStringTemplate), li);
}
} else {
if (componentInstance && componentInstance.getModuleName() !== 'listview') {
const compiledElement: HTMLElement[] = compiledString(
curItem,
componentInstance,
'template',
currentID,
!!curOpt.isStringTemplate,
null,
li);
if (compiledElement) {
append(compiledElement, li);
}
} else {
append(compiledString(curItem, componentInstance, 'template', currentID, !!curOpt.isStringTemplate), li);
}
}
li.setAttribute('data-value', isNullOrUndefined(value) ? 'null' : value);
li.setAttribute('role', 'option');
}
if (curOpt.itemCreated && typeof curOpt.itemCreated === 'function') {
const curData: Object = {
dataSource: dataSource,
curData: curItem,
text: value,
item: li,
options: curOpt,
fields: curFields
};
curOpt.itemCreated(curData);
}
liCollection.push(li);
}
append(liCollection, ulElement);
return ulElement;
}
/**
* Created header items from group template.
*
* @param {string | Function} groupTemplate - that need to convert and generate li element.
*
* @param {{Object}[]} groupDataSource - Specifies local JSON data source.
*
* @param {FieldsMapping} fields - Specifies fields for mapping the dataSource.
*
* @param {Element[]} headerItems - Specifies ListBase header items.
*
* @param {ListBaseOptions} [options] - Optional ListBase options.
*
* @param {*} [componentInstance] - Optional component instance.
*
* @returns {Element[]} - An array of header elements with the rendered group template content.
*/
export function renderGroupTemplate(
groupTemplate: string | Function,
// tslint:disable-next-line
groupDataSource: { [key: string]: Object }[],
fields: FieldsMapping,
headerItems: Element[], options?: ListBaseOptions, componentInstance?: any): Element[] {
const compiledString: Function = compileTemplate(groupTemplate);
const curFields: FieldsMapping = extend({}, defaultMappedFields, fields);
const curOpt: ListBaseOptions = extend({}, defaultListBaseOptions, options);
const category: string = curFields.groupBy;
for (const header of headerItems) {
const headerData: { [key: string]: string; } = {};
headerData[`${category}`] = header.textContent;
header.innerHTML = '';
if (componentInstance && componentInstance.getModuleName() !== 'listview') {
const compiledElement: HTMLElement[] = compiledString(
headerData,
componentInstance,
'groupTemplate',
curOpt.groupTemplateID,
!!curOpt.isStringTemplate, null, header);
if (compiledElement) {
append(compiledElement, header);
}
} else {
append(compiledString(headerData, componentInstance, 'groupTemplate', curOpt.groupTemplateID, !!curOpt.isStringTemplate), header);
}
}
return headerItems;
}
/**
* Generates a random hexadecimal ID string.
*
* @returns {string} - The generated ID string.
*/
export function generateId(): string {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
/**
* Processes the sub-child elements and creates corresponding elements based on the provided field data and options.
*
* @param {Function} createElement - Function for creating elements.
* @param {Object} fieldData - Field data containing sub-child information.
* @param {FieldsMapping} fields - Field mappings.
* @param {Object[]} ds - The data source array containing sub-child elements.
* @param {ListBaseOptions} options - ListBase options.
* @param {HTMLElement} element - The parent HTML element to append sub-child elements to.
* @param {number} level - The level of the sub-child elements.
* @returns {void}
*/
function processSubChild(
createElement: createElementParams, fieldData: { [key: string]: Object }, fields: FieldsMapping, ds: { [key: string]: Object }[],
options: ListBaseOptions, element: HTMLElement, level: number): void {
// Get SubList
const subDS: { [key: string]: Object }[] = <{ [key: string]: Object }[]>fieldData[fields.child] || [];
let hasChildren: boolean = <boolean>fieldData[fields.hasChildren];
//Create Sub child
if (subDS.length) {
hasChildren = true;
element.classList.add(cssClass.hasChild);
if (options.processSubChild) {
const subLi: HTMLElement = <HTMLElement>createListFromJson(createElement, subDS, options, ++level);
element.appendChild(subLi);
}
}
// Create expand and collapse node
if (!!options.expandCollapse && hasChildren && !options.template) {
element.firstElementChild.classList.add(cssClass.iconWrapper);
const expandElement: Function = options.expandIconPosition === 'Left' ? prepend : append;
expandElement(
[createElement('div', { className: 'e-icons ' + options.expandIconClass })],
element.querySelector('.' + cssClass.textContent));
}
}
/**
* Generates a single-level LI (list item) element based on the provided item and field data.
*
* @param {Function} createElement - Function for creating elements.
* @param {string | Object | number} item - The item data.
* @param {Object} fieldData - Field data mapped from the item.
* @param {FieldsMapping} [fields] - Field mappings.
* @param {string} [className] - Optional class name to add to the created LI element.
* @param {HTMLElement[]} [innerElements] - Optional array of inner elements to append to the LI element.
* @param {boolean} [grpLI] - Indicates if the LI element is a group item.
* @param {string} [id] - The ID of the LI element.
* @param {number} [index] - The index of the LI element.
* @param {ListBaseOptions} [options] - Optional ListBase options.
* @returns {HTMLElement} - The generated LI element.
*/
function generateSingleLevelLI(
createElement: createElementParams, item: string | { [key: string]: Object } | number,
fieldData: { [key: string]: Object }, fields?: FieldsMapping, className?: string, innerElements?: HTMLElement[],
grpLI?: boolean, id?: string, index?: number, options?: ListBaseOptions):
HTMLElement {
const curOpt: ListBaseOptions = extend({}, defaultListBaseOptions, options);
const ariaAttributes: AriaAttributesMapping = extend({}, defaultAriaAttributes, curOpt.ariaAttributes);
let text: string = <string>item;
let value: string = <string>item;
let dataSource: { [key: string]: string } | { [key: string]: object };
if (typeof item !== 'string' && typeof item !== 'number' && typeof item !== 'boolean') {
dataSource = <{ [key: string]: Object }>item;
text = (typeof fieldData[fields.text] === 'boolean' || typeof fieldData[fields.text] === 'number') ?
<string>fieldData[fields.text] : (<string>fieldData[fields.text] || '');
value = <string>fieldData[fields.value];
}
let elementID: string;
if (!isNullOrUndefined(dataSource) && !isNullOrUndefined(fieldData[fields.id])
&& fieldData[fields.id] !== '') {
elementID = id;
} else {
elementID = id + '-' + index;
}
const li: HTMLElement = createElement('li', {
className: (grpLI === true ? cssClass.group : cssClass.li) + ' ' + (isNullOrUndefined(className) ? '' : className),
id: elementID, attrs: (ariaAttributes.groupItemRole !== '' && ariaAttributes.itemRole !== '' ?
{ role: (grpLI === true ? ariaAttributes.groupItemRole : ariaAttributes.itemRole) } : {})
});
if (dataSource && Object.prototype.hasOwnProperty.call(fieldData, fields.enabled) && fieldData[fields.enabled].toString() === 'false') {
li.classList.add(cssClass.disabled);
}
if (options && options.enableHtmlSanitizer) {
text = SanitizeHtmlHelper.sanitize(text);
}
if (grpLI) {
li.innerText = text;
} else {
li.setAttribute('data-value', isNullOrUndefined(value) ? 'null' : value);
li.setAttribute('role', 'option');
if (dataSource && Object.prototype.hasOwnProperty.call(fieldData, fields.htmlAttributes) && fieldData[fields.htmlAttributes]) {
setAttribute(li, <{ [key: string]: string }>fieldData[fields.htmlAttributes]);
}
if (innerElements.length && !curOpt.itemNavigable) { append(innerElements, li); }
if (dataSource && (fieldData[fields.url] || (fieldData[fields.urlAttributes] &&
(fieldData[fields.urlAttributes] as { [key: string]: Object }).href))) {
li.appendChild(anchorTag(createElement, dataSource, fields, text, innerElements, curOpt.itemNavigable));
} else {
if (innerElements.length && curOpt.itemNavigable) { append(innerElements, li); }
li.appendChild(document.createTextNode(text));
}
}
return li;
}
/**
* Returns a set of CSS class names based on the provided module name.
*
* @param {string} moduleName - The name of the module.
* @returns {ClassList} - The CSS class names associated with the module.
*/
function getModuleClass(moduleName: string): ClassList {
let moduleClass: ClassList;
// eslint-disable-next-line
return moduleClass = {
li: `e-${moduleName}-item`,
ul: `e-${moduleName}-parent e-ul`,
group: `e-${moduleName}-group-item`,
icon: `e-${moduleName}-icon`,
text: `e-${moduleName}-text`,
check: `e-${moduleName}-check`,
checked: 'e-checked',
selected: 'e-selected',
expanded: 'e-expanded',
textContent: 'e-text-content',
hasChild: 'e-has-child',
level: 'e-level',
url: `e-${moduleName}-url`,
collapsible: 'e-icon-collapsible',
disabled: 'e-disabled',
image: `e-${moduleName}-img`,
iconWrapper: 'e-icon-wrapper',
anchorWrap: 'e-anchor-wrap',
navigable: 'e-navigable'
};
}
/**
* Creates an anchor tag (<a>) element based on the provided data source, fields, and text.
*
* @param {Function} createElement - Function for creating elements.
* @param {object} dataSource - The data source containing URL-related fields.
* @param {FieldsMapping} fields - Field mappings for the data source.
* @param {string} text - The text content of the anchor tag.
* @param {HTMLElement[]} innerElements - Optional array of inner elements to append to the anchor tag.
* @param {boolean} isFullNavigation - Indicates whether the anchor tag should be for full navigation.
* @returns {HTMLElement} The created anchor tag element.
*/
function anchorTag(
createElement: createElementParams, dataSource: { [key: string]: object } | { [key: string]: string },
fields: FieldsMapping, text: string, innerElements: HTMLElement[], isFullNavigation: boolean): HTMLElement {
const fieldData: { [key: string]: Object } = <{ [key: string]: Object }>getFieldValues(dataSource, fields);
const attr: { [key: string]: string } = { href: <string>fieldData[fields.url] };
if (Object.prototype.hasOwnProperty.call(fieldData, fields.urlAttributes) && fieldData[fields.urlAttributes]) {
merge(attr, fieldData[fields.urlAttributes]);
attr.href = <string>fieldData[fields.url] ? <string>fieldData[fields.url] :
(fieldData[fields.urlAttributes] as { [key: string]: Object }).href as string;
}
let anchorTag: HTMLElement;
if (!isFullNavigation) {
anchorTag = createElement('a', { className: cssClass.text + ' ' + cssClass.url, innerHTML: text });
} else {
anchorTag = createElement('a', { className: cssClass.text + ' ' + cssClass.url });
const anchorWrapper: HTMLElement = createElement('div', { className: cssClass.anchorWrap });
if (innerElements && innerElements.length) {
append(innerElements, anchorWrapper);
}
anchorWrapper.appendChild(document.createTextNode(text));
append([anchorWrapper], anchorTag);
}
setAttribute(anchorTag, attr);
return anchorTag;
}
/**
* Generates an LI element based on the provided item and field data.
*
* @param {Function} createElement - Function for creating elements.
* @param {string | Object | number} item - The item data.
* @param {Object} fieldData - Field data mapped from the item.
* @param {FieldsMapping} fields - Field mappings.
* @param {string} [className] - Optional class name to add to the created LI element.
* @param {ListBaseOptions} [options] - Optional ListBase options.
* @param {*} [componentInstance] - Optional component instance.
* @returns {HTMLElement} - The generated LI element.
*/
function generateLI(
createElement: createElementParams, item: string | { [key: string]: Object } | number, fieldData: { [key: string]: Object },
fields: FieldsMapping, className?: string, options?: ListBaseOptions, componentInstance?: any): HTMLElement {
const curOpt: ListBaseOptions = extend({}, defaultListBaseOptions, options);
const ariaAttributes: AriaAttributesMapping = extend({}, defaultAriaAttributes, curOpt.ariaAttributes);
let text: string = <string>item;
let uID: string;
let grpLI: boolean;
let dataSource: { [key: string]: string } | { [key: string]: object };
if (typeof item !== 'string' && typeof item !== 'number') {
dataSource = <{ [key: string]: Object }>item;
text = <string>fiel