@fy-/fws-vue 2.3.10 → 2.3.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,8 @@
1
1
  <script lang="ts" setup>
2
2
  import type { NavLink } from '../../types'
3
3
  import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/vue/24/solid'
4
- import { useStorage } from '@vueuse/core'
4
+ import { useDebounceFn, useStorage } from '@vueuse/core'
5
+ import { computed, shallowRef } from 'vue'
5
6
  import { useRoute } from 'vue-router'
6
7
 
7
8
  const props = withDefaults(
@@ -15,21 +16,48 @@ const props = withDefaults(
15
16
  baseUrl: '/',
16
17
  },
17
18
  )
19
+
18
20
  const route = useRoute()
21
+
22
+ // Cache storage key to avoid string concatenation
23
+ const storageKey = computed(() => `isOpenSidebar-${props.id}`)
24
+
25
+ // Create a cache for active links to avoid recalculation
26
+ const activeLinkCache = shallowRef(new Map<string, string>())
27
+
28
+ // Optimized link active check with memoization
19
29
  function isLinkActive(link: NavLink) {
20
- if (link.to !== props.baseUrl) {
21
- if (route.path === link.to || route.path.includes(link.to)) {
22
- return 'fvside-active'
30
+ const linkTo = link.to
31
+
32
+ // Return from cache if available
33
+ if (activeLinkCache.value.has(linkTo)) {
34
+ return activeLinkCache.value.get(linkTo)
35
+ }
36
+
37
+ let result = ''
38
+ if (linkTo !== props.baseUrl) {
39
+ if (route.path === linkTo || route.path.includes(linkTo)) {
40
+ result = 'fvside-active'
23
41
  }
24
42
  }
25
43
  else {
26
- if (route.path === link.to) {
27
- return 'fvside-active'
44
+ if (route.path === linkTo) {
45
+ result = 'fvside-active'
28
46
  }
29
47
  }
30
- return ''
48
+
49
+ // Cache the result
50
+ activeLinkCache.value.set(linkTo, result)
51
+ return result
31
52
  }
32
- const isOpen = useStorage(`isOpenSidebar-${props.id}`, true)
53
+
54
+ // Debounce storage updates to prevent rapid toggling
55
+ const isOpen = useStorage(storageKey.value, true)
56
+
57
+ // Debounced toggle function
58
+ const toggleSidebar = useDebounceFn(() => {
59
+ isOpen.value = !isOpen.value
60
+ }, 150)
33
61
  </script>
34
62
 
35
63
  <template>
@@ -38,7 +66,7 @@ const isOpen = useStorage(`isOpenSidebar-${props.id}`, true)
38
66
  <button
39
67
  class="btn neutral defaults"
40
68
  aria-controls="side-nav"
41
- @click="isOpen = !isOpen"
69
+ @click="toggleSidebar"
42
70
  >
43
71
  <ArrowLeftIcon v-if="isOpen" />
44
72
  <ArrowRightIcon v-else />
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
- import { computed, onMounted, ref, watch } from 'vue'
2
+ import { useDebounceFn, useVModel } from '@vueuse/core'
3
+ import { computed, onMounted, shallowRef, watch } from 'vue'
3
4
  import { useEventBus } from '../../composables/event-bus'
4
5
 
5
6
  /**
@@ -41,23 +42,41 @@ const props = withDefaults(
41
42
  )
42
43
 
43
44
  /**
44
- * Refs & Data
45
+ * Refs & Data - using shallowRef for DOM elements for better performance
45
46
  */
46
- const textInput = ref<HTMLElement>()
47
- const isMaxReached = ref(false)
48
- const inputContainer = ref<HTMLElement>()
47
+ const textInput = shallowRef<HTMLElement>()
48
+ const isMaxReached = shallowRef(false)
49
+ const inputContainer = shallowRef<HTMLElement>()
49
50
 
50
51
  const emit = defineEmits(['update:modelValue'])
51
52
 
52
53
  /**
53
- * Create a two-way computed property for modelValue
54
+ * Use VueUse's useVModel for more efficient two-way binding
54
55
  */
55
- const model = computed({
56
- get: () => props.modelValue,
57
- set: (items) => {
58
- emit('update:modelValue', items)
59
- },
60
- })
56
+ const model = useVModel(props, 'modelValue', emit)
57
+
58
+ /**
59
+ * Cache regex patterns to avoid creating them on each input
60
+ */
61
+ const getSeparatorRegex = (() => {
62
+ let cachedRegex: RegExp | null = null
63
+ let cachedGlobalRegex: RegExp | null = null
64
+
65
+ return (isGlobal = false) => {
66
+ if (isGlobal) {
67
+ if (!cachedGlobalRegex) {
68
+ cachedGlobalRegex = new RegExp(props.separators.join('|'), 'g')
69
+ }
70
+ return cachedGlobalRegex
71
+ }
72
+ else {
73
+ if (!cachedRegex) {
74
+ cachedRegex = new RegExp(props.separators.join('|'))
75
+ }
76
+ return cachedRegex
77
+ }
78
+ }
79
+ })()
61
80
 
62
81
  /**
63
82
  * Compute aria-describedby IDs if help or error exist
@@ -101,9 +120,11 @@ onMounted(() => {
101
120
  const eventBus = useEventBus()
102
121
 
103
122
  /**
104
- * Copy the tags to clipboard
123
+ * Copy the tags to clipboard with debounce to prevent multiple executions
105
124
  */
106
- async function copyText() {
125
+ const copyText = useDebounceFn(async () => {
126
+ if (!model.value.length) return
127
+
107
128
  const text = model.value.join(', ')
108
129
 
109
130
  try {
@@ -122,35 +143,33 @@ async function copyText() {
122
143
  time: 2500,
123
144
  })
124
145
  }
125
- }
146
+ }, 300)
126
147
 
127
148
  /**
128
149
  * On each character input, check if user typed a separator
129
150
  */
130
- function handleInput(event: Event) {
151
+ const handleInput = useDebounceFn((event: Event) => {
131
152
  const inputEvent = event as InputEvent
132
153
  if (!inputEvent.data) return
133
- const separatorsRegex = new RegExp(props.separators.join('|'))
134
- if (separatorsRegex.test(inputEvent.data)) {
154
+
155
+ if (getSeparatorRegex().test(inputEvent.data)) {
135
156
  addTag()
136
157
  }
137
- }
158
+ }, 50)
138
159
 
139
160
  /**
140
- * Add a tag by splitting on the separator
161
+ * Add a tag by splitting on the separator - optimized with fewer operations
141
162
  */
142
163
  function addTag() {
143
164
  if (!textInput.value || isMaxReached.value) return
144
165
 
145
- const separatorsRegex = new RegExp(props.separators.join('|'))
146
166
  const textContent = textInput.value.textContent?.trim()
147
-
148
167
  if (!textContent) return
149
168
 
150
169
  const newTags = textContent
151
- .split(separatorsRegex)
152
- .map((tag: string) => tag.trim())
153
- .filter((tag: string) => tag.length > 0)
170
+ .split(getSeparatorRegex())
171
+ .map(tag => tag.trim())
172
+ .filter(tag => tag.length > 0)
154
173
 
155
174
  // Remove duplicates if noDuplicates is enabled
156
175
  const filteredTags = props.noDuplicates
@@ -160,28 +179,41 @@ function addTag() {
160
179
  // If maxTags is set, ensure adding tags doesn't exceed the limit
161
180
  if (props.maxTags && props.maxTags > 0) {
162
181
  const slotsAvailable = props.maxTags - model.value.length
163
- filteredTags.splice(slotsAvailable)
182
+ if (slotsAvailable <= 0) {
183
+ // If no slots are available, clear input and return
184
+ textInput.value.textContent = ''
185
+ return
186
+ }
187
+ if (filteredTags.length > slotsAvailable) {
188
+ filteredTags.splice(slotsAvailable)
189
+ }
190
+ }
191
+
192
+ if (filteredTags.length) {
193
+ model.value = [...model.value, ...filteredTags]
164
194
  }
165
195
 
166
- model.value = [...model.value, ...filteredTags]
167
196
  textInput.value.textContent = ''
168
197
  }
169
198
 
170
199
  /**
171
200
  * Remove a tag by index
172
201
  */
173
- function removeTag(index: number) {
202
+ const removeTag = useDebounceFn((index: number) => {
203
+ if (index < 0 || index >= model.value.length) return
204
+
174
205
  const newTags = [...model.value]
175
206
  newTags.splice(index, 1)
176
207
  model.value = newTags
177
208
  focusInput()
178
- }
209
+ }, 50)
179
210
 
180
211
  /**
181
212
  * Handle backspace/delete on an empty input
182
213
  */
183
214
  function removeLastTag() {
184
215
  if (!textInput.value) return
216
+
185
217
  if (textInput.value.textContent === '') {
186
218
  // If input is empty, remove the last tag
187
219
  if (model.value.length > 0) {
@@ -201,16 +233,20 @@ function removeLastTag() {
201
233
 
202
234
  /**
203
235
  * Place the cursor at the end of the contenteditable text
236
+ * Using requestAnimationFrame for better performance
204
237
  */
205
238
  function placeCursorToEnd() {
206
239
  if (!textInput.value) return
207
- const range = document.createRange()
208
- const sel = window.getSelection()
209
- range.selectNodeContents(textInput.value)
210
- range.collapse(false)
211
- if (!sel) return
212
- sel.removeAllRanges()
213
- sel.addRange(range)
240
+
241
+ requestAnimationFrame(() => {
242
+ const range = document.createRange()
243
+ const sel = window.getSelection()
244
+ range.selectNodeContents(textInput.value!)
245
+ range.collapse(false)
246
+ if (!sel) return
247
+ sel.removeAllRanges()
248
+ sel.addRange(range)
249
+ })
214
250
  }
215
251
 
216
252
  /**
@@ -224,32 +260,37 @@ function focusInput() {
224
260
  }
225
261
 
226
262
  /**
227
- * Handle pasting text
263
+ * Handle pasting text with debounce
228
264
  */
229
- function handlePaste(e: ClipboardEvent) {
265
+ const handlePaste = useDebounceFn((e: ClipboardEvent) => {
230
266
  if (!textInput.value || isMaxReached.value) return
231
267
 
232
268
  const clipboardData = e.clipboardData ?? (window as any).clipboardData
233
269
  if (!clipboardData) return
234
270
 
235
271
  const text = clipboardData.getData('text')
236
- const separatorsRegex = new RegExp(props.separators.join('|'), 'g')
237
- const pasteText = text.replace(separatorsRegex, ',')
238
- textInput.value.textContent += pasteText
272
+ if (!text.trim()) return
273
+
274
+ const pasteText = text.replace(getSeparatorRegex(true), ',')
275
+
276
+ // Set the text content directly, rather than appending
277
+ textInput.value.textContent = pasteText
239
278
  e.preventDefault()
240
279
  addTag()
241
- }
280
+ }, 50)
242
281
 
243
282
  /**
244
- * Handle keyboard navigation between tags
283
+ * Handle keyboard navigation between tags - optimized with element lookup caching
245
284
  */
246
285
  function handleKeyNavigation(e: KeyboardEvent, index: number) {
286
+ if (!inputContainer.value) return
287
+
247
288
  if (e.key === 'ArrowLeft' && index > 0) {
248
- const prevTag = inputContainer.value?.querySelector(`[data-index="${index - 1}"] button`) as HTMLElement
289
+ const prevTag = inputContainer.value.querySelector(`[data-index="${index - 1}"] button`) as HTMLElement
249
290
  if (prevTag) prevTag.focus()
250
291
  }
251
292
  else if (e.key === 'ArrowRight' && index < model.value.length - 1) {
252
- const nextTag = inputContainer.value?.querySelector(`[data-index="${index + 1}"] button`) as HTMLElement
293
+ const nextTag = inputContainer.value.querySelector(`[data-index="${index + 1}"] button`) as HTMLElement
253
294
  if (nextTag) nextTag.focus()
254
295
  }
255
296
  }
@@ -1,6 +1,7 @@
1
1
  import type { Ref } from 'vue'
2
2
  import { getLocale, getPrefix, getURL } from '@fy-/fws-js'
3
3
  import { useHead, useSeoMeta } from '@unhead/vue'
4
+ import { computed } from 'vue'
4
5
 
5
6
  export interface LazyHead {
6
7
  name?: string
@@ -26,10 +27,53 @@ export interface LazyHead {
26
27
  twitterCreator?: string
27
28
  }
28
29
 
30
+ // Helper function to process image URLs
31
+ function processImageUrl(image: string | undefined, imageType: string | undefined): string | undefined {
32
+ if (!image) return undefined
33
+
34
+ if (image.includes('?vars=')) {
35
+ if (imageType) {
36
+ return image.replace(
37
+ '?vars=',
38
+ `.${imageType.replace('image/', '')}?vars=`,
39
+ )
40
+ }
41
+ else {
42
+ return image.replace('?vars=', '.png?vars=')
43
+ }
44
+ }
45
+ return image
46
+ }
47
+
48
+ // Helper function to normalize image type
49
+ function normalizeImageType(imageType: string | undefined): 'image/jpeg' | 'image/gif' | 'image/png' | false {
50
+ if (!imageType) return false
51
+
52
+ const type = imageType.includes('image/') ? imageType : `image/${imageType}`
53
+ if (type === 'image/jpeg' || type === 'image/gif' || type === 'image/png') {
54
+ return type as 'image/jpeg' | 'image/gif' | 'image/png'
55
+ }
56
+ return 'image/png'
57
+ }
58
+
29
59
  // eslint-disable-next-line unused-imports/no-unused-vars
30
60
  export function useSeo(seoData: Ref<LazyHead>, initial: boolean = false) {
31
61
  const currentLocale = getLocale()
32
- // const url = getURL()
62
+
63
+ // Cache the URL components
64
+ const urlBase = computed(() => ({
65
+ scheme: getURL().Scheme,
66
+ host: getURL().Host,
67
+ path: getURL().Path,
68
+ canonical: getURL().Canonical,
69
+ prefix: getPrefix(),
70
+ }))
71
+
72
+ // Memoize common values
73
+ const localeForOg = computed(() => currentLocale?.replace('-', '_'))
74
+ const imageUrl = computed(() => processImageUrl(seoData.value.image, seoData.value.imageType))
75
+ const imageType = computed(() => normalizeImageType(seoData.value.imageType))
76
+ const imageAlt = computed(() => seoData.value.image ? seoData.value.title : undefined)
33
77
 
34
78
  useHead({
35
79
  meta: () => {
@@ -64,9 +108,9 @@ export function useSeo(seoData: Ref<LazyHead>, initial: boolean = false) {
64
108
  links.push({
65
109
  rel: 'alternate',
66
110
  hreflang: locale,
67
- href: `${getURL().Scheme}://${
68
- getURL().Host
69
- }/l/${locale}${getURL().Path.replace(getPrefix(), '')}`,
111
+ href: `${urlBase.value.scheme}://${
112
+ urlBase.value.host
113
+ }/l/${locale}${urlBase.value.path.replace(urlBase.value.prefix, '')}`,
70
114
  key: `alternate-${locale}`,
71
115
  })
72
116
  }
@@ -88,12 +132,8 @@ export function useSeo(seoData: Ref<LazyHead>, initial: boolean = false) {
88
132
  })
89
133
 
90
134
  useSeoMeta({
91
- ogUrl: () => `${getURL().Canonical}`,
92
- ogLocale: () => {
93
- if (currentLocale) {
94
- return currentLocale.replace('-', '_')
95
- }
96
- },
135
+ ogUrl: () => urlBase.value.canonical,
136
+ ogLocale: () => localeForOg.value,
97
137
  robots:
98
138
  'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1',
99
139
  title: () => seoData.value.title || '',
@@ -103,97 +143,22 @@ export function useSeo(seoData: Ref<LazyHead>, initial: boolean = false) {
103
143
  ogSiteName: () => seoData.value.name,
104
144
  twitterTitle: () => seoData.value.title,
105
145
  twitterDescription: () => seoData.value.description,
106
- ogImageAlt: () => {
107
- if (seoData.value.image) {
108
- return seoData.value.title
109
- }
110
- return undefined
111
- },
146
+ ogImageAlt: () => imageAlt.value,
112
147
  // @ts-expect-error: Type 'string' is not assignable to type 'undefined'.
113
148
  ogType: () => (seoData.value.type ? seoData.value.type : 'website'),
114
149
  twitterCreator: () => seoData.value.twitterCreator,
115
150
  twitterSite: () => seoData.value.twitterCreator,
116
- twitterImageAlt: () => {
117
- if (seoData.value.image) {
118
- return seoData.value.title
119
- }
120
- return undefined
121
- },
151
+ twitterImageAlt: () => imageAlt.value,
122
152
  description: () => seoData.value.description,
123
153
  keywords: () => seoData.value.keywords,
124
154
  articlePublishedTime: () => seoData.value.published,
125
155
  articleModifiedTime: () => seoData.value.modified,
126
- ogImageSecureUrl: () => {
127
- if (seoData.value.image) {
128
- if (seoData.value.image.includes('?vars=')) {
129
- if (seoData.value.imageType) {
130
- return seoData.value.image.replace(
131
- '?vars=',
132
- `.${seoData.value.imageType.replace('image/', '')}?vars=`,
133
- )
134
- }
135
- else {
136
- return seoData.value.image.replace('?vars=', '.png?vars=')
137
- }
138
- }
139
- return seoData.value.image
140
- }
141
- },
142
- ogImageUrl: () => {
143
- if (seoData.value.image) {
144
- if (seoData.value.image.includes('?vars=')) {
145
- if (seoData.value.imageType) {
146
- return seoData.value.image.replace(
147
- '?vars=',
148
- `.${seoData.value.imageType.replace('image/', '')}?vars=`,
149
- )
150
- }
151
- else {
152
- return seoData.value.image.replace('?vars=', '.png?vars=')
153
- }
154
- }
155
- return seoData.value.image
156
- }
157
- },
158
- ogImageType: () => {
159
- if (seoData.value.imageType) {
160
- const type = seoData.value.imageType.includes('image/')
161
- ? seoData.value.imageType
162
- : `image/${seoData.value.imageType}`
163
- if (type === 'image/jpeg' || type === 'image/gif' || type === 'image/png') {
164
- return type
165
- }
166
- return 'image/png'
167
- }
168
- return undefined
169
- },
170
- twitterImageUrl: () => {
171
- if (seoData.value.image) {
172
- if (seoData.value.image.includes('?vars=')) {
173
- if (seoData.value.imageType) {
174
- return seoData.value.image.replace(
175
- '?vars=',
176
- `.${seoData.value.imageType.replace('image/', '')}?vars=`,
177
- )
178
- }
179
- else {
180
- return seoData.value.image.replace('?vars=', '.png?vars=')
181
- }
182
- }
183
- return seoData.value.image
184
- }
185
- },
156
+ ogImageSecureUrl: () => imageUrl.value,
157
+ ogImageUrl: () => imageUrl.value,
158
+ ogImageType: () => imageType.value,
159
+ twitterImageUrl: () => imageUrl.value,
186
160
  twitterImageType() {
187
- if (seoData.value.imageType) {
188
- const type = seoData.value.imageType.includes('image/')
189
- ? seoData.value.imageType
190
- : `image/${seoData.value.imageType}`
191
- if (type === 'image/jpeg' || type === 'image/gif' || type === 'image/png') {
192
- return type
193
- }
194
- return 'image/png'
195
- }
196
- return undefined
161
+ return imageType.value
197
162
  },
198
163
  })
199
164
  }
@@ -2,86 +2,116 @@ import { getLocale } from '@fy-/fws-js'
2
2
  import { format as formatDateTimeago } from 'timeago.js'
3
3
  import { useTranslation } from './translations'
4
4
 
5
+ // Cache common constants and patterns
6
+ const k = 1024
7
+ const byteSizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
8
+ const dateFormatOptions = {
9
+ date: {
10
+ year: 'numeric',
11
+ month: 'long',
12
+ day: 'numeric',
13
+ },
14
+ datetime: {
15
+ year: 'numeric',
16
+ month: 'long',
17
+ day: 'numeric',
18
+ hour: 'numeric',
19
+ minute: 'numeric',
20
+ second: 'numeric',
21
+ },
22
+ }
23
+
24
+ // Color cache for contrast calculations to avoid repeated calculations
25
+ const colorContrastCache = new Map<string, string>()
26
+
5
27
  function cropText(str: string, ml = 100, end = '...') {
28
+ if (!str) return str
6
29
  if (str.length > ml) {
7
30
  return `${str.slice(0, ml)}${end}`
8
31
  }
9
32
  return str
10
33
  }
34
+
11
35
  function getContrastingTextColor(backgroundColor: string) {
12
- const r = Number.parseInt(backgroundColor.substring(1, 3), 16)
13
- const g = Number.parseInt(backgroundColor.substring(3, 5), 16)
14
- const b = Number.parseInt(backgroundColor.substring(5, 7), 16)
36
+ // Return from cache if available
37
+ if (colorContrastCache.has(backgroundColor)) {
38
+ return colorContrastCache.get(backgroundColor)!
39
+ }
40
+
41
+ // Input validation
42
+ if (!backgroundColor || backgroundColor.length !== 7 || backgroundColor[0] !== '#') {
43
+ return '#000000'
44
+ }
15
45
 
16
- const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
46
+ try {
47
+ const r = Number.parseInt(backgroundColor.substring(1, 3), 16)
48
+ const g = Number.parseInt(backgroundColor.substring(3, 5), 16)
49
+ const b = Number.parseInt(backgroundColor.substring(5, 7), 16)
17
50
 
18
- return luminance > 0.5 ? '#000000' : '#FFFFFF'
51
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
52
+ const result = luminance > 0.5 ? '#000000' : '#FFFFFF'
53
+
54
+ // Cache the result
55
+ colorContrastCache.set(backgroundColor, result)
56
+ return result
57
+ }
58
+ catch {
59
+ return '#000000'
60
+ }
19
61
  }
62
+
20
63
  function formatBytes(bytes: number, decimals = 2) {
21
64
  if (!+bytes) return '0 Bytes'
22
65
 
23
- const k = 1024
24
66
  const dm = decimals < 0 ? 0 : decimals
25
- const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
26
-
27
67
  const i = Math.floor(Math.log(bytes) / Math.log(k))
28
68
 
29
- return `${Number.parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`
69
+ return `${Number.parseFloat((bytes / k ** i).toFixed(dm))} ${byteSizes[i]}`
30
70
  }
31
71
 
32
- function formatDate(dt: Date | string | number) {
33
- let _dt = dt as number
72
+ // Helper to parse date inputs consistently
73
+ function parseDateInput(dt: Date | string | number): number {
74
+ if (dt instanceof Date) {
75
+ return dt.getTime()
76
+ }
77
+
34
78
  if (typeof dt === 'string') {
35
- _dt = Date.parse(dt)
36
- if (Number.isNaN(_dt)) {
37
- _dt = Number.parseInt(dt)
38
- }
79
+ const parsed = Date.parse(dt)
80
+ return Number.isNaN(parsed) ? Number.parseInt(dt) : parsed
39
81
  }
40
82
 
83
+ return dt
84
+ }
85
+
86
+ function formatDate(dt: Date | string | number) {
87
+ const timestamp = parseDateInput(dt)
41
88
  const translate = useTranslation()
89
+
42
90
  return translate('global_datetime', {
43
- val: new Date(_dt),
91
+ val: new Date(timestamp),
44
92
  formatParams: {
45
- val: {
46
- year: 'numeric',
47
- month: 'long',
48
- day: 'numeric',
49
- },
93
+ val: dateFormatOptions.date,
50
94
  },
51
95
  })
52
96
  }
97
+
53
98
  function formatDatetime(dt: Date | string | number) {
54
- let _dt = dt as number
55
- if (typeof dt === 'string') {
56
- _dt = Date.parse(dt)
57
- if (Number.isNaN(_dt)) {
58
- _dt = Number.parseInt(dt)
59
- }
60
- }
99
+ const timestamp = parseDateInput(dt)
61
100
  const translate = useTranslation()
101
+
62
102
  return translate('global_datetime', {
63
- val: new Date(_dt),
103
+ val: new Date(timestamp),
64
104
  formatParams: {
65
- val: {
66
- year: 'numeric',
67
- month: 'long',
68
- day: 'numeric',
69
- hour: 'numeric',
70
- minute: 'numeric',
71
- second: 'numeric',
72
- },
105
+ val: dateFormatOptions.datetime,
73
106
  },
74
107
  })
75
108
  }
109
+
76
110
  function formatTimeago(dt: Date | string | number) {
77
- let _dt = dt as number
78
- if (typeof dt === 'string') {
79
- _dt = Date.parse(dt)
80
- if (Number.isNaN(_dt)) {
81
- _dt = Number.parseInt(dt)
82
- }
83
- }
84
- return formatDateTimeago(new Date(_dt), getLocale().replace('_', '-'))
111
+ const timestamp = parseDateInput(dt)
112
+ const locale = getLocale().replace('_', '-')
113
+
114
+ return formatDateTimeago(new Date(timestamp), locale)
85
115
  }
86
116
 
87
117
  export {