UNPKG

@oruga-ui/oruga-next

Version:

UI components for Vue.js and CSS framework agnostic

360 lines (315 loc) 11.3 kB
import { Comment, Fragment, Text } from "vue"; import type { DeepKeys, DeepType } from "@/types"; /** * +/- function to native math sign */ function signPoly(value: number): number { if (value < 0) return -1; return value > 0 ? 1 : 0; } export const sign = Math.sign || signPoly; /** * Native modulo bug with negative numbers * @param n * @param mod * @returns {number} */ export const mod = (n: number, mod: number): number => ((n % mod) + mod) % mod; /** add a prefix `0` to a 1 digit number */ export const pad = (value: number): string => (value < 10 ? "0" : "") + value; /** * Asserts a value is beetween min and max * @param val * @param min * @param max * @returns {number} */ export function bound(val: number, min: number, max: number): number { return Math.max(min, Math.min(max, val)); } /** * checks if the value is of type object */ export const isObject = (value: unknown): value is object => !!value && typeof value === "object" && !Array.isArray(value); /** * checks if the value is of type date */ export const isDate = (value: unknown): value is Date => !!value && value instanceof Date && !isNaN(value.getTime()); /** * checks if the value is not null or undefined */ export const isDefined = <T>(value: T | undefined | null): value is T => value !== null && typeof value !== "undefined"; /** * Determines if the value of a prop that is either present (true) or not * present (undefined). For example, the prop disabled should disable * by just existing, but what if it is set to the string "false" — then it * should not be disabled. * * @param value - Value to check for undefined. * @returns boolean */ export const isTrueish = (value: unknown): boolean => isDefined(value) && value !== "false" && value !== false; export const blankIfUndefined = (value: string | null | undefined): string => isDefined(value) ? value : ""; export const defaultIfUndefined = <T>( value: T | undefined, defaultValue: T, ): T => (isDefined(value) ? value : defaultValue); export const toCssDimension = ( width: string | number | undefined, dimension: string = "px", ): string | undefined => !isDefined(width) ? undefined : isNaN(width as number) ? String(width) : String(width) + dimension; /** * Sort an array by key without mutating original data. * Call the user sort function if it was passed. */ export function sortBy<T extends object>( array: T[], key: DeepKeys<T>, fn?: (a: T, b: T, asc: boolean) => number, isAsc: boolean = false, mutate: boolean = false, ): T[] { // Sorting without mutating original data if (fn && typeof fn === "function") { return (mutate ? array : [...array]).sort((a, b) => fn(a, b, isAsc)); } else { return (mutate ? array : [...array]).sort((a, b) => { // Get nested values from objects let newA: any = isObject(a) ? getValueByPath(a, key) : a; let newB: any = isObject(b) ? getValueByPath(b, key) : b; // sort boolean type if (typeof newA === "boolean" && typeof newB === "boolean") { return isAsc ? (newA > newB ? 1 : -1) : newA > newB ? -1 : 1; } if (!newA && newA !== 0) return 1; if (!newB && newB !== 0) return -1; if (newA === newB) return 0; newA = typeof newA === "string" ? newA.toUpperCase() : newA; newB = typeof newB === "string" ? newB.toUpperCase() : newB; return isAsc ? (newA > newB ? 1 : -1) : newA > newB ? -1 : 1; }); } } /** * Deeply check if two values are equal */ export function isEqual(valueA: unknown, valueB: unknown): boolean { // Check if only one value is empty. if ((!valueA && !!valueB) || (!!valueA && !valueB)) return false; // If both objects are identical, return true. if (valueA === valueB) return true; // Check if both values are objecs. if (isObject(valueA) && isObject(valueB)) { // Get the keys of both objects. const keys1 = Object.keys(valueA); const keys2 = Object.keys(valueB); // Check if the number of keys is the same. if (keys1.length !== keys2.length) return false; // Iterate through the keys and compare their values recursively. for (const key of keys1) { const val1 = valueA[key]; const val2 = valueB[key]; const areObjects = isObject(val1) && isObject(val2); if ( (areObjects && !isEqual(val1, val2)) || (!areObjects && val1 !== val2) ) return false; } // If all checks pass, the objects are deep equal. return true; } // Check if both values are arrays. if (Array.isArray(valueA) && Array.isArray(valueB)) { // Check if the number of keys is the same. if (valueA.length !== valueB.length) return false; // Check if each value of the array is the same. if (!valueA.every((val, index) => val === valueB[index])) return false; // If all checks pass, the arrays are deep equal. return true; } return false; } /** * @deprecated not used * Returns true if it is a DOM element * @source https://stackoverflow.com/questions/384286/how-do-you-check-if-a-javascript-object-is-a-dom-object */ export function isElement(el: any): el is Element { return typeof HTMLElement === "object" ? el instanceof HTMLElement //DOM2 : el && typeof el === "object" && el !== null && el.nodeType === 1 && typeof el.nodeName === "string"; } /** * Merge function to replace Object.assign with deep merging possibility */ export function merge(target: any, source: any, deep = false): any { if (!isObject(target) || !isObject(source)) return source; if (!deep) return Object.assign(target, source); else return mergeDeep(target, source); } /** * Performs a deep merge of `source` into `target`. * Mutates `target` only but not its objects and arrays. * * @author inspired by [jhildenbiddle](https://stackoverflow.com/a/48218209). */ export function mergeDeep(target: any, source: any): any { if (!isObject(target) || !isObject(source)) return source; Object.getOwnPropertyNames(source).forEach((key) => { const targetValue = target[key]; const sourceValue = source[key]; if (Array.isArray(targetValue) && Array.isArray(sourceValue)) { target[key] = targetValue.concat(sourceValue); } else if (isObject(targetValue) && isObject(sourceValue)) { target[key] = mergeDeep( Object.assign({}, targetValue), sourceValue, ); } else { target[key] = sourceValue; } }); return target; } /** * Return display text for an option. * If option is an object, get the property from path based on given field, or else just the property. * Apply a formatter function to the property if given. * Return the display label. * * @param obj Object to get the label for * @param field Property path of the object to use as display text * @param formatter Function to format the property to a string */ export function getPropertyValue< O, K extends DeepKeys<O>, D extends DeepType<O, K>, >(obj: O, field?: K, formatter?: (value: D, option: O) => string): string { if (!obj) return ""; const property = (field ? getValueByPath<O, K, D>(obj, field) : obj) as D; const label = typeof formatter === "function" ? formatter(property, obj) : property; return String(label || ""); } /** * Get a value of an object property/path even if it's nested */ export function getValueByPath< O, K extends DeepKeys<O>, D extends DeepType<O, K>, >(obj: O, path: K | (string & {})): D | undefined; export function getValueByPath< O, K extends DeepKeys<O>, D extends DeepType<O, K>, >(obj: O, path: K | (string & {}), defaultValue: D): D; export function getValueByPath< O, K extends DeepKeys<O>, D extends DeepType<O, K>, >(obj: O, path: K | (string & {}), defaultValue?: D): D | undefined; export function getValueByPath< O, K extends DeepKeys<O>, D extends DeepType<O, K>, >(obj: O, path: K | (string & {}), defaultValue?: D): D | undefined { if (!obj || typeof obj !== "object" || typeof path !== "string") return defaultValue; const value: any = path .split(".") .reduce((o, i) => (typeof o !== "undefined" ? o[i] : undefined), obj); return typeof value !== "undefined" ? value : defaultValue; } /** * Set a value of an object property/path even if it's nested */ export function setValueByPath<O, K extends DeepKeys<O>>( obj: O, path: K, value: DeepType<O, K>, ): void { if (typeof path !== "string") return; const p = path.split("."); if (p.length === 1) { obj[p[0]] = value; return; } const field = p[0]; if (typeof obj[field] === "undefined") obj[field] = {}; return setValueByPath(obj[field], p.slice(1).join("."), value); } export function removeElement(el: Element): void { if (typeof el.remove !== "undefined") { el.remove(); } else if (typeof el.parentNode !== "undefined" && el.parentNode !== null) { el.parentNode.removeChild(el); } } /** * Escape regex characters * http://stackoverflow.com/a/6969486 */ export function escapeRegExpChars(value: string): string { if (!value) return value; return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); } /** * Remove accents/diacritics in a string * https://stackoverflow.com/a/37511463 */ export function removeDiacriticsFromString(value: string): string { if (!value) return value; return value.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } /** checks if a vue vnode is empty */ export function isVNodeEmpty(vnode): boolean { if (!vnode) return true; if (vnode.type === Comment) return true; if (vnode.type === Text && !vnode.children.trim()) return true; if (vnode.type === Fragment && isVNodeEmpty(vnode.children)) return true; return false; } /** * Mobile detection * https://www.abeautifulsite.net/detecting-mobile-devices-with-javascript */ export const isMobileAgent = { Android: (): boolean => typeof window !== "undefined" && !!window.navigator.userAgent.match(/Android/i), BlackBerry: (): boolean => typeof window !== "undefined" && !!window.navigator.userAgent.match(/BlackBerry/i), iOS: (): boolean => typeof window !== "undefined" && !!window.navigator.userAgent.match(/iPhone|iPad|iPod/i), Opera: (): boolean => typeof window !== "undefined" && !!window.navigator.userAgent.match(/Opera Mini/i), Windows: (): boolean => typeof window !== "undefined" && !!window.navigator.userAgent.match(/IEMobile/i), any: (): boolean => isMobileAgent.Android() || isMobileAgent.BlackBerry() || isMobileAgent.iOS() || isMobileAgent.Opera() || isMobileAgent.Windows(), };