helpers.ts 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. import linkifyStr from 'linkify-string'
  3. import { isEqual } from 'lodash-es'
  4. export { htmlCleanup } from './htmlCleanup.ts'
  5. type Falsy = false | 0 | '' | null | undefined
  6. type IsTruthy<T> = T extends Falsy ? never : T
  7. export const truthy = <T>(value: Maybe<T>): value is IsTruthy<T> => {
  8. return !!value
  9. }
  10. export const edgesToArray = <T>(
  11. object?: Maybe<{ edges?: { node: T }[] }>,
  12. ): T[] => {
  13. return object?.edges?.map((edge) => edge.node) || []
  14. }
  15. export const normalizeEdges = <T>(
  16. object?: Maybe<{ edges?: { node: T }[]; totalCount?: number }>,
  17. ): { array: T[]; totalCount: number } => {
  18. const array = edgesToArray<T>(object)
  19. return {
  20. array,
  21. totalCount: object?.totalCount ?? array.length,
  22. }
  23. }
  24. export const mergeArray = <T extends unknown[]>(a: T, b: T) => {
  25. return [...new Set([...a, ...b])]
  26. }
  27. export const waitForAnimationFrame = () => {
  28. return new Promise((resolve) => requestAnimationFrame(resolve))
  29. }
  30. export const textCleanup = (ascii: string) => {
  31. if (!ascii) return ''
  32. return ascii
  33. .trim()
  34. .replace(/(\r\n|\n\r)/g, '\n') // cleanup
  35. .replace(/\r/g, '\n') // cleanup
  36. .replace(/[ ]\n/g, '\n') // remove tailing spaces
  37. .replace(/\n{3,20}/g, '\n\n') // remove multiple empty lines
  38. }
  39. // taken from App.Utils.text2html for consistency
  40. export const textToHtml = (text: string) => {
  41. text = textCleanup(text)
  42. text = linkifyStr(text)
  43. text = text.replace(/(\n\r|\r\n|\r)/g, '\n')
  44. text = text.replace(/ {2}/g, ' &nbsp;')
  45. text = `<div>${text.replace(/\n/g, '</div><div>')}</div>`
  46. return text.replace(/<div><\/div>/g, '<div><br></div>')
  47. }
  48. export const textTruncate = (text: string, length = 100) => {
  49. if (!text) return text
  50. text = text.replace(/<([^>]+)>/g, '')
  51. if (text.length < length) return text
  52. return `${text.substring(0, length)}…`
  53. }
  54. export const debouncedQuery = <A extends unknown[], R>(
  55. fn: (...args: A) => Promise<R>,
  56. defaultValue: R,
  57. delay = 200,
  58. ) => {
  59. let timeout: number | undefined
  60. let lastResolve: (() => void) | null = null
  61. let lastResult = defaultValue
  62. return (...args: A): Promise<R> => {
  63. if (timeout) {
  64. lastResolve?.()
  65. clearTimeout(timeout)
  66. }
  67. return new Promise<R>((resolve, reject) => {
  68. lastResolve = () => resolve(lastResult)
  69. timeout = window.setTimeout(() => {
  70. fn(...args).then((result) => {
  71. lastResult = result
  72. resolve(result)
  73. }, reject)
  74. }, delay)
  75. })
  76. }
  77. }
  78. export const createDeferred = <T>() => {
  79. let resolve: (value: T | PromiseLike<T>) => void
  80. let reject: (reason?: unknown) => void
  81. const promise = new Promise<T>((res, rej) => {
  82. resolve = res
  83. reject = rej
  84. })
  85. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  86. return { resolve: resolve!, reject: reject!, promise }
  87. }
  88. export const waitForElement = async (
  89. query: string,
  90. tries = 60,
  91. ): Promise<Element | null> => {
  92. if (tries === 0) return null
  93. const element = document.querySelector(query)
  94. if (element) return element
  95. await new Promise((resolve) => requestAnimationFrame(resolve))
  96. return waitForElement(query, tries - 1)
  97. }
  98. /**
  99. * **Note:** @Generic T supports comparing arrays, array buffers, booleans,
  100. * date objects, error objects, maps, numbers, `Object` objects, regexes,
  101. * sets, strings, symbols, and typed arrays.
  102. * `Object` objects are compared
  103. * by their own, not inherited, enumerable properties.
  104. * Functions and DOM
  105. * nodes are **not** supported.
  106. * */
  107. export const findChangedIndex = <T>(oldArray: T[], newArray: T[]) =>
  108. oldArray.findIndex((item, index) => !isEqual(item, newArray[index]))