// Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ import linkifyStr from 'linkify-string' import { isEqual } from 'lodash-es' export { htmlCleanup } from './htmlCleanup.ts' type Falsy = false | 0 | '' | null | undefined type IsTruthy = T extends Falsy ? never : T export const truthy = (value: Maybe): value is IsTruthy => { return !!value } export const edgesToArray = ( object?: Maybe<{ edges?: { node: T }[] }>, ): T[] => { return object?.edges?.map((edge) => edge.node) || [] } export const normalizeEdges = ( object?: Maybe<{ edges?: { node: T }[]; totalCount?: number }>, ): { array: T[]; totalCount: number } => { const array = edgesToArray(object) return { array, totalCount: object?.totalCount ?? array.length, } } export const mergeArray = (a: T, b: T) => { return [...new Set([...a, ...b])] } export const waitForAnimationFrame = () => { return new Promise((resolve) => requestAnimationFrame(resolve)) } export const textCleanup = (ascii: string) => { if (!ascii) return '' return ascii .trim() .replace(/(\r\n|\n\r)/g, '\n') // cleanup .replace(/\r/g, '\n') // cleanup .replace(/[ ]\n/g, '\n') // remove tailing spaces .replace(/\n{3,20}/g, '\n\n') // remove multiple empty lines } // taken from App.Utils.text2html for consistency export const textToHtml = (text: string) => { text = textCleanup(text) text = linkifyStr(text) text = text.replace(/(\n\r|\r\n|\r)/g, '\n') text = text.replace(/ {2}/g, '  ') text = `
${text.replace(/\n/g, '
')}
` return text.replace(/
<\/div>/g, '

') } export const textTruncate = (text: string, length = 100) => { if (!text) return text text = text.replace(/<([^>]+)>/g, '') if (text.length < length) return text return `${text.substring(0, length)}…` } export const debouncedQuery = ( fn: (...args: A) => Promise, defaultValue: R, delay = 200, ) => { let timeout: number | undefined let lastResolve: (() => void) | null = null let lastResult = defaultValue return (...args: A): Promise => { if (timeout) { lastResolve?.() clearTimeout(timeout) } return new Promise((resolve, reject) => { lastResolve = () => resolve(lastResult) timeout = window.setTimeout(() => { fn(...args).then((result) => { lastResult = result resolve(result) }, reject) }, delay) }) } } export const createDeferred = () => { let resolve: (value: T | PromiseLike) => void let reject: (reason?: unknown) => void const promise = new Promise((res, rej) => { resolve = res reject = rej }) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return { resolve: resolve!, reject: reject!, promise } } export const waitForElement = async ( query: string, tries = 60, ): Promise => { if (tries === 0) return null const element = document.querySelector(query) if (element) return element await new Promise((resolve) => requestAnimationFrame(resolve)) return waitForElement(query, tries - 1) } /** * **Note:** @Generic T supports comparing arrays, array buffers, booleans, * date objects, error objects, maps, numbers, `Object` objects, regexes, * sets, strings, symbols, and typed arrays. * `Object` objects are compared * by their own, not inherited, enumerable properties. * Functions and DOM * nodes are **not** supported. * */ export const findChangedIndex = (oldArray: T[], newArray: T[]) => oldArray.findIndex((item, index) => !isEqual(item, newArray[index]))