@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/composables/auth/useAuth.d.ts +29 -0
- package/dist/composables/axios/createFetch.d.ts +2 -3
- package/dist/config/axios/axiosConfig.d.ts +3 -5
- package/dist/config/axios/axiosInstance.d.ts +19 -0
- package/dist/config/axios/index.d.ts +1 -0
- package/dist/config/global/endpointsConfig.d.ts +24 -0
- package/dist/config/global/index.d.ts +2 -0
- package/dist/config/global/tokenConfig.d.ts +15 -0
- package/dist/config/index.d.ts +1 -0
- package/dist/constants/errorsEnum.d.ts +3 -0
- package/dist/constants/index.d.ts +1 -0
- package/dist/index.mjs +2311 -2061
- package/dist/rest/RestStd.d.ts +22 -5
- package/dist/types/ErrorType.d.ts +8 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/utils/errors.d.ts +10 -0
- package/package.json +17 -12
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
|
-
*
|
|
8
|
-
*
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* @
|
|
123
|
-
* @param {
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
*
|
|
137
|
-
*
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
*
|
|
493
|
-
*
|
|
494
|
-
* @param {
|
|
495
|
-
* @param {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
550
|
-
*
|
|
551
|
-
* @param
|
|
552
|
-
* @param
|
|
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
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
*
|
|
572
|
-
* @param {
|
|
573
|
-
* @param {
|
|
574
|
-
* @param {
|
|
575
|
-
* @
|
|
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
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
*
|
|
612
|
-
* @
|
|
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
*
|
|
624
|
-
*
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
*
|
|
637
|
-
* @
|
|
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
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
-
*
|
|
649
|
-
* @readonly
|
|
201
|
+
* Disables the right-click context menu on the window.
|
|
650
202
|
*/
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
-
*
|
|
680
|
-
* @
|
|
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
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
-
*
|
|
696
|
-
* @readonly
|
|
235
|
+
* Enables all previously disabled mouse buttons.
|
|
697
236
|
*/
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
*
|
|
706
|
-
* @
|
|
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
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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
|
-
*
|
|
728
|
-
* @
|
|
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
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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
|
-
*
|
|
739
|
-
* @
|
|
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
|
-
|
|
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
|
-
*
|
|
744
|
-
* @
|
|
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
|
-
|
|
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
|
-
*
|
|
749
|
-
* @constant {string}
|
|
295
|
+
* Disables the F12 key and certain key combinations for developer tools.
|
|
750
296
|
*/
|
|
751
|
-
|
|
297
|
+
function disableF12Key() {
|
|
298
|
+
const handler = function (event) {
|
|
299
|
+
return true;
|
|
300
|
+
};
|
|
301
|
+
document.addEventListener('keydown', handler);
|
|
302
|
+
}
|
|
752
303
|
/**
|
|
753
|
-
*
|
|
754
|
-
* @
|
|
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
|
-
|
|
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
|
-
*
|
|
759
|
-
* @constant {string}
|
|
322
|
+
* Disables the copy (Ctrl + C) functionality on the page.
|
|
760
323
|
*/
|
|
761
|
-
|
|
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
|
-
*
|
|
764
|
-
* @
|
|
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
|
-
|
|
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
|
-
*
|
|
769
|
-
* @
|
|
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
|
-
|
|
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
|
-
*
|
|
774
|
-
* @
|
|
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
|
-
|
|
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
|
-
*
|
|
779
|
-
* @readonly
|
|
380
|
+
* Enables keys that were previously disabled using disableSpecificKeys.
|
|
780
381
|
*/
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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
|
-
*
|
|
790
|
-
* @param {
|
|
791
|
-
* @param {
|
|
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
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
//
|
|
802
|
-
|
|
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
|
-
*
|
|
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
|
|
812
|
-
const
|
|
813
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
827
|
-
* @param {string
|
|
828
|
-
* @param {
|
|
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
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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
|
-
*
|
|
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
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
463
|
+
function stopDetectingKeyHold() {
|
|
464
|
+
for (const handler of detectKeyHold.handlers) {
|
|
465
|
+
document.removeEventListener('keydown', handler);
|
|
466
|
+
}
|
|
467
|
+
detectKeyHold.handlers = [];
|
|
867
468
|
}
|
|
868
469
|
/**
|
|
869
|
-
*
|
|
870
|
-
* @
|
|
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
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
918
|
-
* @param {string}
|
|
919
|
-
* @param {
|
|
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
|
|
925
|
-
const
|
|
926
|
-
const
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
941
|
-
|
|
942
|
-
|
|
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
|
-
*
|
|
956
|
-
* @param {
|
|
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
|
|
959
|
-
const
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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
|
-
|
|
561
|
+
return null;
|
|
970
562
|
}
|
|
971
563
|
/**
|
|
972
|
-
*
|
|
973
|
-
* @param {
|
|
974
|
-
* @
|
|
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
|
|
977
|
-
const
|
|
978
|
-
|
|
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
|
-
*
|
|
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
|
|
985
|
-
const
|
|
986
|
-
|
|
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
|
-
*
|
|
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
|
|
995
|
-
|
|
996
|
-
|
|
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
|
-
*
|
|
1001
|
-
* @param {
|
|
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
|
|
1004
|
-
|
|
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
|
-
*
|
|
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
|
|
1019
|
-
|
|
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
|
-
*
|
|
1027
|
-
* @param {
|
|
1028
|
-
* @
|
|
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
|
|
1031
|
-
|
|
623
|
+
function getStartOfMonth(date) {
|
|
624
|
+
return new Date(date.getFullYear(), date.getMonth(), 1);
|
|
1032
625
|
}
|
|
1033
626
|
/**
|
|
1034
|
-
*
|
|
1035
|
-
* @param {
|
|
1036
|
-
* @
|
|
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
|
|
1039
|
-
|
|
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
|
-
*
|
|
1045
|
-
* @param {
|
|
1046
|
-
* @
|
|
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
|
|
1049
|
-
const
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
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
|
-
*
|
|
1061
|
-
* @param {
|
|
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
|
|
1064
|
-
const
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
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
|
-
*
|
|
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
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
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
|
-
*
|
|
1086
|
-
*
|
|
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
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
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
|
-
*
|
|
1113
|
-
* @param {
|
|
1114
|
-
* @param {
|
|
1115
|
-
* @
|
|
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
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
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
|
-
*
|
|
1130
|
-
* @param {
|
|
1131
|
-
* @param {
|
|
1132
|
-
* @
|
|
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
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
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
|
-
*
|
|
1146
|
-
* @param {
|
|
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
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
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
|
-
*
|
|
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
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
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
|
-
*
|
|
1171
|
-
* @param {
|
|
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
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
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
|
-
*
|
|
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
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
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
|
-
*
|
|
1201
|
-
* @param {
|
|
1202
|
-
* @
|
|
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
|
|
1205
|
-
const
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
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
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
896
|
+
else {
|
|
897
|
+
obj[key] = value;
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
return obj;
|
|
1214
901
|
}
|
|
1215
|
-
addKeyListener.handlers = [];
|
|
1216
902
|
/**
|
|
1217
|
-
*
|
|
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
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
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
|
-
*
|
|
1227
|
-
* @param {
|
|
1228
|
-
* @
|
|
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
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
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
|
-
*
|
|
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
|
|
1245
|
-
|
|
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
|
-
*
|
|
1252
|
-
* @
|
|
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
|
|
1255
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
1277
|
-
* @param {
|
|
1278
|
-
* @param {
|
|
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
|
|
1281
|
-
const
|
|
1282
|
-
|
|
1283
|
-
|
|
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
|
-
|
|
975
|
+
|
|
1297
976
|
/**
|
|
1298
|
-
*
|
|
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
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
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
|
-
|
|
988
|
+
return plainObject;
|
|
1306
989
|
}
|
|
1307
990
|
/**
|
|
1308
|
-
*
|
|
1309
|
-
* @param {
|
|
1310
|
-
* @param {
|
|
1311
|
-
* @
|
|
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
|
|
1315
|
-
|
|
1316
|
-
|
|
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
|
-
*
|
|
1328
|
-
* @param {
|
|
1329
|
-
* @
|
|
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
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
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
|
|
1023
|
+
return true;
|
|
1343
1024
|
}
|
|
1344
1025
|
/**
|
|
1345
|
-
*
|
|
1346
|
-
* @param {
|
|
1347
|
-
* @
|
|
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
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
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
|
-
*
|
|
1363
|
-
* @param {
|
|
1364
|
-
* @
|
|
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
|
|
1368
|
-
|
|
1369
|
-
|
|
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
|
-
*
|
|
1373
|
-
* @param {
|
|
1374
|
-
* @param {
|
|
1375
|
-
* @returns {
|
|
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
|
|
1378
|
-
const
|
|
1379
|
-
|
|
1380
|
-
|
|
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
|
-
*
|
|
1384
|
-
* @param {
|
|
1385
|
-
* @param {
|
|
1386
|
-
* @returns {
|
|
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
|
|
1389
|
-
|
|
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
|
-
*
|
|
1393
|
-
* @param {
|
|
1394
|
-
* @
|
|
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
|
|
1397
|
-
|
|
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
|
-
*
|
|
1401
|
-
* @param {
|
|
1402
|
-
* @returns {
|
|
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
|
|
1405
|
-
return
|
|
1126
|
+
function isEmptyObject(obj) {
|
|
1127
|
+
return Object.keys(obj).length === 0;
|
|
1406
1128
|
}
|
|
1407
1129
|
/**
|
|
1408
|
-
*
|
|
1409
|
-
* @param {
|
|
1410
|
-
* @
|
|
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
|
|
1413
|
-
return
|
|
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
|
-
*
|
|
1417
|
-
* @param {
|
|
1418
|
-
* @returns {
|
|
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
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
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
|
-
*
|
|
1431
|
-
* @param {
|
|
1432
|
-
* @returns {
|
|
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
|
|
1435
|
-
|
|
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
|
-
*
|
|
1446
|
-
* @param {
|
|
1447
|
-
* @
|
|
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
|
|
1451
|
-
|
|
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
|
-
*
|
|
1461
|
-
*
|
|
1462
|
-
* @param
|
|
1463
|
-
* @param
|
|
1464
|
-
* @returns
|
|
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
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1508
|
-
|
|
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
|
-
*
|
|
1518
|
-
*
|
|
1519
|
-
* @param
|
|
1520
|
-
* @
|
|
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
|
|
1523
|
-
|
|
1524
|
-
let
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
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
|
-
|
|
1537
|
-
if (
|
|
1538
|
-
|
|
1539
|
-
|
|
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
|
-
*
|
|
1553
|
-
* @param {
|
|
1554
|
-
* @param {
|
|
1555
|
-
* @
|
|
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
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
|
|
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
|
-
*
|
|
1577
|
-
* @param {
|
|
1578
|
-
* @
|
|
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
|
|
1582
|
-
|
|
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
|
-
*
|
|
1598
|
-
* @param {
|
|
1599
|
-
* @
|
|
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
|
|
1603
|
-
|
|
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
|
-
*
|
|
1627
|
-
* @param {
|
|
1628
|
-
* @
|
|
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
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
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
|
-
*
|
|
1647
|
-
* @param {
|
|
1648
|
-
* @
|
|
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
|
|
1652
|
-
|
|
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
|
-
*
|
|
1664
|
-
* @param {
|
|
1665
|
-
* @returns {
|
|
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
|
|
1668
|
-
|
|
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
|
-
*
|
|
1685
|
-
* @param {
|
|
1686
|
-
* @
|
|
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
|
|
1689
|
-
return
|
|
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
|
-
*
|
|
1698
|
-
* @param {
|
|
1699
|
-
* @returns {
|
|
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
|
|
1702
|
-
return
|
|
1703
|
-
|
|
1704
|
-
|
|
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
|
-
*
|
|
1711
|
-
* @param {string}
|
|
1712
|
-
* @
|
|
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
|
|
1716
|
-
return
|
|
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
|
-
*
|
|
1720
|
-
* @param {
|
|
1721
|
-
* @
|
|
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
|
|
1725
|
-
|
|
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
|
-
*
|
|
1729
|
-
* @param {
|
|
1730
|
-
* @
|
|
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
|
|
1733
|
-
const
|
|
1734
|
-
const
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
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
|
-
*
|
|
1746
|
-
* @param {
|
|
1747
|
-
* @
|
|
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
|
|
1752
|
-
const
|
|
1753
|
-
|
|
1754
|
-
|
|
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
|
-
*
|
|
1759
|
-
* @param {
|
|
1760
|
-
* @returns {
|
|
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
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
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
|
-
|
|
1439
|
+
e.preventDefault();
|
|
1440
|
+
return false;
|
|
1770
1441
|
}
|
|
1442
|
+
// Example usage:
|
|
1443
|
+
// document.addEventListener('keydown', validateNumbers);
|
|
1771
1444
|
/**
|
|
1772
|
-
*
|
|
1773
|
-
* @param {
|
|
1774
|
-
* @
|
|
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
|
|
1778
|
-
|
|
1779
|
-
|
|
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
|
-
*
|
|
1784
|
-
* @param {
|
|
1785
|
-
* @
|
|
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
|
|
1789
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
1808
|
-
* @param {
|
|
1809
|
-
* @returns {
|
|
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
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
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
|
-
*
|
|
1834
|
-
* @param {
|
|
1835
|
-
* @returns {
|
|
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
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
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
|
-
//
|
|
1515
|
+
// console.log(isStrongPassword('Strong1@password')); // true
|
|
1516
|
+
// console.log(isStrongPassword('weakpass')); // false
|
|
1844
1517
|
/**
|
|
1845
|
-
*
|
|
1846
|
-
* @param {
|
|
1847
|
-
* @
|
|
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
|
|
1851
|
-
const
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
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
|
|
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
|
-
*
|
|
1862
|
-
* @param {
|
|
1863
|
-
* @
|
|
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
|
|
1867
|
-
const
|
|
1868
|
-
|
|
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
|
-
//
|
|
1551
|
+
// console.log(isValidHexColor('#FFFFFF')); // true
|
|
1552
|
+
// console.log(isValidHexColor('#FFF')); // true
|
|
1553
|
+
// console.log(isValidHexColor('#12345G')); // false
|
|
1877
1554
|
/**
|
|
1878
|
-
*
|
|
1879
|
-
* @param {
|
|
1880
|
-
* @
|
|
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
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
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
|
-
*
|
|
1904
|
-
* @param {
|
|
1905
|
-
* @returns {boolean} True if the
|
|
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
|
|
1908
|
-
|
|
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
|
-
*
|
|
1912
|
-
* @param {
|
|
1913
|
-
* @
|
|
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
|
|
1917
|
-
|
|
1595
|
+
function isValidZIP(zip) {
|
|
1596
|
+
const zipPattern = /^\d{5}(-\d{4})?$/;
|
|
1597
|
+
return zipPattern.test(zip);
|
|
1918
1598
|
}
|
|
1919
1599
|
// Example usage:
|
|
1920
|
-
//
|
|
1921
|
-
//
|
|
1600
|
+
// console.log(isValidZIP('12345')); // true
|
|
1601
|
+
// console.log(isValidZIP('12345-6789')); // true
|
|
1602
|
+
// console.log(isValidZIP('1234')); // false
|
|
1922
1603
|
/**
|
|
1923
|
-
*
|
|
1924
|
-
* @param {
|
|
1925
|
-
* @returns {
|
|
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
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
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
|
-
//
|
|
1618
|
+
// console.log(isValidExpiryDate('08/24')); // true
|
|
1619
|
+
// console.log(isValidExpiryDate('12/22')); // false
|
|
1937
1620
|
/**
|
|
1938
|
-
*
|
|
1939
|
-
* @param {
|
|
1940
|
-
* @returns {
|
|
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
|
|
1943
|
-
|
|
1625
|
+
function isValidHexColorAlpha(color) {
|
|
1626
|
+
const hexPattern = /^#([0-9A-Fa-f]{8})$/;
|
|
1627
|
+
return hexPattern.test(color);
|
|
1944
1628
|
}
|
|
1945
1629
|
// Example usage:
|
|
1946
|
-
//
|
|
1630
|
+
// console.log(isValidHexColorAlpha('#RRGGBBAA')); // true
|
|
1631
|
+
// console.log(isValidHexColorAlpha('#FFFFFF')); // false
|
|
1947
1632
|
/**
|
|
1948
|
-
*
|
|
1949
|
-
* @param {
|
|
1950
|
-
* @returns {boolean} True if
|
|
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
|
|
1953
|
-
|
|
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
|
-
//
|
|
1957
|
-
//
|
|
1642
|
+
// console.log(isValidUsername('user_name123')); // true
|
|
1643
|
+
// console.log(isValidUsername('us')); // false
|
|
1958
1644
|
/**
|
|
1959
|
-
*
|
|
1960
|
-
* @param {
|
|
1961
|
-
* @
|
|
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
|
|
1966
|
-
|
|
1967
|
-
|
|
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
|
-
//
|
|
1654
|
+
// console.log(isValidAge('25')); // true
|
|
1655
|
+
// console.log(isValidAge('121')); // false
|
|
1979
1656
|
/**
|
|
1980
|
-
*
|
|
1981
|
-
*
|
|
1982
|
-
* @
|
|
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
|
-
|
|
1988
|
-
const
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
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
|
-
*
|
|
2016
|
-
* @
|
|
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
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
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
|
-
*
|
|
2040
|
-
* @
|
|
2041
|
-
* @returns {string} The capitalized string.
|
|
1797
|
+
* Key used for storing the authentication token.
|
|
1798
|
+
* @constant {string}
|
|
2042
1799
|
*/
|
|
2043
|
-
|
|
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
|
-
*
|
|
2050
|
-
* @
|
|
2051
|
-
* @returns {string} The lowercased string.
|
|
1802
|
+
* Key used for storing the locale information.
|
|
1803
|
+
* @constant {string}
|
|
2052
1804
|
*/
|
|
2053
|
-
|
|
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
|
-
*
|
|
2060
|
-
* @
|
|
2061
|
-
* @returns {string} The processed string.
|
|
1807
|
+
* Key used for storing user information.
|
|
1808
|
+
* @constant {string}
|
|
2062
1809
|
*/
|
|
2063
|
-
|
|
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
|
-
*
|
|
2076
|
-
* @
|
|
2077
|
-
* @returns {string} The reversed string.
|
|
1812
|
+
* Key used for storing role information.
|
|
1813
|
+
* @constant {string}
|
|
2078
1814
|
*/
|
|
2079
|
-
|
|
2080
|
-
|
|
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
|
-
|
|
2083
|
-
// console.log(reverseString('hello')); // 'olleh'
|
|
2047
|
+
|
|
2084
2048
|
/**
|
|
2085
|
-
*
|
|
2086
|
-
*
|
|
2087
|
-
* @
|
|
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
|
|
2090
|
-
return
|
|
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
|
-
|
|
2093
|
-
// console.log(countWords('Hello world!')); // 2
|
|
2063
|
+
|
|
2094
2064
|
/**
|
|
2095
|
-
*
|
|
2096
|
-
* @param
|
|
2097
|
-
* @param
|
|
2098
|
-
* @
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
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
|
-
|
|
2104
|
-
|
|
2119
|
+
|
|
2120
|
+
let tokenConfig = {
|
|
2121
|
+
ACCESS_TOKEN: 'authToken',
|
|
2122
|
+
REFRESH_TOKEN: 'refreshToken',
|
|
2123
|
+
};
|
|
2105
2124
|
/**
|
|
2106
|
-
*
|
|
2107
|
-
*
|
|
2108
|
-
* @
|
|
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
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
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
|
-
*
|
|
2119
|
-
*
|
|
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
|
|
2123
|
-
return
|
|
2124
|
-
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
2125
|
-
.replace(/[\s_]+/g, '-')
|
|
2126
|
-
.toLowerCase();
|
|
2140
|
+
function getTokenConfig() {
|
|
2141
|
+
return tokenConfig;
|
|
2127
2142
|
}
|
|
2128
|
-
|
|
2129
|
-
|
|
2143
|
+
|
|
2144
|
+
let endpointsConfig = {
|
|
2145
|
+
LOGIN: '/login',
|
|
2146
|
+
REFRESH: '/refresh',
|
|
2147
|
+
LOGOUT: '/logout',
|
|
2148
|
+
};
|
|
2130
2149
|
/**
|
|
2131
|
-
*
|
|
2132
|
-
*
|
|
2133
|
-
*
|
|
2134
|
-
* @param {string}
|
|
2135
|
-
* @
|
|
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
|
|
2138
|
-
|
|
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
|
-
*
|
|
2144
|
-
*
|
|
2145
|
-
* @returns {
|
|
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
|
|
2148
|
-
|
|
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
|
-
*
|
|
2156
|
-
*
|
|
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
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
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
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
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
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
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
|
-
|
|
2221
|
-
|
|
2222
|
-
}
|
|
2223
|
-
|
|
2224
|
-
|
|
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
|
-
|
|
2260
|
-
|
|
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
|
-
|
|
2264
|
-
|
|
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
|
-
*
|
|
2284
|
-
* @param {string}
|
|
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
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
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
|
-
*
|
|
2300
|
-
* @
|
|
2301
|
-
* @
|
|
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
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
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
|
|
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
|
-
*
|
|
2361
|
-
* @param {string}
|
|
2362
|
-
* @
|
|
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
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
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
|
-
*
|
|
2373
|
-
* @param
|
|
2374
|
-
* @
|
|
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
|
|
2377
|
-
const
|
|
2378
|
-
return
|
|
2339
|
+
function createFetch(fetchComposable, axiosCustomInstance) {
|
|
2340
|
+
const instance = axiosCustomInstance || getAxiosInstance();
|
|
2341
|
+
return (axiosRequestConfig, options) => {
|
|
2342
|
+
return fetchComposable(instance, axiosRequestConfig, options);
|
|
2343
|
+
};
|
|
2379
2344
|
}
|
|
2380
|
-
|
|
2381
|
-
// console.log(isValidZIP('12345')); // true
|
|
2382
|
-
// console.log(isValidZIP('12345-6789')); // true
|
|
2383
|
-
// console.log(isValidZIP('1234')); // false
|
|
2345
|
+
|
|
2384
2346
|
/**
|
|
2385
|
-
*
|
|
2386
|
-
*
|
|
2387
|
-
* @
|
|
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
|
|
2390
|
-
const
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
const
|
|
2394
|
-
const
|
|
2395
|
-
const
|
|
2396
|
-
|
|
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
|
-
|
|
2399
|
-
|
|
2400
|
-
// console.log(isValidExpiryDate('12/22')); // false
|
|
2465
|
+
|
|
2466
|
+
let notified = false;
|
|
2401
2467
|
/**
|
|
2402
|
-
*
|
|
2403
|
-
*
|
|
2404
|
-
* @returns {
|
|
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
|
|
2407
|
-
const
|
|
2408
|
-
|
|
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
|
-
|
|
2411
|
-
// console.log(isValidHexColorAlpha('#RRGGBBAA')); // true
|
|
2412
|
-
// console.log(isValidHexColorAlpha('#FFFFFF')); // false
|
|
2580
|
+
|
|
2413
2581
|
/**
|
|
2414
|
-
*
|
|
2415
|
-
*
|
|
2416
|
-
* @
|
|
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
|
|
2419
|
-
|
|
2420
|
-
|
|
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
|
-
|
|
2423
|
-
// console.log(isValidUsername('user_name123')); // true
|
|
2424
|
-
// console.log(isValidUsername('us')); // false
|
|
2637
|
+
|
|
2425
2638
|
/**
|
|
2426
|
-
*
|
|
2427
|
-
*
|
|
2428
|
-
* @
|
|
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
|
|
2431
|
-
|
|
2432
|
-
|
|
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
|
-
|
|
2435
|
-
// console.log(isValidAge('25')); // true
|
|
2436
|
-
// console.log(isValidAge('121')); // false
|
|
2659
|
+
|
|
2437
2660
|
/**
|
|
2438
|
-
*
|
|
2439
|
-
* @param {
|
|
2440
|
-
* @
|
|
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
|
|
2443
|
-
|
|
2444
|
-
|
|
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 };
|