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.
Files changed (67) hide show
  1. package/README.md +87 -0
  2. package/docs/integration_analysis.md +116 -0
  3. package/docs/onejs_analysis.md +108 -0
  4. package/docs/optimization_implementation_group2.md +458 -0
  5. package/docs/optimization_plan.md +130 -0
  6. package/index.js +16 -0
  7. package/package.json +13 -0
  8. package/src/app.js +61 -0
  9. package/src/core/API.js +72 -0
  10. package/src/core/ChildrenRegistry.js +410 -0
  11. package/src/core/DOMBatcher.js +207 -0
  12. package/src/core/ErrorBoundary.js +226 -0
  13. package/src/core/EventDelegator.js +416 -0
  14. package/src/core/Helper.js +817 -0
  15. package/src/core/LoopContext.js +97 -0
  16. package/src/core/OneDOM.js +246 -0
  17. package/src/core/OneMarkup.js +444 -0
  18. package/src/core/Router.js +996 -0
  19. package/src/core/SEOConfig.js +321 -0
  20. package/src/core/SectionEngine.js +75 -0
  21. package/src/core/TemplateEngine.js +83 -0
  22. package/src/core/View.js +273 -0
  23. package/src/core/ViewConfig.js +229 -0
  24. package/src/core/ViewController.js +1410 -0
  25. package/src/core/ViewControllerOptimized.js +164 -0
  26. package/src/core/ViewIdentifier.js +361 -0
  27. package/src/core/ViewLoader.js +272 -0
  28. package/src/core/ViewManager.js +1962 -0
  29. package/src/core/ViewState.js +761 -0
  30. package/src/core/ViewSystem.js +301 -0
  31. package/src/core/ViewTemplate.js +4 -0
  32. package/src/core/helpers/BindingHelper.js +239 -0
  33. package/src/core/helpers/ConfigHelper.js +37 -0
  34. package/src/core/helpers/EventHelper.js +172 -0
  35. package/src/core/helpers/LifecycleHelper.js +17 -0
  36. package/src/core/helpers/ReactiveHelper.js +169 -0
  37. package/src/core/helpers/RenderHelper.js +15 -0
  38. package/src/core/helpers/ResourceHelper.js +89 -0
  39. package/src/core/helpers/TemplateHelper.js +11 -0
  40. package/src/core/managers/BindingManager.js +671 -0
  41. package/src/core/managers/ConfigurationManager.js +136 -0
  42. package/src/core/managers/EventManager.js +309 -0
  43. package/src/core/managers/LifecycleManager.js +356 -0
  44. package/src/core/managers/ReactiveManager.js +334 -0
  45. package/src/core/managers/RenderEngine.js +292 -0
  46. package/src/core/managers/ResourceManager.js +441 -0
  47. package/src/core/managers/ViewHierarchyManager.js +258 -0
  48. package/src/core/managers/ViewTemplateManager.js +127 -0
  49. package/src/core/reactive/ReactiveComponent.js +592 -0
  50. package/src/core/services/EventService.js +418 -0
  51. package/src/core/services/HttpService.js +106 -0
  52. package/src/core/services/LoggerService.js +57 -0
  53. package/src/core/services/StateService.js +512 -0
  54. package/src/core/services/StorageService.js +856 -0
  55. package/src/core/services/StoreService.js +258 -0
  56. package/src/core/services/TemplateDetectorService.js +361 -0
  57. package/src/core/services/Test.js +18 -0
  58. package/src/helpers/devWarnings.js +205 -0
  59. package/src/helpers/performance.js +226 -0
  60. package/src/helpers/utils.js +287 -0
  61. package/src/init.js +343 -0
  62. package/src/plugins/auto-plugin.js +34 -0
  63. package/src/services/Test.js +18 -0
  64. package/src/types/index.js +193 -0
  65. package/src/utils/date-helper.js +51 -0
  66. package/src/utils/helpers.js +39 -0
  67. 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
+ }