@arex95/vue-core 1.0.5 → 1.0.7

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.
package/dist/index.mjs CHANGED
@@ -1,2450 +1,2700 @@
1
+ import { useTimeoutFn, useBreakpoints, breakpointsTailwind, useWindowSize } from '@vueuse/core';
1
2
  import axios from 'axios';
3
+ import { useRouter } from 'vue-router';
2
4
  import { useQuery } from '@tanstack/vue-query';
3
5
  import { ref, watch, onServerPrefetch, onMounted, computed } from 'vue';
4
- import { useBreakpoints, breakpointsTailwind, useWindowSize, useTimeoutFn } from '@vueuse/core';
5
6
 
6
7
  /**
7
- * A standard REST API interface for handling basic CRUD operations.
8
- * This class can be instantiated with a resource endpoint and a fetch composable for API requests.
8
+ * Creates and downloads a file from Blob data.
9
+ * @param {Blob} blob The Blob containing the file data.
10
+ * @param {string} fileName The name of the file to create.
9
11
  */
10
- class RestStd {
11
- static resource;
12
- static fetchComposable;
13
- /**
14
- * Fetch a list of items from the server.
15
- *
16
- * @param params Query parameters for filtering the results.
17
- * @param options Additional options for the fetch composable.
18
- * @returns The result of the fetch composable (typically a promise).
19
- */
20
- static getMany(params, options = {}) {
21
- return this.fetchComposable({
22
- method: 'GET',
23
- url: this.resource,
24
- params,
25
- }, options);
26
- }
27
- /**
28
- * Fetch a single item by ID from the server.
29
- *
30
- * @param id The ID of the item to fetch.
31
- * @param params Additional query parameters for the request.
32
- * @param options Additional options for the fetch composable.
33
- * @returns The result of the fetch composable (typically a promise).
34
- */
35
- static getOne(id, params, options = {}) {
36
- return this.fetchComposable({
37
- method: 'GET',
38
- url: `${this.resource}/${id}`,
39
- params,
40
- }, options);
41
- }
42
- /**
43
- * Create a new item on the server.
44
- *
45
- * @param data The data for the new item to create.
46
- * @param options Additional options for the fetch composable.
47
- * @returns The result of the fetch composable (typically a promise).
48
- */
49
- static create(data, options = {}) {
50
- return this.fetchComposable({
51
- method: 'POST',
52
- url: this.resource,
53
- data,
54
- }, options);
55
- }
56
- /**
57
- * Update an existing item on the server.
58
- *
59
- * @param id The ID of the item to update.
60
- * @param data The updated data for the item.
61
- * @param options Additional options for the fetch composable.
62
- * @returns The result of the fetch composable (typically a promise).
63
- */
64
- static update(id, data, options = {}) {
65
- return this.fetchComposable({
66
- method: 'PUT',
67
- url: `${this.resource}/${id}`,
68
- data,
69
- }, options);
70
- }
71
- /**
72
- * Partially update an existing item on the server.
73
- *
74
- * @param id The ID of the item to update.
75
- * @param data The updated data for the item.
76
- * @param options Additional options for the fetch composable.
77
- * @returns The result of the fetch composable (typically a promise).
78
- */
79
- static patch(id, data, options = {}) {
80
- return this.fetchComposable({
81
- method: 'PATCH',
82
- url: `${this.resource}/${id}`,
83
- data,
84
- }, options);
85
- }
86
- /**
87
- * Delete an item from the server.
88
- *
89
- * @param id The ID of the item to delete.
90
- * @param options Additional options for the fetch composable.
91
- * @returns The result of the fetch composable (typically a promise).
92
- */
93
- static delete(id, options = {}) {
94
- return this.fetchComposable({
95
- method: 'DELETE',
96
- url: `${this.resource}/${id}`,
97
- }, options);
98
- }
99
- /**
100
- * Custom request method for more flexibility.
101
- *
102
- * @param method HTTP method (GET, POST, etc.).
103
- * @param params Query parameters.
104
- * @param data Request body data.
105
- * @param token Authorization token (optional).
106
- * @param options Additional options for the fetch composable.
107
- * @returns The result of the fetch composable (typically a promise).
108
- */
109
- static customRequest(method, params, data, options = {}) {
110
- return this.fetchComposable({
111
- method: method,
112
- url: this.resource,
113
- params: params,
114
- data: data,
115
- }, options);
12
+ function downloadFile(blob, fileName) {
13
+ const link = document.createElement('a');
14
+ const url = URL.createObjectURL(blob);
15
+ link.setAttribute('href', url);
16
+ link.setAttribute('download', fileName);
17
+ // Append link to the body and trigger a click
18
+ document.body.appendChild(link);
19
+ link.click();
20
+ // Clean up
21
+ document.body.removeChild(link);
22
+ URL.revokeObjectURL(url);
23
+ }
24
+ /**
25
+ * Exports data to a CSV file.
26
+ * @param {string[]} headers The headers for the CSV.
27
+ * @param {any[][]} data The data to export, as an array of arrays.
28
+ * @param {string} fileName The name of the file to create.
29
+ */
30
+ function exportToCSV(headers, data, fileName) {
31
+ const csvRows = [];
32
+ // Add the headers
33
+ csvRows.push(headers.map(header => `"${header.replace(/"/g, '""')}"`).join(','));
34
+ // Add the data rows
35
+ for (const row of data) {
36
+ csvRows.push(row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(','));
116
37
  }
38
+ // Create a Blob with CSV data
39
+ const csvContent = csvRows.join('\n');
40
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
41
+ // Use the download utility function
42
+ downloadFile(blob, fileName);
117
43
  }
118
-
119
44
  /**
120
- * Custom composable function for making API requests with Axios.
121
- *
122
- * @template T The expected return type of the API request.
123
- * @param {AxiosInstance} axios - The Axios instance for making HTTP requests.
124
- * @param {AxiosRequestConfig} axiosRequest - Configuration for the Axios request.
125
- * @returns {Promise<T>} A promise with the result of the API request.
45
+ * Exports data to an Excel file (.xls) using HTML table.
46
+ * @param {string[]} headers The headers for the Excel file.
47
+ * @param {any[][]} data The data to export, as an array of arrays.
48
+ * @param {string} fileName The name of the file to create.
126
49
  */
127
- async function axiosFetch(axios, axiosRequest) {
128
- return axios(axiosRequest)
129
- .then(response => response.data)
130
- .catch(error => {
131
- throw error;
50
+ function exportToExcel(headers, data, fileName) {
51
+ // Create a table element
52
+ const table = document.createElement('table');
53
+ const thead = table.createTHead();
54
+ const tbody = table.createTBody();
55
+ // Create the header row
56
+ const headerRow = thead.insertRow();
57
+ headers.forEach(header => {
58
+ const th = document.createElement('th');
59
+ th.textContent = header;
60
+ headerRow.appendChild(th);
61
+ });
62
+ // Create the data rows
63
+ data.forEach(row => {
64
+ const tr = tbody.insertRow();
65
+ row.forEach(cell => {
66
+ const td = tr.insertCell();
67
+ td.textContent = String(cell);
68
+ });
132
69
  });
70
+ // Create a Blob with the table HTML
71
+ const html = table.outerHTML;
72
+ const blob = new Blob([html], { type: 'application/vnd.ms-excel;charset=utf-8;' });
73
+ // Use the download utility function
74
+ downloadFile(blob, fileName);
133
75
  }
134
-
135
76
  /**
136
- * AxiosService class encapsulates the Axios configuration and logic.
137
- * It manages request and response interceptors and provides methods for making HTTP requests.
77
+ * Exports data to a JSON file.
78
+ * @param {any[]} data The data to export.
79
+ * @param {string} fileName The name of the file to create.
138
80
  */
139
- class AxiosService {
140
- instance;
141
- cancelTokenSource;
142
- activeRequests = 0; // Track active requests
143
- /**
144
- * Initializes the AxiosService instance by creating an Axios instance with default configuration
145
- * and setting up request and response interceptors.
146
- */
147
- constructor(url) {
148
- this.cancelTokenSource = axios.CancelToken.source();
149
- this.instance = axios.create({
150
- baseURL: url ?? '',
151
- timeout: 300000,
152
- headers: {
153
- Accept: 'application/json',
154
- 'Content-Type': 'application/json',
155
- },
156
- withCredentials: false,
157
- });
158
- this.initializeInterceptors();
159
- }
160
- /**
161
- * Initializes request and response interceptors for the Axios instance.
162
- */
163
- initializeInterceptors() {
164
- this.instance.interceptors.request.use((config) => {
165
- const token = typeof window !== 'undefined' ? localStorage.getItem('authToken') : null;
166
- if (token && config.headers) {
167
- config.headers.Authorization = `Bearer ${token}`;
168
- }
169
- config.cancelToken = this.cancelTokenSource.token;
170
- this.activeRequests++;
171
- return config;
172
- }, (error) => {
173
- // handleError(error);
174
- console.error('Request error:', error.message);
175
- this.activeRequests++;
176
- return Promise.reject(error);
177
- });
178
- this.instance.interceptors.response.use((response) => {
179
- this.activeRequests--;
180
- return response;
181
- }, (error) => {
182
- if (axios.isCancel(error)) {
183
- console.warn('Request canceled:', error.message);
184
- }
185
- else if (axios.isAxiosError(error)) {
186
- // handleError(error);
187
- console.error('Response error:', error.response?.status, error.message);
188
- if (error.response?.status === 401) {
189
- this.handleUnauthorized();
190
- }
191
- }
192
- else {
193
- console.error('Unexpected error:', error);
194
- }
195
- this.activeRequests--;
196
- return Promise.reject(error);
197
- });
198
- }
199
- /**
200
- * Returns the number of active requests.
201
- */
202
- getActiveRequests() {
203
- return this.activeRequests;
204
- }
205
- /**
206
- * Handles unauthorized access errors (401).
207
- */
208
- handleUnauthorized() {
209
- console.warn('Unauthorized access - redirecting to login.');
210
- }
211
- /**
212
- * Returns the Axios instance with the configured settings and interceptors.
213
- * @returns {AxiosInstance} The configured Axios instance.
214
- */
215
- getAxiosInstance() {
216
- return this.instance;
217
- }
218
- /**
219
- * Cancels all ongoing requests.
220
- */
221
- cancelAllRequests() {
222
- this.cancelTokenSource.cancel('Operation canceled by the user.');
223
- this.cancelTokenSource = axios.CancelToken.source();
224
- }
225
- /**
226
- * Sets a new header for the Axios instance.
227
- * @param {string} key - The header key.
228
- * @param {string} value - The header value.
229
- */
230
- setHeader(key, value) {
231
- this.instance.defaults.headers.common[key] = value;
232
- }
233
- /**
234
- * Removes a header from the Axios instance.
235
- * @param {string} key - The header key to remove.
236
- */
237
- removeHeader(key) {
238
- delete this.instance.defaults.headers.common[key];
239
- }
240
- }
241
-
242
- /**
243
- * Creates a fetchComposable for Axios requests with an optional custom instance.
244
- * @param fetchComposable - The composable to use for the request (can be any fetch function).
245
- * @param apiUrl - The base URL for the Axios instance.
246
- * @param axiosInstance - An optional custom Axios instance to use for the request.
247
- * @returns A function that handles the request using the provided composable.
248
- */
249
- function createFetch(fetchComposable, apiUrl, axiosInstance) {
250
- const instance = axiosInstance || new AxiosService(apiUrl).getAxiosInstance();
251
- return (axiosRequestConfig, options) => {
252
- return fetchComposable(instance, axiosRequestConfig, options);
253
- };
254
- }
255
-
256
- /**
257
- * Custom composable for integrating Axios requests with Vue Query.
258
- *
259
- * @template T - The type of data expected from the query.
260
- * @param {AxiosInstance} axios - Axios instance used for making HTTP requests.
261
- * @param {AxiosRequestConfig} axiosRequest - Initial Axios request configuration.
262
- * @param {ExtendedQueryOptions<T>} [queryOptions] - Optional Vue Query options with server execution flag.
263
- * @returns {object} Composable functions and query state for managing API requests.
264
- */
265
- function useVueQuery(axios, axiosRequest, queryOptions) {
266
- const key = ref([
267
- queryOptions?.queryKey || `${axiosRequest.url}-${JSON.stringify(axiosRequest)}`
268
- ]);
269
- const axiosRequestRef = ref(axiosRequest);
270
- const queryOptionsRef = ref(queryOptions);
271
- const isInitialized = ref(false);
272
- const errorCallbacks = ref([]);
273
- const resultCallbacks = ref([]);
274
- /**
275
- * Vue Query instance for managing API requests.
276
- */
277
- const query = useQuery({
278
- queryKey: key.value,
279
- queryFn: async () => {
280
- const response = await axios(axiosRequestRef.value);
281
- return response.data;
282
- },
283
- ...(queryOptionsRef.value?.options ?? {})
284
- });
285
- /**
286
- * Executes the query manually with optional new key and request parameters.
287
- *
288
- * @param {Array<any>} [newKey] - New query key to use.
289
- * @param {object} [newRequestParams] - New request parameters to use.
290
- * @param {ExtendedQueryOptions<T>} [newQueryOptions] - New Vue Query options to use.
291
- */
292
- function execute(newKey, newRequestParams, newQueryOptions) {
293
- if (newKey) {
294
- key.value = newKey;
295
- }
296
- if (newRequestParams) {
297
- axiosRequestRef.value.data = newRequestParams;
298
- }
299
- if (newQueryOptions) {
300
- queryOptionsRef.value = newQueryOptions;
301
- }
302
- query.refetch();
303
- }
304
- /**
305
- * Registers a callback to be executed on successful request result.
306
- * If data is already available, the callback will be executed immediately.
307
- *
308
- * @param {(data: T) => void} cb - Callback function to handle the result.
309
- */
310
- function onResult(cb) {
311
- resultCallbacks.value.push(cb);
312
- if (query.data.value !== undefined && query.data.value !== null) {
313
- cb(query.data.value);
314
- }
315
- }
316
- /**
317
- * Registers a callback to be executed when the request results in an error.
318
- * If an error is already present, the callback will be executed immediately.
319
- *
320
- * @param {(e: unknown) => void} callback - Callback function to handle errors.
321
- */
322
- function onError(callback) {
323
- errorCallbacks.value.push(callback);
324
- if (query.error.value !== undefined) {
325
- callback(query.error.value);
326
- }
327
- }
328
- /**
329
- * Watches for changes in the query result and triggers result callbacks.
330
- */
331
- watch(query.data, (newData) => {
332
- if (newData !== undefined && newData !== null && isInitialized.value) {
333
- resultCallbacks.value.forEach((cb) => cb(newData));
334
- }
335
- });
336
- /**
337
- * Watches for changes in query errors and triggers error callbacks.
338
- */
339
- watch(query.error, (newError) => {
340
- if (newError !== undefined && isInitialized.value) {
341
- errorCallbacks.value.forEach((cb) => cb(newError));
342
- }
343
- });
344
- /**
345
- * Handles server-side prefetching for SSR.
346
- * Ensures data is fetched before rendering on the server.
347
- */
348
- onServerPrefetch(async () => {
349
- if (queryOptionsRef.value?.server !== false) {
350
- await query.suspense();
351
- if (query.data.value != null) {
352
- resultCallbacks.value.forEach((cb) => cb(query.data.value));
353
- }
354
- }
355
- });
356
- /**
357
- * Ensures callbacks are executed after the component is mounted on the client-side.
358
- */
359
- onMounted(() => {
360
- isInitialized.value = true;
361
- if (query.data.value != null) {
362
- resultCallbacks.value.forEach((cb) => cb(query.data.value));
363
- }
364
- if (query.error.value !== undefined) {
365
- errorCallbacks.value.forEach((cb) => cb(query.error.value));
366
- }
367
- });
368
- return {
369
- execute,
370
- onResult,
371
- onError,
372
- ...query
373
- };
374
- }
375
-
376
- let notified = false;
377
- /**
378
- * Composable to manage breakpoints using Tailwind CSS and shared window resize listener.
379
- *
380
- * @returns {Object} An object containing the breakpoints and their states.
381
- */
382
- function useBreakpoint() {
383
- const breakpoints = useBreakpoints(breakpointsTailwind);
384
- const current = breakpoints.current();
385
- const active = breakpoints.active();
386
- // Define breakpoints
387
- const sm_S = breakpoints.smaller('sm');
388
- const sm_SE = breakpoints.smallerOrEqual('sm');
389
- const sm_GE = breakpoints.greaterOrEqual('sm');
390
- const sm_md = breakpoints.between('sm', 'md');
391
- const sm_xl = breakpoints.between('sm', 'xl');
392
- const sm_2xl = breakpoints.between('sm', '2xl');
393
- const md_S = breakpoints.smaller('md');
394
- const md_SE = breakpoints.smallerOrEqual('md');
395
- const md_GE = breakpoints.greaterOrEqual('md');
396
- const md_lg = breakpoints.between('md', 'lg');
397
- const md_xl = breakpoints.between('md', 'xl');
398
- const md_2xl = breakpoints.between('md', '2xl');
399
- const lg_S = breakpoints.smaller('lg');
400
- const lg_SE = breakpoints.smallerOrEqual('lg');
401
- const lg_GE = breakpoints.greaterOrEqual('lg');
402
- const lg_xl = breakpoints.between('lg', 'xl');
403
- const lg_2xl = breakpoints.between('lg', '2xl');
404
- const xl_S = breakpoints.smaller('xl');
405
- const xl_SE = breakpoints.smallerOrEqual('xl');
406
- const xl_GE = breakpoints.greaterOrEqual('xl');
407
- const xl_2xl = breakpoints.between('xl', '2xl');
408
- const _2xl_S = breakpoints.smaller('2xl');
409
- const _2xl_SE = breakpoints.smallerOrEqual('2xl');
410
- const _2xl_GE = breakpoints.greaterOrEqual('2xl');
411
- const reactiveBreakpoint = ref('sm');
412
- const isGreaterThanBreakpoint = breakpoints.greaterOrEqual(() => reactiveBreakpoint.value);
413
- // Define device ranges
414
- const mobile = breakpoints.smaller('md'); // <768px
415
- const tablet = breakpoints.between('md', 'lg'); // 768px - 1024px
416
- const laptop = breakpoints.between('lg', 'xl'); // 1024px - 1280px
417
- const desktop = breakpoints.greaterOrEqual('xl'); // >=1280px
418
- // Use VueUse for window size
419
- const { width: windowWidth, height: windowHeight } = useWindowSize();
420
- /**
421
- * Logs a styled "chip" message to the console.
422
- *
423
- * @param {string} label - The label to display inside the chip.
424
- * @param {string} backgroundColor - The background color of the chip.
425
- * @param {string} textColor - The text color of the chip.
426
- */
427
- const logChip = (label, backgroundColor, textColor = 'white') => {
428
- console.log(`%c ${label} `, `background-color: ${backgroundColor}; color: ${textColor}; border-radius: 16px; padding: 4px 8px;`);
429
- };
430
- // Singleton logic: Only notify once
431
- const notifyOnce = () => {
432
- if (notified)
433
- return;
434
- watch([mobile, tablet, laptop, desktop], ([isMobile, isTablet, isLaptop, isDesktop]) => {
435
- if (isMobile) {
436
- logChip('Mobile', '#ff6b6b');
437
- }
438
- else if (isTablet) {
439
- logChip('Tablet', '#4ecdc4');
440
- }
441
- else if (isLaptop) {
442
- logChip('Laptop', '#1a535c');
443
- }
444
- else if (isDesktop) {
445
- logChip('Desktop', '#f7fff7', 'black');
446
- }
447
- notified = true; // Mark as notified
448
- }, { immediate: true });
449
- };
450
- notifyOnce(); // Ensure the notification happens only once globally
451
- return {
452
- current,
453
- active,
454
- sm_S,
455
- sm_SE,
456
- sm_GE,
457
- sm_md,
458
- sm_xl,
459
- sm_2xl,
460
- md_S,
461
- md_SE,
462
- md_GE,
463
- md_lg,
464
- md_xl,
465
- md_2xl,
466
- lg_S,
467
- lg_SE,
468
- lg_GE,
469
- lg_xl,
470
- lg_2xl,
471
- xl_S,
472
- xl_SE,
473
- xl_GE,
474
- xl_2xl,
475
- _2xl_S,
476
- _2xl_SE,
477
- _2xl_GE,
478
- isGreaterThanBreakpoint,
479
- reactiveBreakpoint,
480
- breakpointsTailwind,
481
- windowWidth,
482
- windowHeight,
483
- mobile,
484
- tablet,
485
- laptop,
486
- desktop,
487
- breakpoints,
488
- };
81
+ function exportToJSON(data, fileName) {
82
+ const jsonContent = JSON.stringify(data, null, 2); // Pretty print with 2 spaces
83
+ const blob = new Blob([jsonContent], { type: 'application/json;charset=utf-8;' });
84
+ // Use the download utility function
85
+ downloadFile(blob, fileName);
489
86
  }
490
-
491
87
  /**
492
- * A composable function that filters objects based on a specific field and criteria.
493
- *
494
- * @param {Object[]} items - The list of objects to filter.
495
- * @param {Object} filterConfig - Configuration for the filter, specifying the field, the data type to filter, and the criteria.
496
- * @param {string} filterConfig.field - The field in the objects to filter.
497
- * @param {string} filterConfig.type - The data type of the field ('date', 'string', 'number', 'boolean').
498
- * @param {any} filterConfig.criteria - The criteria for filtering. Can be a date range, a string to search, a number range, or a boolean value.
499
- *
500
- * @returns {Object[]} - The filtered list of objects.
501
- */
502
- function useFilter(items, filterConfig) {
503
- // Ensure the filterConfig is correctly structured
504
- const { field, type, criteria } = filterConfig;
505
- // Define the filter functions
506
- const filterByDate = (objects, field, { startDate, endDate }) => objects.filter(item => {
507
- const date = new Date(item[field]);
508
- return date >= new Date(startDate) && date <= new Date(endDate);
88
+ * Exports data to an XML file.
89
+ * @param {string[]} headers The headers for the XML.
90
+ * @param {any[][]} data The data to export, as an array of arrays.
91
+ * @param {string} fileName The name of the file to create.
92
+ */
93
+ function exportToXML(headers, data, fileName) {
94
+ let xmlContent = '<?xml version="1.0" encoding="UTF-8"?>\n<rows>\n';
95
+ // Add headers
96
+ xmlContent += ' <header>\n';
97
+ headers.forEach(header => {
98
+ xmlContent += ` <column>${header}</column>\n`;
509
99
  });
510
- const filterByString = (objects, field, searchTerm) => {
511
- const normalize = (str) => str.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
512
- const words = normalize(searchTerm).split(/\s+/).filter(Boolean);
513
- return objects.filter(item => {
514
- const normalizedFieldValue = normalize(item[field]);
515
- return words.every(word => normalizedFieldValue.includes(word));
100
+ xmlContent += ' </header>\n';
101
+ // Add data rows
102
+ data.forEach(row => {
103
+ xmlContent += ' <row>\n';
104
+ row.forEach(cell => {
105
+ xmlContent += ` <cell>${cell}</cell>\n`;
516
106
  });
517
- };
518
- const filterByNumber = (objects, field, { min, max }) => objects.filter(item => {
519
- const value = item[field];
520
- return value >= min && value <= max;
521
- });
522
- const filterByBoolean = (objects, field, expectedValue) => objects.filter(item => item[field] === expectedValue);
523
- // Apply the appropriate filter and return the filtered objects
524
- const filteredObjects = computed(() => {
525
- if (!criteria ||
526
- (type === 'date' && (!criteria.startDate || !criteria.endDate)) ||
527
- (type === 'number' && (criteria.min == null || criteria.max == null)) ||
528
- (type === 'boolean' && typeof criteria !== 'boolean')) {
529
- return items; // Return all objects if criteria is not valid
530
- }
531
- // Apply the filter based on the type
532
- switch (type) {
533
- case 'date':
534
- return filterByDate(items, field, criteria);
535
- case 'string':
536
- return filterByString(items, field, criteria);
537
- case 'number':
538
- return filterByNumber(items, field, criteria);
539
- case 'boolean':
540
- return filterByBoolean(items, field, criteria);
541
- default:
542
- return items;
543
- }
107
+ xmlContent += ' </row>\n';
544
108
  });
545
- return filteredObjects.value;
109
+ xmlContent += '</rows>';
110
+ const blob = new Blob([xmlContent], { type: 'application/xml;charset=utf-8;' });
111
+ // Use the download utility function
112
+ downloadFile(blob, fileName);
546
113
  }
547
-
548
114
  /**
549
- * Composable to handle pagination logic.
550
- *
551
- * @param page - Reactive ref for the current page number.
552
- * @param total - Reactive ref for total number of items.
553
- * @param pageSize - Reactive ref for number of items per page.
554
- * @returns An object with pagination controls.
115
+ * Exports data to a plain text file.
116
+ * @param {string[]} headers The headers for the text file.
117
+ * @param {any[][]} data The data to export, as an array of arrays.
118
+ * @param {string} fileName The name of the file to create.
555
119
  */
556
- function usePagination(page, total, pageSize) {
557
- // Calculate the total number of pages
558
- const totalPages = computed(() => Math.ceil(total.value / pageSize.value));
559
- // Determine if the next page can be fetched
560
- const canFetchNextPage = () => page.value < totalPages.value;
561
- // Determine if the previous page can be fetched
562
- const canFetchPreviousPage = () => page.value > 1;
563
- return {
564
- totalPages,
565
- canFetchNextPage,
566
- canFetchPreviousPage
567
- };
120
+ function exportToText(headers, data, fileName) {
121
+ const textRows = [];
122
+ // Add headers
123
+ textRows.push(headers.join('\t'));
124
+ // Add data rows
125
+ data.forEach(row => {
126
+ textRows.push(row.join('\t'));
127
+ });
128
+ // Create a Blob with text data
129
+ const textContent = textRows.join('\n');
130
+ const blob = new Blob([textContent], { type: 'text/plain;charset=utf-8;' });
131
+ // Use the download utility function
132
+ downloadFile(blob, fileName);
568
133
  }
569
134
 
570
135
  /**
571
- * Custom composable for sorting products based on a selected criterion.
572
- * @param {Array} items - The array of products to sort.
573
- * @param {Array} criteriaList - The list of available sorting criteria.
574
- * @param {Number} selectedCriteria - The currently selected sorting criterion.
575
- * @returns {ComputedRef<Array>} - The sorted array of products.
136
+ * Opens a new window with the specified URL and options.
137
+ * @param {string} url The URL to open.
138
+ * @param {Object} [opt] Options for the new window.
139
+ * @param {string} [opt.target='__blank'] The target window name.
140
+ * @param {boolean} [opt.noopener=true] Whether to add 'noopener' attribute.
141
+ * @param {boolean} [opt.noreferrer=true] Whether to add 'noreferrer' attribute.
576
142
  */
577
- function useSorter(items, criteriaList, selectedCriteria) {
578
- return computed(() => {
579
- const criteria = criteriaList.find(item => item.value === Number(selectedCriteria));
580
- if (!criteria)
581
- return items;
582
- return [...items].sort((a, b) => {
583
- const aValue = a[criteria.field];
584
- const bValue = b[criteria.field];
585
- if (criteria.type === 'number') {
586
- const numA = typeof aValue === 'number' ? aValue : parseFloat(aValue) || 0;
587
- const numB = typeof bValue === 'number' ? bValue : parseFloat(bValue) || 0;
588
- return criteria.order === 'asc' ? numA - numB : numB - numA;
589
- }
590
- else if (criteria.type === 'date') {
591
- const dateA = new Date(aValue).getTime();
592
- const dateB = new Date(bValue).getTime();
593
- return criteria.order === 'asc' ? dateA - dateB : dateB - dateA;
594
- }
595
- else if (criteria.type === 'boolean') {
596
- const boolA = !!aValue;
597
- const boolB = !!bValue;
598
- return criteria.order === 'asc' ? (boolA === boolB ? 0 : boolA ? 1 : -1) : (boolA === boolB ? 0 : boolA ? -1 : 1);
599
- }
600
- else {
601
- // Fallback for string sorting
602
- const strA = (aValue || '').toString().toLowerCase();
603
- const strB = (bValue || '').toString().toLowerCase();
604
- return criteria.order === 'asc' ? strA.localeCompare(strB) : strB.localeCompare(strA);
605
- }
606
- });
607
- }).value;
143
+ function openWindow(url, opt) {
144
+ const { target = '__blank', noopener = true, noreferrer = true } = opt || {};
145
+ const features = [];
146
+ // Append noopener and noreferrer attributes if enabled
147
+ if (noopener)
148
+ features.push('noopener=yes');
149
+ if (noreferrer)
150
+ features.push('noreferrer=yes');
151
+ // Open the window with the specified features
152
+ window.open(url, target, features.join(','));
608
153
  }
609
-
610
154
  /**
611
- * Object defining available screen sizes as constants.
612
- * @readonly
155
+ * Copies text to the clipboard.
156
+ * @param {string} text The text to copy.
157
+ * @returns {Promise<void>} A promise that resolves when the text has been copied.
613
158
  */
614
- const sizeEnum = {
615
- XS: 'XS',
616
- SM: 'SM',
617
- MD: 'MD',
618
- LG: 'LG',
619
- XL: 'XL',
620
- XXL: 'XXL',
621
- };
159
+ async function copyToClipboard(text) {
160
+ if (navigator.clipboard) {
161
+ await navigator.clipboard.writeText(text);
162
+ }
163
+ else {
164
+ // Fallback for browsers that do not support the Clipboard API
165
+ const textArea = document.createElement('textarea');
166
+ textArea.value = text;
167
+ document.body.appendChild(textArea);
168
+ textArea.select();
169
+ document.execCommand('copy');
170
+ document.body.removeChild(textArea);
171
+ }
172
+ }
622
173
  /**
623
- * Object defining breakpoints for design based on screen width.
624
- * Values are in pixels.
625
- * @readonly
174
+ * Scrolls the window to the top smoothly.
175
+ * @param {number} [duration=300] Duration of the scroll animation in milliseconds.
626
176
  */
627
- const screenEnum = {
628
- XS: 480,
629
- SM: 576,
630
- MD: 768,
631
- LG: 992,
632
- XL: 1200,
633
- XXL: 1600,
634
- };
177
+ function scrollToTop(duration = 300) {
178
+ const start = window.scrollY;
179
+ const startTime = performance.now();
180
+ function scroll() {
181
+ const elapsed = performance.now() - startTime;
182
+ const progress = Math.min(elapsed / duration, 1);
183
+ window.scrollTo(0, start * (1 - progress));
184
+ if (progress < 1) {
185
+ requestAnimationFrame(scroll);
186
+ }
187
+ }
188
+ requestAnimationFrame(scroll);
189
+ }
635
190
  /**
636
- * Map that associates each screen size defined in `sizeEnum` with its corresponding pixel value from `screenEnum`.
637
- * @type {Map<typeof sizeEnum[keyof typeof sizeEnum], number>}
191
+ * Gets the value of a query parameter from the URL.
192
+ * @param {string} paramName The name of the query parameter.
193
+ * @returns {string | null} The value of the query parameter, or null if it does not exist.
638
194
  */
639
- const screenMap = new Map();
640
- screenMap.set(sizeEnum.XS, screenEnum.XS);
641
- screenMap.set(sizeEnum.SM, screenEnum.SM);
642
- screenMap.set(sizeEnum.MD, screenEnum.MD);
643
- screenMap.set(sizeEnum.LG, screenEnum.LG);
644
- screenMap.set(sizeEnum.XL, screenEnum.XL);
645
- screenMap.set(sizeEnum.XXL, screenEnum.XXL);
195
+ function getQueryParam(paramName) {
196
+ const urlParams = new URLSearchParams(window.location.search);
197
+ return urlParams.get(paramName);
198
+ }
646
199
 
647
200
  /**
648
- * Object defining various exception codes.
649
- * @readonly
201
+ * Disables the right-click context menu on the window.
650
202
  */
651
- const ExceptionEnum = {
652
- /**
653
- * HTTP status code for page access forbidden.
654
- */
655
- PAGE_NOT_ACCESS: 403,
656
- /**
657
- * HTTP status code for page not found.
658
- */
659
- PAGE_NOT_FOUND: 404,
660
- /**
661
- * HTTP status code for general server error.
662
- */
663
- ERROR: 500,
664
- /**
665
- * HTTP status code for bad request.
666
- */
667
- BAD_REQUEST: 406,
668
- /**
669
- * Custom error code for network errors.
670
- */
671
- NET_WORK_ERROR: 10000,
672
- /**
673
- * Custom code indicating no data on the page, not actually an exception page.
674
- */
675
- PAGE_NOT_DATA: 10100,
676
- };
677
-
203
+ function disableRightClick() {
204
+ const handler = (event) => event.preventDefault();
205
+ window.addEventListener('contextmenu', handler);
206
+ // Store handler to allow removal
207
+ disableRightClick.handler = handler;
208
+ }
209
+ disableRightClick.handler = null;
210
+ /**
211
+ * Enables the right-click context menu on the window.
212
+ */
213
+ function enableRightClick() {
214
+ if (disableRightClick.handler) {
215
+ window.removeEventListener('contextmenu', disableRightClick.handler);
216
+ }
217
+ }
678
218
  /**
679
- * Object defining various image MIME types.
680
- * @readonly
219
+ * Disables specific mouse buttons.
220
+ * @param {Array<number>} buttons Array of mouse button codes to disable (0 for left, 1 for middle, 2 for right).
681
221
  */
682
- const ImageTypes = {
683
- apng: 'image/apng',
684
- bmp: 'image/bmp',
685
- gif: 'image/gif',
686
- jpeg: 'image/jpeg',
687
- pjpeg: 'image/pjpeg',
688
- png: 'image/png',
689
- svg: 'image/svg+xml',
690
- tiff: 'image/tiff',
691
- webp: 'image/webp',
692
- xicon: 'image/x-icon',
693
- };
222
+ function disableMouseButtons(buttons) {
223
+ const handler = (event) => {
224
+ if (buttons.includes(event.button)) {
225
+ event.preventDefault();
226
+ }
227
+ };
228
+ window.addEventListener('mousedown', handler);
229
+ window.addEventListener('contextmenu', handler); // Also handles right-click context menu
230
+ // Store handler to allow removal later
231
+ disableMouseButtons.handlers.push(handler);
232
+ }
233
+ disableMouseButtons.handlers = [];
694
234
  /**
695
- * Object defining various file meta types.
696
- * @readonly
235
+ * Enables all previously disabled mouse buttons.
697
236
  */
698
- const FileMetaTypes = {
699
- images: 'image/*',
700
- audios: 'audio/*',
701
- videos: 'video/*',
702
- };
703
-
237
+ function enableMouseButtons() {
238
+ for (const handler of disableMouseButtons.handlers) {
239
+ window.removeEventListener('mousedown', handler);
240
+ window.removeEventListener('contextmenu', handler);
241
+ }
242
+ disableMouseButtons.handlers = [];
243
+ }
704
244
  /**
705
- * Object representing different content types for HTTP headers.
706
- * @readonly
245
+ * Adds a double-click event listener to a specific element.
246
+ * @param {HTMLElement} element The target element.
247
+ * @param {(event: MouseEvent) => void} callback The callback function to execute on double click.
707
248
  */
708
- const ContentTypeEnum = {
709
- /**
710
- * Content type for JSON.
711
- * @constant
712
- */
713
- JSON: 'application/json;charset=UTF-8',
714
- /**
715
- * Content type for URL-encoded form data.
716
- * @constant
717
- */
718
- FORM_URLENCODED: 'application/x-www-form-urlencoded;charset=UTF-8',
719
- /**
720
- * Content type for multipart form data with file uploads.
721
- * @constant
722
- */
723
- FORM_DATA: 'multipart/form-data;charset=UTF-8',
724
- };
725
-
249
+ function addDoubleClickListener(element, callback) {
250
+ element.addEventListener('dblclick', callback);
251
+ }
726
252
  /**
727
- * Object defining various key codes for keyboard events.
728
- * @readonly
253
+ * Removes a double-click event listener from a specific element.
254
+ * @param {HTMLElement} element The target element.
255
+ * @param {(event: MouseEvent) => void} callback The callback function to remove.
729
256
  */
730
- const KeyCodeEnum = {
731
- UP: 38,
732
- DOWN: 40,
733
- ENTER: 13,
734
- ESC: 27,
735
- };
736
-
257
+ function removeDoubleClickListener(element, callback) {
258
+ element.removeEventListener('dblclick', callback);
259
+ }
260
+ // Example usage:
261
+ // addDoubleClickListener(document.body, () => alert('Double clicked!'));
737
262
  /**
738
- * Key used for storing the authentication token.
739
- * @constant {string}
263
+ * Detects a click outside a specific element and triggers a callback.
264
+ * @param {HTMLElement} element The element to detect clicks outside of.
265
+ * @param {() => void} callback The callback function to execute when a click outside is detected.
740
266
  */
741
- const TOKEN_KEY = 'TOKEN__';
267
+ function clickOutside(element, callback) {
268
+ const handler = (event) => {
269
+ if (!element.contains(event.target)) {
270
+ callback();
271
+ }
272
+ };
273
+ document.addEventListener('click', handler);
274
+ // Store handler to allow removal later
275
+ clickOutside.handlers.push({ element, handler });
276
+ }
277
+ clickOutside.handlers = [];
742
278
  /**
743
- * Key used for storing the locale information.
744
- * @constant {string}
279
+ * Removes the click outside listener for a specific element.
280
+ * @param {HTMLElement} element The element to stop detecting clicks outside of.
745
281
  */
746
- const LOCALE_KEY = 'LOCALE__';
282
+ function removeClickOutside(element) {
283
+ const index = clickOutside.handlers.findIndex(h => h.element === element);
284
+ if (index !== -1) {
285
+ document.removeEventListener('click', clickOutside.handlers[index].handler);
286
+ clickOutside.handlers.splice(index, 1);
287
+ }
288
+ }
289
+ // Example usage: Close a menu when clicking outside of it
290
+ // const menu = document.getElementById('menu');
291
+ // if (menu) {
292
+ // clickOutside(menu, () => menu.style.display = 'none');
293
+ // }
747
294
  /**
748
- * Key used for storing user information.
749
- * @constant {string}
295
+ * Disables the F12 key and certain key combinations for developer tools.
750
296
  */
751
- const USER_INFO_KEY = 'USER__INFO__';
297
+ function disableF12Key() {
298
+ const handler = function (event) {
299
+ return true;
300
+ };
301
+ document.addEventListener('keydown', handler);
302
+ }
752
303
  /**
753
- * Key used for storing role information.
754
- * @constant {string}
304
+ * Enables or disables the tab navigation (Tab key) on the page.
305
+ * @param {boolean} enable Whether to enable or disable tab navigation.
755
306
  */
756
- const ROLES_KEY = 'ROLES__KEY__';
307
+ function toggleTabNavigation(enable) {
308
+ if (enable) {
309
+ document.onkeydown = null; // Removes any previously set handler
310
+ }
311
+ else {
312
+ document.onkeydown = (event) => {
313
+ if (event.key === 'Tab') {
314
+ event.preventDefault();
315
+ return false;
316
+ }
317
+ return true;
318
+ };
319
+ }
320
+ }
757
321
  /**
758
- * Key used for storing project configuration.
759
- * @constant {string}
322
+ * Disables the copy (Ctrl + C) functionality on the page.
760
323
  */
761
- const PROJ_CFG_KEY = 'PROJ__CFG__KEY__';
324
+ function disableCopy() {
325
+ document.addEventListener('copy', (event) => {
326
+ event.preventDefault();
327
+ alert('Copying text is disabled on this page.');
328
+ });
329
+ }
762
330
  /**
763
- * Key used for storing lock information.
764
- * @constant {string}
331
+ * Adds a custom keyboard shortcut to execute a given callback function.
332
+ * @param {string} key The key to trigger the callback.
333
+ * @param {Function} callback The function to execute on the key press.
334
+ * @param {boolean} [ctrlKey=false] Whether Ctrl key should be pressed.
335
+ * @param {boolean} [shiftKey=false] Whether Shift key should be pressed.
765
336
  */
766
- const LOCK_INFO_KEY = 'LOCK__INFO__KEY__';
337
+ function addCustomKeyboardShortcut(key, callback, ctrlKey = false, shiftKey = false) {
338
+ document.addEventListener('keydown', (event) => {
339
+ if (event.key === key &&
340
+ event.ctrlKey === ctrlKey &&
341
+ event.shiftKey === shiftKey) {
342
+ event.preventDefault();
343
+ callback();
344
+ }
345
+ });
346
+ }
767
347
  /**
768
- * Key used for base global local cache.
769
- * @constant {string}
348
+ * Removes a custom keyboard shortcut by key and modifiers.
349
+ * @param {string} key The key to trigger the callback.
350
+ * @param {boolean} [ctrlKey=false] Whether Ctrl key should be pressed.
351
+ * @param {boolean} [shiftKey=false] Whether Shift key should be pressed.
770
352
  */
771
- const APP_LOCAL_CACHE_KEY = 'COMMON__LOCAL__KEY';
353
+ function removeCustomKeyboardShortcut(key, ctrlKey = false, shiftKey = false) {
354
+ const handler = (event) => {
355
+ if (event.key === key &&
356
+ event.ctrlKey === ctrlKey &&
357
+ event.shiftKey === shiftKey) {
358
+ event.preventDefault();
359
+ }
360
+ };
361
+ document.removeEventListener('keydown', handler);
362
+ }
772
363
  /**
773
- * Key used for base global session cache.
774
- * @constant {string}
364
+ * Disables specific keys or key combinations.
365
+ * @param {Array<string>} keys Array of key names to disable (e.g., ['F1', 'F5', 'Control+S']).
775
366
  */
776
- const APP_SESSION_CACHE_KEY = 'COMMON__SESSION__KEY';
367
+ function disableSpecificKeys(keys) {
368
+ const handler = (event) => {
369
+ const keyCombination = `${event.ctrlKey ? 'Control+' : ''}${event.shiftKey ? 'Shift+' : ''}${event.altKey ? 'Alt+' : ''}${event.key}`;
370
+ if (keys.includes(event.key) || keys.includes(keyCombination)) {
371
+ event.preventDefault();
372
+ }
373
+ };
374
+ document.addEventListener('keydown', handler);
375
+ // Storing the handler to allow removal later
376
+ disableSpecificKeys.handlers.push(handler);
377
+ }
378
+ disableSpecificKeys.handlers = [];
777
379
  /**
778
- * Object defining types of cache storage.
779
- * @readonly
380
+ * Enables keys that were previously disabled using disableSpecificKeys.
780
381
  */
781
- const CacheTypeEnum = {
782
- /** Represents session storage */
783
- SESSION: 0,
784
- /** Represents local storage */
785
- LOCAL: 1,
786
- };
787
-
382
+ function enableSpecificKeys() {
383
+ for (const handler of disableSpecificKeys.handlers) {
384
+ document.removeEventListener('keydown', handler);
385
+ }
386
+ disableSpecificKeys.handlers = [];
387
+ }
388
+ /**
389
+ * Registers multiple keyboard shortcuts with their respective callback functions.
390
+ * @param {Array<{ key: string, ctrlKey?: boolean, shiftKey?: boolean, altKey?: boolean, callback: Function }>} shortcuts Array of shortcut objects.
391
+ */
392
+ function registerKeyboardShortcuts(shortcuts) {
393
+ const handler = (event) => {
394
+ for (const shortcut of shortcuts) {
395
+ if (event.key === shortcut.key &&
396
+ (shortcut.ctrlKey ? event.ctrlKey : true) &&
397
+ (shortcut.shiftKey ? event.shiftKey : true) &&
398
+ (shortcut.altKey ? event.altKey : true)) {
399
+ event.preventDefault();
400
+ shortcut.callback();
401
+ }
402
+ }
403
+ };
404
+ document.addEventListener('keydown', handler);
405
+ // Storing the handler to allow removal later
406
+ registerKeyboardShortcuts.handlers.push(handler);
407
+ }
408
+ registerKeyboardShortcuts.handlers = [];
409
+ /**
410
+ * Unregisters all keyboard shortcuts that were registered with registerKeyboardShortcuts.
411
+ */
412
+ function unregisterKeyboardShortcuts() {
413
+ for (const handler of registerKeyboardShortcuts.handlers) {
414
+ document.removeEventListener('keydown', handler);
415
+ }
416
+ registerKeyboardShortcuts.handlers = [];
417
+ }
788
418
  /**
789
- * Creates and downloads a file from Blob data.
790
- * @param {Blob} blob The Blob containing the file data.
791
- * @param {string} fileName The name of the file to create.
419
+ * Adds a listener for a specific key to trigger a custom event.
420
+ * @param {string} key The key to listen for (e.g., 'Enter', 'Escape').
421
+ * @param {Function} callback The function to execute when the key is pressed.
792
422
  */
793
- function downloadFile(blob, fileName) {
794
- const link = document.createElement('a');
795
- const url = URL.createObjectURL(blob);
796
- link.setAttribute('href', url);
797
- link.setAttribute('download', fileName);
798
- // Append link to the body and trigger a click
799
- document.body.appendChild(link);
800
- link.click();
801
- // Clean up
802
- document.body.removeChild(link);
803
- URL.revokeObjectURL(url);
423
+ function addKeyListener(key, callback) {
424
+ const handler = (event) => {
425
+ if (event.key === key) {
426
+ event.preventDefault();
427
+ callback();
428
+ }
429
+ };
430
+ document.addEventListener('keydown', handler);
431
+ // Storing the handler to allow removal later
432
+ addKeyListener.handlers.push(handler);
804
433
  }
434
+ addKeyListener.handlers = [];
805
435
  /**
806
- * Exports data to a CSV file.
807
- * @param {string[]} headers The headers for the CSV.
808
- * @param {any[][]} data The data to export, as an array of arrays.
809
- * @param {string} fileName The name of the file to create.
436
+ * Removes all custom key listeners added by addKeyListener.
810
437
  */
811
- function exportToCSV(headers, data, fileName) {
812
- const csvRows = [];
813
- // Add the headers
814
- csvRows.push(headers.map(header => `"${header.replace(/"/g, '""')}"`).join(','));
815
- // Add the data rows
816
- for (const row of data) {
817
- csvRows.push(row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(','));
438
+ function removeKeyListeners() {
439
+ for (const handler of addKeyListener.handlers) {
440
+ document.removeEventListener('keydown', handler);
818
441
  }
819
- // Create a Blob with CSV data
820
- const csvContent = csvRows.join('\n');
821
- const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
822
- // Use the download utility function
823
- downloadFile(blob, fileName);
442
+ addKeyListener.handlers = [];
824
443
  }
825
444
  /**
826
- * Exports data to an Excel file (.xls) using HTML table.
827
- * @param {string[]} headers The headers for the Excel file.
828
- * @param {any[][]} data The data to export, as an array of arrays.
829
- * @param {string} fileName The name of the file to create.
445
+ * Detects if a specific key is held down.
446
+ * @param {string} key The key to detect (e.g., 'Shift', 'Control', 'Alt', 'a').
447
+ * @param {Function} onHold Callback function to execute while the key is held down.
830
448
  */
831
- function exportToExcel(headers, data, fileName) {
832
- // Create a table element
833
- const table = document.createElement('table');
834
- const thead = table.createTHead();
835
- const tbody = table.createTBody();
836
- // Create the header row
837
- const headerRow = thead.insertRow();
838
- headers.forEach(header => {
839
- const th = document.createElement('th');
840
- th.textContent = header;
841
- headerRow.appendChild(th);
842
- });
843
- // Create the data rows
844
- data.forEach(row => {
845
- const tr = tbody.insertRow();
846
- row.forEach(cell => {
847
- const td = tr.insertCell();
848
- td.textContent = String(cell);
849
- });
850
- });
851
- // Create a Blob with the table HTML
852
- const html = table.outerHTML;
853
- const blob = new Blob([html], { type: 'application/vnd.ms-excel;charset=utf-8;' });
854
- // Use the download utility function
855
- downloadFile(blob, fileName);
449
+ function detectKeyHold(key, onHold) {
450
+ const keyDownHandler = (event) => {
451
+ if (event.key === key) {
452
+ onHold();
453
+ }
454
+ };
455
+ document.addEventListener('keydown', keyDownHandler);
456
+ // Storing the handler to allow removal later
457
+ detectKeyHold.handlers.push(keyDownHandler);
856
458
  }
459
+ detectKeyHold.handlers = [];
857
460
  /**
858
- * Exports data to a JSON file.
859
- * @param {any[]} data The data to export.
860
- * @param {string} fileName The name of the file to create.
461
+ * Stops detecting if a specific key is held down.
861
462
  */
862
- function exportToJSON(data, fileName) {
863
- const jsonContent = JSON.stringify(data, null, 2); // Pretty print with 2 spaces
864
- const blob = new Blob([jsonContent], { type: 'application/json;charset=utf-8;' });
865
- // Use the download utility function
866
- downloadFile(blob, fileName);
463
+ function stopDetectingKeyHold() {
464
+ for (const handler of detectKeyHold.handlers) {
465
+ document.removeEventListener('keydown', handler);
466
+ }
467
+ detectKeyHold.handlers = [];
867
468
  }
868
469
  /**
869
- * Exports data to an XML file.
870
- * @param {string[]} headers The headers for the XML.
871
- * @param {any[][]} data The data to export, as an array of arrays.
872
- * @param {string} fileName The name of the file to create.
470
+ * Tracks currently pressed keys and provides a map of active keys.
471
+ * @returns {Set<string>} A set of currently pressed keys.
873
472
  */
874
- function exportToXML(headers, data, fileName) {
875
- let xmlContent = '<?xml version="1.0" encoding="UTF-8"?>\n<rows>\n';
876
- // Add headers
877
- xmlContent += ' <header>\n';
878
- headers.forEach(header => {
879
- xmlContent += ` <column>${header}</column>\n`;
880
- });
881
- xmlContent += ' </header>\n';
882
- // Add data rows
883
- data.forEach(row => {
884
- xmlContent += ' <row>\n';
885
- row.forEach(cell => {
886
- xmlContent += ` <cell>${cell}</cell>\n`;
887
- });
888
- xmlContent += ' </row>\n';
889
- });
890
- xmlContent += '</rows>';
891
- const blob = new Blob([xmlContent], { type: 'application/xml;charset=utf-8;' });
892
- // Use the download utility function
893
- downloadFile(blob, fileName);
473
+ function createKeyMap() {
474
+ const pressedKeys = new Set();
475
+ const keyDownHandler = (event) => {
476
+ pressedKeys.add(event.key);
477
+ };
478
+ const keyUpHandler = (event) => {
479
+ pressedKeys.delete(event.key);
480
+ };
481
+ document.addEventListener('keydown', keyDownHandler);
482
+ document.addEventListener('keyup', keyUpHandler);
483
+ // Function to remove listeners
484
+ createKeyMap.clearListeners = () => {
485
+ document.removeEventListener('keydown', keyDownHandler);
486
+ document.removeEventListener('keyup', keyUpHandler);
487
+ };
488
+ return pressedKeys;
894
489
  }
895
490
  /**
896
- * Exports data to a plain text file.
897
- * @param {string[]} headers The headers for the text file.
898
- * @param {any[][]} data The data to export, as an array of arrays.
899
- * @param {string} fileName The name of the file to create.
491
+ * Clears listeners for the key map tracking.
900
492
  */
901
- function exportToText(headers, data, fileName) {
902
- const textRows = [];
903
- // Add headers
904
- textRows.push(headers.join('\t'));
905
- // Add data rows
906
- data.forEach(row => {
907
- textRows.push(row.join('\t'));
908
- });
909
- // Create a Blob with text data
910
- const textContent = textRows.join('\n');
911
- const blob = new Blob([textContent], { type: 'text/plain;charset=utf-8;' });
912
- // Use the download utility function
913
- downloadFile(blob, fileName);
914
- }
915
-
493
+ createKeyMap.clearListeners = () => { };
916
494
  /**
917
- * Opens a new window with the specified URL and options.
918
- * @param {string} url The URL to open.
919
- * @param {Object} [opt] Options for the new window.
920
- * @param {string} [opt.target='__blank'] The target window name.
921
- * @param {boolean} [opt.noopener=true] Whether to add 'noopener' attribute.
922
- * @param {boolean} [opt.noreferrer=true] Whether to add 'noreferrer' attribute.
495
+ * Sets up custom keyboard shortcuts with flexible order.
496
+ * @param {Array<string>} keys The combination of keys for the shortcut.
497
+ * @param {Function} callback The callback function to execute when the combination is detected.
923
498
  */
924
- function openWindow(url, opt) {
925
- const { target = '__blank', noopener = true, noreferrer = true } = opt || {};
926
- const features = [];
927
- // Append noopener and noreferrer attributes if enabled
928
- if (noopener)
929
- features.push('noopener=yes');
930
- if (noreferrer)
931
- features.push('noreferrer=yes');
932
- // Open the window with the specified features
933
- window.open(url, target, features.join(','));
499
+ function customShortcut(keys, callback) {
500
+ const pressedKeys = new Set();
501
+ const keyDownHandler = (event) => {
502
+ pressedKeys.add(event.key);
503
+ if (keys.every(key => pressedKeys.has(key))) {
504
+ callback();
505
+ }
506
+ };
507
+ const keyUpHandler = (event) => {
508
+ pressedKeys.delete(event.key);
509
+ };
510
+ document.addEventListener('keydown', keyDownHandler);
511
+ document.addEventListener('keyup', keyUpHandler);
512
+ // Store the handlers for later removal
513
+ customShortcut.handlers.push({ keyDownHandler, keyUpHandler });
934
514
  }
515
+ customShortcut.handlers = [];
935
516
  /**
936
- * Copies text to the clipboard.
937
- * @param {string} text The text to copy.
938
- * @returns {Promise<void>} A promise that resolves when the text has been copied.
517
+ * Removes all custom keyboard shortcuts added by customShortcut.
939
518
  */
940
- async function copyToClipboard(text) {
941
- if (navigator.clipboard) {
942
- await navigator.clipboard.writeText(text);
943
- }
944
- else {
945
- // Fallback for browsers that do not support the Clipboard API
946
- const textArea = document.createElement('textarea');
947
- textArea.value = text;
948
- document.body.appendChild(textArea);
949
- textArea.select();
950
- document.execCommand('copy');
951
- document.body.removeChild(textArea);
519
+ function removeCustomShortcuts() {
520
+ for (const { keyDownHandler, keyUpHandler } of customShortcut.handlers) {
521
+ document.removeEventListener('keydown', keyDownHandler);
522
+ document.removeEventListener('keyup', keyUpHandler);
952
523
  }
524
+ customShortcut.handlers = [];
953
525
  }
954
526
  /**
955
- * Scrolls the window to the top smoothly.
956
- * @param {number} [duration=300] Duration of the scroll animation in milliseconds.
527
+ * Simulates a key press event.
528
+ * @param {string} key The key to simulate (e.g., 'Enter', 'a').
529
+ * @param {boolean} ctrlKey If true, include Ctrl key in the event.
530
+ * @param {boolean} shiftKey If true, include Shift key in the event.
531
+ * @param {boolean} altKey If true, include Alt key in the event.
957
532
  */
958
- function scrollToTop(duration = 300) {
959
- const start = window.scrollY;
960
- const startTime = performance.now();
961
- function scroll() {
962
- const elapsed = performance.now() - startTime;
963
- const progress = Math.min(elapsed / duration, 1);
964
- window.scrollTo(0, start * (1 - progress));
965
- if (progress < 1) {
966
- requestAnimationFrame(scroll);
533
+ function simulateKeyPress(key, ctrlKey = false, shiftKey = false, altKey = false) {
534
+ const event = new KeyboardEvent('keydown', {
535
+ key,
536
+ ctrlKey,
537
+ shiftKey,
538
+ altKey,
539
+ bubbles: true,
540
+ cancelable: true
541
+ });
542
+ document.dispatchEvent(event);
543
+ }
544
+
545
+ /**
546
+ * Parses a date string into a Date object.
547
+ * @param {string} dateString The date string in 'YYYY-MM-DD' format.
548
+ * @returns {Date | null} The parsed Date object or null if the format is invalid.
549
+ */
550
+ function parseDate(dateString) {
551
+ const parts = dateString.split('-');
552
+ if (parts.length === 3) {
553
+ const year = parseInt(parts[0], 10);
554
+ const month = parseInt(parts[1], 10) - 1; // Months are zero-based
555
+ const day = parseInt(parts[2], 10);
556
+ const date = new Date(year, month, day);
557
+ if (date.getFullYear() === year && date.getMonth() === month && date.getDate() === day) {
558
+ return date;
967
559
  }
968
560
  }
969
- requestAnimationFrame(scroll);
561
+ return null;
970
562
  }
971
563
  /**
972
- * Gets the value of a query parameter from the URL.
973
- * @param {string} paramName The name of the query parameter.
974
- * @returns {string | null} The value of the query parameter, or null if it does not exist.
564
+ * Formats a Date object into a string.
565
+ * @param {Date} date The date to format.
566
+ * @param {string} format The format string (e.g., 'YYYY-MM-DD').
567
+ * @returns {string} The formatted date string.
975
568
  */
976
- function getQueryParam(paramName) {
977
- const urlParams = new URLSearchParams(window.location.search);
978
- return urlParams.get(paramName);
569
+ function formatDate(date, format) {
570
+ const map = {
571
+ 'YYYY': date.getFullYear(),
572
+ 'MM': String(date.getMonth() + 1).padStart(2, '0'),
573
+ 'DD': String(date.getDate()).padStart(2, '0'),
574
+ 'HH': String(date.getHours()).padStart(2, '0'),
575
+ 'mm': String(date.getMinutes()).padStart(2, '0'),
576
+ 'ss': String(date.getSeconds()).padStart(2, '0')
577
+ };
578
+ return format.replace(/YYYY|MM|DD|HH|mm|ss/g, (matched) => (map[matched] || matched).toString());
979
579
  }
980
-
981
580
  /**
982
- * Disables the right-click context menu on the window.
581
+ * Calculates the number of days between two dates.
582
+ * @param {Date} startDate The start date.
583
+ * @param {Date} endDate The end date.
584
+ * @returns {number} The number of days between the two dates.
983
585
  */
984
- function disableRightClick() {
985
- const handler = (event) => event.preventDefault();
986
- window.addEventListener('contextmenu', handler);
987
- // Store handler to allow removal
988
- disableRightClick.handler = handler;
586
+ function daysBetween(startDate, endDate) {
587
+ const millisecondsPerDay = 86400000; // Number of milliseconds in one day
588
+ return Math.round((endDate.getTime() - startDate.getTime()) / millisecondsPerDay);
989
589
  }
990
- disableRightClick.handler = null;
991
590
  /**
992
- * Enables the right-click context menu on the window.
591
+ * Adds a specified number of days to a date.
592
+ * @param {Date} date The date to modify.
593
+ * @param {number} days The number of days to add.
594
+ * @returns {Date} The new date with days added.
993
595
  */
994
- function enableRightClick() {
995
- if (disableRightClick.handler) {
996
- window.removeEventListener('contextmenu', disableRightClick.handler);
997
- }
596
+ function addDays(date, days) {
597
+ const result = new Date(date);
598
+ result.setDate(result.getDate() + days);
599
+ return result;
998
600
  }
999
601
  /**
1000
- * Disables specific mouse buttons.
1001
- * @param {Array<number>} buttons Array of mouse button codes to disable (0 for left, 1 for middle, 2 for right).
602
+ * Subtracts a specified number of days from a date.
603
+ * @param {Date} date The date to modify.
604
+ * @param {number} days The number of days to subtract.
605
+ * @returns {Date} The new date with days subtracted.
1002
606
  */
1003
- function disableMouseButtons(buttons) {
1004
- const handler = (event) => {
1005
- if (buttons.includes(event.button)) {
1006
- event.preventDefault();
1007
- }
1008
- };
1009
- window.addEventListener('mousedown', handler);
1010
- window.addEventListener('contextmenu', handler); // Also handles right-click context menu
1011
- // Store handler to allow removal later
1012
- disableMouseButtons.handlers.push(handler);
607
+ function subtractDays(date, days) {
608
+ return addDays(date, -days);
1013
609
  }
1014
- disableMouseButtons.handlers = [];
1015
610
  /**
1016
- * Enables all previously disabled mouse buttons.
611
+ * Determines if a year is a leap year.
612
+ * @param {number} year The year to check.
613
+ * @returns {boolean} True if the year is a leap year, false otherwise.
1017
614
  */
1018
- function enableMouseButtons() {
1019
- for (const handler of disableMouseButtons.handlers) {
1020
- window.removeEventListener('mousedown', handler);
1021
- window.removeEventListener('contextmenu', handler);
1022
- }
1023
- disableMouseButtons.handlers = [];
615
+ function isLeapYear(year) {
616
+ return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
1024
617
  }
1025
618
  /**
1026
- * Adds a double-click event listener to a specific element.
1027
- * @param {HTMLElement} element The target element.
1028
- * @param {(event: MouseEvent) => void} callback The callback function to execute on double click.
619
+ * Gets the first day of the month for a given date.
620
+ * @param {Date} date The date to use.
621
+ * @returns {Date} The first day of the month.
1029
622
  */
1030
- function addDoubleClickListener(element, callback) {
1031
- element.addEventListener('dblclick', callback);
623
+ function getStartOfMonth(date) {
624
+ return new Date(date.getFullYear(), date.getMonth(), 1);
1032
625
  }
1033
626
  /**
1034
- * Removes a double-click event listener from a specific element.
1035
- * @param {HTMLElement} element The target element.
1036
- * @param {(event: MouseEvent) => void} callback The callback function to remove.
627
+ * Gets the last day of the month for a given date.
628
+ * @param {Date} date The date to use.
629
+ * @returns {Date} The last day of the month.
1037
630
  */
1038
- function removeDoubleClickListener(element, callback) {
1039
- element.removeEventListener('dblclick', callback);
631
+ function getEndOfMonth(date) {
632
+ return new Date(date.getFullYear(), date.getMonth() + 1, 0);
1040
633
  }
1041
- // Example usage:
1042
- // addDoubleClickListener(document.body, () => alert('Double clicked!'));
1043
634
  /**
1044
- * Detects a click outside a specific element and triggers a callback.
1045
- * @param {HTMLElement} element The element to detect clicks outside of.
1046
- * @param {() => void} callback The callback function to execute when a click outside is detected.
635
+ * Calculates age from a given birth date.
636
+ * @param {Date} birthDate The birth date.
637
+ * @returns {number} The calculated age.
1047
638
  */
1048
- function clickOutside(element, callback) {
1049
- const handler = (event) => {
1050
- if (!element.contains(event.target)) {
1051
- callback();
1052
- }
1053
- };
1054
- document.addEventListener('click', handler);
1055
- // Store handler to allow removal later
1056
- clickOutside.handlers.push({ element, handler });
639
+ function calculateAge(birthDate) {
640
+ const today = new Date();
641
+ let age = today.getFullYear() - birthDate.getFullYear();
642
+ const m = today.getMonth() - birthDate.getMonth();
643
+ if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) {
644
+ age--;
645
+ }
646
+ return age;
1057
647
  }
1058
- clickOutside.handlers = [];
1059
648
  /**
1060
- * Removes the click outside listener for a specific element.
1061
- * @param {HTMLElement} element The element to stop detecting clicks outside of.
649
+ * Calculates the number of days until the next birthday.
650
+ * @param {Date} birthDate The birth date.
651
+ * @returns {number} The number of days until the next birthday.
1062
652
  */
1063
- function removeClickOutside(element) {
1064
- const index = clickOutside.handlers.findIndex(h => h.element === element);
1065
- if (index !== -1) {
1066
- document.removeEventListener('click', clickOutside.handlers[index].handler);
1067
- clickOutside.handlers.splice(index, 1);
653
+ function daysToNextBirthday(birthDate) {
654
+ const today = new Date();
655
+ const currentYear = today.getFullYear();
656
+ const nextBirthday = new Date(birthDate);
657
+ nextBirthday.setFullYear(currentYear);
658
+ if (nextBirthday < today) {
659
+ nextBirthday.setFullYear(currentYear + 1);
1068
660
  }
661
+ return daysBetween(today, nextBirthday);
1069
662
  }
1070
- // Example usage: Close a menu when clicking outside of it
1071
- // const menu = document.getElementById('menu');
1072
- // if (menu) {
1073
- // clickOutside(menu, () => menu.style.display = 'none');
1074
- // }
1075
663
  /**
1076
- * Disables the F12 key and certain key combinations for developer tools.
664
+ * Calculates the age at a specific date.
665
+ * @param {Date} birthDate The birth date.
666
+ * @param {Date} atDate The date to calculate the age at.
667
+ * @returns {number} The calculated age.
1077
668
  */
1078
- function disableF12Key() {
1079
- const handler = function (event) {
1080
- return true;
1081
- };
1082
- document.addEventListener('keydown', handler);
669
+ function ageAtDate(birthDate, atDate) {
670
+ let age = atDate.getFullYear() - birthDate.getFullYear();
671
+ const m = atDate.getMonth() - birthDate.getMonth();
672
+ if (m < 0 || (m === 0 && atDate.getDate() < birthDate.getDate())) {
673
+ age--;
674
+ }
675
+ return age;
1083
676
  }
677
+
1084
678
  /**
1085
- * Enables or disables the tab navigation (Tab key) on the page.
1086
- * @param {boolean} enable Whether to enable or disable tab navigation.
679
+ * Creates a debounced asynchronous validator function.
680
+ *
681
+ * @param validator - The async validator function to debounce.
682
+ * @param delay - The debounce delay in milliseconds.
683
+ * @returns A debounced version of the validator function.
1087
684
  */
1088
- function toggleTabNavigation(enable) {
1089
- if (enable) {
1090
- document.onkeydown = null; // Removes any previously set handler
1091
- }
1092
- else {
1093
- document.onkeydown = (event) => {
1094
- if (event.key === 'Tab') {
1095
- event.preventDefault();
1096
- return false;
685
+ function debounceAsyncValidator(validator, delay) {
686
+ let currentPromiseReject = null;
687
+ /**
688
+ * Creates a debounce delay.
689
+ *
690
+ * @returns A promise that resolves after the debounce delay.
691
+ */
692
+ function debounce() {
693
+ return new Promise((resolve, reject) => {
694
+ const { start, stop } = useTimeoutFn(() => {
695
+ currentPromiseReject = null;
696
+ resolve();
697
+ }, delay);
698
+ if (currentPromiseReject) {
699
+ currentPromiseReject(new Error('replaced'));
1097
700
  }
1098
- return true;
1099
- };
701
+ currentPromiseReject = reject;
702
+ start();
703
+ });
1100
704
  }
705
+ return function (value) {
706
+ if (currentPromiseReject) {
707
+ currentPromiseReject(new Error('replaced'));
708
+ currentPromiseReject = null;
709
+ }
710
+ return validator.call(this, value, debounce);
711
+ };
1101
712
  }
1102
713
  /**
1103
- * Disables the copy (Ctrl + C) functionality on the page.
714
+ * Creates a debounced version of an asynchronous function.
715
+ * @param {Function} func The asynchronous function to debounce.
716
+ * @param {number} wait The number of milliseconds to wait before invoking the function.
717
+ * @returns {Function} The debounced function.
1104
718
  */
1105
- function disableCopy() {
1106
- document.addEventListener('copy', (event) => {
1107
- event.preventDefault();
1108
- alert('Copying text is disabled on this page.');
1109
- });
719
+ function debounceAsync(func, wait) {
720
+ let timeoutReject = null;
721
+ return function (...args) {
722
+ if (timeoutReject) {
723
+ timeoutReject(new Error('replaced'));
724
+ timeoutReject = null;
725
+ }
726
+ return new Promise((resolve, reject) => {
727
+ const { start } = useTimeoutFn(() => {
728
+ func.apply(this, args).then(resolve).catch(reject);
729
+ }, wait);
730
+ timeoutReject = reject;
731
+ start();
732
+ });
733
+ };
1110
734
  }
1111
735
  /**
1112
- * Adds a custom keyboard shortcut to execute a given callback function.
1113
- * @param {string} key The key to trigger the callback.
1114
- * @param {Function} callback The function to execute on the key press.
1115
- * @param {boolean} [ctrlKey=false] Whether Ctrl key should be pressed.
1116
- * @param {boolean} [shiftKey=false] Whether Shift key should be pressed.
736
+ * Creates a debounced asynchronous function that executes immediately on the first call.
737
+ * @param {Function} func The asynchronous function to debounce.
738
+ * @param {number} wait The number of milliseconds to wait before invoking the function.
739
+ * @returns {Function} The debounced function with immediate execution on the first call.
1117
740
  */
1118
- function addCustomKeyboardShortcut(key, callback, ctrlKey = false, shiftKey = false) {
1119
- document.addEventListener('keydown', (event) => {
1120
- if (event.key === key &&
1121
- event.ctrlKey === ctrlKey &&
1122
- event.shiftKey === shiftKey) {
1123
- event.preventDefault();
1124
- callback();
741
+ function debounceAsyncWithImmediate(func, wait) {
742
+ let timeoutReject = null;
743
+ let callNow = true;
744
+ return function (...args) {
745
+ const context = this;
746
+ if (callNow) {
747
+ callNow = false;
748
+ return func.apply(context, args).finally(() => {
749
+ const { start } = useTimeoutFn(() => {
750
+ callNow = true;
751
+ }, wait);
752
+ start();
753
+ });
1125
754
  }
1126
- });
755
+ else {
756
+ if (timeoutReject) {
757
+ timeoutReject(new Error('replaced'));
758
+ timeoutReject = null;
759
+ }
760
+ return new Promise((resolve, reject) => {
761
+ const { start } = useTimeoutFn(() => {
762
+ func.apply(context, args).then(resolve).catch(reject);
763
+ }, wait);
764
+ timeoutReject = reject;
765
+ start();
766
+ });
767
+ }
768
+ };
1127
769
  }
1128
770
  /**
1129
- * Removes a custom keyboard shortcut by key and modifiers.
1130
- * @param {string} key The key to trigger the callback.
1131
- * @param {boolean} [ctrlKey=false] Whether Ctrl key should be pressed.
1132
- * @param {boolean} [shiftKey=false] Whether Shift key should be pressed.
771
+ * Creates a debounced version of a function that executes on the leading edge.
772
+ * @param {Function} func The function to debounce.
773
+ * @param {number} wait The number of milliseconds to wait before invoking the function.
774
+ * @returns {Function} The debounced function.
1133
775
  */
1134
- function removeCustomKeyboardShortcut(key, ctrlKey = false, shiftKey = false) {
1135
- const handler = (event) => {
1136
- if (event.key === key &&
1137
- event.ctrlKey === ctrlKey &&
1138
- event.shiftKey === shiftKey) {
1139
- event.preventDefault();
776
+ function debounceLeading(func, wait) {
777
+ let timeoutReject = null;
778
+ return function (...args) {
779
+ const context = this;
780
+ if (!timeoutReject) {
781
+ func.apply(context, args);
1140
782
  }
783
+ if (timeoutReject) {
784
+ timeoutReject(new Error('replaced'));
785
+ timeoutReject = null;
786
+ }
787
+ const { start } = useTimeoutFn(() => {
788
+ timeoutReject = null;
789
+ }, wait);
790
+ timeoutReject = () => { };
791
+ start();
1141
792
  };
1142
- document.removeEventListener('keydown', handler);
1143
793
  }
1144
794
  /**
1145
- * Disables specific keys or key combinations.
1146
- * @param {Array<string>} keys Array of key names to disable (e.g., ['F1', 'F5', 'Control+S']).
795
+ * Creates a debounced version of a function that executes on the trailing edge.
796
+ * @param {Function} func The function to debounce.
797
+ * @param {number} wait The number of milliseconds to wait before invoking the function.
798
+ * @returns {Function} The debounced function.
1147
799
  */
1148
- function disableSpecificKeys(keys) {
1149
- const handler = (event) => {
1150
- const keyCombination = `${event.ctrlKey ? 'Control+' : ''}${event.shiftKey ? 'Shift+' : ''}${event.altKey ? 'Alt+' : ''}${event.key}`;
1151
- if (keys.includes(event.key) || keys.includes(keyCombination)) {
1152
- event.preventDefault();
800
+ function debounceTrailing(func, wait) {
801
+ let timeoutReject = null;
802
+ return function (...args) {
803
+ const context = this;
804
+ if (timeoutReject) {
805
+ timeoutReject(new Error('replaced'));
806
+ timeoutReject = null;
1153
807
  }
808
+ const { start } = useTimeoutFn(() => {
809
+ func.apply(context, args);
810
+ }, wait);
811
+ timeoutReject = () => { };
812
+ start();
1154
813
  };
1155
- document.addEventListener('keydown', handler);
1156
- // Storing the handler to allow removal later
1157
- disableSpecificKeys.handlers.push(handler);
1158
814
  }
1159
- disableSpecificKeys.handlers = [];
1160
815
  /**
1161
- * Enables keys that were previously disabled using disableSpecificKeys.
816
+ * Creates a debounced version of a function that executes on both leading and trailing edges.
817
+ * @param {Function} func The function to debounce.
818
+ * @param {number} wait The number of milliseconds to wait before invoking the function.
819
+ * @returns {Function} The debounced function.
1162
820
  */
1163
- function enableSpecificKeys() {
1164
- for (const handler of disableSpecificKeys.handlers) {
1165
- document.removeEventListener('keydown', handler);
1166
- }
1167
- disableSpecificKeys.handlers = [];
821
+ function debounceLeadingTrailing(func, wait) {
822
+ let timeoutReject = null;
823
+ let lastCall = 0;
824
+ return function (...args) {
825
+ const context = this;
826
+ const now = Date.now();
827
+ if (timeoutReject) {
828
+ timeoutReject(new Error('replaced'));
829
+ timeoutReject = null;
830
+ }
831
+ if (now - lastCall >= wait) {
832
+ func.apply(context, args);
833
+ lastCall = now;
834
+ }
835
+ else {
836
+ const { start } = useTimeoutFn(() => {
837
+ func.apply(context, args);
838
+ }, wait - (now - lastCall));
839
+ timeoutReject = () => { };
840
+ start();
841
+ }
842
+ };
1168
843
  }
1169
844
  /**
1170
- * Registers multiple keyboard shortcuts with their respective callback functions.
1171
- * @param {Array<{ key: string, ctrlKey?: boolean, shiftKey?: boolean, altKey?: boolean, callback: Function }>} shortcuts Array of shortcut objects.
845
+ * Creates a debounced version of a function.
846
+ * @param {Function} func The function to debounce.
847
+ * @param {number} wait The number of milliseconds to wait before invoking the function.
848
+ * @returns {Function} The debounced function.
1172
849
  */
1173
- function registerKeyboardShortcuts(shortcuts) {
1174
- const handler = (event) => {
1175
- for (const shortcut of shortcuts) {
1176
- if (event.key === shortcut.key &&
1177
- (shortcut.ctrlKey ? event.ctrlKey : true) &&
1178
- (shortcut.shiftKey ? event.shiftKey : true) &&
1179
- (shortcut.altKey ? event.altKey : true)) {
1180
- event.preventDefault();
1181
- shortcut.callback();
1182
- }
850
+ function debounce(func, wait) {
851
+ let timeoutReject = null;
852
+ return function (...args) {
853
+ if (timeoutReject) {
854
+ timeoutReject(new Error('replaced'));
855
+ timeoutReject = null;
1183
856
  }
857
+ const { start } = useTimeoutFn(() => {
858
+ func.apply(this, args);
859
+ }, wait);
860
+ timeoutReject = () => { };
861
+ start();
1184
862
  };
1185
- document.addEventListener('keydown', handler);
1186
- // Storing the handler to allow removal later
1187
- registerKeyboardShortcuts.handlers.push(handler);
1188
863
  }
1189
- registerKeyboardShortcuts.handlers = [];
1190
864
  /**
1191
- * Unregisters all keyboard shortcuts that were registered with registerKeyboardShortcuts.
865
+ * Creates a throttled version of a function.
866
+ * @param {Function} func The function to throttle.
867
+ * @param {number} limit The number of milliseconds to wait between function calls.
868
+ * @returns {Function} The throttled function.
1192
869
  */
1193
- function unregisterKeyboardShortcuts() {
1194
- for (const handler of registerKeyboardShortcuts.handlers) {
1195
- document.removeEventListener('keydown', handler);
1196
- }
1197
- registerKeyboardShortcuts.handlers = [];
870
+ function throttle(func, limit) {
871
+ let lastCall = 0;
872
+ return function (...args) {
873
+ const now = Date.now();
874
+ if (now - lastCall >= limit) {
875
+ lastCall = now;
876
+ func.apply(this, args);
877
+ }
878
+ };
1198
879
  }
880
+
1199
881
  /**
1200
- * Adds a listener for a specific key to trigger a custom event.
1201
- * @param {string} key The key to listen for (e.g., 'Enter', 'Escape').
1202
- * @param {Function} callback The function to execute when the key is pressed.
882
+ * Converts a FormData object to a plain JavaScript object.
883
+ * @param {FormData} formData The FormData object to convert.
884
+ * @returns {Record<string, any>} The plain JavaScript object.
1203
885
  */
1204
- function addKeyListener(key, callback) {
1205
- const handler = (event) => {
1206
- if (event.key === key) {
1207
- event.preventDefault();
1208
- callback();
886
+ function formDataToObject(formData) {
887
+ const obj = {};
888
+ formData.forEach((value, key) => {
889
+ // Handle multiple values for the same key
890
+ if (obj[key]) {
891
+ if (!Array.isArray(obj[key])) {
892
+ obj[key] = [obj[key]];
893
+ }
894
+ obj[key].push(value);
1209
895
  }
1210
- };
1211
- document.addEventListener('keydown', handler);
1212
- // Storing the handler to allow removal later
1213
- addKeyListener.handlers.push(handler);
896
+ else {
897
+ obj[key] = value;
898
+ }
899
+ });
900
+ return obj;
1214
901
  }
1215
- addKeyListener.handlers = [];
1216
902
  /**
1217
- * Removes all custom key listeners added by addKeyListener.
903
+ * Reads a file as text.
904
+ * @param {File} file The file to read.
905
+ * @returns {Promise<string>} A promise that resolves with the file content.
1218
906
  */
1219
- function removeKeyListeners() {
1220
- for (const handler of addKeyListener.handlers) {
1221
- document.removeEventListener('keydown', handler);
1222
- }
1223
- addKeyListener.handlers = [];
907
+ function readFileAsText(file) {
908
+ return new Promise((resolve, reject) => {
909
+ const reader = new FileReader();
910
+ reader.onload = () => resolve(reader.result);
911
+ reader.onerror = reject;
912
+ reader.readAsText(file);
913
+ });
1224
914
  }
1225
915
  /**
1226
- * Detects if a specific key is held down.
1227
- * @param {string} key The key to detect (e.g., 'Shift', 'Control', 'Alt', 'a').
1228
- * @param {Function} onHold Callback function to execute while the key is held down.
916
+ * Reads a file as a Data URL.
917
+ * @param {File} file The file to read.
918
+ * @returns {Promise<string>} A promise that resolves with the Data URL.
1229
919
  */
1230
- function detectKeyHold(key, onHold) {
1231
- const keyDownHandler = (event) => {
1232
- if (event.key === key) {
1233
- onHold();
1234
- }
1235
- };
1236
- document.addEventListener('keydown', keyDownHandler);
1237
- // Storing the handler to allow removal later
1238
- detectKeyHold.handlers.push(keyDownHandler);
920
+ function readFileAsDataURL(file) {
921
+ return new Promise((resolve, reject) => {
922
+ const reader = new FileReader();
923
+ reader.onload = () => resolve(reader.result);
924
+ reader.onerror = reject;
925
+ reader.readAsDataURL(file);
926
+ });
1239
927
  }
1240
- detectKeyHold.handlers = [];
1241
928
  /**
1242
- * Stops detecting if a specific key is held down.
929
+ * Creates a Blob from a string.
930
+ * @param {string} content The string content for the Blob.
931
+ * @param {string} [type='text/plain'] The MIME type of the Blob.
932
+ * @returns {Blob} The Blob object.
1243
933
  */
1244
- function stopDetectingKeyHold() {
1245
- for (const handler of detectKeyHold.handlers) {
1246
- document.removeEventListener('keydown', handler);
1247
- }
1248
- detectKeyHold.handlers = [];
934
+ function stringToBlob(content, type = 'text/plain') {
935
+ return new Blob([content], { type });
1249
936
  }
1250
937
  /**
1251
- * Tracks currently pressed keys and provides a map of active keys.
1252
- * @returns {Set<string>} A set of currently pressed keys.
938
+ * Creates a Blob from an ArrayBuffer.
939
+ * @param {ArrayBuffer} buffer The ArrayBuffer to convert.
940
+ * @param {string} [type='application/octet-stream'] The MIME type of the Blob.
941
+ * @returns {Blob} The Blob object.
1253
942
  */
1254
- function createKeyMap() {
1255
- const pressedKeys = new Set();
1256
- const keyDownHandler = (event) => {
1257
- pressedKeys.add(event.key);
1258
- };
1259
- const keyUpHandler = (event) => {
1260
- pressedKeys.delete(event.key);
1261
- };
1262
- document.addEventListener('keydown', keyDownHandler);
1263
- document.addEventListener('keyup', keyUpHandler);
1264
- // Function to remove listeners
1265
- createKeyMap.clearListeners = () => {
1266
- document.removeEventListener('keydown', keyDownHandler);
1267
- document.removeEventListener('keyup', keyUpHandler);
1268
- };
1269
- return pressedKeys;
943
+ function bufferToBlob(buffer, type = 'application/octet-stream') {
944
+ return new Blob([buffer], { type });
1270
945
  }
1271
946
  /**
1272
- * Clears listeners for the key map tracking.
947
+ * Creates and downloads a file from Blob data.
948
+ * @param {Blob} blob The Blob containing the file data.
949
+ * @param {string} fileName The name of the file to create.
1273
950
  */
1274
- createKeyMap.clearListeners = () => { };
951
+ function downloadBlob(blob, fileName) {
952
+ const link = document.createElement('a');
953
+ const url = URL.createObjectURL(blob);
954
+ link.setAttribute('href', url);
955
+ link.setAttribute('download', fileName);
956
+ // Append link to the body and trigger a click
957
+ document.body.appendChild(link);
958
+ link.click();
959
+ // Clean up
960
+ document.body.removeChild(link);
961
+ URL.revokeObjectURL(url);
962
+ }
1275
963
  /**
1276
- * Sets up custom keyboard shortcuts with flexible order.
1277
- * @param {Array<string>} keys The combination of keys for the shortcut.
1278
- * @param {Function} callback The callback function to execute when the combination is detected.
964
+ * Creates a FormData object containing a Blob.
965
+ * @param {Blob} blob The Blob to include in the FormData.
966
+ * @param {string} name The name of the form field.
967
+ * @param {string} [fileName='file'] The file name for the Blob.
968
+ * @returns {FormData} The FormData object.
1279
969
  */
1280
- function customShortcut(keys, callback) {
1281
- const pressedKeys = new Set();
1282
- const keyDownHandler = (event) => {
1283
- pressedKeys.add(event.key);
1284
- if (keys.every(key => pressedKeys.has(key))) {
1285
- callback();
1286
- }
1287
- };
1288
- const keyUpHandler = (event) => {
1289
- pressedKeys.delete(event.key);
1290
- };
1291
- document.addEventListener('keydown', keyDownHandler);
1292
- document.addEventListener('keyup', keyUpHandler);
1293
- // Store the handlers for later removal
1294
- customShortcut.handlers.push({ keyDownHandler, keyUpHandler });
970
+ function blobToFormData(blob, name, fileName = 'file') {
971
+ const formData = new FormData();
972
+ formData.append(name, blob, fileName);
973
+ return formData;
1295
974
  }
1296
- customShortcut.handlers = [];
975
+
1297
976
  /**
1298
- * Removes all custom keyboard shortcuts added by customShortcut.
977
+ * Converts a Proxy object to a plain object.
978
+ * @param {ProxyConstructor} proxy The Proxy object to convert.
979
+ * @returns {Object} The plain object.
1299
980
  */
1300
- function removeCustomShortcuts() {
1301
- for (const { keyDownHandler, keyUpHandler } of customShortcut.handlers) {
1302
- document.removeEventListener('keydown', keyDownHandler);
1303
- document.removeEventListener('keyup', keyUpHandler);
981
+ function proxyToPlainObject(proxy) {
982
+ if (!proxy)
983
+ return {};
984
+ const plainObject = {};
985
+ for (const property of Object.keys(proxy)) {
986
+ plainObject[property] = proxy[property];
1304
987
  }
1305
- customShortcut.handlers = [];
988
+ return plainObject;
1306
989
  }
1307
990
  /**
1308
- * Simulates a key press event.
1309
- * @param {string} key The key to simulate (e.g., 'Enter', 'a').
1310
- * @param {boolean} ctrlKey If true, include Ctrl key in the event.
1311
- * @param {boolean} shiftKey If true, include Shift key in the event.
1312
- * @param {boolean} altKey If true, include Alt key in the event.
991
+ * Compares two objects to check if they have the same keys.
992
+ * @param {Object} object1 The first object to compare.
993
+ * @param {Object} object2 The second object to compare.
994
+ * @returns {boolean} True if the objects have the same keys, otherwise false.
1313
995
  */
1314
- function simulateKeyPress(key, ctrlKey = false, shiftKey = false, altKey = false) {
1315
- const event = new KeyboardEvent('keydown', {
1316
- key,
1317
- ctrlKey,
1318
- shiftKey,
1319
- altKey,
1320
- bubbles: true,
1321
- cancelable: true
996
+ function compareObject(object1, object2) {
997
+ return Object.keys(object1).every(function (element) {
998
+ return Object.keys(object2).includes(element);
1322
999
  });
1323
- document.dispatchEvent(event);
1324
1000
  }
1325
-
1326
1001
  /**
1327
- * Parses a date string into a Date object.
1328
- * @param {string} dateString The date string in 'YYYY-MM-DD' format.
1329
- * @returns {Date | null} The parsed Date object or null if the format is invalid.
1002
+ * Deeply compares two objects to check if they are equal.
1003
+ * @param {Object} object1 The first object to compare.
1004
+ * @param {Object} object2 The second object to compare.
1005
+ * @returns {boolean} True if the objects are deeply equal, otherwise false.
1330
1006
  */
1331
- function parseDate(dateString) {
1332
- const parts = dateString.split('-');
1333
- if (parts.length === 3) {
1334
- const year = parseInt(parts[0], 10);
1335
- const month = parseInt(parts[1], 10) - 1; // Months are zero-based
1336
- const day = parseInt(parts[2], 10);
1337
- const date = new Date(year, month, day);
1338
- if (date.getFullYear() === year && date.getMonth() === month && date.getDate() === day) {
1339
- return date;
1007
+ function deepEqual(object1, object2) {
1008
+ if (object1 === object2)
1009
+ return true;
1010
+ if (typeof object1 !== 'object' || typeof object2 !== 'object' || object1 === null || object2 === null) {
1011
+ return false;
1012
+ }
1013
+ const keys1 = Object.keys(object1);
1014
+ const keys2 = Object.keys(object2);
1015
+ if (keys1.length !== keys2.length) {
1016
+ return false;
1017
+ }
1018
+ for (const key of keys1) {
1019
+ if (!keys2.includes(key) || !deepEqual(object1[key], object2[key])) {
1020
+ return false;
1340
1021
  }
1341
1022
  }
1342
- return null;
1023
+ return true;
1343
1024
  }
1344
1025
  /**
1345
- * Formats a Date object into a string.
1346
- * @param {Date} date The date to format.
1347
- * @param {string} format The format string (e.g., 'YYYY-MM-DD').
1348
- * @returns {string} The formatted date string.
1026
+ * Deeply clones an object.
1027
+ * @param {Object} obj The object to clone.
1028
+ * @returns {Object} The cloned object.
1349
1029
  */
1350
- function formatDate(date, format) {
1351
- const map = {
1352
- 'YYYY': date.getFullYear(),
1353
- 'MM': String(date.getMonth() + 1).padStart(2, '0'),
1354
- 'DD': String(date.getDate()).padStart(2, '0'),
1355
- 'HH': String(date.getHours()).padStart(2, '0'),
1356
- 'mm': String(date.getMinutes()).padStart(2, '0'),
1357
- 'ss': String(date.getSeconds()).padStart(2, '0')
1358
- };
1359
- return format.replace(/YYYY|MM|DD|HH|mm|ss/g, (matched) => (map[matched] || matched).toString());
1030
+ function deepClone(obj) {
1031
+ if (obj === null || typeof obj !== 'object') {
1032
+ return obj;
1033
+ }
1034
+ if (obj instanceof Date) {
1035
+ return new Date(obj.getTime());
1036
+ }
1037
+ if (obj instanceof Array) {
1038
+ return obj.map(item => deepClone(item));
1039
+ }
1040
+ if (obj instanceof Object) {
1041
+ const copy = {};
1042
+ for (const key in obj) {
1043
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
1044
+ copy[key] = deepClone(obj[key]);
1045
+ }
1046
+ }
1047
+ return copy;
1048
+ }
1049
+ throw new Error('Unable to clone object! Its type is not supported.');
1360
1050
  }
1361
1051
  /**
1362
- * Calculates the number of days between two dates.
1363
- * @param {Date} startDate The start date.
1364
- * @param {Date} endDate The end date.
1365
- * @returns {number} The number of days between the two dates.
1052
+ * Converts an object to a query string.
1053
+ * @param {Object} obj The object to convert.
1054
+ * @returns {string} The query string.
1366
1055
  */
1367
- function daysBetween(startDate, endDate) {
1368
- const millisecondsPerDay = 86400000; // Number of milliseconds in one day
1369
- return Math.round((endDate.getTime() - startDate.getTime()) / millisecondsPerDay);
1056
+ function objectToQueryString(obj) {
1057
+ return Object.keys(obj)
1058
+ .map(key => encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]))
1059
+ .join('&');
1370
1060
  }
1061
+ // Example usage:
1062
+ // objectToQueryString({ name: 'John Doe', age: 30 }); // 'name=John%20Doe&age=30'
1371
1063
  /**
1372
- * Adds a specified number of days to a date.
1373
- * @param {Date} date The date to modify.
1374
- * @param {number} days The number of days to add.
1375
- * @returns {Date} The new date with days added.
1064
+ * Gets the differences between two objects.
1065
+ * @param {Object} object1 The first object.
1066
+ * @param {Object} object2 The second object.
1067
+ * @returns {Object} An object containing the differences.
1376
1068
  */
1377
- function addDays(date, days) {
1378
- const result = new Date(date);
1379
- result.setDate(result.getDate() + days);
1380
- return result;
1069
+ function getObjectDifferences(object1, object2) {
1070
+ const differences = {};
1071
+ const keys = new Set([...Object.keys(object1), ...Object.keys(object2)]);
1072
+ for (const key of keys) {
1073
+ if (object1[key] !== object2[key]) {
1074
+ differences[key] = { object1: object1[key], object2: object2[key] };
1075
+ }
1076
+ }
1077
+ return differences;
1381
1078
  }
1382
1079
  /**
1383
- * Subtracts a specified number of days from a date.
1384
- * @param {Date} date The date to modify.
1385
- * @param {number} days The number of days to subtract.
1386
- * @returns {Date} The new date with days subtracted.
1080
+ * Filters an object by a list of keys.
1081
+ * @param {Object} obj The object to filter.
1082
+ * @param {Array<string>} keys The keys to keep.
1083
+ * @returns {Object} The filtered object.
1387
1084
  */
1388
- function subtractDays(date, days) {
1389
- return addDays(date, -days);
1085
+ function filterObjectByKeys(obj, keys) {
1086
+ const filteredObject = {};
1087
+ for (const key of keys) {
1088
+ if (key in obj) {
1089
+ filteredObject[key] = obj[key];
1090
+ }
1091
+ }
1092
+ return filteredObject;
1390
1093
  }
1094
+ // Example usage:
1095
+ // filterObjectByKeys({ name: 'John', age: 30, job: 'Developer' }, ['name', 'job']); // { name: 'John', job: 'Developer' }
1391
1096
  /**
1392
- * Determines if a year is a leap year.
1393
- * @param {number} year The year to check.
1394
- * @returns {boolean} True if the year is a leap year, false otherwise.
1097
+ * Deeply merges two objects.
1098
+ * @param {Object} target The target object to merge into.
1099
+ * @param {Object} source The source object to merge from.
1100
+ * @returns {Object} The merged object.
1395
1101
  */
1396
- function isLeapYear(year) {
1397
- return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
1102
+ function deepMerge(target, source) {
1103
+ if (target === null || typeof target !== 'object' || typeof source !== 'object') {
1104
+ return target;
1105
+ }
1106
+ for (const key in source) {
1107
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
1108
+ if (source[key] && typeof source[key] === 'object') {
1109
+ if (target && !target[key]) {
1110
+ Object.assign(target, { [key]: {} });
1111
+ }
1112
+ deepMerge(target[key], source[key]);
1113
+ }
1114
+ else {
1115
+ Object.assign(target, { [key]: source[key] });
1116
+ }
1117
+ }
1118
+ }
1119
+ return target;
1398
1120
  }
1399
1121
  /**
1400
- * Gets the first day of the month for a given date.
1401
- * @param {Date} date The date to use.
1402
- * @returns {Date} The first day of the month.
1122
+ * Checks if an object is empty.
1123
+ * @param {Object} obj The object to check.
1124
+ * @returns {boolean} True if the object is empty, otherwise false.
1403
1125
  */
1404
- function getStartOfMonth(date) {
1405
- return new Date(date.getFullYear(), date.getMonth(), 1);
1126
+ function isEmptyObject(obj) {
1127
+ return Object.keys(obj).length === 0;
1406
1128
  }
1407
1129
  /**
1408
- * Gets the last day of the month for a given date.
1409
- * @param {Date} date The date to use.
1410
- * @returns {Date} The last day of the month.
1130
+ * Safely accesses nested properties in an object.
1131
+ * @param {Object} obj The object to access.
1132
+ * @param {Array<string>} keys The array of keys representing the path.
1133
+ * @returns {any} The value at the nested path, or undefined if not found.
1411
1134
  */
1412
- function getEndOfMonth(date) {
1413
- return new Date(date.getFullYear(), date.getMonth() + 1, 0);
1135
+ function safeGet(obj, keys) {
1136
+ return keys.reduce((acc, key) => (acc && acc[key] !== undefined) ? acc[key] : undefined, obj);
1414
1137
  }
1138
+ // Example usage:
1139
+ // safeGet({ a: { b: { c: 10 } } }, ['a', 'b', 'c']); // 10
1140
+ // safeGet({ a: { b: { c: 10 } } }, ['a', 'x', 'c']); // undefined
1415
1141
  /**
1416
- * Calculates age from a given birth date.
1417
- * @param {Date} birthDate The birth date.
1418
- * @returns {number} The calculated age.
1142
+ * Removes empty properties (null, undefined, or empty string) from an object.
1143
+ * @param {Object} obj The object to clean.
1144
+ * @returns {Object} A new object without empty properties.
1419
1145
  */
1420
- function calculateAge(birthDate) {
1421
- const today = new Date();
1422
- let age = today.getFullYear() - birthDate.getFullYear();
1423
- const m = today.getMonth() - birthDate.getMonth();
1424
- if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) {
1425
- age--;
1426
- }
1427
- return age;
1146
+ function removeEmptyProperties(obj) {
1147
+ return Object.keys(obj)
1148
+ .filter(key => obj[key] !== null && obj[key] !== undefined && obj[key] !== '')
1149
+ .reduce((acc, key) => {
1150
+ acc[key] = obj[key];
1151
+ return acc;
1152
+ }, {});
1428
1153
  }
1154
+ // Example usage:
1155
+ // removeEmptyProperties({ a: null, b: 2, c: undefined, d: '', e: 'hello' }); // { b: 2, e: 'hello' }
1429
1156
  /**
1430
- * Calculates the number of days until the next birthday.
1431
- * @param {Date} birthDate The birth date.
1432
- * @returns {number} The number of days until the next birthday.
1157
+ * Retrieves all keys of an object as an array.
1158
+ * @param {Object} obj The object to retrieve keys from.
1159
+ * @returns {Array<string>} The array of keys.
1433
1160
  */
1434
- function daysToNextBirthday(birthDate) {
1435
- const today = new Date();
1436
- const currentYear = today.getFullYear();
1437
- const nextBirthday = new Date(birthDate);
1438
- nextBirthday.setFullYear(currentYear);
1439
- if (nextBirthday < today) {
1440
- nextBirthday.setFullYear(currentYear + 1);
1441
- }
1442
- return daysBetween(today, nextBirthday);
1161
+ function getObjectKeys(obj) {
1162
+ return Object.keys(obj);
1443
1163
  }
1164
+ // Example usage:
1165
+ // getObjectKeys({ name: 'John', age: 30 }); // ['name', 'age']
1444
1166
  /**
1445
- * Calculates the age at a specific date.
1446
- * @param {Date} birthDate The birth date.
1447
- * @param {Date} atDate The date to calculate the age at.
1448
- * @returns {number} The calculated age.
1167
+ * Checks if an object has nested properties.
1168
+ * @param {Object} obj The object to check.
1169
+ * @returns {boolean} True if there are nested properties, false otherwise.
1449
1170
  */
1450
- function ageAtDate(birthDate, atDate) {
1451
- let age = atDate.getFullYear() - birthDate.getFullYear();
1452
- const m = atDate.getMonth() - birthDate.getMonth();
1453
- if (m < 0 || (m === 0 && atDate.getDate() < birthDate.getDate())) {
1454
- age--;
1455
- }
1456
- return age;
1171
+ function hasNestedProperties(obj) {
1172
+ return Object.values(obj).some(value => typeof value === 'object' && value !== null);
1457
1173
  }
1458
-
1174
+ // Example usage:
1175
+ // hasNestedProperties({ a: 1, b: { c: 2 } }); // true
1176
+ // hasNestedProperties({ a: 1, b: 2 }); // false
1459
1177
  /**
1460
- * Creates a debounced asynchronous validator function.
1461
- *
1462
- * @param validator - The async validator function to debounce.
1463
- * @param delay - The debounce delay in milliseconds.
1464
- * @returns A debounced version of the validator function.
1178
+ * Converts an object to FormData, handling nested objects.
1179
+ * @param {Object} obj The object to convert.
1180
+ * @param {FormData} [formData] The FormData object to append to.
1181
+ * @param {string} [parentKey] The parent key for nested objects.
1182
+ * @returns {FormData} The FormData object.
1465
1183
  */
1466
- function debounceAsyncValidator(validator, delay) {
1467
- let currentPromiseReject = null;
1468
- /**
1469
- * Creates a debounce delay.
1470
- *
1471
- * @returns A promise that resolves after the debounce delay.
1472
- */
1473
- function debounce() {
1474
- return new Promise((resolve, reject) => {
1475
- const { start, stop } = useTimeoutFn(() => {
1476
- currentPromiseReject = null;
1477
- resolve();
1478
- }, delay);
1479
- if (currentPromiseReject) {
1480
- currentPromiseReject(new Error('replaced'));
1481
- }
1482
- currentPromiseReject = reject;
1483
- start();
1484
- });
1485
- }
1486
- return function (value) {
1487
- if (currentPromiseReject) {
1488
- currentPromiseReject(new Error('replaced'));
1489
- currentPromiseReject = null;
1184
+ function objectToFormDataEnhanced(obj, formData = new FormData(), parentKey = '') {
1185
+ Object.entries(obj).forEach(([key, value]) => {
1186
+ const finalKey = parentKey ? `${parentKey}[${key}]` : key;
1187
+ if (value && typeof value === 'object' && !(value instanceof File)) {
1188
+ objectToFormDataEnhanced(value, formData, finalKey);
1490
1189
  }
1491
- return validator.call(this, value, debounce);
1492
- };
1493
- }
1494
- /**
1495
- * Creates a debounced version of an asynchronous function.
1496
- * @param {Function} func The asynchronous function to debounce.
1497
- * @param {number} wait The number of milliseconds to wait before invoking the function.
1498
- * @returns {Function} The debounced function.
1499
- */
1500
- function debounceAsync(func, wait) {
1501
- let timeoutReject = null;
1502
- return function (...args) {
1503
- if (timeoutReject) {
1504
- timeoutReject(new Error('replaced'));
1505
- timeoutReject = null;
1190
+ else {
1191
+ formData.append(finalKey, value);
1506
1192
  }
1507
- return new Promise((resolve, reject) => {
1508
- const { start } = useTimeoutFn(() => {
1509
- func.apply(this, args).then(resolve).catch(reject);
1510
- }, wait);
1511
- timeoutReject = reject;
1512
- start();
1513
- });
1514
- };
1193
+ });
1194
+ return formData;
1515
1195
  }
1196
+ // Example usage:
1197
+ // objectToFormDataEnhanced({ user: { name: 'John', age: 30 } });
1516
1198
  /**
1517
- * Creates a debounced asynchronous function that executes immediately on the first call.
1518
- * @param {Function} func The asynchronous function to debounce.
1519
- * @param {number} wait The number of milliseconds to wait before invoking the function.
1520
- * @returns {Function} The debounced function with immediate execution on the first call.
1199
+ * Converts a JavaScript object into FormData.
1200
+ *
1201
+ * @param obj - The object to be converted.
1202
+ * @param form - An optional FormData instance to use.
1203
+ * @param namespace - An optional namespace to use for nested objects.
1204
+ * @returns The FormData instance with the object's key-value pairs.
1521
1205
  */
1522
- function debounceAsyncWithImmediate(func, wait) {
1523
- let timeoutReject = null;
1524
- let callNow = true;
1525
- return function (...args) {
1526
- const context = this;
1527
- if (callNow) {
1528
- callNow = false;
1529
- return func.apply(context, args).finally(() => {
1530
- const { start } = useTimeoutFn(() => {
1531
- callNow = true;
1532
- }, wait);
1533
- start();
1534
- });
1206
+ const objectToFormData = function (obj, form, namespace) {
1207
+ const fd = form || new FormData();
1208
+ let formKey;
1209
+ for (const property in obj) {
1210
+ if (obj[property] === undefined) {
1211
+ continue;
1535
1212
  }
1536
- else {
1537
- if (timeoutReject) {
1538
- timeoutReject(new Error('replaced'));
1539
- timeoutReject = null;
1213
+ if (Object.prototype.hasOwnProperty.call(obj, property)) {
1214
+ if (namespace) {
1215
+ formKey = `${namespace}[${property}]`;
1216
+ }
1217
+ else {
1218
+ formKey = property;
1219
+ }
1220
+ if (typeof obj[property] === 'object' && !(obj[property] instanceof File)) {
1221
+ // Recursively handle nested objects
1222
+ objectToFormData(obj[property], fd, formKey);
1223
+ }
1224
+ else {
1225
+ // Convert boolean values to 1/0
1226
+ const value = obj[property] === true || obj[property] === false ? Number(obj[property]) : obj[property];
1227
+ fd.append(formKey, value);
1540
1228
  }
1541
- return new Promise((resolve, reject) => {
1542
- const { start } = useTimeoutFn(() => {
1543
- func.apply(context, args).then(resolve).catch(reject);
1544
- }, wait);
1545
- timeoutReject = reject;
1546
- start();
1547
- });
1548
1229
  }
1549
- };
1550
- }
1230
+ }
1231
+ return fd;
1232
+ };
1551
1233
  /**
1552
- * Creates a debounced version of a function that executes on the leading edge.
1553
- * @param {Function} func The function to debounce.
1554
- * @param {number} wait The number of milliseconds to wait before invoking the function.
1555
- * @returns {Function} The debounced function.
1234
+ * Flattens a nested object, bringing all properties to the top level.
1235
+ * @param {Object} obj The object to flatten.
1236
+ * @param {string} [parentKey] The parent key for nested properties.
1237
+ * @param {Object} [result] The resulting flattened object.
1238
+ * @returns {Object} The flattened object.
1556
1239
  */
1557
- function debounceLeading(func, wait) {
1558
- let timeoutReject = null;
1559
- return function (...args) {
1560
- const context = this;
1561
- if (!timeoutReject) {
1562
- func.apply(context, args);
1563
- }
1564
- if (timeoutReject) {
1565
- timeoutReject(new Error('replaced'));
1566
- timeoutReject = null;
1567
- }
1568
- const { start } = useTimeoutFn(() => {
1569
- timeoutReject = null;
1570
- }, wait);
1571
- timeoutReject = () => { };
1572
- start();
1573
- };
1240
+ function flattenObject(obj, parentKey = '', result = {}) {
1241
+ for (const key in obj) {
1242
+ if (obj.hasOwnProperty(key)) {
1243
+ const propName = parentKey ? `${parentKey}.${key}` : key;
1244
+ if (typeof obj[key] === 'object' && obj[key] !== null) {
1245
+ flattenObject(obj[key], propName, result);
1246
+ }
1247
+ else {
1248
+ result[propName] = obj[key];
1249
+ }
1250
+ }
1251
+ }
1252
+ return result;
1574
1253
  }
1254
+ // Example usage:
1255
+ // flattenObject({ a: 1, b: { c: 2, d: { e: 3 } } }); // { 'a': 1, 'b.c': 2, 'b.d.e': 3 }
1256
+
1575
1257
  /**
1576
- * Creates a debounced version of a function that executes on the trailing edge.
1577
- * @param {Function} func The function to debounce.
1578
- * @param {number} wait The number of milliseconds to wait before invoking the function.
1579
- * @returns {Function} The debounced function.
1258
+ * Capitalizes the first character of a string.
1259
+ * @param {string} s The string to capitalize.
1260
+ * @returns {string} The capitalized string.
1580
1261
  */
1581
- function debounceTrailing(func, wait) {
1582
- let timeoutReject = null;
1583
- return function (...args) {
1584
- const context = this;
1585
- if (timeoutReject) {
1586
- timeoutReject(new Error('replaced'));
1587
- timeoutReject = null;
1588
- }
1589
- const { start } = useTimeoutFn(() => {
1590
- func.apply(context, args);
1591
- }, wait);
1592
- timeoutReject = () => { };
1593
- start();
1594
- };
1262
+ function upperFirst(s) {
1263
+ return s.charAt(0).toUpperCase() + s.slice(1);
1595
1264
  }
1265
+ // Example usage:
1266
+ // console.log(upperFirst('hello')); // 'Hello'
1596
1267
  /**
1597
- * Creates a debounced version of a function that executes on both leading and trailing edges.
1598
- * @param {Function} func The function to debounce.
1599
- * @param {number} wait The number of milliseconds to wait before invoking the function.
1600
- * @returns {Function} The debounced function.
1268
+ * Lowercases the first character of a string.
1269
+ * @param {string} s The string to lowercase.
1270
+ * @returns {string} The lowercased string.
1601
1271
  */
1602
- function debounceLeadingTrailing(func, wait) {
1603
- let timeoutReject = null;
1604
- let lastCall = 0;
1605
- return function (...args) {
1606
- const context = this;
1607
- const now = Date.now();
1608
- if (timeoutReject) {
1609
- timeoutReject(new Error('replaced'));
1610
- timeoutReject = null;
1611
- }
1612
- if (now - lastCall >= wait) {
1613
- func.apply(context, args);
1614
- lastCall = now;
1615
- }
1616
- else {
1617
- const { start } = useTimeoutFn(() => {
1618
- func.apply(context, args);
1619
- }, wait - (now - lastCall));
1620
- timeoutReject = () => { };
1621
- start();
1622
- }
1623
- };
1272
+ function lowerFirst(s) {
1273
+ return s.charAt(0).toLowerCase() + s.slice(1);
1624
1274
  }
1275
+ // Example usage:
1276
+ // console.log(lowerFirst('Hello')); // 'hello'
1625
1277
  /**
1626
- * Creates a debounced version of a function.
1627
- * @param {Function} func The function to debounce.
1628
- * @param {number} wait The number of milliseconds to wait before invoking the function.
1629
- * @returns {Function} The debounced function.
1278
+ * Removes accents and special characters from a string and converts it to a URL-friendly format.
1279
+ * @param {string} input The string to process.
1280
+ * @returns {string} The processed string.
1630
1281
  */
1631
- function debounce(func, wait) {
1632
- let timeoutReject = null;
1633
- return function (...args) {
1634
- if (timeoutReject) {
1635
- timeoutReject(new Error('replaced'));
1636
- timeoutReject = null;
1637
- }
1638
- const { start } = useTimeoutFn(() => {
1639
- func.apply(this, args);
1640
- }, wait);
1641
- timeoutReject = () => { };
1642
- start();
1643
- };
1282
+ function removeAccent(input) {
1283
+ return input
1284
+ .toLowerCase()
1285
+ .trim()
1286
+ .replace(/[\s_-]+/g, "-")
1287
+ .replace(/^-+|-+$/g, "")
1288
+ .normalize("NFD")
1289
+ .replace(/[\u0300-\u036f]/g, "");
1644
1290
  }
1291
+ // Example usage:
1292
+ // console.log(removeAccent('Café du Nord')); // 'cafe-du-nord'
1645
1293
  /**
1646
- * Creates a throttled version of a function.
1647
- * @param {Function} func The function to throttle.
1648
- * @param {number} limit The number of milliseconds to wait between function calls.
1649
- * @returns {Function} The throttled function.
1294
+ * Reverses a string.
1295
+ * @param {string} str The string to reverse.
1296
+ * @returns {string} The reversed string.
1650
1297
  */
1651
- function throttle(func, limit) {
1652
- let lastCall = 0;
1653
- return function (...args) {
1654
- const now = Date.now();
1655
- if (now - lastCall >= limit) {
1656
- lastCall = now;
1657
- func.apply(this, args);
1658
- }
1659
- };
1298
+ function reverseString(str) {
1299
+ return str.split('').reverse().join('');
1660
1300
  }
1661
-
1301
+ // Example usage:
1302
+ // console.log(reverseString('hello')); // 'olleh'
1662
1303
  /**
1663
- * Converts a FormData object to a plain JavaScript object.
1664
- * @param {FormData} formData The FormData object to convert.
1665
- * @returns {Record<string, any>} The plain JavaScript object.
1304
+ * Counts the number of words in a string.
1305
+ * @param {string} str The string to analyze.
1306
+ * @returns {number} The word count.
1666
1307
  */
1667
- function formDataToObject(formData) {
1668
- const obj = {};
1669
- formData.forEach((value, key) => {
1670
- // Handle multiple values for the same key
1671
- if (obj[key]) {
1672
- if (!Array.isArray(obj[key])) {
1673
- obj[key] = [obj[key]];
1674
- }
1675
- obj[key].push(value);
1676
- }
1677
- else {
1678
- obj[key] = value;
1679
- }
1680
- });
1681
- return obj;
1308
+ function countWords(str) {
1309
+ return str.trim().split(/\s+/).length;
1682
1310
  }
1311
+ // Example usage:
1312
+ // console.log(countWords('Hello world!')); // 2
1683
1313
  /**
1684
- * Reads a file as text.
1685
- * @param {File} file The file to read.
1686
- * @returns {Promise<string>} A promise that resolves with the file content.
1314
+ * Truncates a string to the specified length and adds ellipsis if necessary.
1315
+ * @param {string} str The string to truncate.
1316
+ * @param {number} maxLength The maximum length of the string.
1317
+ * @returns {string} The truncated string.
1687
1318
  */
1688
- function readFileAsText(file) {
1689
- return new Promise((resolve, reject) => {
1690
- const reader = new FileReader();
1691
- reader.onload = () => resolve(reader.result);
1692
- reader.onerror = reject;
1693
- reader.readAsText(file);
1694
- });
1319
+ function truncateString(str, maxLength) {
1320
+ return str.length > maxLength ? str.slice(0, maxLength) + '...' : str;
1695
1321
  }
1322
+ // Example usage:
1323
+ // console.log(truncateString('This is a long string', 10)); // 'This is a...'
1696
1324
  /**
1697
- * Reads a file as a Data URL.
1698
- * @param {File} file The file to read.
1699
- * @returns {Promise<string>} A promise that resolves with the Data URL.
1325
+ * Converts a string to camel case.
1326
+ * @param {string} str The string to convert.
1327
+ * @returns {string} The camel cased string.
1700
1328
  */
1701
- function readFileAsDataURL(file) {
1702
- return new Promise((resolve, reject) => {
1703
- const reader = new FileReader();
1704
- reader.onload = () => resolve(reader.result);
1705
- reader.onerror = reject;
1706
- reader.readAsDataURL(file);
1707
- });
1329
+ function toCamelCase(str) {
1330
+ return str
1331
+ .toLowerCase()
1332
+ .replace(/[^a-zA-Z0-9]+(.)/g, (match, chr) => chr.toUpperCase());
1708
1333
  }
1334
+ // Example usage:
1335
+ // console.log(toCamelCase('hello world example')); // 'helloWorldExample'
1709
1336
  /**
1710
- * Creates a Blob from a string.
1711
- * @param {string} content The string content for the Blob.
1712
- * @param {string} [type='text/plain'] The MIME type of the Blob.
1713
- * @returns {Blob} The Blob object.
1337
+ * Converts a string to kebab case.
1338
+ * @param {string} str The string to convert.
1339
+ * @returns {string} The kebab cased string.
1714
1340
  */
1715
- function stringToBlob(content, type = 'text/plain') {
1716
- return new Blob([content], { type });
1341
+ function toKebabCase(str) {
1342
+ return str
1343
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
1344
+ .replace(/[\s_]+/g, '-')
1345
+ .toLowerCase();
1346
+ }
1347
+ // Example usage:
1348
+ // console.log(toKebabCase('Hello World Example')); // 'hello-world-example'
1349
+ /**
1350
+ * Replaces all instances of a substring within a string.
1351
+ * @param {string} str The original string.
1352
+ * @param {string} find The substring to find.
1353
+ * @param {string} replace The substring to replace with.
1354
+ * @returns {string} The modified string.
1355
+ */
1356
+ function replaceAll(str, find, replace) {
1357
+ return str.split(find).join(replace);
1717
1358
  }
1359
+ // Example usage:
1360
+ // console.log(replaceAll('hello world', 'o', 'a')); // 'hella warld'
1718
1361
  /**
1719
- * Creates a Blob from an ArrayBuffer.
1720
- * @param {ArrayBuffer} buffer The ArrayBuffer to convert.
1721
- * @param {string} [type='application/octet-stream'] The MIME type of the Blob.
1722
- * @returns {Blob} The Blob object.
1362
+ * Generates a random string of a specific length.
1363
+ * @param {number} length The length of the string to generate.
1364
+ * @returns {string} The random string.
1723
1365
  */
1724
- function bufferToBlob(buffer, type = 'application/octet-stream') {
1725
- return new Blob([buffer], { type });
1366
+ function generateRandomString(length) {
1367
+ const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
1368
+ return Array.from({ length }, () => characters.charAt(Math.floor(Math.random() * characters.length))).join('');
1726
1369
  }
1370
+ // Example usage:
1371
+ // console.log(generateRandomString(10)); // 'A1b2C3d4E5'
1372
+
1727
1373
  /**
1728
- * Creates and downloads a file from Blob data.
1729
- * @param {Blob} blob The Blob containing the file data.
1730
- * @param {string} fileName The name of the file to create.
1374
+ * Validates if the key pressed is a valid letter or special character.
1375
+ * @param {KeyboardEvent} e The keyboard event.
1376
+ * @returns {boolean} True if the key is valid, otherwise false.
1731
1377
  */
1732
- function downloadBlob(blob, fileName) {
1733
- const link = document.createElement('a');
1734
- const url = URL.createObjectURL(blob);
1735
- link.setAttribute('href', url);
1736
- link.setAttribute('download', fileName);
1737
- // Append link to the body and trigger a click
1738
- document.body.appendChild(link);
1739
- link.click();
1740
- // Clean up
1741
- document.body.removeChild(link);
1742
- URL.revokeObjectURL(url);
1378
+ function validateLetters(e) {
1379
+ const key = e.keyCode;
1380
+ const validKeys = [
1381
+ ...Array.from({ length: 26 }, (_, i) => i + 65), // Letters A-Z
1382
+ ...Array.from({ length: 26 }, (_, i) => i + 97), // Letters a-z
1383
+ 45, // Hyphen
1384
+ 32, // Space
1385
+ 241, // ñ
1386
+ 209, // Ñ
1387
+ 225, // á
1388
+ 233, // é
1389
+ 237, // í
1390
+ 243, // ó
1391
+ 250, // ú
1392
+ 193, // Á
1393
+ 201, // É
1394
+ 205, // Í
1395
+ 211, // Ó
1396
+ 218 // Ú
1397
+ ];
1398
+ if (validKeys.includes(key)) {
1399
+ return true;
1400
+ }
1401
+ e.preventDefault();
1402
+ return false;
1743
1403
  }
1404
+ // Example usage:
1405
+ // document.addEventListener('keydown', validateLetters);
1744
1406
  /**
1745
- * Creates a FormData object containing a Blob.
1746
- * @param {Blob} blob The Blob to include in the FormData.
1747
- * @param {string} name The name of the form field.
1748
- * @param {string} [fileName='file'] The file name for the Blob.
1749
- * @returns {FormData} The FormData object.
1407
+ * Validates if the key pressed is a valid alphanumeric character.
1408
+ * @param {KeyboardEvent} e The keyboard event.
1409
+ * @returns {boolean} True if the key is valid, otherwise false.
1750
1410
  */
1751
- function blobToFormData(blob, name, fileName = 'file') {
1752
- const formData = new FormData();
1753
- formData.append(name, blob, fileName);
1754
- return formData;
1411
+ function validateAlphanumeric(e) {
1412
+ const key = e.keyCode;
1413
+ const validKeys = [
1414
+ ...Array.from({ length: 10 }, (_, i) => i + 48), // Numbers 0-9
1415
+ ...Array.from({ length: 26 }, (_, i) => i + 65), // Letters A-Z
1416
+ ...Array.from({ length: 26 }, (_, i) => i + 97), // Letters a-z
1417
+ 45, // Hyphen
1418
+ 95, // Underscore
1419
+ 32 // Space
1420
+ ];
1421
+ if (validKeys.includes(key)) {
1422
+ return true;
1423
+ }
1424
+ e.preventDefault();
1425
+ return false;
1755
1426
  }
1756
-
1427
+ // Example usage:
1428
+ // document.addEventListener('keydown', validateAlphanumeric);
1757
1429
  /**
1758
- * Converts a Proxy object to a plain object.
1759
- * @param {ProxyConstructor} proxy The Proxy object to convert.
1760
- * @returns {Object} The plain object.
1430
+ * Validates if the key pressed is a number.
1431
+ * @param {KeyboardEvent} e The keyboard event.
1432
+ * @returns {boolean} True if the key is a number, otherwise false.
1761
1433
  */
1762
- function proxyToPlainObject(proxy) {
1763
- if (!proxy)
1764
- return {};
1765
- const plainObject = {};
1766
- for (const property of Object.keys(proxy)) {
1767
- plainObject[property] = proxy[property];
1434
+ function validateNumbers(e) {
1435
+ const key = e.keyCode;
1436
+ if ((key >= 48 && key <= 57) || key === 46 || key === 8 || key === 37 || key === 39) {
1437
+ return true;
1768
1438
  }
1769
- return plainObject;
1439
+ e.preventDefault();
1440
+ return false;
1770
1441
  }
1442
+ // Example usage:
1443
+ // document.addEventListener('keydown', validateNumbers);
1771
1444
  /**
1772
- * Compares two objects to check if they have the same keys.
1773
- * @param {Object} object1 The first object to compare.
1774
- * @param {Object} object2 The second object to compare.
1775
- * @returns {boolean} True if the objects have the same keys, otherwise false.
1445
+ * Validates if a phone number is valid.
1446
+ * @param {string} phoneNumber The phone number to validate.
1447
+ * @returns {boolean} True if the phone number is valid, otherwise false.
1776
1448
  */
1777
- function compareObject(object1, object2) {
1778
- return Object.keys(object1).every(function (element) {
1779
- return Object.keys(object2).includes(element);
1780
- });
1449
+ function isValidPhoneNumber(phoneNumber) {
1450
+ const phonePattern = /^[0-9]{10}$/; // Example pattern for 10-digit phone numbers
1451
+ return phonePattern.test(phoneNumber);
1781
1452
  }
1453
+ // Example usage:
1454
+ // console.log(isValidPhoneNumber('1234567890')); // true
1455
+ // console.log(isValidPhoneNumber('123-456-7890')); // false
1782
1456
  /**
1783
- * Deeply compares two objects to check if they are equal.
1784
- * @param {Object} object1 The first object to compare.
1785
- * @param {Object} object2 The second object to compare.
1786
- * @returns {boolean} True if the objects are deeply equal, otherwise false.
1457
+ * Validates if a string is a valid email address.
1458
+ * @param {string} email The email address to validate.
1459
+ * @returns {boolean} True if the email address is valid, otherwise false.
1787
1460
  */
1788
- function deepEqual(object1, object2) {
1789
- if (object1 === object2)
1461
+ function isValidEmail(email) {
1462
+ const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1463
+ return emailPattern.test(email);
1464
+ }
1465
+ // Example usage:
1466
+ // console.log(isValidEmail('example@domain.com')); // true
1467
+ // console.log(isValidEmail('invalid-email')); // false
1468
+ /**
1469
+ * Validates if a string is a valid URL.
1470
+ * @param {string} url The URL to validate.
1471
+ * @returns {boolean} True if the URL is valid, otherwise false.
1472
+ */
1473
+ function isValidURL(url) {
1474
+ try {
1475
+ new URL(url);
1790
1476
  return true;
1791
- if (typeof object1 !== 'object' || typeof object2 !== 'object' || object1 === null || object2 === null) {
1792
- return false;
1793
1477
  }
1794
- const keys1 = Object.keys(object1);
1795
- const keys2 = Object.keys(object2);
1796
- if (keys1.length !== keys2.length) {
1478
+ catch {
1797
1479
  return false;
1798
1480
  }
1799
- for (const key of keys1) {
1800
- if (!keys2.includes(key) || !deepEqual(object1[key], object2[key])) {
1801
- return false;
1802
- }
1803
- }
1804
- return true;
1805
1481
  }
1482
+ // Example usage:
1483
+ // console.log(isValidURL('https://www.example.com')); // true
1484
+ // console.log(isValidURL('invalid-url')); // false
1806
1485
  /**
1807
- * Deeply clones an object.
1808
- * @param {Object} obj The object to clone.
1809
- * @returns {Object} The cloned object.
1486
+ * Validates if a string is a valid date in YYYY-MM-DD format.
1487
+ * @param {string} date The date string to validate.
1488
+ * @returns {boolean} True if the date is valid, otherwise false.
1810
1489
  */
1811
- function deepClone(obj) {
1812
- if (obj === null || typeof obj !== 'object') {
1813
- return obj;
1814
- }
1815
- if (obj instanceof Date) {
1816
- return new Date(obj.getTime());
1817
- }
1818
- if (obj instanceof Array) {
1819
- return obj.map(item => deepClone(item));
1820
- }
1821
- if (obj instanceof Object) {
1822
- const copy = {};
1823
- for (const key in obj) {
1824
- if (Object.prototype.hasOwnProperty.call(obj, key)) {
1825
- copy[key] = deepClone(obj[key]);
1826
- }
1827
- }
1828
- return copy;
1829
- }
1830
- throw new Error('Unable to clone object! Its type is not supported.');
1490
+ function isValidDate(date) {
1491
+ const datePattern = /^\d{4}-\d{2}-\d{2}$/;
1492
+ if (!datePattern.test(date))
1493
+ return false;
1494
+ const [year, month, day] = date.split('-').map(Number);
1495
+ const dateObj = new Date(year, month - 1, day);
1496
+ return dateObj.getFullYear() === year && dateObj.getMonth() === month - 1 && dateObj.getDate() === day;
1831
1497
  }
1498
+ // Example usage:
1499
+ // console.log(isValidDate('2024-08-31')); // true
1500
+ // console.log(isValidDate('2024-02-30')); // false
1832
1501
  /**
1833
- * Converts an object to a query string.
1834
- * @param {Object} obj The object to convert.
1835
- * @returns {string} The query string.
1502
+ * Validates if a password meets certain strength criteria.
1503
+ * @param {string} password The password to validate.
1504
+ * @returns {boolean} True if the password is strong, otherwise false.
1836
1505
  */
1837
- function objectToQueryString(obj) {
1838
- return Object.keys(obj)
1839
- .map(key => encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]))
1840
- .join('&');
1506
+ function isStrongPassword(password) {
1507
+ const minLength = 8;
1508
+ const hasUpperCase = /[A-Z]/.test(password);
1509
+ const hasLowerCase = /[a-z]/.test(password);
1510
+ const hasNumbers = /[0-9]/.test(password);
1511
+ const hasSpecialChars = /[!@#$%^&*(),.?":{}|<>]/.test(password);
1512
+ return password.length >= minLength && hasUpperCase && hasLowerCase && hasNumbers && hasSpecialChars;
1841
1513
  }
1842
1514
  // Example usage:
1843
- // objectToQueryString({ name: 'John Doe', age: 30 }); // 'name=John%20Doe&age=30'
1515
+ // console.log(isStrongPassword('Strong1@password')); // true
1516
+ // console.log(isStrongPassword('weakpass')); // false
1844
1517
  /**
1845
- * Gets the differences between two objects.
1846
- * @param {Object} object1 The first object.
1847
- * @param {Object} object2 The second object.
1848
- * @returns {Object} An object containing the differences.
1518
+ * Validates a credit card number using the Luhn algorithm.
1519
+ * @param {string} cardNumber The credit card number to validate.
1520
+ * @returns {boolean} True if the credit card number is valid, otherwise false.
1849
1521
  */
1850
- function getObjectDifferences(object1, object2) {
1851
- const differences = {};
1852
- const keys = new Set([...Object.keys(object1), ...Object.keys(object2)]);
1853
- for (const key of keys) {
1854
- if (object1[key] !== object2[key]) {
1855
- differences[key] = { object1: object1[key], object2: object2[key] };
1522
+ function isValidCreditCard(cardNumber) {
1523
+ const sanitized = cardNumber.replace(/\D/g, '');
1524
+ let sum = 0;
1525
+ let shouldDouble = false;
1526
+ for (let i = sanitized.length - 1; i >= 0; i--) {
1527
+ let digit = parseInt(sanitized.charAt(i), 10);
1528
+ if (shouldDouble) {
1529
+ digit *= 2;
1530
+ if (digit > 9)
1531
+ digit -= 9;
1856
1532
  }
1533
+ sum += digit;
1534
+ shouldDouble = !shouldDouble;
1857
1535
  }
1858
- return differences;
1536
+ return sum % 10 === 0;
1859
1537
  }
1538
+ // Example usage:
1539
+ // console.log(isValidCreditCard('4111111111111111')); // true
1540
+ // console.log(isValidCreditCard('1234567812345670')); // false
1860
1541
  /**
1861
- * Filters an object by a list of keys.
1862
- * @param {Object} obj The object to filter.
1863
- * @param {Array<string>} keys The keys to keep.
1864
- * @returns {Object} The filtered object.
1542
+ * Validates if a string is a valid hex color code.
1543
+ * @param {string} color The color code to validate.
1544
+ * @returns {boolean} True if the color code is valid, otherwise false.
1865
1545
  */
1866
- function filterObjectByKeys(obj, keys) {
1867
- const filteredObject = {};
1868
- for (const key of keys) {
1869
- if (key in obj) {
1870
- filteredObject[key] = obj[key];
1871
- }
1872
- }
1873
- return filteredObject;
1546
+ function isValidHexColor(color) {
1547
+ const hexPattern = /^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})$/;
1548
+ return hexPattern.test(color);
1874
1549
  }
1875
1550
  // Example usage:
1876
- // filterObjectByKeys({ name: 'John', age: 30, job: 'Developer' }, ['name', 'job']); // { name: 'John', job: 'Developer' }
1551
+ // console.log(isValidHexColor('#FFFFFF')); // true
1552
+ // console.log(isValidHexColor('#FFF')); // true
1553
+ // console.log(isValidHexColor('#12345G')); // false
1877
1554
  /**
1878
- * Deeply merges two objects.
1879
- * @param {Object} target The target object to merge into.
1880
- * @param {Object} source The source object to merge from.
1881
- * @returns {Object} The merged object.
1555
+ * Validates if a string is a valid time in HH:MM format.
1556
+ * @param {string} time The time string to validate.
1557
+ * @returns {boolean} True if the time is valid, otherwise false.
1882
1558
  */
1883
- function deepMerge(target, source) {
1884
- if (target === null || typeof target !== 'object' || typeof source !== 'object') {
1885
- return target;
1886
- }
1887
- for (const key in source) {
1888
- if (Object.prototype.hasOwnProperty.call(source, key)) {
1889
- if (source[key] && typeof source[key] === 'object') {
1890
- if (target && !target[key]) {
1891
- Object.assign(target, { [key]: {} });
1892
- }
1893
- deepMerge(target[key], source[key]);
1894
- }
1895
- else {
1896
- Object.assign(target, { [key]: source[key] });
1897
- }
1898
- }
1899
- }
1900
- return target;
1559
+ function isValidTime(time) {
1560
+ const timePattern = /^([01]\d|2[0-3]):([0-5]\d)$/;
1561
+ return timePattern.test(time);
1562
+ }
1563
+ // Example usage:
1564
+ // console.log(isValidTime('14:30')); // true
1565
+ // console.log(isValidTime('25:00')); // false
1566
+ /**
1567
+ * Validates if a string is a valid IPv4 address.
1568
+ * @param {string} ip The IP address to validate.
1569
+ * @returns {boolean} True if the IP address is valid, otherwise false.
1570
+ */
1571
+ function isValidIP(ip) {
1572
+ const ipPattern = /^(25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)$/;
1573
+ return ipPattern.test(ip);
1901
1574
  }
1575
+ // Example usage:
1576
+ // console.log(isValidIP('192.168.1.1')); // true
1577
+ // console.log(isValidIP('999.999.999.999')); // false
1902
1578
  /**
1903
- * Checks if an object is empty.
1904
- * @param {Object} obj The object to check.
1905
- * @returns {boolean} True if the object is empty, otherwise false.
1579
+ * Validates if a string is a valid U.S. Social Security Number (SSN).
1580
+ * @param {string} ssn The SSN to validate.
1581
+ * @returns {boolean} True if the SSN is valid, otherwise false.
1906
1582
  */
1907
- function isEmptyObject(obj) {
1908
- return Object.keys(obj).length === 0;
1583
+ function isValidSSN(ssn) {
1584
+ const ssnPattern = /^\d{3}-\d{2}-\d{4}$/;
1585
+ return ssnPattern.test(ssn);
1909
1586
  }
1587
+ // Example usage:
1588
+ // console.log(isValidSSN('123-45-6789')); // true
1589
+ // console.log(isValidSSN('123-45-678')); // false
1910
1590
  /**
1911
- * Safely accesses nested properties in an object.
1912
- * @param {Object} obj The object to access.
1913
- * @param {Array<string>} keys The array of keys representing the path.
1914
- * @returns {any} The value at the nested path, or undefined if not found.
1591
+ * Validates if a string is a valid U.S. ZIP code.
1592
+ * @param {string} zip The ZIP code to validate.
1593
+ * @returns {boolean} True if the ZIP code is valid, otherwise false.
1915
1594
  */
1916
- function safeGet(obj, keys) {
1917
- return keys.reduce((acc, key) => (acc && acc[key] !== undefined) ? acc[key] : undefined, obj);
1595
+ function isValidZIP(zip) {
1596
+ const zipPattern = /^\d{5}(-\d{4})?$/;
1597
+ return zipPattern.test(zip);
1918
1598
  }
1919
1599
  // Example usage:
1920
- // safeGet({ a: { b: { c: 10 } } }, ['a', 'b', 'c']); // 10
1921
- // safeGet({ a: { b: { c: 10 } } }, ['a', 'x', 'c']); // undefined
1600
+ // console.log(isValidZIP('12345')); // true
1601
+ // console.log(isValidZIP('12345-6789')); // true
1602
+ // console.log(isValidZIP('1234')); // false
1922
1603
  /**
1923
- * Removes empty properties (null, undefined, or empty string) from an object.
1924
- * @param {Object} obj The object to clean.
1925
- * @returns {Object} A new object without empty properties.
1604
+ * Validates if a string is a valid credit card expiry date in MM/YY format.
1605
+ * @param {string} expiryDate The expiry date to validate.
1606
+ * @returns {boolean} True if the expiry date is valid, otherwise false.
1926
1607
  */
1927
- function removeEmptyProperties(obj) {
1928
- return Object.keys(obj)
1929
- .filter(key => obj[key] !== null && obj[key] !== undefined && obj[key] !== '')
1930
- .reduce((acc, key) => {
1931
- acc[key] = obj[key];
1932
- return acc;
1933
- }, {});
1608
+ function isValidExpiryDate(expiryDate) {
1609
+ const expiryPattern = /^(0[1-9]|1[0-2])\/\d{2}$/;
1610
+ if (!expiryPattern.test(expiryDate))
1611
+ return false;
1612
+ const [month, year] = expiryDate.split('/').map(Number);
1613
+ const currentYear = new Date().getFullYear() % 100;
1614
+ const currentMonth = new Date().getMonth() + 1;
1615
+ return (year > currentYear) || (year === currentYear && month >= currentMonth);
1934
1616
  }
1935
1617
  // Example usage:
1936
- // removeEmptyProperties({ a: null, b: 2, c: undefined, d: '', e: 'hello' }); // { b: 2, e: 'hello' }
1618
+ // console.log(isValidExpiryDate('08/24')); // true
1619
+ // console.log(isValidExpiryDate('12/22')); // false
1937
1620
  /**
1938
- * Retrieves all keys of an object as an array.
1939
- * @param {Object} obj The object to retrieve keys from.
1940
- * @returns {Array<string>} The array of keys.
1621
+ * Validates if a string is a valid 8-character hexadecimal color code (including alpha).
1622
+ * @param {string} color The color code to validate.
1623
+ * @returns {boolean} True if the color code is valid, otherwise false.
1941
1624
  */
1942
- function getObjectKeys(obj) {
1943
- return Object.keys(obj);
1625
+ function isValidHexColorAlpha(color) {
1626
+ const hexPattern = /^#([0-9A-Fa-f]{8})$/;
1627
+ return hexPattern.test(color);
1944
1628
  }
1945
1629
  // Example usage:
1946
- // getObjectKeys({ name: 'John', age: 30 }); // ['name', 'age']
1630
+ // console.log(isValidHexColorAlpha('#RRGGBBAA')); // true
1631
+ // console.log(isValidHexColorAlpha('#FFFFFF')); // false
1947
1632
  /**
1948
- * Checks if an object has nested properties.
1949
- * @param {Object} obj The object to check.
1950
- * @returns {boolean} True if there are nested properties, false otherwise.
1633
+ * Validates if a username meets specific criteria.
1634
+ * @param {string} username The username to validate.
1635
+ * @returns {boolean} True if the username is valid, otherwise false.
1951
1636
  */
1952
- function hasNestedProperties(obj) {
1953
- return Object.values(obj).some(value => typeof value === 'object' && value !== null);
1637
+ function isValidUsername(username) {
1638
+ const usernamePattern = /^[a-zA-Z0-9_]{3,16}$/; // 3 to 16 characters, letters, numbers, and underscores only
1639
+ return usernamePattern.test(username);
1954
1640
  }
1955
1641
  // Example usage:
1956
- // hasNestedProperties({ a: 1, b: { c: 2 } }); // true
1957
- // hasNestedProperties({ a: 1, b: 2 }); // false
1642
+ // console.log(isValidUsername('user_name123')); // true
1643
+ // console.log(isValidUsername('us')); // false
1958
1644
  /**
1959
- * Converts an object to FormData, handling nested objects.
1960
- * @param {Object} obj The object to convert.
1961
- * @param {FormData} [formData] The FormData object to append to.
1962
- * @param {string} [parentKey] The parent key for nested objects.
1963
- * @returns {FormData} The FormData object.
1645
+ * Validates if a string represents a valid age between 0 and 120.
1646
+ * @param {string} age The age to validate.
1647
+ * @returns {boolean} True if the age is valid, otherwise false.
1964
1648
  */
1965
- function objectToFormDataEnhanced(obj, formData = new FormData(), parentKey = '') {
1966
- Object.entries(obj).forEach(([key, value]) => {
1967
- const finalKey = parentKey ? `${parentKey}[${key}]` : key;
1968
- if (value && typeof value === 'object' && !(value instanceof File)) {
1969
- objectToFormDataEnhanced(value, formData, finalKey);
1970
- }
1971
- else {
1972
- formData.append(finalKey, value);
1973
- }
1974
- });
1975
- return formData;
1649
+ function isValidAge(age) {
1650
+ const ageNumber = parseInt(age, 10);
1651
+ return !isNaN(ageNumber) && ageNumber >= 0 && ageNumber <= 120;
1976
1652
  }
1977
1653
  // Example usage:
1978
- // objectToFormDataEnhanced({ user: { name: 'John', age: 30 } });
1654
+ // console.log(isValidAge('25')); // true
1655
+ // console.log(isValidAge('121')); // false
1979
1656
  /**
1980
- * Converts a JavaScript object into FormData.
1981
- *
1982
- * @param obj - The object to be converted.
1983
- * @param form - An optional FormData instance to use.
1984
- * @param namespace - An optional namespace to use for nested objects.
1985
- * @returns The FormData instance with the object's key-value pairs.
1657
+ * Validates if a string is a valid hexadecimal number.
1658
+ * @param {string} hex The hexadecimal number to validate.
1659
+ * @returns {boolean} True if the number is valid, otherwise false.
1986
1660
  */
1987
- const objectToFormData = function (obj, form, namespace) {
1988
- const fd = form || new FormData();
1989
- let formKey;
1990
- for (const property in obj) {
1991
- if (obj[property] === undefined) {
1992
- continue;
1993
- }
1994
- if (Object.prototype.hasOwnProperty.call(obj, property)) {
1995
- if (namespace) {
1996
- formKey = `${namespace}[${property}]`;
1997
- }
1998
- else {
1999
- formKey = property;
2000
- }
2001
- if (typeof obj[property] === 'object' && !(obj[property] instanceof File)) {
2002
- // Recursively handle nested objects
2003
- objectToFormData(obj[property], fd, formKey);
2004
- }
2005
- else {
2006
- // Convert boolean values to 1/0
2007
- const value = obj[property] === true || obj[property] === false ? Number(obj[property]) : obj[property];
2008
- fd.append(formKey, value);
2009
- }
2010
- }
2011
- }
2012
- return fd;
1661
+ function isValidHexNumber(hex) {
1662
+ const hexPattern = /^[0-9A-Fa-f]+$/;
1663
+ return hexPattern.test(hex);
1664
+ }
1665
+ // Example usage:
1666
+ // console.log(isValidHexNumber('1A3F')); // true
1667
+ // console.log(isValidHexNumber('GHIJ')); // false
1668
+
1669
+ /**
1670
+ * Object defining available screen sizes as constants.
1671
+ * @readonly
1672
+ */
1673
+ const sizeEnum = {
1674
+ XS: 'XS',
1675
+ SM: 'SM',
1676
+ MD: 'MD',
1677
+ LG: 'LG',
1678
+ XL: 'XL',
1679
+ XXL: 'XXL',
1680
+ };
1681
+ /**
1682
+ * Object defining breakpoints for design based on screen width.
1683
+ * Values are in pixels.
1684
+ * @readonly
1685
+ */
1686
+ const screenEnum = {
1687
+ XS: 480,
1688
+ SM: 576,
1689
+ MD: 768,
1690
+ LG: 992,
1691
+ XL: 1200,
1692
+ XXL: 1600,
1693
+ };
1694
+ /**
1695
+ * Map that associates each screen size defined in `sizeEnum` with its corresponding pixel value from `screenEnum`.
1696
+ * @type {Map<typeof sizeEnum[keyof typeof sizeEnum], number>}
1697
+ */
1698
+ const screenMap = new Map();
1699
+ screenMap.set(sizeEnum.XS, screenEnum.XS);
1700
+ screenMap.set(sizeEnum.SM, screenEnum.SM);
1701
+ screenMap.set(sizeEnum.MD, screenEnum.MD);
1702
+ screenMap.set(sizeEnum.LG, screenEnum.LG);
1703
+ screenMap.set(sizeEnum.XL, screenEnum.XL);
1704
+ screenMap.set(sizeEnum.XXL, screenEnum.XXL);
1705
+
1706
+ /**
1707
+ * Object defining various exception codes.
1708
+ * @readonly
1709
+ */
1710
+ const ExceptionEnum = {
1711
+ /**
1712
+ * HTTP status code for page access forbidden.
1713
+ */
1714
+ PAGE_NOT_ACCESS: 403,
1715
+ /**
1716
+ * HTTP status code for page not found.
1717
+ */
1718
+ PAGE_NOT_FOUND: 404,
1719
+ /**
1720
+ * HTTP status code for general server error.
1721
+ */
1722
+ ERROR: 500,
1723
+ /**
1724
+ * HTTP status code for bad request.
1725
+ */
1726
+ BAD_REQUEST: 406,
1727
+ /**
1728
+ * Custom error code for network errors.
1729
+ */
1730
+ NET_WORK_ERROR: 10000,
1731
+ /**
1732
+ * Custom code indicating no data on the page, not actually an exception page.
1733
+ */
1734
+ PAGE_NOT_DATA: 10100,
1735
+ };
1736
+
1737
+ /**
1738
+ * Object defining various image MIME types.
1739
+ * @readonly
1740
+ */
1741
+ const ImageTypes = {
1742
+ apng: 'image/apng',
1743
+ bmp: 'image/bmp',
1744
+ gif: 'image/gif',
1745
+ jpeg: 'image/jpeg',
1746
+ pjpeg: 'image/pjpeg',
1747
+ png: 'image/png',
1748
+ svg: 'image/svg+xml',
1749
+ tiff: 'image/tiff',
1750
+ webp: 'image/webp',
1751
+ xicon: 'image/x-icon',
1752
+ };
1753
+ /**
1754
+ * Object defining various file meta types.
1755
+ * @readonly
1756
+ */
1757
+ const FileMetaTypes = {
1758
+ images: 'image/*',
1759
+ audios: 'audio/*',
1760
+ videos: 'video/*',
1761
+ };
1762
+
1763
+ /**
1764
+ * Object representing different content types for HTTP headers.
1765
+ * @readonly
1766
+ */
1767
+ const ContentTypeEnum = {
1768
+ /**
1769
+ * Content type for JSON.
1770
+ * @constant
1771
+ */
1772
+ JSON: 'application/json;charset=UTF-8',
1773
+ /**
1774
+ * Content type for URL-encoded form data.
1775
+ * @constant
1776
+ */
1777
+ FORM_URLENCODED: 'application/x-www-form-urlencoded;charset=UTF-8',
1778
+ /**
1779
+ * Content type for multipart form data with file uploads.
1780
+ * @constant
1781
+ */
1782
+ FORM_DATA: 'multipart/form-data;charset=UTF-8',
2013
1783
  };
1784
+
2014
1785
  /**
2015
- * Flattens a nested object, bringing all properties to the top level.
2016
- * @param {Object} obj The object to flatten.
2017
- * @param {string} [parentKey] The parent key for nested properties.
2018
- * @param {Object} [result] The resulting flattened object.
2019
- * @returns {Object} The flattened object.
1786
+ * Object defining various key codes for keyboard events.
1787
+ * @readonly
2020
1788
  */
2021
- function flattenObject(obj, parentKey = '', result = {}) {
2022
- for (const key in obj) {
2023
- if (obj.hasOwnProperty(key)) {
2024
- const propName = parentKey ? `${parentKey}.${key}` : key;
2025
- if (typeof obj[key] === 'object' && obj[key] !== null) {
2026
- flattenObject(obj[key], propName, result);
2027
- }
2028
- else {
2029
- result[propName] = obj[key];
2030
- }
2031
- }
2032
- }
2033
- return result;
2034
- }
2035
- // Example usage:
2036
- // flattenObject({ a: 1, b: { c: 2, d: { e: 3 } } }); // { 'a': 1, 'b.c': 2, 'b.d.e': 3 }
1789
+ const KeyCodeEnum = {
1790
+ UP: 38,
1791
+ DOWN: 40,
1792
+ ENTER: 13,
1793
+ ESC: 27,
1794
+ };
2037
1795
 
2038
1796
  /**
2039
- * Capitalizes the first character of a string.
2040
- * @param {string} s The string to capitalize.
2041
- * @returns {string} The capitalized string.
1797
+ * Key used for storing the authentication token.
1798
+ * @constant {string}
2042
1799
  */
2043
- function upperFirst(s) {
2044
- return s.charAt(0).toUpperCase() + s.slice(1);
2045
- }
2046
- // Example usage:
2047
- // console.log(upperFirst('hello')); // 'Hello'
1800
+ const TOKEN_KEY = 'TOKEN__';
2048
1801
  /**
2049
- * Lowercases the first character of a string.
2050
- * @param {string} s The string to lowercase.
2051
- * @returns {string} The lowercased string.
1802
+ * Key used for storing the locale information.
1803
+ * @constant {string}
2052
1804
  */
2053
- function lowerFirst(s) {
2054
- return s.charAt(0).toLowerCase() + s.slice(1);
2055
- }
2056
- // Example usage:
2057
- // console.log(lowerFirst('Hello')); // 'hello'
1805
+ const LOCALE_KEY = 'LOCALE__';
2058
1806
  /**
2059
- * Removes accents and special characters from a string and converts it to a URL-friendly format.
2060
- * @param {string} input The string to process.
2061
- * @returns {string} The processed string.
1807
+ * Key used for storing user information.
1808
+ * @constant {string}
2062
1809
  */
2063
- function removeAccent(input) {
2064
- return input
2065
- .toLowerCase()
2066
- .trim()
2067
- .replace(/[\s_-]+/g, "-")
2068
- .replace(/^-+|-+$/g, "")
2069
- .normalize("NFD")
2070
- .replace(/[\u0300-\u036f]/g, "");
2071
- }
2072
- // Example usage:
2073
- // console.log(removeAccent('Café du Nord')); // 'cafe-du-nord'
1810
+ const USER_INFO_KEY = 'USER__INFO__';
2074
1811
  /**
2075
- * Reverses a string.
2076
- * @param {string} str The string to reverse.
2077
- * @returns {string} The reversed string.
1812
+ * Key used for storing role information.
1813
+ * @constant {string}
2078
1814
  */
2079
- function reverseString(str) {
2080
- return str.split('').reverse().join('');
1815
+ const ROLES_KEY = 'ROLES__KEY__';
1816
+ /**
1817
+ * Key used for storing project configuration.
1818
+ * @constant {string}
1819
+ */
1820
+ const PROJ_CFG_KEY = 'PROJ__CFG__KEY__';
1821
+ /**
1822
+ * Key used for storing lock information.
1823
+ * @constant {string}
1824
+ */
1825
+ const LOCK_INFO_KEY = 'LOCK__INFO__KEY__';
1826
+ /**
1827
+ * Key used for base global local cache.
1828
+ * @constant {string}
1829
+ */
1830
+ const APP_LOCAL_CACHE_KEY = 'COMMON__LOCAL__KEY';
1831
+ /**
1832
+ * Key used for base global session cache.
1833
+ * @constant {string}
1834
+ */
1835
+ const APP_SESSION_CACHE_KEY = 'COMMON__SESSION__KEY';
1836
+ /**
1837
+ * Object defining types of cache storage.
1838
+ * @readonly
1839
+ */
1840
+ const CacheTypeEnum = {
1841
+ /** Represents session storage */
1842
+ SESSION: 0,
1843
+ /** Represents local storage */
1844
+ LOCAL: 1,
1845
+ };
1846
+
1847
+ const ERROR_MESSAGES = {
1848
+ warning: 'Algo no está del todo bien. Verifica y vuelve a intentar.',
1849
+ error: 'Ocurrió un error inesperado.',
1850
+ critical: 'Error crítico. Contacta al soporte técnico.',
1851
+ validation: 'Hay errores en el formulario. Revisa los campos.',
1852
+ component: 'Error en la carga del componente. Intenta recargar.',
1853
+ network: 'Problema de conexión. Verifica tu internet.',
1854
+ authentication: 'No tienes permisos para realizar esta acción.',
1855
+ runtime: 'Error de ejecución. Intenta nuevamente.',
1856
+ type: 'Tipo de error no identificado.',
1857
+ reference: 'Referencia no válida.',
1858
+ syntax: 'Error de sintaxis en el código.',
1859
+ range: 'Error en el rango de valores.',
1860
+ eval: 'Error en la evaluación.',
1861
+ uri: 'URI no válida.',
1862
+ };
1863
+ const ERROR_STYLES = {
1864
+ warning: 'color: orange; font-weight: bold;',
1865
+ error: 'color: red; font-weight: bold;',
1866
+ critical: 'color: white; background-color: red; font-weight: bold; padding: 2px;',
1867
+ validation: 'color: blue; font-weight: bold;',
1868
+ component: 'color: purple; font-weight: bold;',
1869
+ network: 'color: cyan; font-weight: bold;',
1870
+ authentication: 'color: brown; font-weight: bold;',
1871
+ runtime: 'color: darkred; font-weight: bold;',
1872
+ type: 'color: gray; font-weight: bold;',
1873
+ reference: 'color: darkorange; font-weight: bold;',
1874
+ syntax: 'color: darkblue; font-weight: bold;',
1875
+ range: 'color: darkgreen; font-weight: bold;',
1876
+ eval: 'color: darkviolet; font-weight: bold;',
1877
+ uri: 'color: darkcyan; font-weight: bold;',
1878
+ };
1879
+
1880
+ class RestStd {
1881
+ static resource;
1882
+ static isFormData = false;
1883
+ static headers = {};
1884
+ static fetchComposable;
1885
+ /**
1886
+ * Set global headers for all requests.
1887
+ * @param headers Object containing headers to be set globally.
1888
+ */
1889
+ static setHeaders(headers) {
1890
+ this.headers = { ...this.headers, ...headers };
1891
+ }
1892
+ /**
1893
+ * Convert data to FormData if isFormData is true, otherwise return the data as is.
1894
+ * @param data Data to be converted.
1895
+ * @returns Data in FormData format or as is.
1896
+ */
1897
+ static transformData(data) {
1898
+ if (this.isFormData) {
1899
+ const formData = objectToFormData(data);
1900
+ return formData;
1901
+ }
1902
+ return data;
1903
+ }
1904
+ /**
1905
+ * Fetch a list of items from the server.
1906
+ *
1907
+ * @param params Query parameters for filtering the results.
1908
+ * @param options Additional options for the fetch composable.
1909
+ * @returns The result of the fetch composable (typically a promise).
1910
+ */
1911
+ static getMany(params, options = {}) {
1912
+ return this.fetchComposable({
1913
+ method: 'GET',
1914
+ url: this.resource,
1915
+ params,
1916
+ headers: this.headers,
1917
+ }, options);
1918
+ }
1919
+ /**
1920
+ * Fetch a single item by ID from the server.
1921
+ *
1922
+ * @param id The ID of the item to fetch.
1923
+ * @param params Additional query parameters for the request.
1924
+ * @param options Additional options for the fetch composable.
1925
+ * @returns The result of the fetch composable (typically a promise).
1926
+ */
1927
+ static getOne(id, params, options = {}) {
1928
+ return this.fetchComposable({
1929
+ method: 'GET',
1930
+ url: `${this.resource}/${id}`,
1931
+ params,
1932
+ headers: this.headers,
1933
+ }, options);
1934
+ }
1935
+ /**
1936
+ * Create a new item on the server.
1937
+ *
1938
+ * @param data The data for the new item to create.
1939
+ * @param options Additional options for the fetch composable.
1940
+ * @returns The result of the fetch composable (typically a promise).
1941
+ */
1942
+ static create(data, options = {}) {
1943
+ const transformedData = this.transformData(data);
1944
+ return this.fetchComposable({
1945
+ method: 'POST',
1946
+ url: this.resource,
1947
+ data: transformedData,
1948
+ headers: {
1949
+ ...this.headers,
1950
+ 'Content-Type': this.isFormData ? ContentTypeEnum.FORM_DATA : ContentTypeEnum.JSON,
1951
+ },
1952
+ }, options);
1953
+ }
1954
+ /**
1955
+ * Update an existing item on the server.
1956
+ *
1957
+ * @param id The ID of the item to update.
1958
+ * @param data The updated data for the item.
1959
+ * @param options Additional options for the fetch composable.
1960
+ * @returns The result of the fetch composable (typically a promise).
1961
+ */
1962
+ static update(id, data, options = {}) {
1963
+ const transformedData = this.transformData(data);
1964
+ return this.fetchComposable({
1965
+ method: 'PUT',
1966
+ url: `${this.resource}/${id}`,
1967
+ data: transformedData,
1968
+ headers: {
1969
+ ...this.headers,
1970
+ 'Content-Type': this.isFormData ? ContentTypeEnum.FORM_DATA : ContentTypeEnum.JSON,
1971
+ },
1972
+ }, options);
1973
+ }
1974
+ /**
1975
+ * Partially update an existing item on the server.
1976
+ *
1977
+ * @param id The ID of the item to update.
1978
+ * @param data The updated data for the item.
1979
+ * @param options Additional options for the fetch composable.
1980
+ * @returns The result of the fetch composable (typically a promise).
1981
+ */
1982
+ static patch(id, data, options = {}) {
1983
+ const transformedData = this.transformData(data);
1984
+ return this.fetchComposable({
1985
+ method: 'PATCH',
1986
+ url: `${this.resource}/${id}`,
1987
+ data: transformedData,
1988
+ headers: {
1989
+ ...this.headers,
1990
+ 'Content-Type': this.isFormData ? ContentTypeEnum.FORM_DATA : ContentTypeEnum.JSON,
1991
+ },
1992
+ }, options);
1993
+ }
1994
+ /**
1995
+ * Delete an item from the server.
1996
+ *
1997
+ * @param id The ID of the item to delete.
1998
+ * @param options Additional options for the fetch composable.
1999
+ * @returns The result of the fetch composable (typically a promise).
2000
+ */
2001
+ static delete(id, options = {}) {
2002
+ return this.fetchComposable({
2003
+ method: 'DELETE',
2004
+ url: `${this.resource}/${id}`,
2005
+ headers: this.headers,
2006
+ }, options);
2007
+ }
2008
+ /**
2009
+ * Custom request method for more flexibility.
2010
+ *
2011
+ * @param method HTTP method (GET, POST, etc.).
2012
+ * @param params Query parameters.
2013
+ * @param data Request body data.
2014
+ * @param options Additional options for the fetch composable.
2015
+ * @returns The result of the fetch composable (typically a promise).
2016
+ */
2017
+ static customRequest(method, params, data, options = {}) {
2018
+ const transformedData = this.transformData(data);
2019
+ return this.fetchComposable({
2020
+ method: method,
2021
+ url: this.resource,
2022
+ params: params,
2023
+ data: transformedData,
2024
+ headers: {
2025
+ ...this.headers,
2026
+ 'Content-Type': this.isFormData ? ContentTypeEnum.FORM_DATA : ContentTypeEnum.JSON,
2027
+ },
2028
+ }, options);
2029
+ }
2030
+ /**
2031
+ * Save method that decides whether to create or update based on the presence of 'id'.
2032
+ * It calls either create or update based on the data.
2033
+ *
2034
+ * @param data The data for the item to create or update.
2035
+ * @param options Additional options for the fetch composable.
2036
+ * @returns The result of the fetch composable (typically a promise).
2037
+ */
2038
+ static save(data, options = {}) {
2039
+ if (data.id) {
2040
+ return this.update(data.id, data, options);
2041
+ }
2042
+ else {
2043
+ return this.create(data, options);
2044
+ }
2045
+ }
2081
2046
  }
2082
- // Example usage:
2083
- // console.log(reverseString('hello')); // 'olleh'
2047
+
2084
2048
  /**
2085
- * Counts the number of words in a string.
2086
- * @param {string} str The string to analyze.
2087
- * @returns {number} The word count.
2049
+ * Custom composable function for making API requests with Axios.
2050
+ *
2051
+ * @template T The expected return type of the API request.
2052
+ * @param {AxiosInstance} axios - The Axios instance for making HTTP requests.
2053
+ * @param {AxiosRequestConfig} axiosRequest - Configuration for the Axios request.
2054
+ * @returns {Promise<T>} A promise with the result of the API request.
2088
2055
  */
2089
- function countWords(str) {
2090
- return str.trim().split(/\s+/).length;
2056
+ async function axiosFetch(axios, axiosRequest) {
2057
+ return axios(axiosRequest)
2058
+ .then(response => response.data)
2059
+ .catch(error => {
2060
+ throw error;
2061
+ });
2091
2062
  }
2092
- // Example usage:
2093
- // console.log(countWords('Hello world!')); // 2
2063
+
2094
2064
  /**
2095
- * Truncates a string to the specified length and adds ellipsis if necessary.
2096
- * @param {string} str The string to truncate.
2097
- * @param {number} maxLength The maximum length of the string.
2098
- * @returns {string} The truncated string.
2099
- */
2100
- function truncateString(str, maxLength) {
2101
- return str.length > maxLength ? str.slice(0, maxLength) + '...' : str;
2065
+ * Maneja y registra errores en la consola, además de permitir redirecciones configurables.
2066
+ * @param error - Error capturado (string o Error)
2067
+ * @param redirect - `true` si se quiere redirigir a una página de error
2068
+ * @param route - Ruta personalizada para la página de error (por defecto: `/error`)
2069
+ * @param passMethod - Método para pasar el error a la ruta (`query`, `localStorage`, `sessionStorage`)
2070
+ * @returns Mensaje user-friendly del error
2071
+ */
2072
+ function handleError(error, redirect = false, route = '/error', passMethod = 'query') {
2073
+ if (!(error instanceof Error)) {
2074
+ console.error("Unrecognized error:", error);
2075
+ return;
2076
+ }
2077
+ const router = useRouter();
2078
+ const type = inferErrorType(error);
2079
+ const message = ERROR_MESSAGES[type] || 'Error desconocido.';
2080
+ // Mostrar el error en la consola con estilos
2081
+ console.log(`%c[${type.toUpperCase()}] ${message}`, ERROR_STYLES[type]);
2082
+ // Pasar el error según el método elegido
2083
+ if (redirect) {
2084
+ if (passMethod === 'query') {
2085
+ router.push({ path: route, query: { errorMessage: encodeURIComponent(message) } });
2086
+ }
2087
+ else if (passMethod === 'localStorage') {
2088
+ localStorage.setItem('errorMessage', message);
2089
+ router.push(route);
2090
+ }
2091
+ else if (passMethod === 'sessionStorage') {
2092
+ sessionStorage.setItem('errorMessage', message);
2093
+ router.push(route);
2094
+ }
2095
+ }
2096
+ return message;
2097
+ }
2098
+ /**
2099
+ * Infiera el tipo de error basado en su mensaje o nombre.
2100
+ * @param error - El error que se desea analizar.
2101
+ * @returns El tipo de error determinado.
2102
+ */
2103
+ function inferErrorType(error) {
2104
+ const msg = typeof error === 'string' ? error : error.message;
2105
+ if (typeof error === 'string')
2106
+ return 'error';
2107
+ if (error.name === 'ValidationError')
2108
+ return 'validation';
2109
+ if (error.name === 'NetworkError' || msg.includes('Network'))
2110
+ return 'network';
2111
+ if (error.name === 'AuthenticationError' || msg.includes('Unauthorized'))
2112
+ return 'authentication';
2113
+ if (msg.includes('Component'))
2114
+ return 'component';
2115
+ if (msg.includes('Runtime'))
2116
+ return 'runtime';
2117
+ return 'error';
2102
2118
  }
2103
- // Example usage:
2104
- // console.log(truncateString('This is a long string', 10)); // 'This is a...'
2119
+
2120
+ let tokenConfig = {
2121
+ ACCESS_TOKEN: 'authToken',
2122
+ REFRESH_TOKEN: 'refreshToken',
2123
+ };
2105
2124
  /**
2106
- * Converts a string to camel case.
2107
- * @param {string} str The string to convert.
2108
- * @returns {string} The camel cased string.
2125
+ * Función para configurar las claves globalmente.
2126
+ * Esta función congela el objeto para evitar modificaciones posteriores.
2127
+ * @param {string} accessTokenKey - Nombre de la clave del token de acceso
2128
+ * @param {string} refreshTokenKey - Nombre de la clave del refresh token
2109
2129
  */
2110
- function toCamelCase(str) {
2111
- return str
2112
- .toLowerCase()
2113
- .replace(/[^a-zA-Z0-9]+(.)/g, (match, chr) => chr.toUpperCase());
2130
+ function configureTokenKeys(accessTokenKey, refreshTokenKey) {
2131
+ tokenConfig = Object.freeze({
2132
+ ACCESS_TOKEN: accessTokenKey,
2133
+ REFRESH_TOKEN: refreshTokenKey,
2134
+ });
2114
2135
  }
2115
- // Example usage:
2116
- // console.log(toCamelCase('hello world example')); // 'helloWorldExample'
2117
2136
  /**
2118
- * Converts a string to kebab case.
2119
- * @param {string} str The string to convert.
2120
- * @returns {string} The kebab cased string.
2137
+ * Obtiene las claves configuradas para los tokens.
2138
+ * El objeto está congelado, por lo que no se puede modificar directamente.
2121
2139
  */
2122
- function toKebabCase(str) {
2123
- return str
2124
- .replace(/([a-z])([A-Z])/g, '$1-$2')
2125
- .replace(/[\s_]+/g, '-')
2126
- .toLowerCase();
2140
+ function getTokenConfig() {
2141
+ return tokenConfig;
2127
2142
  }
2128
- // Example usage:
2129
- // console.log(toKebabCase('Hello World Example')); // 'hello-world-example'
2143
+
2144
+ let endpointsConfig = {
2145
+ LOGIN: '/login',
2146
+ REFRESH: '/refresh',
2147
+ LOGOUT: '/logout',
2148
+ };
2130
2149
  /**
2131
- * Replaces all instances of a substring within a string.
2132
- * @param {string} str The original string.
2133
- * @param {string} find The substring to find.
2134
- * @param {string} replace The substring to replace with.
2135
- * @returns {string} The modified string.
2150
+ * Configura las URLs de los endpoints de autenticación globalmente.
2151
+ * Esta función congela el objeto para evitar modificaciones posteriores.
2152
+ *
2153
+ * @param {string} loginEndpoint - URL del endpoint para el login.
2154
+ * @param {string} refreshEndpoint - URL del endpoint para el refresh token.
2155
+ * @param {string} logoutEndpoint - URL del endpoint para el logout.
2156
+ *
2157
+ * @returns {void} No retorna nada, pero congela el objeto de configuración de endpoints.
2136
2158
  */
2137
- function replaceAll(str, find, replace) {
2138
- return str.split(find).join(replace);
2159
+ function configureEndpoints(loginEndpoint, refreshEndpoint, logoutEndpoint) {
2160
+ endpointsConfig = Object.freeze({
2161
+ LOGIN: loginEndpoint,
2162
+ REFRESH: refreshEndpoint,
2163
+ LOGOUT: logoutEndpoint,
2164
+ });
2139
2165
  }
2140
- // Example usage:
2141
- // console.log(replaceAll('hello world', 'o', 'a')); // 'hella warld'
2142
2166
  /**
2143
- * Generates a random string of a specific length.
2144
- * @param {number} length The length of the string to generate.
2145
- * @returns {string} The random string.
2167
+ * Obtiene las URLs de los endpoints de autenticación configurados.
2168
+ *
2169
+ * @returns {Object} Objeto con las URLs de los endpoints configurados.
2170
+ * @returns {string} returns.LOGIN - URL del endpoint de login.
2171
+ * @returns {string} returns.REFRESH - URL del endpoint de refresh token.
2172
+ * @returns {string} returns.LOGOUT - URL del endpoint de logout.
2146
2173
  */
2147
- function generateRandomString(length) {
2148
- const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
2149
- return Array.from({ length }, () => characters.charAt(Math.floor(Math.random() * characters.length))).join('');
2174
+ function getEndpointsConfig() {
2175
+ return endpointsConfig;
2150
2176
  }
2151
- // Example usage:
2152
- // console.log(generateRandomString(10)); // 'A1b2C3d4E5'
2153
2177
 
2154
2178
  /**
2155
- * Validates if the key pressed is a valid letter or special character.
2156
- * @param {KeyboardEvent} e The keyboard event.
2157
- * @returns {boolean} True if the key is valid, otherwise false.
2179
+ * AxiosService class encapsulates the Axios configuration and logic.
2180
+ * It manages request and response interceptors and provides methods for making HTTP requests.
2158
2181
  */
2159
- function validateLetters(e) {
2160
- const key = e.keyCode;
2161
- const validKeys = [
2162
- ...Array.from({ length: 26 }, (_, i) => i + 65), // Letters A-Z
2163
- ...Array.from({ length: 26 }, (_, i) => i + 97), // Letters a-z
2164
- 45, // Hyphen
2165
- 32, // Space
2166
- 241, // ñ
2167
- 209, // Ñ
2168
- 225, // á
2169
- 233, // é
2170
- 237, // í
2171
- 243, // ó
2172
- 250, // ú
2173
- 193, // Á
2174
- 201, // É
2175
- 205, // Í
2176
- 211, // Ó
2177
- 218 // Ú
2178
- ];
2179
- if (validKeys.includes(key)) {
2180
- return true;
2182
+ class AxiosService {
2183
+ instance;
2184
+ cancelTokenSource;
2185
+ activeRequests = 0;
2186
+ refreshTokenInProgress = false;
2187
+ refreshTokenUrl;
2188
+ /**
2189
+ * Initializes the AxiosService instance by creating an Axios instance with default configuration
2190
+ * and setting up request and response interceptors.
2191
+ */
2192
+ constructor(url, headers = {}) {
2193
+ this.cancelTokenSource = axios.CancelToken.source();
2194
+ this.instance = axios.create({
2195
+ baseURL: url ?? '',
2196
+ timeout: 300000,
2197
+ headers: {
2198
+ 'Accept': 'application/json',
2199
+ 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
2200
+ 'Pragma': 'no-cache',
2201
+ 'Expires': '0',
2202
+ ...headers,
2203
+ },
2204
+ withCredentials: false,
2205
+ });
2206
+ const endpointsConfig = getEndpointsConfig();
2207
+ this.refreshTokenUrl = endpointsConfig.REFRESH;
2208
+ this.initializeInterceptors();
2181
2209
  }
2182
- e.preventDefault();
2183
- return false;
2184
- }
2185
- // Example usage:
2186
- // document.addEventListener('keydown', validateLetters);
2187
- /**
2188
- * Validates if the key pressed is a valid alphanumeric character.
2189
- * @param {KeyboardEvent} e The keyboard event.
2190
- * @returns {boolean} True if the key is valid, otherwise false.
2191
- */
2192
- function validateAlphanumeric(e) {
2193
- const key = e.keyCode;
2194
- const validKeys = [
2195
- ...Array.from({ length: 10 }, (_, i) => i + 48), // Numbers 0-9
2196
- ...Array.from({ length: 26 }, (_, i) => i + 65), // Letters A-Z
2197
- ...Array.from({ length: 26 }, (_, i) => i + 97), // Letters a-z
2198
- 45, // Hyphen
2199
- 95, // Underscore
2200
- 32 // Space
2201
- ];
2202
- if (validKeys.includes(key)) {
2203
- return true;
2210
+ /**
2211
+ * Initializes request and response interceptors for the Axios instance.
2212
+ */
2213
+ initializeInterceptors() {
2214
+ const { ACCESS_TOKEN, REFRESH_TOKEN } = getTokenConfig();
2215
+ this.instance.interceptors.request.use((config) => {
2216
+ const token = typeof window !== 'undefined' ? localStorage.getItem(ACCESS_TOKEN) : null;
2217
+ if (token && config.headers) {
2218
+ config.headers.Authorization = `Bearer ${token}`;
2219
+ }
2220
+ config.cancelToken = this.cancelTokenSource.token;
2221
+ this.activeRequests++;
2222
+ return config;
2223
+ }, (error) => {
2224
+ handleError(error, false);
2225
+ this.activeRequests++;
2226
+ return Promise.reject(error);
2227
+ });
2228
+ this.instance.interceptors.response.use((response) => {
2229
+ this.activeRequests--;
2230
+ return response;
2231
+ }, async (error) => {
2232
+ this.activeRequests--;
2233
+ // Check for unauthorized access (401)
2234
+ if (axios.isAxiosError(error) && error.response?.status === 401 && !this.refreshTokenInProgress) {
2235
+ const refreshToken = localStorage.getItem(REFRESH_TOKEN);
2236
+ if (refreshToken) {
2237
+ this.refreshTokenInProgress = true;
2238
+ try {
2239
+ // Attempt to refresh the token
2240
+ const refreshResponse = await axios.post(this.refreshTokenUrl, { refreshToken });
2241
+ const { token: newAccessToken } = refreshResponse.data;
2242
+ if (newAccessToken) {
2243
+ // Save the new access token
2244
+ localStorage.setItem(ACCESS_TOKEN, newAccessToken);
2245
+ // Retry the original request with the new token
2246
+ if (error.config && error.config.headers) {
2247
+ error.config.headers['Authorization'] = `Bearer ${newAccessToken}`;
2248
+ }
2249
+ this.refreshTokenInProgress = false;
2250
+ if (error.config) {
2251
+ return this.instance(error.config);
2252
+ }
2253
+ }
2254
+ }
2255
+ catch (refreshError) {
2256
+ handleError(refreshError, false);
2257
+ this.refreshTokenInProgress = false;
2258
+ localStorage.removeItem(ACCESS_TOKEN);
2259
+ localStorage.removeItem(REFRESH_TOKEN);
2260
+ }
2261
+ }
2262
+ }
2263
+ handleError(error, false);
2264
+ return Promise.reject(error);
2265
+ });
2204
2266
  }
2205
- e.preventDefault();
2206
- return false;
2207
- }
2208
- // Example usage:
2209
- // document.addEventListener('keydown', validateAlphanumeric);
2210
- /**
2211
- * Validates if the key pressed is a number.
2212
- * @param {KeyboardEvent} e The keyboard event.
2213
- * @returns {boolean} True if the key is a number, otherwise false.
2214
- */
2215
- function validateNumbers(e) {
2216
- const key = e.keyCode;
2217
- if ((key >= 48 && key <= 57) || key === 46 || key === 8 || key === 37 || key === 39) {
2218
- return true;
2267
+ /**
2268
+ * Returns the number of active requests.
2269
+ */
2270
+ getActiveRequests() {
2271
+ return this.activeRequests;
2219
2272
  }
2220
- e.preventDefault();
2221
- return false;
2222
- }
2223
- // Example usage:
2224
- // document.addEventListener('keydown', validateNumbers);
2225
- /**
2226
- * Validates if a phone number is valid.
2227
- * @param {string} phoneNumber The phone number to validate.
2228
- * @returns {boolean} True if the phone number is valid, otherwise false.
2229
- */
2230
- function isValidPhoneNumber(phoneNumber) {
2231
- const phonePattern = /^[0-9]{10}$/; // Example pattern for 10-digit phone numbers
2232
- return phonePattern.test(phoneNumber);
2233
- }
2234
- // Example usage:
2235
- // console.log(isValidPhoneNumber('1234567890')); // true
2236
- // console.log(isValidPhoneNumber('123-456-7890')); // false
2237
- /**
2238
- * Validates if a string is a valid email address.
2239
- * @param {string} email The email address to validate.
2240
- * @returns {boolean} True if the email address is valid, otherwise false.
2241
- */
2242
- function isValidEmail(email) {
2243
- const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2244
- return emailPattern.test(email);
2245
- }
2246
- // Example usage:
2247
- // console.log(isValidEmail('example@domain.com')); // true
2248
- // console.log(isValidEmail('invalid-email')); // false
2249
- /**
2250
- * Validates if a string is a valid URL.
2251
- * @param {string} url The URL to validate.
2252
- * @returns {boolean} True if the URL is valid, otherwise false.
2253
- */
2254
- function isValidURL(url) {
2255
- try {
2256
- new URL(url);
2257
- return true;
2273
+ /**
2274
+ * Returns the Axios instance with the configured settings and interceptors.
2275
+ * @returns {AxiosInstance} The configured Axios instance.
2276
+ */
2277
+ getAxiosInstance() {
2278
+ return this.instance;
2258
2279
  }
2259
- catch {
2260
- return false;
2280
+ /**
2281
+ * Cancels all ongoing requests.
2282
+ */
2283
+ cancelAllRequests() {
2284
+ this.cancelTokenSource.cancel('Operation canceled by the user.');
2285
+ this.cancelTokenSource = axios.CancelToken.source();
2286
+ }
2287
+ /**
2288
+ * Sets a new header for the Axios instance.
2289
+ * @param {string} key - The header key.
2290
+ * @param {string} value - The header value.
2291
+ */
2292
+ setHeader(key, value) {
2293
+ this.instance.defaults.headers.common[key] = value;
2294
+ }
2295
+ /**
2296
+ * Removes a header from the Axios instance.
2297
+ * @param {string} key - The header key to remove.
2298
+ */
2299
+ removeHeader(key) {
2300
+ delete this.instance.defaults.headers.common[key];
2261
2301
  }
2262
2302
  }
2263
- // Example usage:
2264
- // console.log(isValidURL('https://www.example.com')); // true
2265
- // console.log(isValidURL('invalid-url')); // false
2266
- /**
2267
- * Validates if a string is a valid date in YYYY-MM-DD format.
2268
- * @param {string} date The date string to validate.
2269
- * @returns {boolean} True if the date is valid, otherwise false.
2270
- */
2271
- function isValidDate(date) {
2272
- const datePattern = /^\d{4}-\d{2}-\d{2}$/;
2273
- if (!datePattern.test(date))
2274
- return false;
2275
- const [year, month, day] = date.split('-').map(Number);
2276
- const dateObj = new Date(year, month - 1, day);
2277
- return dateObj.getFullYear() === year && dateObj.getMonth() === month - 1 && dateObj.getDate() === day;
2278
- }
2279
- // Example usage:
2280
- // console.log(isValidDate('2024-08-31')); // true
2281
- // console.log(isValidDate('2024-02-30')); // false
2303
+
2304
+ let axiosInstance;
2282
2305
  /**
2283
- * Validates if a password meets certain strength criteria.
2284
- * @param {string} password The password to validate.
2285
- * @returns {boolean} True if the password is strong, otherwise false.
2306
+ * Configures the global Axios instance with a base URL.
2307
+ * @param {string} baseURL - The base URL for the Axios instance.
2286
2308
  */
2287
- function isStrongPassword(password) {
2288
- const minLength = 8;
2289
- const hasUpperCase = /[A-Z]/.test(password);
2290
- const hasLowerCase = /[a-z]/.test(password);
2291
- const hasNumbers = /[0-9]/.test(password);
2292
- const hasSpecialChars = /[!@#$%^&*(),.?":{}|<>]/.test(password);
2293
- return password.length >= minLength && hasUpperCase && hasLowerCase && hasNumbers && hasSpecialChars;
2294
- }
2295
- // Example usage:
2296
- // console.log(isStrongPassword('Strong1@password')); // true
2297
- // console.log(isStrongPassword('weakpass')); // false
2309
+ const configureAxios = (baseURL) => {
2310
+ axiosInstance = new AxiosService(baseURL);
2311
+ };
2298
2312
  /**
2299
- * Validates a credit card number using the Luhn algorithm.
2300
- * @param {string} cardNumber The credit card number to validate.
2301
- * @returns {boolean} True if the credit card number is valid, otherwise false.
2313
+ * Retrieves the configured Axios instance.
2314
+ * @returns {AxiosService} The configured Axios instance.
2315
+ * @throws Will throw an error if the Axios instance is not configured.
2302
2316
  */
2303
- function isValidCreditCard(cardNumber) {
2304
- const sanitized = cardNumber.replace(/\D/g, '');
2305
- let sum = 0;
2306
- let shouldDouble = false;
2307
- for (let i = sanitized.length - 1; i >= 0; i--) {
2308
- let digit = parseInt(sanitized.charAt(i), 10);
2309
- if (shouldDouble) {
2310
- digit *= 2;
2311
- if (digit > 9)
2312
- digit -= 9;
2313
- }
2314
- sum += digit;
2315
- shouldDouble = !shouldDouble;
2317
+ const getAxiosInstance = () => {
2318
+ if (!axiosInstance) {
2319
+ throw new Error('Axios instance not configured. Call configureAxios first.');
2316
2320
  }
2317
- return sum % 10 === 0;
2318
- }
2319
- // Example usage:
2320
- // console.log(isValidCreditCard('4111111111111111')); // true
2321
- // console.log(isValidCreditCard('1234567812345670')); // false
2322
- /**
2323
- * Validates if a string is a valid hex color code.
2324
- * @param {string} color The color code to validate.
2325
- * @returns {boolean} True if the color code is valid, otherwise false.
2326
- */
2327
- function isValidHexColor(color) {
2328
- const hexPattern = /^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})$/;
2329
- return hexPattern.test(color);
2330
- }
2331
- // Example usage:
2332
- // console.log(isValidHexColor('#FFFFFF')); // true
2333
- // console.log(isValidHexColor('#FFF')); // true
2334
- // console.log(isValidHexColor('#12345G')); // false
2335
- /**
2336
- * Validates if a string is a valid time in HH:MM format.
2337
- * @param {string} time The time string to validate.
2338
- * @returns {boolean} True if the time is valid, otherwise false.
2339
- */
2340
- function isValidTime(time) {
2341
- const timePattern = /^([01]\d|2[0-3]):([0-5]\d)$/;
2342
- return timePattern.test(time);
2343
- }
2344
- // Example usage:
2345
- // console.log(isValidTime('14:30')); // true
2346
- // console.log(isValidTime('25:00')); // false
2347
- /**
2348
- * Validates if a string is a valid IPv4 address.
2349
- * @param {string} ip The IP address to validate.
2350
- * @returns {boolean} True if the IP address is valid, otherwise false.
2351
- */
2352
- function isValidIP(ip) {
2353
- const ipPattern = /^(25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)$/;
2354
- return ipPattern.test(ip);
2355
- }
2356
- // Example usage:
2357
- // console.log(isValidIP('192.168.1.1')); // true
2358
- // console.log(isValidIP('999.999.999.999')); // false
2321
+ return axiosInstance.getAxiosInstance();
2322
+ };
2359
2323
  /**
2360
- * Validates if a string is a valid U.S. Social Security Number (SSN).
2361
- * @param {string} ssn The SSN to validate.
2362
- * @returns {boolean} True if the SSN is valid, otherwise false.
2324
+ * Creates a new AxiosService instance with custom headers.
2325
+ * @param {string} baseURL - The base URL for the Axios instance.
2326
+ * @param {Record<string, string>} headers - Custom headers to set.
2327
+ * @returns {AxiosService} The new AxiosService instance.
2363
2328
  */
2364
- function isValidSSN(ssn) {
2365
- const ssnPattern = /^\d{3}-\d{2}-\d{4}$/;
2366
- return ssnPattern.test(ssn);
2367
- }
2368
- // Example usage:
2369
- // console.log(isValidSSN('123-45-6789')); // true
2370
- // console.log(isValidSSN('123-45-678')); // false
2329
+ const createCustomAxiosInstance = (baseURL, headers) => {
2330
+ return new AxiosService(baseURL, headers).getAxiosInstance();
2331
+ };
2332
+
2371
2333
  /**
2372
- * Validates if a string is a valid U.S. ZIP code.
2373
- * @param {string} zip The ZIP code to validate.
2374
- * @returns {boolean} True if the ZIP code is valid, otherwise false.
2334
+ * Creates a fetchComposable for Axios requests with an optional custom instance.
2335
+ * @param fetchComposable - The composable to use for the request (can be any fetch function).
2336
+ * @param axiosCustomInstance - An optional custom Axios instance to use for the request.
2337
+ * @returns A function that handles the request using the provided composable.
2375
2338
  */
2376
- function isValidZIP(zip) {
2377
- const zipPattern = /^\d{5}(-\d{4})?$/;
2378
- return zipPattern.test(zip);
2339
+ function createFetch(fetchComposable, axiosCustomInstance) {
2340
+ const instance = axiosCustomInstance || getAxiosInstance();
2341
+ return (axiosRequestConfig, options) => {
2342
+ return fetchComposable(instance, axiosRequestConfig, options);
2343
+ };
2379
2344
  }
2380
- // Example usage:
2381
- // console.log(isValidZIP('12345')); // true
2382
- // console.log(isValidZIP('12345-6789')); // true
2383
- // console.log(isValidZIP('1234')); // false
2345
+
2384
2346
  /**
2385
- * Validates if a string is a valid credit card expiry date in MM/YY format.
2386
- * @param {string} expiryDate The expiry date to validate.
2387
- * @returns {boolean} True if the expiry date is valid, otherwise false.
2347
+ * Custom composable for integrating Axios requests with Vue Query.
2348
+ *
2349
+ * @template T - The type of data expected from the query.
2350
+ * @param {AxiosInstance} axios - Axios instance used for making HTTP requests.
2351
+ * @param {AxiosRequestConfig} axiosRequest - Initial Axios request configuration.
2352
+ * @param {ExtendedQueryOptions<T>} [queryOptions] - Optional Vue Query options with server execution flag.
2353
+ * @returns {object} Composable functions and query state for managing API requests.
2388
2354
  */
2389
- function isValidExpiryDate(expiryDate) {
2390
- const expiryPattern = /^(0[1-9]|1[0-2])\/\d{2}$/;
2391
- if (!expiryPattern.test(expiryDate))
2392
- return false;
2393
- const [month, year] = expiryDate.split('/').map(Number);
2394
- const currentYear = new Date().getFullYear() % 100;
2395
- const currentMonth = new Date().getMonth() + 1;
2396
- return (year > currentYear) || (year === currentYear && month >= currentMonth);
2355
+ function useVueQuery(axios, axiosRequest, queryOptions) {
2356
+ const key = ref([
2357
+ queryOptions?.queryKey || `${axiosRequest.url}-${JSON.stringify(axiosRequest)}`
2358
+ ]);
2359
+ const axiosRequestRef = ref(axiosRequest);
2360
+ const queryOptionsRef = ref(queryOptions);
2361
+ const isInitialized = ref(false);
2362
+ const errorCallbacks = ref([]);
2363
+ const resultCallbacks = ref([]);
2364
+ /**
2365
+ * Vue Query instance for managing API requests.
2366
+ */
2367
+ const query = useQuery({
2368
+ queryKey: key.value,
2369
+ queryFn: async () => {
2370
+ const response = await axios(axiosRequestRef.value);
2371
+ return response.data;
2372
+ },
2373
+ ...(queryOptionsRef.value?.options ?? {})
2374
+ });
2375
+ /**
2376
+ * Executes the query manually with optional new key and request parameters.
2377
+ *
2378
+ * @param {Array<any>} [newKey] - New query key to use.
2379
+ * @param {object} [newRequestParams] - New request parameters to use.
2380
+ * @param {ExtendedQueryOptions<T>} [newQueryOptions] - New Vue Query options to use.
2381
+ */
2382
+ function execute(newKey, newRequestParams, newQueryOptions) {
2383
+ if (newKey) {
2384
+ key.value = newKey;
2385
+ }
2386
+ if (newRequestParams) {
2387
+ axiosRequestRef.value.data = newRequestParams;
2388
+ }
2389
+ if (newQueryOptions) {
2390
+ queryOptionsRef.value = newQueryOptions;
2391
+ }
2392
+ query.refetch();
2393
+ }
2394
+ /**
2395
+ * Registers a callback to be executed on successful request result.
2396
+ * If data is already available, the callback will be executed immediately.
2397
+ *
2398
+ * @param {(data: T) => void} cb - Callback function to handle the result.
2399
+ */
2400
+ function onResult(cb) {
2401
+ resultCallbacks.value.push(cb);
2402
+ if (query.data.value !== undefined && query.data.value !== null) {
2403
+ cb(query.data.value);
2404
+ }
2405
+ }
2406
+ /**
2407
+ * Registers a callback to be executed when the request results in an error.
2408
+ * If an error is already present, the callback will be executed immediately.
2409
+ *
2410
+ * @param {(e: unknown) => void} callback - Callback function to handle errors.
2411
+ */
2412
+ function onError(callback) {
2413
+ errorCallbacks.value.push(callback);
2414
+ if (query.error.value !== undefined) {
2415
+ callback(query.error.value);
2416
+ }
2417
+ }
2418
+ /**
2419
+ * Watches for changes in the query result and triggers result callbacks.
2420
+ */
2421
+ watch(query.data, (newData) => {
2422
+ if (newData !== undefined && newData !== null && isInitialized.value) {
2423
+ resultCallbacks.value.forEach((cb) => cb(newData));
2424
+ }
2425
+ });
2426
+ /**
2427
+ * Watches for changes in query errors and triggers error callbacks.
2428
+ */
2429
+ watch(query.error, (newError) => {
2430
+ if (newError !== undefined && isInitialized.value) {
2431
+ errorCallbacks.value.forEach((cb) => cb(newError));
2432
+ }
2433
+ });
2434
+ /**
2435
+ * Handles server-side prefetching for SSR.
2436
+ * Ensures data is fetched before rendering on the server.
2437
+ */
2438
+ onServerPrefetch(async () => {
2439
+ if (queryOptionsRef.value?.server !== false) {
2440
+ await query.suspense();
2441
+ if (query.data.value != null) {
2442
+ resultCallbacks.value.forEach((cb) => cb(query.data.value));
2443
+ }
2444
+ }
2445
+ });
2446
+ /**
2447
+ * Ensures callbacks are executed after the component is mounted on the client-side.
2448
+ */
2449
+ onMounted(() => {
2450
+ isInitialized.value = true;
2451
+ if (query.data.value != null) {
2452
+ resultCallbacks.value.forEach((cb) => cb(query.data.value));
2453
+ }
2454
+ if (query.error.value !== undefined) {
2455
+ errorCallbacks.value.forEach((cb) => cb(query.error.value));
2456
+ }
2457
+ });
2458
+ return {
2459
+ execute,
2460
+ onResult,
2461
+ onError,
2462
+ ...query
2463
+ };
2397
2464
  }
2398
- // Example usage:
2399
- // console.log(isValidExpiryDate('08/24')); // true
2400
- // console.log(isValidExpiryDate('12/22')); // false
2465
+
2466
+ let notified = false;
2401
2467
  /**
2402
- * Validates if a string is a valid 8-character hexadecimal color code (including alpha).
2403
- * @param {string} color The color code to validate.
2404
- * @returns {boolean} True if the color code is valid, otherwise false.
2468
+ * Composable to manage breakpoints using Tailwind CSS and shared window resize listener.
2469
+ *
2470
+ * @returns {Object} An object containing the breakpoints and their states.
2405
2471
  */
2406
- function isValidHexColorAlpha(color) {
2407
- const hexPattern = /^#([0-9A-Fa-f]{8})$/;
2408
- return hexPattern.test(color);
2472
+ function useBreakpoint() {
2473
+ const breakpoints = useBreakpoints(breakpointsTailwind);
2474
+ const current = breakpoints.current();
2475
+ const active = breakpoints.active();
2476
+ // Define breakpoints
2477
+ const sm_S = breakpoints.smaller('sm');
2478
+ const sm_SE = breakpoints.smallerOrEqual('sm');
2479
+ const sm_GE = breakpoints.greaterOrEqual('sm');
2480
+ const sm_md = breakpoints.between('sm', 'md');
2481
+ const sm_xl = breakpoints.between('sm', 'xl');
2482
+ const sm_2xl = breakpoints.between('sm', '2xl');
2483
+ const md_S = breakpoints.smaller('md');
2484
+ const md_SE = breakpoints.smallerOrEqual('md');
2485
+ const md_GE = breakpoints.greaterOrEqual('md');
2486
+ const md_lg = breakpoints.between('md', 'lg');
2487
+ const md_xl = breakpoints.between('md', 'xl');
2488
+ const md_2xl = breakpoints.between('md', '2xl');
2489
+ const lg_S = breakpoints.smaller('lg');
2490
+ const lg_SE = breakpoints.smallerOrEqual('lg');
2491
+ const lg_GE = breakpoints.greaterOrEqual('lg');
2492
+ const lg_xl = breakpoints.between('lg', 'xl');
2493
+ const lg_2xl = breakpoints.between('lg', '2xl');
2494
+ const xl_S = breakpoints.smaller('xl');
2495
+ const xl_SE = breakpoints.smallerOrEqual('xl');
2496
+ const xl_GE = breakpoints.greaterOrEqual('xl');
2497
+ const xl_2xl = breakpoints.between('xl', '2xl');
2498
+ const _2xl_S = breakpoints.smaller('2xl');
2499
+ const _2xl_SE = breakpoints.smallerOrEqual('2xl');
2500
+ const _2xl_GE = breakpoints.greaterOrEqual('2xl');
2501
+ const reactiveBreakpoint = ref('sm');
2502
+ const isGreaterThanBreakpoint = breakpoints.greaterOrEqual(() => reactiveBreakpoint.value);
2503
+ // Define device ranges
2504
+ const mobile = breakpoints.smaller('md'); // <768px
2505
+ const tablet = breakpoints.between('md', 'lg'); // 768px - 1024px
2506
+ const laptop = breakpoints.between('lg', 'xl'); // 1024px - 1280px
2507
+ const desktop = breakpoints.greaterOrEqual('xl'); // >=1280px
2508
+ // Use VueUse for window size
2509
+ const { width: windowWidth, height: windowHeight } = useWindowSize();
2510
+ /**
2511
+ * Logs a styled "chip" message to the console.
2512
+ *
2513
+ * @param {string} label - The label to display inside the chip.
2514
+ * @param {string} backgroundColor - The background color of the chip.
2515
+ * @param {string} textColor - The text color of the chip.
2516
+ */
2517
+ const logChip = (label, backgroundColor, textColor = 'white') => {
2518
+ console.log(`%c ${label} `, `background-color: ${backgroundColor}; color: ${textColor}; border-radius: 16px; padding: 4px 8px;`);
2519
+ };
2520
+ // Singleton logic: Only notify once
2521
+ const notifyOnce = () => {
2522
+ if (notified)
2523
+ return;
2524
+ watch([mobile, tablet, laptop, desktop], ([isMobile, isTablet, isLaptop, isDesktop]) => {
2525
+ if (isMobile) {
2526
+ logChip('Mobile', '#ff6b6b');
2527
+ }
2528
+ else if (isTablet) {
2529
+ logChip('Tablet', '#4ecdc4');
2530
+ }
2531
+ else if (isLaptop) {
2532
+ logChip('Laptop', '#1a535c');
2533
+ }
2534
+ else if (isDesktop) {
2535
+ logChip('Desktop', '#f7fff7', 'black');
2536
+ }
2537
+ notified = true; // Mark as notified
2538
+ }, { immediate: true });
2539
+ };
2540
+ notifyOnce(); // Ensure the notification happens only once globally
2541
+ return {
2542
+ current,
2543
+ active,
2544
+ sm_S,
2545
+ sm_SE,
2546
+ sm_GE,
2547
+ sm_md,
2548
+ sm_xl,
2549
+ sm_2xl,
2550
+ md_S,
2551
+ md_SE,
2552
+ md_GE,
2553
+ md_lg,
2554
+ md_xl,
2555
+ md_2xl,
2556
+ lg_S,
2557
+ lg_SE,
2558
+ lg_GE,
2559
+ lg_xl,
2560
+ lg_2xl,
2561
+ xl_S,
2562
+ xl_SE,
2563
+ xl_GE,
2564
+ xl_2xl,
2565
+ _2xl_S,
2566
+ _2xl_SE,
2567
+ _2xl_GE,
2568
+ isGreaterThanBreakpoint,
2569
+ reactiveBreakpoint,
2570
+ breakpointsTailwind,
2571
+ windowWidth,
2572
+ windowHeight,
2573
+ mobile,
2574
+ tablet,
2575
+ laptop,
2576
+ desktop,
2577
+ breakpoints,
2578
+ };
2409
2579
  }
2410
- // Example usage:
2411
- // console.log(isValidHexColorAlpha('#RRGGBBAA')); // true
2412
- // console.log(isValidHexColorAlpha('#FFFFFF')); // false
2580
+
2413
2581
  /**
2414
- * Validates if a username meets specific criteria.
2415
- * @param {string} username The username to validate.
2416
- * @returns {boolean} True if the username is valid, otherwise false.
2582
+ * A composable function that filters objects based on a specific field and criteria.
2583
+ *
2584
+ * @param {Object[]} items - The list of objects to filter.
2585
+ * @param {Object} filterConfig - Configuration for the filter, specifying the field, the data type to filter, and the criteria.
2586
+ * @param {string} filterConfig.field - The field in the objects to filter.
2587
+ * @param {string} filterConfig.type - The data type of the field ('date', 'string', 'number', 'boolean').
2588
+ * @param {any} filterConfig.criteria - The criteria for filtering. Can be a date range, a string to search, a number range, or a boolean value.
2589
+ *
2590
+ * @returns {Object[]} - The filtered list of objects.
2417
2591
  */
2418
- function isValidUsername(username) {
2419
- const usernamePattern = /^[a-zA-Z0-9_]{3,16}$/; // 3 to 16 characters, letters, numbers, and underscores only
2420
- return usernamePattern.test(username);
2592
+ function useFilter(items, filterConfig) {
2593
+ // Ensure the filterConfig is correctly structured
2594
+ const { field, type, criteria } = filterConfig;
2595
+ // Define the filter functions
2596
+ const filterByDate = (objects, field, { startDate, endDate }) => objects.filter(item => {
2597
+ const date = new Date(item[field]);
2598
+ return date >= new Date(startDate) && date <= new Date(endDate);
2599
+ });
2600
+ const filterByString = (objects, field, searchTerm) => {
2601
+ const normalize = (str) => str.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
2602
+ const words = normalize(searchTerm).split(/\s+/).filter(Boolean);
2603
+ return objects.filter(item => {
2604
+ const normalizedFieldValue = normalize(item[field]);
2605
+ return words.every(word => normalizedFieldValue.includes(word));
2606
+ });
2607
+ };
2608
+ const filterByNumber = (objects, field, { min, max }) => objects.filter(item => {
2609
+ const value = item[field];
2610
+ return value >= min && value <= max;
2611
+ });
2612
+ const filterByBoolean = (objects, field, expectedValue) => objects.filter(item => item[field] === expectedValue);
2613
+ // Apply the appropriate filter and return the filtered objects
2614
+ const filteredObjects = computed(() => {
2615
+ if (!criteria ||
2616
+ (type === 'date' && (!criteria.startDate || !criteria.endDate)) ||
2617
+ (type === 'number' && (criteria.min == null || criteria.max == null)) ||
2618
+ (type === 'boolean' && typeof criteria !== 'boolean')) {
2619
+ return items; // Return all objects if criteria is not valid
2620
+ }
2621
+ // Apply the filter based on the type
2622
+ switch (type) {
2623
+ case 'date':
2624
+ return filterByDate(items, field, criteria);
2625
+ case 'string':
2626
+ return filterByString(items, field, criteria);
2627
+ case 'number':
2628
+ return filterByNumber(items, field, criteria);
2629
+ case 'boolean':
2630
+ return filterByBoolean(items, field, criteria);
2631
+ default:
2632
+ return items;
2633
+ }
2634
+ });
2635
+ return filteredObjects.value;
2421
2636
  }
2422
- // Example usage:
2423
- // console.log(isValidUsername('user_name123')); // true
2424
- // console.log(isValidUsername('us')); // false
2637
+
2425
2638
  /**
2426
- * Validates if a string represents a valid age between 0 and 120.
2427
- * @param {string} age The age to validate.
2428
- * @returns {boolean} True if the age is valid, otherwise false.
2639
+ * Composable to handle pagination logic.
2640
+ *
2641
+ * @param page - Reactive ref for the current page number.
2642
+ * @param total - Reactive ref for total number of items.
2643
+ * @param pageSize - Reactive ref for number of items per page.
2644
+ * @returns An object with pagination controls.
2429
2645
  */
2430
- function isValidAge(age) {
2431
- const ageNumber = parseInt(age, 10);
2432
- return !isNaN(ageNumber) && ageNumber >= 0 && ageNumber <= 120;
2646
+ function usePagination(page, total, pageSize) {
2647
+ // Calculate the total number of pages
2648
+ const totalPages = computed(() => Math.ceil(total.value / pageSize.value));
2649
+ // Determine if the next page can be fetched
2650
+ const canFetchNextPage = () => page.value < totalPages.value;
2651
+ // Determine if the previous page can be fetched
2652
+ const canFetchPreviousPage = () => page.value > 1;
2653
+ return {
2654
+ totalPages,
2655
+ canFetchNextPage,
2656
+ canFetchPreviousPage
2657
+ };
2433
2658
  }
2434
- // Example usage:
2435
- // console.log(isValidAge('25')); // true
2436
- // console.log(isValidAge('121')); // false
2659
+
2437
2660
  /**
2438
- * Validates if a string is a valid hexadecimal number.
2439
- * @param {string} hex The hexadecimal number to validate.
2440
- * @returns {boolean} True if the number is valid, otherwise false.
2661
+ * Custom composable for sorting products based on a selected criterion.
2662
+ * @param {Array} items - The array of products to sort.
2663
+ * @param {Array} criteriaList - The list of available sorting criteria.
2664
+ * @param {Number} selectedCriteria - The currently selected sorting criterion.
2665
+ * @returns {ComputedRef<Array>} - The sorted array of products.
2441
2666
  */
2442
- function isValidHexNumber(hex) {
2443
- const hexPattern = /^[0-9A-Fa-f]+$/;
2444
- return hexPattern.test(hex);
2667
+ function useSorter(items, criteriaList, selectedCriteria) {
2668
+ return computed(() => {
2669
+ const criteria = criteriaList.find(item => item.value === Number(selectedCriteria));
2670
+ if (!criteria)
2671
+ return items;
2672
+ return [...items].sort((a, b) => {
2673
+ const aValue = a[criteria.field];
2674
+ const bValue = b[criteria.field];
2675
+ if (criteria.type === 'number') {
2676
+ const numA = typeof aValue === 'number' ? aValue : parseFloat(aValue) || 0;
2677
+ const numB = typeof bValue === 'number' ? bValue : parseFloat(bValue) || 0;
2678
+ return criteria.order === 'asc' ? numA - numB : numB - numA;
2679
+ }
2680
+ else if (criteria.type === 'date') {
2681
+ const dateA = new Date(aValue).getTime();
2682
+ const dateB = new Date(bValue).getTime();
2683
+ return criteria.order === 'asc' ? dateA - dateB : dateB - dateA;
2684
+ }
2685
+ else if (criteria.type === 'boolean') {
2686
+ const boolA = !!aValue;
2687
+ const boolB = !!bValue;
2688
+ return criteria.order === 'asc' ? (boolA === boolB ? 0 : boolA ? 1 : -1) : (boolA === boolB ? 0 : boolA ? -1 : 1);
2689
+ }
2690
+ else {
2691
+ // Fallback for string sorting
2692
+ const strA = (aValue || '').toString().toLowerCase();
2693
+ const strB = (bValue || '').toString().toLowerCase();
2694
+ return criteria.order === 'asc' ? strA.localeCompare(strB) : strB.localeCompare(strA);
2695
+ }
2696
+ });
2697
+ }).value;
2445
2698
  }
2446
- // Example usage:
2447
- // console.log(isValidHexNumber('1A3F')); // true
2448
- // console.log(isValidHexNumber('GHIJ')); // false
2449
2699
 
2450
- export { APP_LOCAL_CACHE_KEY, APP_SESSION_CACHE_KEY, AxiosService, CacheTypeEnum, ContentTypeEnum, ExceptionEnum, FileMetaTypes, ImageTypes, KeyCodeEnum, LOCALE_KEY, LOCK_INFO_KEY, PROJ_CFG_KEY, ROLES_KEY, RestStd, TOKEN_KEY, USER_INFO_KEY, addCustomKeyboardShortcut, addDays, addDoubleClickListener, addKeyListener, ageAtDate, axiosFetch, blobToFormData, bufferToBlob, calculateAge, clickOutside, compareObject, copyToClipboard, countWords, createFetch, createKeyMap, customShortcut, daysBetween, daysToNextBirthday, debounce, debounceAsync, debounceAsyncValidator, debounceAsyncWithImmediate, debounceLeading, debounceLeadingTrailing, debounceTrailing, deepClone, deepEqual, deepMerge, detectKeyHold, disableCopy, disableF12Key, disableMouseButtons, disableRightClick, disableSpecificKeys, downloadBlob, enableMouseButtons, enableRightClick, enableSpecificKeys, exportToCSV, exportToExcel, exportToJSON, exportToText, exportToXML, filterObjectByKeys, flattenObject, formDataToObject, formatDate, generateRandomString, getEndOfMonth, getObjectDifferences, getObjectKeys, getQueryParam, getStartOfMonth, hasNestedProperties, isEmptyObject, isLeapYear, isStrongPassword, isValidAge, isValidCreditCard, isValidDate, isValidEmail, isValidExpiryDate, isValidHexColor, isValidHexColorAlpha, isValidHexNumber, isValidIP, isValidPhoneNumber, isValidSSN, isValidTime, isValidURL, isValidUsername, isValidZIP, lowerFirst, objectToFormData, objectToFormDataEnhanced, objectToQueryString, openWindow, parseDate, proxyToPlainObject, readFileAsDataURL, readFileAsText, registerKeyboardShortcuts, removeAccent, removeClickOutside, removeCustomKeyboardShortcut, removeCustomShortcuts, removeDoubleClickListener, removeEmptyProperties, removeKeyListeners, replaceAll, reverseString, safeGet, screenEnum, screenMap, scrollToTop, simulateKeyPress, sizeEnum, stopDetectingKeyHold, stringToBlob, subtractDays, throttle, toCamelCase, toKebabCase, toggleTabNavigation, truncateString, unregisterKeyboardShortcuts, upperFirst, useBreakpoint, useFilter, usePagination, useSorter, useVueQuery, validateAlphanumeric, validateLetters, validateNumbers };
2700
+ export { APP_LOCAL_CACHE_KEY, APP_SESSION_CACHE_KEY, AxiosService, CacheTypeEnum, ContentTypeEnum, ERROR_MESSAGES, ERROR_STYLES, ExceptionEnum, FileMetaTypes, ImageTypes, KeyCodeEnum, LOCALE_KEY, LOCK_INFO_KEY, PROJ_CFG_KEY, ROLES_KEY, RestStd, TOKEN_KEY, USER_INFO_KEY, addCustomKeyboardShortcut, addDays, addDoubleClickListener, addKeyListener, ageAtDate, axiosFetch, blobToFormData, bufferToBlob, calculateAge, clickOutside, compareObject, configureAxios, configureEndpoints, configureTokenKeys, copyToClipboard, countWords, createCustomAxiosInstance, createFetch, createKeyMap, customShortcut, daysBetween, daysToNextBirthday, debounce, debounceAsync, debounceAsyncValidator, debounceAsyncWithImmediate, debounceLeading, debounceLeadingTrailing, debounceTrailing, deepClone, deepEqual, deepMerge, detectKeyHold, disableCopy, disableF12Key, disableMouseButtons, disableRightClick, disableSpecificKeys, downloadBlob, enableMouseButtons, enableRightClick, enableSpecificKeys, exportToCSV, exportToExcel, exportToJSON, exportToText, exportToXML, filterObjectByKeys, flattenObject, formDataToObject, formatDate, generateRandomString, getAxiosInstance, getEndOfMonth, getEndpointsConfig, getObjectDifferences, getObjectKeys, getQueryParam, getStartOfMonth, getTokenConfig, hasNestedProperties, isEmptyObject, isLeapYear, isStrongPassword, isValidAge, isValidCreditCard, isValidDate, isValidEmail, isValidExpiryDate, isValidHexColor, isValidHexColorAlpha, isValidHexNumber, isValidIP, isValidPhoneNumber, isValidSSN, isValidTime, isValidURL, isValidUsername, isValidZIP, lowerFirst, objectToFormData, objectToFormDataEnhanced, objectToQueryString, openWindow, parseDate, proxyToPlainObject, readFileAsDataURL, readFileAsText, registerKeyboardShortcuts, removeAccent, removeClickOutside, removeCustomKeyboardShortcut, removeCustomShortcuts, removeDoubleClickListener, removeEmptyProperties, removeKeyListeners, replaceAll, reverseString, safeGet, screenEnum, screenMap, scrollToTop, simulateKeyPress, sizeEnum, stopDetectingKeyHold, stringToBlob, subtractDays, throttle, toCamelCase, toKebabCase, toggleTabNavigation, truncateString, unregisterKeyboardShortcuts, upperFirst, useBreakpoint, useFilter, usePagination, useSorter, useVueQuery, validateAlphanumeric, validateLetters, validateNumbers };