@bagelink/vue 1.2.58 → 1.2.61

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