@bagelink/vue 1.2.58 → 1.2.63

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.
@@ -0,0 +1,398 @@
1
+ import type { Ref, ComputedRef, MaybeRefOrGetter } from 'vue'
2
+ import { computed, ref, watch, toValue } from 'vue'
3
+ /**
4
+ * Clears HTML tags from a string
5
+ * @param html - HTML string to clean
6
+ * @returns Plain text without HTML tags
7
+ */
8
+
9
+ export function clearHtml(value?: string) {
10
+ if (!value) return ''
11
+ return value
12
+ .replace(/<[^>]*>?/g, '')
13
+ .replace(/&nbsp;/g, ' ')
14
+ .replace(/&quot;/g, '"')
15
+ .replace(/&apos;/g, `'`)
16
+ .replace(/&amp;/g, '&')
17
+ .replace(/&lt;/g, '<')
18
+ .replace(/&gt;/g, '>')
19
+ }
20
+
21
+ /**
22
+ * Normalizes text by removing special characters and converting to lowercase
23
+ * @param text - Text to normalize
24
+ * @returns Normalized text
25
+ */
26
+ export function normalizeText(text: string): string {
27
+ const searchChars = /[\p{N}\p{L}\s]/gu
28
+ return text.match(searchChars)?.join('').toLowerCase() ?? ''
29
+ }
30
+
31
+ /**
32
+ * Gets a value from an object by path
33
+ * Supports nested paths like "user.address.street"
34
+ * @param obj - The object to get the value from
35
+ * @param path - The path to the value (e.g., "user.address.street")
36
+ * @returns The value at the path or undefined if not found
37
+ */
38
+ function getValueByPath(obj: any, path: string): any {
39
+ const keys = path.split('.')
40
+ let result = obj
41
+
42
+ for (const key of keys) {
43
+ if (result === null || result === undefined || typeof result !== 'object') {
44
+ return undefined
45
+ }
46
+ result = result[key]
47
+ }
48
+
49
+ return result
50
+ }
51
+
52
+ /**
53
+ * Calculate relevance score based on search terms matching
54
+ * Higher score = more relevant
55
+ * @param stringValue - Text to check
56
+ * @param searchTerms - Array of search terms
57
+ * @returns Relevance score
58
+ */
59
+ function calculateRelevance(stringValue: string, searchTerms: string[]): number {
60
+ let score = 0
61
+
62
+ if (stringValue.length === 0) return 0
63
+
64
+ // Track matched character count for density calculation
65
+ let totalMatchedChars = 0
66
+
67
+ // Base score is the number of terms that match
68
+ for (const term of searchTerms) {
69
+ if (stringValue.includes(term)) {
70
+ // Count occurrences for total matched text calculation
71
+ const regex = new RegExp(term, 'g')
72
+ const matches = stringValue.match(regex)
73
+ if (matches) {
74
+ totalMatchedChars += matches.length * term.length
75
+ }
76
+
77
+ score += 1
78
+
79
+ // Give additional weight to exact matches (not just contains)
80
+ // Example: "jordan" is more relevant for "jordan" than "jordanian history"
81
+ const words = stringValue.split(/\s+/)
82
+ if (words.includes(term)) {
83
+ score += 0.5
84
+ }
85
+
86
+ // Give more weight to matches at the beginning of the text
87
+ if (stringValue.startsWith(term)) {
88
+ score += 0.5
89
+ }
90
+ }
91
+ }
92
+
93
+ // Calculate the percentage of text that was matched (0 to 1)
94
+ // Cap at 1.0 for cases where the same text is matched multiple times
95
+ const matchDensity = Math.min(1.0, totalMatchedChars / stringValue.length)
96
+
97
+ // Weight by match density (multiply by a factor to make it significant)
98
+ // This gives higher relevance to fields where the match covers more of the content
99
+ score *= (1.0 + matchDensity * 2)
100
+
101
+ return score
102
+ }
103
+
104
+ export interface SearchItemParams<T> {
105
+ searchTerm?: MaybeRefOrGetter<string>
106
+ items?: T[] | Ref<T[]>
107
+ keysToSearch?: (keyof T extends string ? keyof T : string)[] // Allow string paths and handle primitive types
108
+ fieldWeights?: Record<string, number> // Use simple string keys for weights
109
+ minChars?: number // Minimum characters required to trigger search
110
+ serverSearch?: (query: string) => Promise<T[]> // Function to perform server-side search
111
+ debounceMs?: number // Debounce time for server requests in milliseconds
112
+ }
113
+
114
+ export interface SearchResult<T> {
115
+ results: ComputedRef<readonly T[]>
116
+ resultCount: ComputedRef<number>
117
+ hasResults: ComputedRef<boolean>
118
+ isSearching: ComputedRef<boolean>
119
+ isLoading: ComputedRef<boolean> // New property to track server-side loading state
120
+ }
121
+
122
+ /**
123
+ * Check if a value is a primitive type (string, number, boolean)
124
+ * @param value - The value to check
125
+ * @returns True if the value is a primitive
126
+ */
127
+ function isPrimitive(value: any): boolean {
128
+ return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'
129
+ }
130
+
131
+ /**
132
+ * Safely convert any value to a searchable string
133
+ * @param value - Value to convert
134
+ * @returns String representation or empty string if value can't be converted
135
+ */
136
+ function toSearchableString(value: any): string {
137
+ if (value === null || value === undefined) return ''
138
+ if (typeof value === 'string') return clearHtml(value)
139
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value)
140
+ if (typeof value === 'object' && value instanceof Date) return value.toISOString()
141
+ return ''
142
+ }
143
+
144
+ /**
145
+ * Generic search function that searches for a term within specified object properties
146
+ * If keysToSearch is not provided, searches all keys (including nested ones)
147
+ * @param params - Search parameters including searchTerm, items, keys and weights
148
+ * @returns Filtered and sorted array of items that match the search terms
149
+ */
150
+ export function searchItems<T>(params: SearchItemParams<T>): T[] {
151
+ const { searchTerm, items = [], keysToSearch, fieldWeights = {}, minChars = 2 } = params
152
+
153
+ // Handle both string and ref values
154
+ const searchValue = toValue(searchTerm)
155
+ const itemsArray = Array.isArray(items) ? items : toValue(items) || []
156
+
157
+ // Return all items if search is empty or doesn't meet minimum character threshold
158
+ if (!searchValue || searchValue.length < minChars) return itemsArray
159
+
160
+ // Split search term into individual words for better matching
161
+ const searchTerms = normalizeText(searchValue)
162
+ .split(/\s+/)
163
+ .filter(term => term.length > 1)
164
+
165
+ if (searchTerms.length === 0) return itemsArray
166
+
167
+ // ---- Field weight configuration ----
168
+ // Default field weights for common fields
169
+ const defaultWeights: Record<string, number> = {
170
+ // Give higher weight to name/title fields
171
+ name: 2,
172
+ first_name: 2,
173
+ last_name: 2,
174
+ title: 2,
175
+ headline: 2,
176
+ // Lower weight for longer text fields
177
+ description: 0.7,
178
+ subtitle: 0.7,
179
+ bio: 0.7,
180
+ content: 0.7,
181
+ body: 0.7,
182
+ email: 0.7,
183
+ phone: 0.7,
184
+ // Even lower for metadata
185
+ id: 0.3,
186
+ created_at: 0.3,
187
+ updated_at: 0.3
188
+ }
189
+
190
+ /**
191
+ * Get the weight for a specific field path
192
+ * @param path - Path to the field
193
+ * @returns Appropriate weight for the field
194
+ */
195
+ function getFieldWeight(path: string): number {
196
+ // First check custom weights
197
+ if (path in fieldWeights) {
198
+ const customWeight = fieldWeights[path]
199
+ if (typeof customWeight === 'number') {
200
+ return customWeight
201
+ }
202
+ }
203
+
204
+ // Try with just the leaf key (last part of the path)
205
+ const leafKey = path.split('.').pop() || ''
206
+ if (leafKey in defaultWeights) {
207
+ return defaultWeights[leafKey]
208
+ }
209
+
210
+ // Default weight
211
+ return 1
212
+ }
213
+
214
+ /**
215
+ * Recursively collect all keys from an object, including nested ones
216
+ * @param obj - Object to collect keys from
217
+ * @param prefix - Prefix for nested keys
218
+ * @returns Array of flattened key paths
219
+ */
220
+ function collectAllKeys(obj: any, prefix = ''): string[] {
221
+ if (!obj || typeof obj !== 'object') return []
222
+
223
+ return Object.keys(obj).flatMap((key) => {
224
+ const value = obj[key]
225
+ const newPrefix = prefix ? `${prefix}.${key}` : key
226
+
227
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
228
+ // Recurse for nested objects
229
+ return [newPrefix, ...collectAllKeys(value, newPrefix)]
230
+ }
231
+
232
+ return [newPrefix]
233
+ })
234
+ }
235
+
236
+ /**
237
+ * Calculate relevance score for a single item against search terms
238
+ * @param item - Item to score
239
+ * @returns Relevance score
240
+ */
241
+ function calculateItemRelevance(item: any): number {
242
+ let totalRelevance = 0
243
+
244
+ // CASE 1: Handle primitive types directly
245
+ if (isPrimitive(item)) {
246
+ const normalizedValue = normalizeText(String(item))
247
+ return calculateRelevance(normalizedValue, searchTerms)
248
+ }
249
+
250
+ // CASE 2: Handle arrays of primitives
251
+ if (Array.isArray(item)) {
252
+ for (const element of item) {
253
+ if (isPrimitive(element)) {
254
+ const normalizedValue = normalizeText(String(element))
255
+ totalRelevance += calculateRelevance(normalizedValue, searchTerms)
256
+ }
257
+ }
258
+
259
+ return totalRelevance
260
+ }
261
+
262
+ // CASE 3: Handle regular objects
263
+ // Determine which keys to search in
264
+ const keysToProcess = keysToSearch || collectAllKeys(item)
265
+
266
+ // Process each field
267
+ for (const keyPath of keysToProcess) {
268
+ const value = getValueByPath(item, String(keyPath))
269
+
270
+ // Skip null/undefined values
271
+ if (value === null || value === undefined) continue
272
+
273
+ // CASE 3.1: Handle array field values
274
+ if (Array.isArray(value)) {
275
+ for (const element of value) {
276
+ if (isPrimitive(element)) {
277
+ const cleanValue = clearHtml(String(element))
278
+ const normalizedValue = normalizeText(cleanValue)
279
+ const relevance = calculateRelevance(normalizedValue, searchTerms)
280
+ const fieldWeight = getFieldWeight(String(keyPath))
281
+
282
+ totalRelevance += relevance * fieldWeight
283
+ }
284
+ }
285
+ continue
286
+ }
287
+
288
+ // CASE 3.2: Handle object fields (skip and process their properties separately)
289
+ if (typeof value === 'object' && !isPrimitive(value)) {
290
+ continue
291
+ }
292
+
293
+ // CASE 3.3: Handle primitive field values
294
+ const stringValue = toSearchableString(value)
295
+ const normalizedValue = normalizeText(stringValue)
296
+ const baseRelevance = calculateRelevance(normalizedValue, searchTerms)
297
+ const fieldWeight = getFieldWeight(String(keyPath))
298
+
299
+ totalRelevance += baseRelevance * fieldWeight
300
+ }
301
+
302
+ return totalRelevance
303
+ }
304
+
305
+ // Score each item and create [item, score] pairs
306
+ const scoredItems = itemsArray.map((item) => {
307
+ const relevance = calculateItemRelevance(item)
308
+ return [item, relevance] as [T, number]
309
+ })
310
+
311
+ // Filter items with non-zero relevance and sort by relevance (descending)
312
+ return scoredItems
313
+ .filter(([, score]) => score > 0)
314
+ .sort(([, scoreA], [, scoreB]) => scoreB - scoreA)
315
+ .map(([item]) => item)
316
+ }
317
+
318
+ /**
319
+ * Vue composable for searching items with reactive results
320
+ * Supports both client-side filtering and server-side search
321
+ * @param params Search parameters
322
+ * @returns Reactive search results and metadata
323
+ */
324
+ export function useSearch<T>(
325
+ params: SearchItemParams<T>
326
+ ): SearchResult<T> {
327
+ const { searchTerm, minChars = 2, serverSearch, debounceMs = 300 } = params
328
+
329
+ // For tracking server-side loading state
330
+ const isLoading = ref(false)
331
+ const serverResults = ref<T[]>([])
332
+ let debounceTimeout: number | null = null
333
+
334
+ // Watch for changes in the search term to trigger server-side search
335
+ if (serverSearch) {
336
+ watch(
337
+ () => toValue(searchTerm),
338
+ async (newTerm) => {
339
+ // Clear previous timeout if it exists
340
+ if (debounceTimeout !== null) {
341
+ clearTimeout(debounceTimeout)
342
+ }
343
+
344
+ // Skip search if term is too short
345
+ if (!newTerm || typeof newTerm !== 'string' || newTerm.length < minChars) {
346
+ serverResults.value = []
347
+ return
348
+ }
349
+
350
+ // Set up debounce
351
+ debounceTimeout = window.setTimeout(async () => {
352
+ try {
353
+ isLoading.value = true
354
+ serverResults.value = await serverSearch(newTerm)
355
+ } catch (error) {
356
+ console.error('Server search error:', error)
357
+ serverResults.value = []
358
+ } finally {
359
+ isLoading.value = false
360
+ }
361
+ }, debounceMs)
362
+ }
363
+ )
364
+ }
365
+
366
+ // Create a reactive function to get current search term value
367
+ const getSearchTermValue = () => toValue(searchTerm)
368
+
369
+ // Function to get filtered results
370
+ const getFilteredResults = (): T[] => {
371
+ const term = getSearchTermValue()
372
+
373
+ // If using server-side search and has search term, return server results
374
+ if (serverSearch && term && typeof term === 'string' && term.length >= minChars) {
375
+ return serverResults.value as T[]
376
+ }
377
+
378
+ // Otherwise use client-side filtering
379
+ return searchItems(params)
380
+ }
381
+
382
+ // Create computed references
383
+ const results = computed(() => getFilteredResults())
384
+ const resultCount = computed(() => results.value.length)
385
+ const hasResults = computed(() => resultCount.value > 0)
386
+ const isSearching = computed(() => {
387
+ const term = getSearchTermValue()
388
+ return !!term && typeof term === 'string' && term.length >= minChars
389
+ })
390
+
391
+ return {
392
+ results,
393
+ resultCount,
394
+ hasResults,
395
+ isSearching,
396
+ isLoading: computed(() => isLoading.value),
397
+ }
398
+ }
@@ -1,23 +0,0 @@
1
- export interface TimeUnit {
2
- singular: string;
3
- plural: string;
4
- }
5
- export type TranslationValue = string | TimeUnit;
6
- export interface LanguageTranslations {
7
- [key: string]: TranslationValue;
8
- }
9
- export type AvailableTimeLanguages = 'en' | 'es' | 'fr' | 'he';
10
- export type DayFormatTypes = 'DD' | 'DDD' | 'DDDD';
11
- export type MonthFormatTypes = 'MM' | 'MMM' | 'MMMM';
12
- export type YearFormatTypes = 'YY' | 'YYYY';
13
- export type HourFormatTypes = 'HH';
14
- export type MinuteFormatTypes = 'mm';
15
- export type SecondFormatTypes = 'ss';
16
- export type MillisecondFormatTypes = 'sss';
17
- export type AmPmFormatTypes = 'AmPm';
18
- export type DateFormatSeparatorTypes = '/' | '-' | ' ' | ':' | '.';
19
- export type CommonDateFormats = `${DayFormatTypes}${DateFormatSeparatorTypes}${MonthFormatTypes}${DateFormatSeparatorTypes}${YearFormatTypes}` | 'DD.MM.YY' | 'DD.MM.YYYY' | 'DD/MM/YY' | 'DD/MM/YYYY' | 'MM.DD.YY' | 'MM.DD.YYYY' | 'MM/DD/YY' | 'MM/DD/YYYY' | 'YYYY-MM-DD' | 'YY-MM-DD' | 'DD MMM YYYY' | 'DD MMMM YYYY' | 'DDD, DD MMM' | 'DDDD, DD MMMM' | 'MMM DD' | 'MMMM DD';
20
- export type CommonTimeFormats = 'HH:mm' | 'HH:mm:ss' | 'HH:mm:ss:sss' | 'HH:MM' | 'HH:mm AmPm';
21
- export type CommonDateTimeFormats = `${CommonDateFormats} ${CommonTimeFormats}` | `${CommonTimeFormats}, ${CommonDateFormats}` | 'YYYY-MM-DD HH:MM';
22
- export type DateTimeAcceptedFormats = CommonDateFormats | CommonTimeFormats | CommonDateTimeFormats;
23
- //# sourceMappingURL=timeago.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"timeago.d.ts","sourceRoot":"","sources":["../../src/types/timeago.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,QAAQ;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;CACd;AAED,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,QAAQ,CAAA;AAEhD,MAAM,WAAW,oBAAoB;IACpC,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,CAAA;CAC/B;AAED,MAAM,MAAM,sBAAsB,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;AAE9D,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,KAAK,GAAG,MAAM,CAAA;AAClD,MAAM,MAAM,gBAAgB,GAAG,IAAI,GAAG,KAAK,GAAG,MAAM,CAAA;AACpD,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,MAAM,CAAA;AAC3C,MAAM,MAAM,eAAe,GAAG,IAAI,CAAA;AAClC,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAA;AACpC,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAA;AACpC,MAAM,MAAM,sBAAsB,GAAG,KAAK,CAAA;AAC1C,MAAM,MAAM,eAAe,GAAG,MAAM,CAAA;AAEpC,MAAM,MAAM,wBAAwB,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAA;AAGlE,MAAM,MAAM,iBAAiB,GAC1B,GAAG,cAAc,GAAG,wBAAwB,GAAG,gBAAgB,GAAG,wBAAwB,GAAG,eAAe,EAAE,GAC9G,UAAU,GAAG,YAAY,GAAG,UAAU,GAAG,YAAY,GACrD,UAAU,GAAG,YAAY,GAAG,UAAU,GAAG,YAAY,GACrD,YAAY,GAAG,UAAU,GACzB,aAAa,GAAG,cAAc,GAC9B,aAAa,GAAG,eAAe,GAC/B,QAAQ,GAAG,SAAS,CAAA;AAEvB,MAAM,MAAM,iBAAiB,GAC1B,OAAO,GAAG,UAAU,GAAG,cAAc,GACrC,OAAO,GACP,YAAY,CAAA;AAGf,MAAM,MAAM,qBAAqB,GAC9B,GAAG,iBAAiB,IAAI,iBAAiB,EAAE,GAC3C,GAAG,iBAAiB,KAAK,iBAAiB,EAAE,GAC5C,kBAAkB,CAAA;AAGrB,MAAM,MAAM,uBAAuB,GAChC,iBAAiB,GACjB,iBAAiB,GACjB,qBAAqB,CAAA"}