UNPKG

igniteui-angular-sovn

Version:

Ignite UI for Angular is a dependency-free Angular toolkit for building modern web apps

837 lines (766 loc) 24.4 kB
import { AnimationReferenceMetadata } from '@angular/animations'; import { CurrencyPipe, formatDate as _formatDate, isPlatformBrowser } from '@angular/common'; import { Inject, Injectable, PLATFORM_ID } from '@angular/core'; import { mergeWith } from 'lodash-es'; import { Observable } from 'rxjs'; import { blink, fadeIn, fadeOut, flipBottom, flipHorBck, flipHorFwd, flipLeft, flipRight, flipTop, flipVerBck, flipVerFwd, growVerIn, growVerOut, heartbeat, pulsateBck, pulsateFwd, rotateInBl, rotateInBottom, rotateInBr, rotateInCenter, rotateInDiagonal1, rotateInDiagonal2, rotateInHor, rotateInLeft, rotateInRight, rotateInTl, rotateInTop, rotateInTr, rotateInVer, rotateOutBl, rotateOutBottom, rotateOutBr, rotateOutCenter, rotateOutDiagonal1, rotateOutDiagonal2, rotateOutHor, rotateOutLeft, rotateOutRight, rotateOutTl, rotateOutTop, rotateOutTr, rotateOutVer, scaleInBl, scaleInBottom, scaleInBr, scaleInCenter, scaleInHorCenter, scaleInHorLeft, scaleInHorRight, scaleInLeft, scaleInRight, scaleInTl, scaleInTop, scaleInTr, scaleInVerBottom, scaleInVerCenter, scaleInVerTop, scaleOutBl, scaleOutBottom, scaleOutBr, scaleOutCenter, scaleOutHorCenter, scaleOutHorLeft, scaleOutHorRight, scaleOutLeft, scaleOutRight, scaleOutTl, scaleOutTop, scaleOutTr, scaleOutVerBottom, scaleOutVerCenter, scaleOutVerTop, shakeBl, shakeBottom, shakeBr, shakeCenter, shakeHor, shakeLeft, shakeRight, shakeTl, shakeTop, shakeTr, shakeVer, slideInBl, slideInBottom, slideInBr, slideInLeft, slideInRight, slideInTl, slideInTop, slideInTr, slideOutBl, slideOutBottom, slideOutBr, slideOutLeft, slideOutRight, slideOutTl, slideOutTop, slideOutTr, swingInBottomBck, swingInBottomFwd, swingInLeftBck, swingInLeftFwd, swingInRightBck, swingInRightFwd, swingInTopBck, swingInTopFwd, swingOutBottomBck, swingOutBottomFwd, swingOutLeftBck, swingOutLefttFwd, swingOutRightBck, swingOutRightFwd, swingOutTopBck, swingOutTopFwd } from '../animations/main'; import { setImmediate } from './setImmediate'; import { isDevMode } from '@angular/core'; /** * @hidden */ export const showMessage = (message: string, isMessageShown: boolean): boolean => { if (!isMessageShown && isDevMode()) { console.warn(message); } return true; }; export const mkenum = <T extends { [index: string]: U }, U extends string>(x: T) => x; /** * * @hidden @internal */ export const getResizeObserver = () => window.ResizeObserver; /** * @hidden */ export const cloneArray = (array: any[], deep?: boolean) => { const arr = []; if (!array) { return arr; } let i = array.length; while (i--) { arr[i] = deep ? cloneValue(array[i]) : array[i]; } return arr; }; /** * Doesn't clone leaf items * * @hidden */ export const cloneHierarchicalArray = (array: any[], childDataKey: any): any[] => { const result: any[] = []; if (!array) { return result; } for (const item of array) { const clonedItem = cloneValue(item); if (Array.isArray(item[childDataKey])) { clonedItem[childDataKey] = cloneHierarchicalArray(clonedItem[childDataKey], childDataKey); } result.push(clonedItem); } return result; }; /** * Creates an object with prototype from provided source and copies * all properties descriptors from provided source * @param obj Source to copy prototype and descriptors from * @returns New object with cloned prototype and property descriptors */ export const copyDescriptors = (obj) => { if (obj) { return Object.create( Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj) ); } } /** * Deep clones all first level keys of Obj2 and merges them to Obj1 * * @param obj1 Object to merge into * @param obj2 Object to merge from * @returns Obj1 with merged cloned keys from Obj2 * @hidden */ export const mergeObjects = (obj1: any, obj2: any): any => mergeWith(obj1, obj2, (objValue, srcValue) => { if (Array.isArray(srcValue)) { return objValue = srcValue; } }); /** * Creates deep clone of provided value. * Supports primitive values, dates and objects. * If passed value is array returns shallow copy of the array. * * @param value value to clone * @returns Deep copy of provided value * @hidden */ export const cloneValue = (value: any): any => { if (isDate(value)) { return new Date(value.getTime()); } if (Array.isArray(value)) { return [...value]; } if (value instanceof Map || value instanceof Set) { return value; } if (isObject(value)) { const result = {}; for (const key of Object.keys(value)) { result[key] = cloneValue(value[key]); } return result; } return value; }; /** * Creates deep clone of provided value. * Supports primitive values, dates and objects. * If passed value is array returns shallow copy of the array. * For Objects property values and references are cached and reused. * This allows for circular references to same objects. * * @param value value to clone * @param cache map of cached values already parsed * @returns Deep copy of provided value * @hidden */ export const cloneValueCached = (value: any, cache: Map<any, any>): any => { if (isDate(value)) { return new Date(value.getTime()); } if (Array.isArray(value)) { return [...value]; } if (value instanceof Map || value instanceof Set) { return value; } if (isObject(value)) { if (cache.has(value)) { return cache.get(value); } const result = {}; cache.set(value, result); for (const key of Object.keys(value)) { result[key] = cloneValueCached(value[key], cache); } return result; } return value; }; /** * Parse provided input to Date. * * @param value input to parse * @returns Date if parse succeed or null * @hidden */ export const parseDate = (value: any): Date | null => { // if value is Invalid Date return null if (isDate(value)) { return !isNaN(value.getTime()) ? value : null; } return value ? new Date(value) : null; }; /** * Returns an array with unique dates only. * * @param columnValues collection of date values (might be numbers or ISO 8601 strings) * @returns collection of unique dates. * @hidden */ export const uniqueDates = (columnValues: any[]) => columnValues.reduce((a, c) => { if (!a.cache[c.label]) { a.result.push(c); } a.cache[c.label] = true; return a; }, { result: [], cache: {} }).result; /** * Checks if provided variable is Object * * @param value Value to check * @returns true if provided variable is Object * @hidden */ export const isObject = (value: any): boolean => !!(value && value.toString() === '[object Object]'); /** * Checks if provided variable is Date * * @param value Value to check * @returns true if provided variable is Date * @hidden */ export const isDate = (value: any): value is Date => value instanceof Date; /** * Checks if the two passed arguments are equal * Currently supports date objects * * @param obj1 * @param obj2 * @returns: `boolean` * @hidden */ export const isEqual = (obj1, obj2): boolean => { if (isDate(obj1) && isDate(obj2)) { return obj1.getTime() === obj2.getTime(); } return obj1 === obj2; }; /** * Checks if provided variable is the value NaN * * @param value Value to check * @returns true if provided variable is NaN * @hidden */ export const isNaNvalue = (value: any): boolean => isNaN(value) && value !== undefined && typeof value !== 'string'; /** * Utility service taking care of various utility functions such as * detecting browser features, general cross browser DOM manipulation, etc. * * @hidden @internal */ @Injectable({ providedIn: 'root' }) export class PlatformUtil { public isBrowser: boolean = isPlatformBrowser(this.platformId); public isIOS = this.isBrowser && /iPad|iPhone|iPod/.test(navigator.userAgent) && !('MSStream' in window); public isFirefox = this.isBrowser && /Firefox[\/\s](\d+\.\d+)/.test(navigator.userAgent); public isEdge = this.isBrowser && /Edge[\/\s](\d+\.\d+)/.test(navigator.userAgent); public isChromium = this.isBrowser && (/Chrom|e?ium/g.test(navigator.userAgent) || /Google Inc/g.test(navigator.vendor)) && !/Edge/g.test(navigator.userAgent); public KEYMAP = mkenum({ ENTER: 'Enter', SPACE: ' ', ESCAPE: 'Escape', ARROW_DOWN: 'ArrowDown', ARROW_UP: 'ArrowUp', ARROW_LEFT: 'ArrowLeft', ARROW_RIGHT: 'ArrowRight', END: 'End', HOME: 'Home', PAGE_DOWN: 'PageDown', PAGE_UP: 'PageUp', F2: 'F2', TAB: 'Tab', SEMICOLON: ';', // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values#editing_keys DELETE: 'Delete', BACKSPACE: 'Backspace', CONTROL: 'Control', X: 'x', Y: 'y', Z: 'z' }); constructor(@Inject(PLATFORM_ID) private platformId: any) { } /** * @hidden @internal * Returns the actual size of the node content, using Range * ```typescript * let range = document.createRange(); * let column = this.grid.columnList.filter(c => c.field === 'ID')[0]; * * let size = getNodeSizeViaRange(range, column.cells[0].nativeElement); * * @remarks * The last parameter is useful when the size of the element to measure is modified by a * parent element that has explicit size. In such cases the calculated size is never lower * and the function may instead remove the parent size while measuring to get the correct value. * ``` */ public getNodeSizeViaRange(range: Range, node: HTMLElement, sizeHoldingNode?: HTMLElement) { let overflow = null; let nodeStyles; if (!this.isFirefox) { overflow = node.style.overflow; // we need that hack - otherwise content won't be measured correctly in IE/Edge node.style.overflow = 'visible'; } if (sizeHoldingNode) { const style = sizeHoldingNode.style; nodeStyles = [style.width, style.minWidth, style.flexBasis]; style.width = ''; style.minWidth = ''; style.flexBasis = ''; } range.selectNodeContents(node); const scale = node.getBoundingClientRect().width / node.offsetWidth; const width = range.getBoundingClientRect().width / scale; if (!this.isFirefox) { // we need that hack - otherwise content won't be measured correctly in IE/Edge node.style.overflow = overflow; } if (sizeHoldingNode) { sizeHoldingNode.style.width = nodeStyles[0]; sizeHoldingNode.style.minWidth = nodeStyles[1]; sizeHoldingNode.style.flexBasis = nodeStyles[2]; } return width; } /** * Returns true if the current keyboard event is an activation key (Enter/Space bar) * * @hidden * @internal * * @memberof PlatformUtil */ public isActivationKey(event: KeyboardEvent) { return event.key === this.KEYMAP.ENTER || event.key === this.KEYMAP.SPACE; } /** * Returns true if the current keyboard event is a combination that closes the filtering UI of the grid. (Escape/Ctrl+Shift+L) * * @hidden * @internal * @param event * @memberof PlatformUtil */ public isFilteringKeyCombo(event: KeyboardEvent) { return event.key === this.KEYMAP.ESCAPE || (event.ctrlKey && event.shiftKey && event.key.toLowerCase() === 'l'); } /** * @hidden @internal */ public isLeftClick(event: PointerEvent | MouseEvent) { return event.button === 0; } /** * @hidden @internal */ public isNavigationKey(key: string) { return [ this.KEYMAP.HOME, this.KEYMAP.END, this.KEYMAP.SPACE, this.KEYMAP.ARROW_DOWN, this.KEYMAP.ARROW_LEFT, this.KEYMAP.ARROW_RIGHT, this.KEYMAP.ARROW_UP ].includes(key as any); } } /** * @hidden */ export const flatten = (arr: any[]) => { let result = []; arr.forEach(el => { result.push(el); if (el.children) { const children = Array.isArray(el.children) ? el.children : el.children.toArray(); result = result.concat(flatten(children)); } }); return result; }; export interface CancelableEventArgs { /** * Provides the ability to cancel the event. */ cancel: boolean; } export interface IBaseEventArgs { /** * Provides reference to the owner component. */ owner?: any; } export interface CancelableBrowserEventArgs extends CancelableEventArgs { /** Browser event */ event?: Event; } export interface IBaseCancelableBrowserEventArgs extends CancelableBrowserEventArgs, IBaseEventArgs { } export interface IBaseCancelableEventArgs extends CancelableEventArgs, IBaseEventArgs { } export const HORIZONTAL_NAV_KEYS = new Set(['arrowleft', 'left', 'arrowright', 'right', 'home', 'end']); export const NAVIGATION_KEYS = new Set([ 'down', 'up', 'left', 'right', 'arrowdown', 'arrowup', 'arrowleft', 'arrowright', 'home', 'end', 'space', 'spacebar', ' ' ]); export const ACCORDION_NAVIGATION_KEYS = new Set('up down arrowup arrowdown home end'.split(' ')); export const ROW_EXPAND_KEYS = new Set('right down arrowright arrowdown'.split(' ')); export const ROW_COLLAPSE_KEYS = new Set('left up arrowleft arrowup'.split(' ')); export const ROW_ADD_KEYS = new Set(['+', 'add', '≠', '±', '=']); export const SUPPORTED_KEYS = new Set([...Array.from(NAVIGATION_KEYS), ...Array.from(ROW_ADD_KEYS), 'enter', 'f2', 'escape', 'esc', 'pagedown', 'pageup']); export const HEADER_KEYS = new Set([...Array.from(NAVIGATION_KEYS), 'escape', 'esc', 'l', /** This symbol corresponds to the Alt + L combination under MAC. */ '¬']); /** * @hidden * @internal * * Creates a new ResizeObserver on `target` and returns it as an Observable. * Run the resizeObservable outside angular zone, because it patches the MutationObserver which causes an infinite loop. * Related issue: https://github.com/angular/angular/issues/31712 */ export const resizeObservable = (target: HTMLElement): Observable<ResizeObserverEntry[]> => new Observable((observer) => { const instance = new (getResizeObserver())((entries: ResizeObserverEntry[]) => { observer.next(entries); }); instance.observe(target); const unsubscribe = () => instance.disconnect(); return unsubscribe; }); /** * @hidden * @internal * * Compares two maps. */ export const compareMaps = (map1: Map<any, any>, map2: Map<any, any>): boolean => { if (!map2) { return !map1 ? true : false; } if (map1.size !== map2.size) { return false; } let match = true; const keys = Array.from(map2.keys()); for (const key of keys) { if (map1.has(key)) { match = map1.get(key) === map2.get(key); } else { match = false; } if (!match) { break; } } return match; }; /** * * Given a property access path in the format `x.y.z` resolves and returns * the value of the `z` property in the passed object. * * @hidden * @internal */ export const resolveNestedPath = (obj: any, path: string) => { const parts = path?.split('.') ?? []; let current = obj[parts.shift()]; parts.forEach(prop => { if (current) { current = current[prop]; } }); return current; }; /** * * Given a property access path in the format `x.y.z` and a value * this functions builds and returns an object following the access path. * * @example * ```typescript * console.log('x.y.z.', 42); * >> { x: { y: { z: 42 } } } * ``` * * @hidden * @internal */ export const reverseMapper = (path: string, value: any) => { const obj = {}; const parts = path?.split('.') ?? []; let _prop = parts.shift(); let mapping: any; // Initial binding for first level bindings obj[_prop] = value; mapping = obj; parts.forEach(prop => { // Start building the hierarchy mapping[_prop] = {}; // Go down a level mapping = mapping[_prop]; // Bind the value and move the key mapping[prop] = value; _prop = prop; }); return obj; }; export const yieldingLoop = (count: number, chunkSize: number, callback: (index: number) => void, done: () => void) => { let i = 0; const chunk = () => { const end = Math.min(i + chunkSize, count); for (; i < end; ++i) { callback(i); } if (i < count) { setImmediate(chunk); } else { done(); } }; chunk(); }; export const isConstructor = (ref: any) => typeof ref === 'function' && Boolean(ref.prototype) && Boolean(ref.prototype.constructor); export const reverseAnimationResolver = (animation: AnimationReferenceMetadata): AnimationReferenceMetadata => oppositeAnimation.get(animation) ?? animation; export const isHorizontalAnimation = (animation: AnimationReferenceMetadata): boolean => horizontalAnimations.includes(animation); export const isVerticalAnimation = (animation: AnimationReferenceMetadata): boolean => verticalAnimations.includes(animation); const oppositeAnimation: Map<AnimationReferenceMetadata, AnimationReferenceMetadata> = new Map([ [fadeIn, fadeIn], [fadeOut, fadeOut], [flipTop, flipBottom], [flipBottom, flipTop], [flipRight, flipLeft], [flipLeft, flipRight], [flipHorFwd, flipHorBck], [flipHorBck, flipHorFwd], [flipVerFwd, flipVerBck], [flipVerBck, flipVerFwd], [growVerIn, growVerIn], [growVerOut, growVerOut], [heartbeat, heartbeat], [pulsateFwd, pulsateBck], [pulsateBck, pulsateFwd], [blink, blink], [shakeHor, shakeHor], [shakeVer, shakeVer], [shakeTop, shakeTop], [shakeBottom, shakeBottom], [shakeRight, shakeRight], [shakeLeft, shakeLeft], [shakeCenter, shakeCenter], [shakeTr, shakeTr], [shakeBr, shakeBr], [shakeBl, shakeBl], [shakeTl, shakeTl], [rotateInCenter, rotateInCenter], [rotateOutCenter, rotateOutCenter], [rotateInTop, rotateInBottom], [rotateOutTop, rotateOutBottom], [rotateInRight, rotateInLeft], [rotateOutRight, rotateOutLeft], [rotateInLeft, rotateInRight], [rotateOutLeft, rotateOutRight], [rotateInBottom, rotateInTop], [rotateOutBottom, rotateOutTop], [rotateInTr, rotateInBl], [rotateOutTr, rotateOutBl], [rotateInBr, rotateInTl], [rotateOutBr, rotateOutTl], [rotateInBl, rotateInTr], [rotateOutBl, rotateOutTr], [rotateInTl, rotateInBr], [rotateOutTl, rotateOutBr], [rotateInDiagonal1, rotateInDiagonal1], [rotateOutDiagonal1, rotateOutDiagonal1], [rotateInDiagonal2, rotateInDiagonal2], [rotateOutDiagonal2, rotateOutDiagonal2], [rotateInHor, rotateInHor], [rotateOutHor, rotateOutHor], [rotateInVer, rotateInVer], [rotateOutVer, rotateOutVer], [scaleInTop, scaleInBottom], [scaleOutTop, scaleOutBottom], [scaleInRight, scaleInLeft], [scaleOutRight, scaleOutLeft], [scaleInBottom, scaleInTop], [scaleOutBottom, scaleOutTop], [scaleInLeft, scaleInRight], [scaleOutLeft, scaleOutRight], [scaleInCenter, scaleInCenter], [scaleOutCenter, scaleOutCenter], [scaleInTr, scaleInBl], [scaleOutTr, scaleOutBl], [scaleInBr, scaleInTl], [scaleOutBr, scaleOutTl], [scaleInBl, scaleInTr], [scaleOutBl, scaleOutTr], [scaleInTl, scaleInBr], [scaleOutTl, scaleOutBr], [scaleInVerTop, scaleInVerBottom], [scaleOutVerTop, scaleOutVerBottom], [scaleInVerBottom, scaleInVerTop], [scaleOutVerBottom, scaleOutVerTop], [scaleInVerCenter, scaleInVerCenter], [scaleOutVerCenter, scaleOutVerCenter], [scaleInHorCenter, scaleInHorCenter], [scaleOutHorCenter, scaleOutHorCenter], [scaleInHorLeft, scaleInHorRight], [scaleOutHorLeft, scaleOutHorRight], [scaleInHorRight, scaleInHorLeft], [scaleOutHorRight, scaleOutHorLeft], [slideInTop, slideInBottom], [slideOutTop, slideOutBottom], [slideInRight, slideInLeft], [slideOutRight, slideOutLeft], [slideInBottom, slideInTop], [slideOutBottom, slideOutTop], [slideInLeft, slideInRight], [slideOutLeft, slideOutRight], [slideInTr, slideInBl], [slideOutTr, slideOutBl], [slideInBr, slideInTl], [slideOutBr, slideOutTl], [slideInBl, slideInTr], [slideOutBl, slideOutTr], [slideInTl, slideInBr], [slideOutTl, slideOutBr], [swingInTopFwd, swingInBottomFwd], [swingOutTopFwd, swingOutBottomFwd], [swingInRightFwd, swingInLeftFwd], [swingOutRightFwd, swingOutLefttFwd], [swingInLeftFwd, swingInRightFwd], [swingOutLefttFwd, swingOutRightFwd], [swingInBottomFwd, swingInTopFwd], [swingOutBottomFwd, swingOutTopFwd], [swingInTopBck, swingInBottomBck], [swingOutTopBck, swingOutBottomBck], [swingInRightBck, swingInLeftBck], [swingOutRightBck, swingOutLeftBck], [swingInBottomBck, swingInTopBck], [swingOutBottomBck, swingOutTopBck], [swingInLeftBck, swingInRightBck], [swingOutLeftBck, swingOutRightBck], ]); const horizontalAnimations: AnimationReferenceMetadata[] = [ flipRight, flipLeft, flipVerFwd, flipVerBck, rotateInRight, rotateOutRight, rotateInLeft, rotateOutLeft, rotateInTr, rotateOutTr, rotateInBr, rotateOutBr, rotateInBl, rotateOutBl, rotateInTl, rotateOutTl, scaleInRight, scaleOutRight, scaleInLeft, scaleOutLeft, scaleInTr, scaleOutTr, scaleInBr, scaleOutBr, scaleInBl, scaleOutBl, scaleInTl, scaleOutTl, scaleInHorLeft, scaleOutHorLeft, scaleInHorRight, scaleOutHorRight, slideInRight, slideOutRight, slideInLeft, slideOutLeft, slideInTr, slideOutTr, slideInBr, slideOutBr, slideInBl, slideOutBl, slideInTl, slideOutTl, swingInRightFwd, swingOutRightFwd, swingInLeftFwd, swingOutLefttFwd, swingInRightBck, swingOutRightBck, swingInLeftBck, swingOutLeftBck, ]; const verticalAnimations: AnimationReferenceMetadata[] = [ flipTop, flipBottom, flipHorFwd, flipHorBck, growVerIn, growVerOut, rotateInTop, rotateOutTop, rotateInBottom, rotateOutBottom, rotateInTr, rotateOutTr, rotateInBr, rotateOutBr, rotateInBl, rotateOutBl, rotateInTl, rotateOutTl, scaleInTop, scaleOutTop, scaleInBottom, scaleOutBottom, scaleInTr, scaleOutTr, scaleInBr, scaleOutBr, scaleInBl, scaleOutBl, scaleInTl, scaleOutTl, scaleInVerTop, scaleOutVerTop, scaleInVerBottom, scaleOutVerBottom, slideInTop, slideOutTop, slideInBottom, slideOutBottom, slideInTr, slideOutTr, slideInBr, slideOutBr, slideInBl, slideOutBl, slideInTl, slideOutTl, swingInTopFwd, swingOutTopFwd, swingInBottomFwd, swingOutBottomFwd, swingInTopBck, swingOutTopBck, swingInBottomBck, swingOutBottomBck, ]; /** * Similar to Angular's formatDate. However it will not throw on `undefined | null | ''` instead * coalescing to an empty string. */ export const formatDate = (value: string | number | Date, format: string, locale: string, timezone?: string): string => { if (value === null || value === undefined || value === '') { return ''; } return _formatDate(value, format, locale, timezone); }; export const formatCurrency = new CurrencyPipe(undefined).transform; /** Converts pixel values to their rem counterparts for a base value */ export const rem = (value: number | string) => { const base = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('font-size')) return Number(value) / base; }