onelaraveljs 1.0.0
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/README.md +87 -0
- package/docs/integration_analysis.md +116 -0
- package/docs/onejs_analysis.md +108 -0
- package/docs/optimization_implementation_group2.md +458 -0
- package/docs/optimization_plan.md +130 -0
- package/index.js +16 -0
- package/package.json +13 -0
- package/src/app.js +61 -0
- package/src/core/API.js +72 -0
- package/src/core/ChildrenRegistry.js +410 -0
- package/src/core/DOMBatcher.js +207 -0
- package/src/core/ErrorBoundary.js +226 -0
- package/src/core/EventDelegator.js +416 -0
- package/src/core/Helper.js +817 -0
- package/src/core/LoopContext.js +97 -0
- package/src/core/OneDOM.js +246 -0
- package/src/core/OneMarkup.js +444 -0
- package/src/core/Router.js +996 -0
- package/src/core/SEOConfig.js +321 -0
- package/src/core/SectionEngine.js +75 -0
- package/src/core/TemplateEngine.js +83 -0
- package/src/core/View.js +273 -0
- package/src/core/ViewConfig.js +229 -0
- package/src/core/ViewController.js +1410 -0
- package/src/core/ViewControllerOptimized.js +164 -0
- package/src/core/ViewIdentifier.js +361 -0
- package/src/core/ViewLoader.js +272 -0
- package/src/core/ViewManager.js +1962 -0
- package/src/core/ViewState.js +761 -0
- package/src/core/ViewSystem.js +301 -0
- package/src/core/ViewTemplate.js +4 -0
- package/src/core/helpers/BindingHelper.js +239 -0
- package/src/core/helpers/ConfigHelper.js +37 -0
- package/src/core/helpers/EventHelper.js +172 -0
- package/src/core/helpers/LifecycleHelper.js +17 -0
- package/src/core/helpers/ReactiveHelper.js +169 -0
- package/src/core/helpers/RenderHelper.js +15 -0
- package/src/core/helpers/ResourceHelper.js +89 -0
- package/src/core/helpers/TemplateHelper.js +11 -0
- package/src/core/managers/BindingManager.js +671 -0
- package/src/core/managers/ConfigurationManager.js +136 -0
- package/src/core/managers/EventManager.js +309 -0
- package/src/core/managers/LifecycleManager.js +356 -0
- package/src/core/managers/ReactiveManager.js +334 -0
- package/src/core/managers/RenderEngine.js +292 -0
- package/src/core/managers/ResourceManager.js +441 -0
- package/src/core/managers/ViewHierarchyManager.js +258 -0
- package/src/core/managers/ViewTemplateManager.js +127 -0
- package/src/core/reactive/ReactiveComponent.js +592 -0
- package/src/core/services/EventService.js +418 -0
- package/src/core/services/HttpService.js +106 -0
- package/src/core/services/LoggerService.js +57 -0
- package/src/core/services/StateService.js +512 -0
- package/src/core/services/StorageService.js +856 -0
- package/src/core/services/StoreService.js +258 -0
- package/src/core/services/TemplateDetectorService.js +361 -0
- package/src/core/services/Test.js +18 -0
- package/src/helpers/devWarnings.js +205 -0
- package/src/helpers/performance.js +226 -0
- package/src/helpers/utils.js +287 -0
- package/src/init.js +343 -0
- package/src/plugins/auto-plugin.js +34 -0
- package/src/services/Test.js +18 -0
- package/src/types/index.js +193 -0
- package/src/utils/date-helper.js +51 -0
- package/src/utils/helpers.js +39 -0
- package/src/utils/validation.js +32 -0
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ViewHelper - Utility functions for view operations
|
|
3
|
+
* Contains helper methods that are not directly related to view management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { uniqId } from '../helpers/utils.js';
|
|
7
|
+
import DOMBatcher from './DOMBatcher.js';
|
|
8
|
+
|
|
9
|
+
export class Helper {
|
|
10
|
+
constructor(App = null) {
|
|
11
|
+
this.App = App;
|
|
12
|
+
this.config = {};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
setApp(App) {
|
|
16
|
+
this.App = App;
|
|
17
|
+
}
|
|
18
|
+
setConfig(config) {
|
|
19
|
+
let { base_url } = config;
|
|
20
|
+
if (base_url && base_url.endsWith('/')) {
|
|
21
|
+
base_url = base_url.slice(0, -1);
|
|
22
|
+
}
|
|
23
|
+
this.config = {
|
|
24
|
+
...this.config,
|
|
25
|
+
base_url
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get URL
|
|
31
|
+
* @param {string} path - Path to append to base URL
|
|
32
|
+
* @returns {string} Full URL
|
|
33
|
+
*/
|
|
34
|
+
url(path = '') {
|
|
35
|
+
const baseUrl = typeof this.config?.base_url === 'string' ? this.config.base_url : '';
|
|
36
|
+
return `${baseUrl}${path ? (path.startsWith('/') ? path : `/${path}`) : ''}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Trim string
|
|
41
|
+
* @param {string} str - String to trim
|
|
42
|
+
* @param {string} char - Character to trim
|
|
43
|
+
* @returns {string} Trimmed string
|
|
44
|
+
*/
|
|
45
|
+
trim(str, char = ' ') {
|
|
46
|
+
if (typeof str !== 'string') return str;
|
|
47
|
+
const regex = new RegExp(`^${char}+|${char}+$`, 'g');
|
|
48
|
+
return str.replace(regex, '');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if string is HTML
|
|
53
|
+
* @param {string} str - String to check
|
|
54
|
+
* @returns {boolean} True if HTML
|
|
55
|
+
*/
|
|
56
|
+
isHtmlString(str) {
|
|
57
|
+
if (typeof str !== 'string') return false;
|
|
58
|
+
return /<[a-z][\s\S]*>/i.test(str);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
substr(str, start, length) {
|
|
62
|
+
if (typeof str !== 'string') return str;
|
|
63
|
+
return str.substring(start, length);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
strtolower(str) {
|
|
67
|
+
if (typeof str !== 'string') return str;
|
|
68
|
+
return str.toLowerCase();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
strtoupper(str) {
|
|
72
|
+
if (typeof str !== 'string') return str;
|
|
73
|
+
return str.toUpperCase();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Format date
|
|
79
|
+
* @param {Date|string} date - Date to format
|
|
80
|
+
* @param {string} format - Format string
|
|
81
|
+
* @returns {string} Formatted date
|
|
82
|
+
*/
|
|
83
|
+
formatDate(date, format = 'YYYY-MM-DD') {
|
|
84
|
+
const d = new Date(date);
|
|
85
|
+
if (isNaN(d.getTime())) return '';
|
|
86
|
+
|
|
87
|
+
const year = d.getFullYear();
|
|
88
|
+
const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
89
|
+
const day = String(d.getDate()).padStart(2, '0');
|
|
90
|
+
const hours = String(d.getHours()).padStart(2, '0');
|
|
91
|
+
const minutes = String(d.getMinutes()).padStart(2, '0');
|
|
92
|
+
const seconds = String(d.getSeconds()).padStart(2, '0');
|
|
93
|
+
|
|
94
|
+
return format
|
|
95
|
+
.replace('YYYY', year)
|
|
96
|
+
.replace('MM', month)
|
|
97
|
+
.replace('DD', day)
|
|
98
|
+
.replace('HH', hours)
|
|
99
|
+
.replace('mm', minutes)
|
|
100
|
+
.replace('ss', seconds);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Format number
|
|
105
|
+
* @param {number} num - Number to format
|
|
106
|
+
* @param {Object} options - Formatting options
|
|
107
|
+
* @returns {string} Formatted number
|
|
108
|
+
*/
|
|
109
|
+
formatNumber(num, options = {}) {
|
|
110
|
+
if (typeof num !== 'number' || isNaN(num)) return '0';
|
|
111
|
+
|
|
112
|
+
const {
|
|
113
|
+
decimals = 2,
|
|
114
|
+
thousandsSeparator = ',',
|
|
115
|
+
decimalSeparator = '.',
|
|
116
|
+
prefix = '',
|
|
117
|
+
suffix = ''
|
|
118
|
+
} = options;
|
|
119
|
+
|
|
120
|
+
const parts = num.toFixed(decimals).split('.');
|
|
121
|
+
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thousandsSeparator);
|
|
122
|
+
|
|
123
|
+
return prefix + parts.join(decimalSeparator) + suffix;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* PHP-like number_format function
|
|
128
|
+
* @param {number} number - Number to format
|
|
129
|
+
* @param {number} decimals - Number of decimal points (default: 0)
|
|
130
|
+
* @param {string} decimalSeparator - Decimal separator (default: '.')
|
|
131
|
+
* @param {string} thousandsSeparator - Thousands separator (default: ',')
|
|
132
|
+
* @returns {string} Formatted number
|
|
133
|
+
*/
|
|
134
|
+
number_format(number, decimals = 0, decimalSeparator = '.', thousandsSeparator = ',') {
|
|
135
|
+
// Convert to number if string
|
|
136
|
+
const num = typeof number === 'string' ? parseFloat(number) : number;
|
|
137
|
+
|
|
138
|
+
if (typeof num !== 'number' || isNaN(num)) return '0';
|
|
139
|
+
|
|
140
|
+
// Use toFixed for decimal precision
|
|
141
|
+
const parts = num.toFixed(decimals).split('.');
|
|
142
|
+
|
|
143
|
+
// Add thousands separator
|
|
144
|
+
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thousandsSeparator);
|
|
145
|
+
|
|
146
|
+
// Join integer and decimal parts
|
|
147
|
+
return decimals > 0 ? parts.join(decimalSeparator) : parts[0];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Format currency
|
|
152
|
+
* @param {number} amount - Amount to format
|
|
153
|
+
* @param {string} currency - Currency code
|
|
154
|
+
* @returns {string} Formatted currency
|
|
155
|
+
*/
|
|
156
|
+
formatCurrency(amount, currency = 'USD') {
|
|
157
|
+
const currencySymbols = {
|
|
158
|
+
'USD': '$',
|
|
159
|
+
'EUR': '€',
|
|
160
|
+
'GBP': '£',
|
|
161
|
+
'JPY': '¥',
|
|
162
|
+
'VND': '₫'
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const symbol = currencySymbols[currency] || currency;
|
|
166
|
+
return symbol + this.formatNumber(amount, { decimals: 2 });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Truncate text
|
|
171
|
+
* @param {string} text - Text to truncate
|
|
172
|
+
* @param {number} length - Maximum length
|
|
173
|
+
* @param {string} suffix - Suffix to add
|
|
174
|
+
* @returns {string} Truncated text
|
|
175
|
+
*/
|
|
176
|
+
truncate(text, length = 100, suffix = '...') {
|
|
177
|
+
if (typeof text !== 'string' || text.length <= length) {
|
|
178
|
+
return text;
|
|
179
|
+
}
|
|
180
|
+
return text.substring(0, length).trim() + suffix;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Generate random string
|
|
185
|
+
* @param {number} length - Length of string
|
|
186
|
+
* @returns {string} Random string
|
|
187
|
+
*/
|
|
188
|
+
randomString(length = 8) {
|
|
189
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
190
|
+
let result = '';
|
|
191
|
+
for (let i = 0; i < length; i++) {
|
|
192
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
193
|
+
}
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Deep clone object
|
|
199
|
+
* @param {*} obj - Object to clone
|
|
200
|
+
* @returns {*} Cloned object
|
|
201
|
+
*/
|
|
202
|
+
deepClone(obj) {
|
|
203
|
+
if (obj === null || typeof obj !== 'object') return obj;
|
|
204
|
+
if (obj instanceof Date) return new Date(obj.getTime());
|
|
205
|
+
if (obj instanceof Array) return obj.map(item => this.deepClone(item));
|
|
206
|
+
if (typeof obj === 'object') {
|
|
207
|
+
const cloned = {};
|
|
208
|
+
Object.keys(obj).forEach(key => {
|
|
209
|
+
cloned[key] = this.deepClone(obj[key]);
|
|
210
|
+
});
|
|
211
|
+
return cloned;
|
|
212
|
+
}
|
|
213
|
+
return obj;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Debounce function
|
|
218
|
+
* @param {Function} func - Function to debounce
|
|
219
|
+
* @param {number} wait - Wait time in ms
|
|
220
|
+
* @returns {Function} Debounced function
|
|
221
|
+
*/
|
|
222
|
+
debounce(func, wait) {
|
|
223
|
+
let timeout;
|
|
224
|
+
return function executedFunction(...args) {
|
|
225
|
+
const later = () => {
|
|
226
|
+
clearTimeout(timeout);
|
|
227
|
+
func(...args);
|
|
228
|
+
};
|
|
229
|
+
clearTimeout(timeout);
|
|
230
|
+
timeout = setTimeout(later, wait);
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Throttle function
|
|
236
|
+
* @param {Function} func - Function to throttle
|
|
237
|
+
* @param {number} limit - Limit time in ms
|
|
238
|
+
* @returns {Function} Throttled function
|
|
239
|
+
*/
|
|
240
|
+
throttle(func, limit) {
|
|
241
|
+
let inThrottle;
|
|
242
|
+
return function executedFunction(...args) {
|
|
243
|
+
if (!inThrottle) {
|
|
244
|
+
func.apply(this, args);
|
|
245
|
+
inThrottle = true;
|
|
246
|
+
setTimeout(() => inThrottle = false, limit);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Get URL parameters
|
|
254
|
+
* @param {string} url - URL to parse
|
|
255
|
+
* @returns {Object} URL parameters
|
|
256
|
+
*/
|
|
257
|
+
getUrlParams(url = window.location.href) {
|
|
258
|
+
const params = {};
|
|
259
|
+
const urlObj = new URL(url);
|
|
260
|
+
urlObj.searchParams.forEach((value, key) => {
|
|
261
|
+
params[key] = value;
|
|
262
|
+
});
|
|
263
|
+
return params;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Build URL with parameters
|
|
268
|
+
* @param {string} baseUrl - Base URL
|
|
269
|
+
* @param {Object} params - Parameters to add
|
|
270
|
+
* @returns {string} Built URL
|
|
271
|
+
*/
|
|
272
|
+
buildUrl(baseUrl, params = {}) {
|
|
273
|
+
const url = new URL(baseUrl);
|
|
274
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
275
|
+
if (value !== null && value !== undefined) {
|
|
276
|
+
url.searchParams.set(key, value);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
return url.toString();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Check if element is in viewport
|
|
284
|
+
* Uses DOMBatcher for optimized read operations
|
|
285
|
+
*
|
|
286
|
+
* @param {Element} element - Element to check
|
|
287
|
+
* @param {boolean} useBatcher - Use DOMBatcher for batched reads (default: false)
|
|
288
|
+
* @returns {boolean|Promise<boolean>} True if in viewport (or Promise if batched)
|
|
289
|
+
*/
|
|
290
|
+
isInViewport(element, useBatcher = false) {
|
|
291
|
+
if (useBatcher) {
|
|
292
|
+
return DOMBatcher.read(() => {
|
|
293
|
+
const rect = element.getBoundingClientRect();
|
|
294
|
+
return (
|
|
295
|
+
rect.top >= 0 &&
|
|
296
|
+
rect.left >= 0 &&
|
|
297
|
+
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
|
298
|
+
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
|
299
|
+
);
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Legacy synchronous behavior
|
|
304
|
+
const rect = element.getBoundingClientRect();
|
|
305
|
+
return (
|
|
306
|
+
rect.top >= 0 &&
|
|
307
|
+
rect.left >= 0 &&
|
|
308
|
+
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
|
309
|
+
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Scroll to element
|
|
315
|
+
* @param {Element|string} element - Element or selector
|
|
316
|
+
* @param {Object} options - Scroll options
|
|
317
|
+
*/
|
|
318
|
+
scrollTo(element, options = {}) {
|
|
319
|
+
const target = typeof element === 'string' ? document.querySelector(element) : element;
|
|
320
|
+
if (!target) return;
|
|
321
|
+
|
|
322
|
+
const {
|
|
323
|
+
behavior = 'smooth',
|
|
324
|
+
block = 'start',
|
|
325
|
+
inline = 'nearest',
|
|
326
|
+
offset = 0
|
|
327
|
+
} = options;
|
|
328
|
+
|
|
329
|
+
const targetPosition = target.offsetTop - offset;
|
|
330
|
+
window.scrollTo({
|
|
331
|
+
top: targetPosition,
|
|
332
|
+
behavior
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Copy text to clipboard
|
|
338
|
+
* @param {string} text - Text to copy
|
|
339
|
+
* @returns {Promise<boolean>} Success status
|
|
340
|
+
*/
|
|
341
|
+
async copyToClipboard(text) {
|
|
342
|
+
try {
|
|
343
|
+
await navigator.clipboard.writeText(text);
|
|
344
|
+
return true;
|
|
345
|
+
} catch (error) {
|
|
346
|
+
console.error('Failed to copy to clipboard:', error);
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Download file
|
|
353
|
+
* @param {string} content - File content
|
|
354
|
+
* @param {string} filename - Filename
|
|
355
|
+
* @param {string} mimeType - MIME type
|
|
356
|
+
*/
|
|
357
|
+
downloadFile(content, filename, mimeType = 'text/plain') {
|
|
358
|
+
const blob = new Blob([content], { type: mimeType });
|
|
359
|
+
const url = URL.createObjectURL(blob);
|
|
360
|
+
const link = document.createElement('a');
|
|
361
|
+
link.href = url;
|
|
362
|
+
link.download = filename;
|
|
363
|
+
document.body.appendChild(link);
|
|
364
|
+
link.click();
|
|
365
|
+
document.body.removeChild(link);
|
|
366
|
+
URL.revokeObjectURL(url);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Get device type
|
|
371
|
+
* @returns {string} Device type
|
|
372
|
+
*/
|
|
373
|
+
getDeviceType() {
|
|
374
|
+
const width = window.innerWidth;
|
|
375
|
+
if (width < 768) return 'mobile';
|
|
376
|
+
if (width < 1024) return 'tablet';
|
|
377
|
+
return 'desktop';
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ============================================================================
|
|
381
|
+
// ADDITIONAL UTILITY FUNCTIONS (moved from View)
|
|
382
|
+
// ============================================================================
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Count items
|
|
386
|
+
* @param {Array|Object} items - Items to count
|
|
387
|
+
* @returns {number} Count
|
|
388
|
+
*/
|
|
389
|
+
count(items) {
|
|
390
|
+
if (Array.isArray(items)) {
|
|
391
|
+
return items.length;
|
|
392
|
+
} else if (typeof items === 'object' && items !== null) {
|
|
393
|
+
return Object.keys(items).length;
|
|
394
|
+
}
|
|
395
|
+
return 0;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Check if value is null
|
|
400
|
+
* @param {*} value - Value to check
|
|
401
|
+
* @returns {boolean} True if null
|
|
402
|
+
*/
|
|
403
|
+
isNull(value) {
|
|
404
|
+
return value === null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Check if value is array
|
|
409
|
+
* @param {*} value - Value to check
|
|
410
|
+
* @returns {boolean} True if array
|
|
411
|
+
*/
|
|
412
|
+
isArray(value) {
|
|
413
|
+
return Array.isArray(value);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Check if value is string
|
|
418
|
+
* @param {*} value - Value to check
|
|
419
|
+
* @returns {boolean} True if string
|
|
420
|
+
*/
|
|
421
|
+
isString(value) {
|
|
422
|
+
return typeof value === 'string';
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Check if value is numeric
|
|
427
|
+
* @param {*} value - Value to check
|
|
428
|
+
* @returns {boolean} True if numeric
|
|
429
|
+
*/
|
|
430
|
+
isNumeric(value) {
|
|
431
|
+
return !isNaN(parseFloat(value)) && isFinite(value);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Get current timestamp with format method
|
|
436
|
+
* @returns {Object} Current timestamp with format method
|
|
437
|
+
*/
|
|
438
|
+
now() {
|
|
439
|
+
const now = new Date();
|
|
440
|
+
return {
|
|
441
|
+
format: function (format) {
|
|
442
|
+
if (!format) return now.toISOString();
|
|
443
|
+
|
|
444
|
+
// Simple format implementation
|
|
445
|
+
const year = now.getFullYear();
|
|
446
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
447
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
448
|
+
const hours = String(now.getHours()).padStart(2, '0');
|
|
449
|
+
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
450
|
+
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
451
|
+
|
|
452
|
+
return format
|
|
453
|
+
.replace('Y', year)
|
|
454
|
+
.replace('m', month)
|
|
455
|
+
.replace('d', day)
|
|
456
|
+
.replace('H', hours)
|
|
457
|
+
.replace('i', minutes)
|
|
458
|
+
.replace('s', seconds);
|
|
459
|
+
},
|
|
460
|
+
getTime: function () {
|
|
461
|
+
return now.getTime();
|
|
462
|
+
},
|
|
463
|
+
toString: function () {
|
|
464
|
+
return now.toString();
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Get current date
|
|
471
|
+
* @returns {Date} Current date
|
|
472
|
+
*/
|
|
473
|
+
today() {
|
|
474
|
+
return new Date();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Format date theo PHP date format
|
|
479
|
+
* @param {string} format - Format string (PHP style: Y-m-d H:i:s)
|
|
480
|
+
* @param {Date|string|number} dateValue - Date value (optional, mặc định: current date)
|
|
481
|
+
* @returns {string} Formatted date
|
|
482
|
+
*
|
|
483
|
+
* @example
|
|
484
|
+
* date('Y-m-d') // '2025-01-21'
|
|
485
|
+
* date('Y-m-d H:i:s') // '2025-01-21 10:30:45'
|
|
486
|
+
* date('d/m/Y', new Date('2025-01-21')) // '21/01/2025'
|
|
487
|
+
* date('l, F j, Y') // 'Tuesday, January 21, 2025'
|
|
488
|
+
*/
|
|
489
|
+
date(format = 'Y-m-d H:i:s', dateValue = null) {
|
|
490
|
+
try {
|
|
491
|
+
const d = dateValue ? new Date(dateValue) : new Date();
|
|
492
|
+
|
|
493
|
+
// Validate date
|
|
494
|
+
if (isNaN(d.getTime())) {
|
|
495
|
+
console.warn('App.Helper.date: Invalid date value', dateValue);
|
|
496
|
+
return '';
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Map PHP date format to JavaScript
|
|
500
|
+
const year = d.getFullYear();
|
|
501
|
+
const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
502
|
+
const day = String(d.getDate()).padStart(2, '0');
|
|
503
|
+
const hours = String(d.getHours()).padStart(2, '0');
|
|
504
|
+
const minutes = String(d.getMinutes()).padStart(2, '0');
|
|
505
|
+
const seconds = String(d.getSeconds()).padStart(2, '0');
|
|
506
|
+
const dayOfWeek = d.getDay(); // 0 = Sunday, 6 = Saturday
|
|
507
|
+
const dayOfYear = Math.floor((d - new Date(d.getFullYear(), 0, 0)) / 86400000);
|
|
508
|
+
|
|
509
|
+
// Day names
|
|
510
|
+
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
511
|
+
const dayNamesFull = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
512
|
+
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
513
|
+
const monthNamesFull = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
514
|
+
|
|
515
|
+
// Build replacement map (order matters - longer tokens first)
|
|
516
|
+
const replacements = [
|
|
517
|
+
// Year (4-digit first, then 2-digit)
|
|
518
|
+
{ pattern: /YYYY/g, value: year },
|
|
519
|
+
{ pattern: /YY/g, value: String(year).slice(-2) },
|
|
520
|
+
{ pattern: /Y/g, value: year },
|
|
521
|
+
{ pattern: /y/g, value: String(year).slice(-2) },
|
|
522
|
+
|
|
523
|
+
// Month (full names first, then short, then numeric)
|
|
524
|
+
{ pattern: /MMMM/g, value: monthNamesFull[d.getMonth()] },
|
|
525
|
+
{ pattern: /MMM/g, value: monthNames[d.getMonth()] },
|
|
526
|
+
{ pattern: /MM/g, value: month },
|
|
527
|
+
{ pattern: /M/g, value: monthNames[d.getMonth()] },
|
|
528
|
+
{ pattern: /F/g, value: monthNamesFull[d.getMonth()] },
|
|
529
|
+
{ pattern: /m/g, value: month },
|
|
530
|
+
{ pattern: /n/g, value: String(d.getMonth() + 1) },
|
|
531
|
+
{ pattern: /t/g, value: String(new Date(year, d.getMonth() + 1, 0).getDate()) },
|
|
532
|
+
|
|
533
|
+
// Day (full names first, then short, then numeric)
|
|
534
|
+
{ pattern: /DDDD/g, value: dayNamesFull[dayOfWeek] },
|
|
535
|
+
{ pattern: /DDD/g, value: dayNames[dayOfWeek] },
|
|
536
|
+
{ pattern: /DD/g, value: day },
|
|
537
|
+
{ pattern: /D/g, value: dayNames[dayOfWeek] },
|
|
538
|
+
{ pattern: /l/g, value: dayNamesFull[dayOfWeek] },
|
|
539
|
+
{ pattern: /d/g, value: day },
|
|
540
|
+
{ pattern: /j/g, value: String(d.getDate()) },
|
|
541
|
+
{ pattern: /N/g, value: dayOfWeek === 0 ? '7' : String(dayOfWeek) },
|
|
542
|
+
{ pattern: /w/g, value: String(dayOfWeek) },
|
|
543
|
+
{ pattern: /z/g, value: String(dayOfYear) },
|
|
544
|
+
|
|
545
|
+
// Time (24-hour first, then 12-hour)
|
|
546
|
+
{ pattern: /HH/g, value: hours },
|
|
547
|
+
{ pattern: /H/g, value: hours },
|
|
548
|
+
{ pattern: /G/g, value: String(d.getHours()) },
|
|
549
|
+
{ pattern: /hh/g, value: String(d.getHours() % 12 || 12).padStart(2, '0') },
|
|
550
|
+
{ pattern: /h/g, value: String(d.getHours() % 12 || 12).padStart(2, '0') },
|
|
551
|
+
{ pattern: /g/g, value: String(d.getHours() % 12 || 12) },
|
|
552
|
+
{ pattern: /ii/g, value: minutes },
|
|
553
|
+
{ pattern: /i/g, value: minutes },
|
|
554
|
+
{ pattern: /mm/g, value: minutes },
|
|
555
|
+
{ pattern: /ss/g, value: seconds },
|
|
556
|
+
{ pattern: /s/g, value: seconds },
|
|
557
|
+
{ pattern: /a/g, value: d.getHours() < 12 ? 'am' : 'pm' },
|
|
558
|
+
{ pattern: /A/g, value: d.getHours() < 12 ? 'AM' : 'PM' },
|
|
559
|
+
|
|
560
|
+
// Timezone
|
|
561
|
+
{ pattern: /P/g, value: this._getTimezoneOffset(d, true) },
|
|
562
|
+
{ pattern: /O/g, value: this._getTimezoneOffset(d) },
|
|
563
|
+
{ pattern: /T/g, value: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC' },
|
|
564
|
+
|
|
565
|
+
// Timestamp
|
|
566
|
+
{ pattern: /U/g, value: String(Math.floor(d.getTime() / 1000)) },
|
|
567
|
+
];
|
|
568
|
+
|
|
569
|
+
// Apply replacements in order
|
|
570
|
+
let result = format;
|
|
571
|
+
for (const { pattern, value } of replacements) {
|
|
572
|
+
result = result.replace(pattern, value);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Handle escape characters (remove backslashes before next char)
|
|
576
|
+
result = result.replace(/\\(.)/g, '$1');
|
|
577
|
+
|
|
578
|
+
return result;
|
|
579
|
+
}
|
|
580
|
+
catch (error) {
|
|
581
|
+
console.error('App.Helper.date error:', error);
|
|
582
|
+
return '';
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Get timezone offset
|
|
588
|
+
* @private
|
|
589
|
+
* @param {Date} date - Date object
|
|
590
|
+
* @param {boolean} withColon - Include colon in offset
|
|
591
|
+
* @returns {string} Timezone offset
|
|
592
|
+
*/
|
|
593
|
+
_getTimezoneOffset(date, withColon = false) {
|
|
594
|
+
const offset = -date.getTimezoneOffset();
|
|
595
|
+
const sign = offset >= 0 ? '+' : '-';
|
|
596
|
+
const hours = String(Math.floor(Math.abs(offset) / 60)).padStart(2, '0');
|
|
597
|
+
const minutes = String(Math.abs(offset) % 60).padStart(2, '0');
|
|
598
|
+
return withColon ? `${sign}${hours}:${minutes}` : `${sign}${hours}${minutes}`;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Get environment value
|
|
604
|
+
* @param {string} key - Environment key
|
|
605
|
+
* @param {*} defaultValue - Default value
|
|
606
|
+
* @returns {*} Environment value
|
|
607
|
+
*/
|
|
608
|
+
env(key, defaultValue = null) {
|
|
609
|
+
// This should be implemented based on your environment system
|
|
610
|
+
// For now, return default value as placeholder
|
|
611
|
+
return defaultValue;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ============================================================================
|
|
615
|
+
// PHP-LIKE FUNCTIONS (for Blade compatibility)
|
|
616
|
+
// ============================================================================
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Hàm tương tự như PHP: ucfirst
|
|
620
|
+
* Viết hoa ký tự đầu tiên của chuỗi
|
|
621
|
+
* @param {string} str
|
|
622
|
+
* @returns {string}
|
|
623
|
+
*/
|
|
624
|
+
ucfirst(str) {
|
|
625
|
+
if (typeof str !== 'string' || str.length === 0) return str;
|
|
626
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Hàm tương tự như PHP: lcfirst
|
|
631
|
+
* Viết thường ký tự đầu tiên của chuỗi
|
|
632
|
+
* @param {string} str
|
|
633
|
+
* @returns {string}
|
|
634
|
+
*/
|
|
635
|
+
lcfirst(str) {
|
|
636
|
+
if (typeof str !== 'string' || str.length === 0) return str;
|
|
637
|
+
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Hàm tương tự như PHP: str_replace
|
|
642
|
+
* Thay thế tất cả các chuỗi con trong chuỗi
|
|
643
|
+
* @param {string|string[]} search
|
|
644
|
+
* @param {string|string[]} replace
|
|
645
|
+
* @param {string} subject
|
|
646
|
+
* @returns {string}
|
|
647
|
+
*/
|
|
648
|
+
str_replace(search, replace, subject) {
|
|
649
|
+
if (Array.isArray(search)) {
|
|
650
|
+
let result = subject;
|
|
651
|
+
search.forEach((s, i) => {
|
|
652
|
+
const r = Array.isArray(replace) ? (replace[i] !== undefined ? replace[i] : '') : replace;
|
|
653
|
+
result = result.split(s).join(r);
|
|
654
|
+
});
|
|
655
|
+
return result;
|
|
656
|
+
} else {
|
|
657
|
+
return subject.split(search).join(replace);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Hàm tương tự như PHP: explode
|
|
663
|
+
* Cắt chuỗi thành mảng theo ký tự phân tách
|
|
664
|
+
* @param {string} delimiter
|
|
665
|
+
* @param {string} str
|
|
666
|
+
* @returns {Array}
|
|
667
|
+
*/
|
|
668
|
+
explode(delimiter, str) {
|
|
669
|
+
if (typeof str !== 'string' || typeof delimiter !== 'string') return [];
|
|
670
|
+
return str.split(delimiter);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Hàm tương tự như PHP: implode
|
|
675
|
+
* Nối các phần tử của mảng thành chuỗi với ký tự phân tách
|
|
676
|
+
* @param {string} glue
|
|
677
|
+
* @param {Array} pieces
|
|
678
|
+
* @returns {string}
|
|
679
|
+
*/
|
|
680
|
+
implode(glue, pieces) {
|
|
681
|
+
if (!Array.isArray(pieces)) return '';
|
|
682
|
+
return pieces.join(glue);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Hàm tương tự như PHP: in_array
|
|
687
|
+
* Kiểm tra giá trị có trong mảng không
|
|
688
|
+
* @param {any} needle
|
|
689
|
+
* @param {Array} haystack
|
|
690
|
+
* @returns {boolean}
|
|
691
|
+
*/
|
|
692
|
+
in_array(needle, haystack) {
|
|
693
|
+
if (!Array.isArray(haystack)) return false;
|
|
694
|
+
return haystack.includes(needle);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Hàm tương tự như PHP: array_unique
|
|
699
|
+
* Loại bỏ các phần tử trùng lặp trong mảng
|
|
700
|
+
* @param {Array} arr
|
|
701
|
+
* @returns {Array}
|
|
702
|
+
*/
|
|
703
|
+
array_unique(arr) {
|
|
704
|
+
if (!Array.isArray(arr)) return [];
|
|
705
|
+
return Array.from(new Set(arr));
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Hàm tương tự như PHP: array_merge
|
|
710
|
+
* Gộp nhiều mảng thành một mảng
|
|
711
|
+
* @param {...Array} arrays
|
|
712
|
+
* @returns {Array}
|
|
713
|
+
*/
|
|
714
|
+
array_merge(...arrays) {
|
|
715
|
+
return [].concat(...arrays);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Hàm tương tự như PHP: is_array
|
|
720
|
+
* Kiểm tra biến có phải là mảng không
|
|
721
|
+
* @param {any} value
|
|
722
|
+
* @returns {boolean}
|
|
723
|
+
*/
|
|
724
|
+
is_array(value) {
|
|
725
|
+
return Array.isArray(value);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Hàm tương tự như PHP: is_string
|
|
730
|
+
* Kiểm tra biến có phải là chuỗi không
|
|
731
|
+
* @param {any} value
|
|
732
|
+
* @returns {boolean}
|
|
733
|
+
*/
|
|
734
|
+
is_string(value) {
|
|
735
|
+
return typeof value === 'string';
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Hàm tương tự như PHP: is_numeric
|
|
740
|
+
* Kiểm tra biến có phải là số hoặc chuỗi số không
|
|
741
|
+
* @param {any} value
|
|
742
|
+
* @returns {boolean}
|
|
743
|
+
*/
|
|
744
|
+
is_numeric(value) {
|
|
745
|
+
return !isNaN(parseFloat(value)) && isFinite(value);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Hàm tương tự như PHP: empty
|
|
750
|
+
* Kiểm tra biến có "rỗng" không (null, undefined, '', 0, false, [], {})
|
|
751
|
+
* @param {any} value
|
|
752
|
+
* @returns {boolean}
|
|
753
|
+
*/
|
|
754
|
+
empty(value) {
|
|
755
|
+
if (value === null || value === undefined) return true;
|
|
756
|
+
if (typeof value === 'string' && value.trim() === '') return true;
|
|
757
|
+
if (Array.isArray(value) && value.length === 0) return true;
|
|
758
|
+
if (typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0) return true;
|
|
759
|
+
if (value === false) return true;
|
|
760
|
+
if (value === 0) return true;
|
|
761
|
+
return false;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Hàm tương tự như PHP: isset
|
|
766
|
+
* Kiểm tra biến có được định nghĩa và khác null không
|
|
767
|
+
* @param {any} value
|
|
768
|
+
* @returns {boolean}
|
|
769
|
+
*/
|
|
770
|
+
isset(value) {
|
|
771
|
+
return value !== undefined && value !== null;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Hàm tương tự như PHP: json_encode
|
|
776
|
+
* Chuyển đổi giá trị thành chuỗi JSON
|
|
777
|
+
* @param {any} value
|
|
778
|
+
* @returns {string}
|
|
779
|
+
*/
|
|
780
|
+
json_encode(value) {
|
|
781
|
+
return JSON.stringify(value);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Hàm tương tự như PHP: json_decode
|
|
786
|
+
* Chuyển đổi chuỗi JSON thành giá trị
|
|
787
|
+
* @param {string} value
|
|
788
|
+
* @returns {any}
|
|
789
|
+
*/
|
|
790
|
+
json_decode(value) {
|
|
791
|
+
return JSON.parse(value);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Check if device is mobile
|
|
796
|
+
* @returns {boolean} True if mobile
|
|
797
|
+
*/
|
|
798
|
+
isMobile() {
|
|
799
|
+
return this.getDeviceType() === 'mobile';
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Check if device is tablet
|
|
804
|
+
* @returns {boolean} True if tablet
|
|
805
|
+
*/
|
|
806
|
+
isTablet() {
|
|
807
|
+
return this.getDeviceType() === 'tablet';
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Check if device is desktop
|
|
812
|
+
* @returns {boolean} True if desktop
|
|
813
|
+
*/
|
|
814
|
+
isDesktop() {
|
|
815
|
+
return this.getDeviceType() === 'desktop';
|
|
816
|
+
}
|
|
817
|
+
}
|