@progress/kendo-angular-dropdowns
Version:
A wide variety of native Angular dropdown components including AutoComplete, ComboBox, DropDownList, DropDownTree, MultiColumnComboBox, MultiSelect, and MultiSelectTree
416 lines (415 loc) • 12.6 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* Copyright © 2025 Progress Software Corporation. All rights reserved.
* Licensed under commercial license. See LICENSE.md in the project root for more information
*-------------------------------------------------------------------------------------------*/
/* eslint-disable no-bitwise */
import { isDocumentAvailable } from '@progress/kendo-angular-common';
import { getter as fieldAccessor } from '@progress/kendo-common';
/**
* @hidden
*/
export const isPresent = (value) => value !== null && value !== undefined;
/**
* @hidden
*/
export const isNumber = (value) => !isNaN(value);
/**
* @hidden
*/
export const guid = () => {
let id = "";
let i;
let random;
for (i = 0; i < 32; i++) {
random = Math.random() * 16 | 0;
if (i === 8 || i === 12 || i === 16 || i === 20) {
id += "-";
}
id += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random)).toString(16);
}
return id;
};
/**
* @hidden
*/
export const combineStr = (begin, end) => {
return begin.concat(end.substr(end.toLowerCase().indexOf(begin.toLowerCase()) + begin.length));
};
/**
* @hidden
*/
export const isWindowAvailable = () => typeof window !== 'undefined';
/**
* @hidden
*/
export const isArray = (value) => Array.isArray(value);
/**
* @hidden
*/
export const isObject = (value) => isPresent(value) && typeof value === 'object';
/**
* @hidden
*/
export const isEmptyString = (value) => typeof value === 'string' && value.length === 0;
/**
* @hidden
*/
export const resolveValuesInArray = (values, data = [], valueField) => values
.map(value => {
return data.find(item => getter(item, valueField) === value);
})
.filter(value => value !== undefined);
/**
* @hidden
*/
export const validateComplexValues = (values, valueField) => isArray(values) && values.filter(item => {
return isObject(item) && getter(item, valueField) !== undefined;
});
/**
* @hidden
*/
export const resolveAllValues = (value, data, valueField) => {
const customValues = validateComplexValues(value, valueField) || [];
const resolvedValues = resolveValuesInArray(value, data, valueField) || [];
return resolvedValues.concat(customValues);
};
/**
* @hidden
*/
export const isObjectArray = (values) => {
return isArray(values) && values.every(item => isObject(item));
};
/**
* @hidden
*/
export const selectedIndices = (values, data, valueField) => {
const extractedValues = data.map(item => {
return isPresent(item) && isPresent(getter(item, valueField)) ? getter(item, valueField) : item;
});
return values.reduce((arr, item) => {
const value = isPresent(item) && isPresent(getter(item, valueField)) ? getter(item, valueField) : item;
const index = extractedValues.indexOf(value);
if (index !== -1) {
arr.push(index);
}
return arr;
}, []);
};
/**
* @hidden
*/
export const getter = (dataItem, field) => {
if (!isPresent(dataItem)) {
return null;
}
if (!isPresent(field) || !isObject(dataItem)) {
return dataItem;
}
// creates a field accessor supporting nested fields processing
const valueFrom = fieldAccessor(field);
return valueFrom(dataItem);
};
/**
* @hidden
*/
export const resolveValue = (args) => {
let dataItem;
if (isPresent(args.value)) {
const data = [args.defaultItem, ...args.data];
dataItem = data.find(element => getter(element, args.valueField) === args.value);
return {
dataItem: dataItem,
focused: args.data.indexOf(dataItem),
selected: args.data.indexOf(dataItem)
};
}
else if (args.index) {
dataItem = args.data[args.index];
return {
dataItem: args.data[args.index],
focused: args.index,
selected: args.index
};
}
return {
dataItem: args.defaultItem,
focused: -1,
selected: -1
};
};
/**
* @hidden
*/
export const sameCharsOnly = (word, character) => {
for (let idx = 0; idx < word.length; idx++) {
if (word.charAt(idx) !== character) {
return false;
}
}
return true;
};
/**
* @hidden
*/
export const shuffleData = (data, splitIndex, defaultItem) => {
let result = data;
if (defaultItem) {
result = [defaultItem].concat(result);
}
return result.slice(splitIndex).concat(result.slice(0, splitIndex));
};
/**
* @hidden
*/
export const matchText = (text, word, ignoreCase) => {
if (!isPresent(text)) {
return false;
}
let temp = String(text);
if (ignoreCase) {
temp = temp.toLowerCase();
}
return temp.indexOf(word) === 0;
};
/**
* @hidden
*/
export const elementFromPoint = (x, y) => {
if (!isDocumentAvailable()) {
return;
}
return document.elementFromPoint(x, y);
};
/**
* @hidden
*
* Checks whether the passed object has all of the listed properties.
*/
export const hasProps = (obj, props) => {
if (!isPresent(obj)) {
return false;
}
return props.every(prop => obj.hasOwnProperty(prop));
};
/**
* @hidden
*
* Checks whether an element is untouched by looking for the ng-untouched css class
*/
export const isUntouched = (element) => element.className.includes('ng-untouched');
/**
* @hidden
*/
export const noop = (_) => { };
/**
* IE element `matches` polyfill.
* https://developer.mozilla.org/en-US/docs/Web/API/Element/matches
*/
const matches = (element, selector) => {
const matcher = element.matches || element.msMatchesSelector || element.webkitMatchesSelector;
if (!matcher) {
return false;
}
return matcher.call(element, selector);
};
/**
* @hidden
*
* IE element `closest` polyfill.
* https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
*/
export const closest = (element, selector) => {
let parent = element;
while (parent !== null && parent.nodeType === 1) {
if (matches(parent, selector)) {
return parent;
}
parent = parent.parentElement || parent.parentNode;
}
return null;
};
/**
* @hidden
*
* Parses a provided value to its type 'number' representation.
* If the parsed value (via Number(value)) is NaN, the provided default value is returned.
* Uses 0 as default value if a second param is not provided.
*/
export const parseNumber = (num, defaultValue = 0) => {
const normalizedValue = Number(num);
return isNaN(normalizedValue) ? defaultValue : normalizedValue;
};
/**
* @hidden
*
* Checks whether the passed target element is inside the provided host or popupRef.
*/
export const inDropDown = (host, target, popupRef) => {
return host.nativeElement.contains(target) || (popupRef && popupRef.popupElement.contains(target));
};
/**
* @hidden
*
* Calculates the hierarchical level of an item, based on the provided index.
* The result level is zero-based (starts from 0).
*/
export const getHierarchicalItemLevel = (index) => {
return (index || '').split('_').length - 1;
};
/**
* @hidden
*
* Retrieves all descendant nodes' lookups which are currently registered in the provided lookup item as a flat array.
*/
export const fetchDescendentNodes = (lookup, filterExpression) => {
if (!isPresent(lookup) || lookup.children.length === 0) {
return [];
}
let descendants = lookup.children;
if (isPresent(filterExpression)) {
descendants = descendants.filter(descendent => filterExpression(descendent.item));
}
descendants.forEach(child => descendants = descendants.concat(fetchDescendentNodes(child, filterExpression)));
return descendants;
};
/**
* @hidden
*
* Retrieves the correct value based on the item's level and the provided value field/s.
* Used in the MultiSelectTree component.
*/
export const valueFrom = ({ dataItem, index, level }, valueField) => {
const fields = Array.isArray(valueField) ? valueField : [valueField];
// either use the explicitly provided value level, or infer it from the item index
const valueLevel = isPresent(level) ? level : getHierarchicalItemLevel(index);
// fall-back to the last available one, if the current node is in a deeper level
const normalizedLevel = Math.min(valueLevel, fields.length - 1);
const field = fields[normalizedLevel];
return fieldAccessor(field)(dataItem);
};
/**
* @hidden
* Returns the size class based on the component and size input.
*/
export const getSizeClass = (component, size) => {
const SIZE_CLASSES = {
'small': `k-${component}-sm`,
'medium': `k-${component}-md`,
'large': `k-${component}-lg`
};
return SIZE_CLASSES[size];
};
/**
* @hidden
* Returns the rounded class based on the rounded input.
*/
export const getRoundedClass = (rounded) => {
const ROUNDED_CLASSES = {
'small': 'k-rounded-sm',
'medium': 'k-rounded-md',
'large': 'k-rounded-lg',
'full': 'k-rounded-full'
};
return ROUNDED_CLASSES[rounded];
};
/**
* @hidden
* Return the fillMode class based on the component and fillMode input.
*/
export const getFillModeClass = (component, fillMode) => {
const FILLMODE_CLASSES = {
'solid': `k-${component}-solid`,
'flat': `k-${component}-flat`,
'outline': `k-${component}-outline`
};
return FILLMODE_CLASSES[fillMode];
};
/**
* @hidden
*/
export const filterAndMap = (arr, predicate, mapper) => arr.reduce((acc, curr) => predicate(curr) ? [...acc, mapper(curr)] : acc, []);
/**
* @hidden
*
* Checks if input is Japanese IME
*/
export const isJapanese = (input) => {
const japaneseRegex = /[\u3000-\u303F]|[\u3040-\u309F]|[\u30A0-\u30FF]|[\uFF00-\uFFEF]|[\u4E00-\u9FAF]|[\u2605-\u2606]|[\u2190-\u2195]|\u203B/g;
return japaneseRegex.test(input);
};
/**
* @hidden
*/
export const isLetter = (text) => {
const isLetter = /[a-zA-Z]/;
return isLetter.test(text) && text?.length === 1;
};
/**
* @hidden
*/
export const getTextField = (field, level) => {
if (isArray(field)) {
return field[level];
}
return field;
};
/**
* @hidden
*/
export const getSearchableItems = (treeViewId, element) => {
const nodeSeletor = `kendo-treeview[id='${treeViewId}'] li.k-treeview-item`;
const liElements = Array.from(element.querySelectorAll(nodeSeletor));
return liElements.map((liElement) => {
return { text: liElement.innerText, index: liElement.getAttribute('data-treeindex') };
});
};
/**
* @hidden
*/
export const isTruthy = (value) => !!value;
/**
* @hidden
*/
export const updateActionSheetAdaptiveAppearance = (actionSheet, windowSize, renderer) => {
const element = actionSheet['element'].nativeElement.querySelector('.k-actionsheet');
const animationContainer = actionSheet['element'].nativeElement.querySelector('.k-child-animation-container');
if (windowSize === 'medium') {
renderer.removeClass(element, 'k-actionsheet-fullscreen');
renderer.addClass(element, 'k-actionsheet-bottom');
renderer.addClass(element, 'k-adaptive-actionsheet');
renderer.removeStyle(animationContainer, 'top');
renderer.removeStyle(animationContainer, 'height');
renderer.setStyle(animationContainer, 'bottom', '0px');
}
else if (windowSize === 'small') {
renderer.removeClass(element, 'k-actionsheet-bottom');
renderer.addClass(element, 'k-actionsheet-fullscreen');
renderer.addClass(element, 'k-adaptive-actionsheet');
renderer.setStyle(animationContainer, 'bottom', '0px');
renderer.setStyle(animationContainer, 'height', '100%');
}
};
/**
* @hidden
*/
export const setListBoxAriaLabelledBy = (optionsList, element, renderer) => {
const listBox = optionsList.wrapper.nativeElement.querySelector('kendo-list ul');
const ariaLabel = element.nativeElement.getAttribute('aria-labelledby') || element.nativeElement.getAttribute('data-kendo-label-id');
if (ariaLabel) {
renderer.setAttribute(listBox, 'aria-labelledby', ariaLabel);
}
};
/**
* @hidden
*/
export const setActionSheetTitle = (element, actionSheetTitle) => {
const ariaLabel = element.nativeElement.getAttribute('aria-labelledby') || element.nativeElement.getAttribute('data-kendo-label-id');
if (!actionSheetTitle && ariaLabel) {
return document.getElementById(ariaLabel).innerText;
}
return actionSheetTitle;
};
/**
* @hidden
*/
export const animationDuration = 300;