import Vue from 'vue' import { VNode, VNodeDirective } from 'vue/types' import { VuetifyIcon } from 'vuetify/types/services/icons' import { DataTableCompareFunction, SelectItemKey, ItemGroup } from 'vuetify/types' export function createSimpleFunctional ( c: string, el = 'div', name?: string ) { return Vue.extend({ name: name || c.replace(/__/g, '-'), functional: true, render (h, { data, children }): VNode { data.staticClass = (`${c} ${data.staticClass || ''}`).trim() return h(el, data, children) }, }) } export type BindingConfig = Pick export function directiveConfig (binding: BindingConfig, defaults = {}): VNodeDirective { return { ...defaults, ...binding.modifiers, value: binding.arg, ...(binding.value || {}), } } export function addOnceEventListener ( el: EventTarget, eventName: string, cb: (event: Event) => void, options: boolean | AddEventListenerOptions = false ): void { var once = (event: Event) => { cb(event) el.removeEventListener(eventName, once, options) } el.addEventListener(eventName, once, options) } let passiveSupported = false try { if (typeof window !== 'undefined') { const testListenerOpts = Object.defineProperty({}, 'passive', { get: () => { passiveSupported = true }, }) window.addEventListener('testListener', testListenerOpts, testListenerOpts) window.removeEventListener('testListener', testListenerOpts, testListenerOpts) } } catch (e) { console.warn(e) } export { passiveSupported } export function addPassiveEventListener ( el: EventTarget, event: string, cb: EventHandlerNonNull | (() => void), options: {} ): void { el.addEventListener(event, cb, passiveSupported ? options : false) } export function getNestedValue (obj: any, path: (string | number)[], fallback?: any): any { const last = path.length - 1 if (last < 0) return obj === undefined ? fallback : obj for (let i = 0; i < last; i++) { if (obj == null) { return fallback } obj = obj[path[i]] } if (obj == null) return fallback return obj[path[last]] === undefined ? fallback : obj[path[last]] } export function deepEqual (a: any, b: any): boolean { if (a === b) return true if ( a instanceof Date && b instanceof Date && a.getTime() !== b.getTime() ) { // If the values are Date, compare them as timestamps return false } if (a !== Object(a) || b !== Object(b)) { // If the values aren't objects, they were already checked for equality return false } const props = Object.keys(a) if (props.length !== Object.keys(b).length) { // Different number of props, don't bother to check return false } return props.every(p => deepEqual(a[p], b[p])) } export function getObjectValueByPath (obj: any, path: string, fallback?: any): any { // credit: http://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-with-string-key#comment55278413_6491621 if (obj == null || !path || typeof path !== 'string') return fallback if (obj[path] !== undefined) return obj[path] path = path.replace(/\[(\w+)\]/g, '.$1') // convert indexes to properties path = path.replace(/^\./, '') // strip a leading dot return getNestedValue(obj, path.split('.'), fallback) } export function getPropertyFromItem ( item: object, property: SelectItemKey, fallback?: any ): any { if (property == null) return item === undefined ? fallback : item if (item !== Object(item)) return fallback === undefined ? item : fallback if (typeof property === 'string') return getObjectValueByPath(item, property, fallback) if (Array.isArray(property)) return getNestedValue(item, property, fallback) if (typeof property !== 'function') return fallback const value = property(item, fallback) return typeof value === 'undefined' ? fallback : value } export function createRange (length: number): number[] { return Array.from({ length }, (v, k) => k) } export function getZIndex (el?: Element | null): number { if (!el || el.nodeType !== Node.ELEMENT_NODE) return 0 const index = +window.getComputedStyle(el).getPropertyValue('z-index') if (!index) return getZIndex(el.parentNode as Element) return index } const tagsToReplace = { '&': '&', '<': '<', '>': '>', } as any export function escapeHTML (str: string): string { return str.replace(/[&<>]/g, tag => tagsToReplace[tag] || tag) } export function filterObjectOnKeys (obj: T, keys: K[]): { [N in K]: T[N] } { const filtered = {} as { [N in K]: T[N] } for (let i = 0; i < keys.length; i++) { const key = keys[i] if (typeof obj[key] !== 'undefined') { filtered[key] = obj[key] } } return filtered } export function convertToUnit (str: string | number | null | undefined, unit = 'px'): string | undefined { if (str == null || str === '') { return undefined } else if (isNaN(+str!)) { return String(str) } else { return `${Number(str)}${unit}` } } export function kebabCase (str: string): string { return (str || '').replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() } export function isObject (obj: any): obj is object { return obj !== null && typeof obj === 'object' } // KeyboardEvent.keyCode aliases export const keyCodes = Object.freeze({ enter: 13, tab: 9, delete: 46, esc: 27, space: 32, up: 38, down: 40, left: 37, right: 39, end: 35, home: 36, del: 46, backspace: 8, insert: 45, pageup: 33, pagedown: 34, }) /** * This remaps internal names like '$cancel' or '$vuetify.icons.cancel' * to the current name or component for that icon. */ export function remapInternalIcon (vm: Vue, iconName: string): VuetifyIcon { // Look for custom component in the configuration const component = vm.$vuetify.icons.component // Look for overrides if (iconName.startsWith('$')) { // Get the target icon name const iconPath = `$vuetify.icons.values.${iconName.split('$').pop()!.split('.').pop()}` // Now look up icon indirection name, // e.g. '$vuetify.icons.values.cancel' const override = getObjectValueByPath(vm, iconPath, iconName) if (typeof override === 'string') iconName = override else return override } if (component == null) { return iconName } return { component, props: { icon: iconName, }, } } export function keys (o: O) { return Object.keys(o) as (keyof O)[] } /** * Camelize a hyphen-delimited string. */ const camelizeRE = /-(\w)/g export const camelize = (str: string): string => { return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '') } /** * Returns the set difference of B and A, i.e. the set of elements in B but not in A */ export function arrayDiff (a: any[], b: any[]): any[] { const diff: any[] = [] for (let i = 0; i < b.length; i++) { if (a.indexOf(b[i]) < 0) diff.push(b[i]) } return diff } /** * Makes the first character of a string uppercase */ export function upperFirst (str: string): string { return str.charAt(0).toUpperCase() + str.slice(1) } export function groupItems ( items: T[], groupBy: string[], groupDesc: boolean[] ): ItemGroup[] { const key = groupBy[0] const groups: ItemGroup[] = [] let current for (var i = 0; i < items.length; i++) { const item = items[i] const val = getObjectValueByPath(item, key, null) if (current !== val) { current = val groups.push({ name: val ?? '', items: [], }) } groups[groups.length - 1].items.push(item) } return groups } export function wrapInArray (v: T | T[] | null | undefined): T[] { return v != null ? Array.isArray(v) ? v : [v] : [] } export function sortItems ( items: T[], sortBy: string[], sortDesc: boolean[], locale: string, customSorters?: Record> ): T[] { if (sortBy === null || !sortBy.length) return items const stringCollator = new Intl.Collator(locale, { sensitivity: 'accent', usage: 'sort' }) return items.sort((a, b) => { for (let i = 0; i < sortBy.length; i++) { const sortKey = sortBy[i] let sortA = getObjectValueByPath(a, sortKey) let sortB = getObjectValueByPath(b, sortKey) if (sortDesc[i]) { [sortA, sortB] = [sortB, sortA] } if (customSorters && customSorters[sortKey]) { const customResult = customSorters[sortKey](sortA, sortB) if (!customResult) continue return customResult } // Check if both cannot be evaluated if (sortA === null && sortB === null) { continue } [sortA, sortB] = [sortA, sortB].map(s => (s || '').toString().toLocaleLowerCase()) if (sortA !== sortB) { if (!isNaN(sortA) && !isNaN(sortB)) return Number(sortA) - Number(sortB) return stringCollator.compare(sortA, sortB) } } return 0 }) } export function defaultFilter (value: any, search: string | null, item: any) { return value != null && search != null && typeof value !== 'boolean' && value.toString().toLocaleLowerCase().indexOf(search.toLocaleLowerCase()) !== -1 } export function searchItems (items: T[], search: string): T[] { if (!search) return items search = search.toString().toLowerCase() if (search.trim() === '') return items return items.filter((item: any) => Object.keys(item).some(key => defaultFilter(getObjectValueByPath(item, key), search, item))) } /** * Returns: * - 'normal' for old style slots - `