-
Konrad Mohrfeldt authoredKonrad Mohrfeldt authored
util.ts 8.79 KiB
import { computedAsync, useNow } from '@vueuse/core'
import { formatDistanceToNow, formatISO, parseISO } from 'date-fns'
import DateFnEnUsLocale from 'date-fns/locale/en-US'
import DOMPurify from 'dompurify'
import {
computed,
ComputedGetter,
ComputedRef,
MaybeRefOrGetter,
readonly,
Ref,
ref,
shallowReadonly,
shallowRef,
toRef,
toValue,
watch,
watchEffect,
} from 'vue'
import { useI18n } from '@/i18n'
import { SteeringUser } from '@/stores/auth'
const dateFnLocales = {
de: () => import('date-fns/locale/de'),
}
export function computedIter<T>(fn: ComputedGetter<Iterable<T>>): ComputedRef<T[]> {
return computed(() => Array.from(fn()))
}
export const useId = (() => {
let _id = 0
return function useId(prefix = 'component') {
return readonly(ref(`${prefix}-${_id++}`))
}
})()
export function useAsyncFunction<F extends (...args: never[]) => Promise<unknown>>(fn: F) {
const isProcessing = ref(false)
async function wrapper(...args: Parameters<F>): Promise<Awaited<ReturnType<F>>> {
isProcessing.value = true
try {
return (await fn(...args)) as Awaited<ReturnType<F>>
} finally {
isProcessing.value = false
}
}
return { fn: wrapper, isProcessing }
}
export function useFormattedISODate(date: Ref<Date>) {
return computed({
get() {
return formatISO(date.value, { representation: 'date' })
},
set(dateValue: string) {
date.value = parseISO(dateValue)
},
})
}
export function getClosestSlot(slotDurationMinutes: number, date?: Date): Date {
date = date ?? new Date()
const slotDurationMillis = slotDurationMinutes * 60 * 1000
const slots = Math.floor(date.getTime() / slotDurationMillis)
const closestSlotTimestamp = slots * slotDurationMillis
return new Date(closestSlotTimestamp)
}
export function getNextAvailableSlot(slotDurationMinutes: number, date?: Date): Date {
date = date ?? new Date()
const slotDurationMillis = slotDurationMinutes * 60 * 1000
const slots = Math.ceil(date.getTime() / slotDurationMillis)
const nextSlotTimestamp = slots * slotDurationMillis
return new Date(nextSlotTimestamp)
}
export function ensureDate(date: Date | string) {
return typeof date === 'string' ? parseISO(date) : date
}
export function stripSecondsFromTimeString(time: string) {
return /\d+:\d+:\d+$/.test(time) ? time.replace(/:\d+$/, '') : time
}
export function calculateDurationSeconds(start: Date | string, end: Date | string): number {
return Math.abs(ensureDate(end).getTime() - ensureDate(start).getTime()) / 1000
}
export function sanitizeHTML(
html: string,
preset?: 'inline-noninteractive' | 'safe-html' | 'strip',
): string {
const inlineElements = [
'b',
'big',
'i',
'small',
'tt',
'abbr',
'acronym',
'cite',
'code',
'dfn',
'em',
'kbd',
'strong',
'samp',
'var',
'br',
'q',
's',
'span',
'sub',
'sup',
]
if (preset === 'inline-noninteractive') {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: inlineElements,
ALLOWED_ATTR: ['style', 'title'],
})
}
if (preset === 'safe-html') {
return DOMPurify.sanitize(html, { USE_PROFILES: { html: true } })
}
return DOMPurify.sanitize(html, { ALLOWED_TAGS: [] })
}
export function secondsToDurationString(seconds: number): string {
const isNegative = seconds < 0
const _seconds = Math.abs(Math.round(seconds))
if (_seconds === 0) return `0s`
const h = Math.floor(_seconds / 3600)
const m = Math.floor((_seconds % 3600) / 60)
const s = Math.round(_seconds % 60)
const representation = `${h ? h + 'h ' : ''}${m ? m + 'min ' : ''}${s ? s + 's' : ''}`.trim()
return isNegative ? `-${representation}` : representation
}
export function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max)
}
export function mapToDomain(
value: number,
inputDomain: [number, number],
outputDomain: [number, number],
) {
const [x1, y1] = inputDomain
const [x2, y2] = outputDomain
value = clamp(value, x1, y1)
return ((value - x1) * (y2 - x2)) / (y1 - x1) + x2
}
export function asyncWritableComputed<T>(
initialValue: T,
config: {
get: () => Promise<T>
set: (value: T) => Promise<void> | void
},
) {
const data = shallowRef(initialValue)
watchEffect(async () => {
data.value = await config.get()
})
return computed<T>({
get: () => data.value,
set: (value: T) => {
config.set(value)
},
})
}
export function matchesSearch(sourceString: string, searchString: string) {
const source = sourceString.toLowerCase()
const terms = searchString
.toLowerCase()
.split(/\s+/)
.map((s) => s.trim())
.filter((s) => !!s)
for (const term of terms) {
if (!source.includes(term)) return false
}
return true
}
export function computedDebounced<T>(
value: Ref<T> | (() => T),
delayBySeconds: MaybeRefOrGetter<number> | ((newValue: T, oldValue: T) => number),
) {
const debouncedValue = ref<T>(toValue(value)) as Ref<T>
let timer: ReturnType<typeof setTimeout> | undefined
watch(toRef(value), (newValue, oldValue) => {
if (timer) clearTimeout(timer)
const delay =
typeof delayBySeconds === 'function'
? delayBySeconds(newValue, oldValue)
: toValue(delayBySeconds)
if (delay === 0) {
debouncedValue.value = newValue
} else {
timer = setTimeout(() => {
debouncedValue.value = newValue
}, delay * 1000)
}
})
return shallowReadonly(debouncedValue)
}
export function useDateFnLocale(language: MaybeRefOrGetter<string>) {
return computedAsync(async () => {
const localeString = toValue(language) as keyof typeof dateFnLocales
let locale: Locale = DateFnEnUsLocale
if (localeString in dateFnLocales) {
locale = (await dateFnLocales[localeString]()).default
}
return locale
}, DateFnEnUsLocale)
}
export function useRelativeDistanceToNow(
date: MaybeRefOrGetter<Date | string | null | undefined>,
options: { includeSeconds?: boolean; addSuffix?: boolean } = {},
) {
const { locale: appLocale } = useI18n()
const dateFnLocale = useDateFnLocale(appLocale)
return computedAsync(async () => {
const dateValue = toValue(date)
if (!dateValue) return ''
return formatDistanceToNow(ensureDate(dateValue), { locale: dateFnLocale.value, ...options })
}, '')
}
export function usePersonName(user: MaybeRefOrGetter<SteeringUser>) {
return computed(() => {
const _user = toValue(user)
const firstName = (_user?.firstName ?? '').trim()
const lastName = (_user.lastName ?? '').trim()
const username = _user.username.trim()
const name = [firstName, lastName].join(' ')
const initials =
firstName && lastName
? [firstName[0], lastName[0]]
: lastName
? [lastName[0]]
: firstName
? [firstName[0]]
: [username[0]]
return {
name,
username,
initials: initials.join('').toUpperCase(),
}
})
}
export function useIsToday(
date: MaybeRefOrGetter<Date | string>,
today?: MaybeRefOrGetter<Date | string>,
) {
const datetime = computed(() => ensureDate(toValue(date)))
const now =
typeof today === 'undefined'
? useNow({ interval: 60 * 1000 })
: computed(() => ensureDate(toValue(today)))
return computed(() => {
return (
now.value.getDate() === datetime.value.getDate() &&
now.value.getMonth() === datetime.value.getMonth() &&
now.value.getFullYear() === datetime.value.getFullYear()
)
})
}
export function areSetsEqual<T extends string | number | symbol>(
firstIterable: Iterable<T>,
secondIterable: Iterable<T>,
): boolean {
const set1 = new Set(firstIterable)
const set2 = new Set(secondIterable)
if (set1.size !== set2.size) return false
for (const item of set1) {
if (!set2.has(item)) return false
}
return true
}
export function getFilenameFromURL(url: string, defaultValue?: string) {
const basename = (url.split(/[\\/]/).pop() as string).trim()
return !basename && defaultValue ? defaultValue : basename
}
function arrayPadLeft<T>(array: T[], maxLength: number, fillValue: T): T[] {
return Array(Math.max(0, maxLength - array.length))
.fill(fillValue)
.concat(array)
}
export function parseTime(time: string): number {
let [days, hours, minutes, seconds] = Array(4).fill(0)
if (time.includes(':')) {
;[days, hours, minutes, seconds] = arrayPadLeft<number | string>(time.split(':'), 4, 0)
} else {
// parses strings like: "3m32", "3 h 2s ", "9d2h3min1"
const format =
/^\s*(?:(?<days>\d+)\s*d)?\s*(?:(?<hours>\d+)\s*h)?\s*(?:(?<minutes>\d+)\s*m(?:in)?)?\s*(?:(?<seconds>\d+)\s*s?)?\s*$/
;({ days, hours, minutes, seconds } = time.match(format)?.groups ?? {})
}
return (
parseInt(days || 0) * 24 * 60 * 60 +
parseInt(hours || 0) * 60 * 60 +
parseInt(minutes || 0) * 60 +
parseInt(seconds || 0)
)
}