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,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Development Warnings Helper
|
|
3
|
+
* Provides helpful warnings during development without impacting production
|
|
4
|
+
*
|
|
5
|
+
* @module helpers/devWarnings
|
|
6
|
+
* @author OneLaravel Team
|
|
7
|
+
* @since 2025-12-29
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if running in development mode
|
|
12
|
+
* @returns {boolean}
|
|
13
|
+
*/
|
|
14
|
+
function isDevelopment() {
|
|
15
|
+
return typeof process !== 'undefined' && process.env?.NODE_ENV === 'development';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Warn if condition is false (development only)
|
|
20
|
+
*
|
|
21
|
+
* @param {boolean} condition - Condition to check
|
|
22
|
+
* @param {string} message - Warning message
|
|
23
|
+
* @param {*} data - Optional additional data
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* warn(view.mounted, 'Accessing view before mount');
|
|
27
|
+
* warn(!circularRef, 'Circular reference detected', { view: view.path });
|
|
28
|
+
*/
|
|
29
|
+
export function warn(condition, message, data = null) {
|
|
30
|
+
if (!condition && isDevelopment()) {
|
|
31
|
+
const fullMessage = `[OneLaravel Warning] ${message}`;
|
|
32
|
+
|
|
33
|
+
if (data) {
|
|
34
|
+
console.warn(fullMessage, data);
|
|
35
|
+
} else {
|
|
36
|
+
console.warn(fullMessage);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Show stack trace in development
|
|
40
|
+
if (console.trace) {
|
|
41
|
+
console.trace();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Deprecation warning
|
|
48
|
+
*
|
|
49
|
+
* @param {string} oldName - Deprecated feature name
|
|
50
|
+
* @param {string} newName - Replacement feature name
|
|
51
|
+
* @param {string} version - Version when it will be removed
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* deprecate('ViewEngine', 'ViewController', '2.0.0');
|
|
55
|
+
*/
|
|
56
|
+
export function deprecate(oldName, newName, version = '2.0.0') {
|
|
57
|
+
if (isDevelopment()) {
|
|
58
|
+
console.warn(
|
|
59
|
+
`[OneLaravel Deprecation] ${oldName} is deprecated and will be removed in v${version}. ` +
|
|
60
|
+
`Use ${newName} instead.`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Invariant check - throws error if condition is false
|
|
67
|
+
* Used for critical checks that should never fail
|
|
68
|
+
*
|
|
69
|
+
* @param {boolean} condition - Condition to check
|
|
70
|
+
* @param {string} message - Error message
|
|
71
|
+
* @throws {Error}
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* invariant(App, 'App instance is required');
|
|
75
|
+
* invariant(viewId, 'View ID cannot be null');
|
|
76
|
+
*/
|
|
77
|
+
export function invariant(condition, message) {
|
|
78
|
+
if (!condition) {
|
|
79
|
+
throw new Error(`[OneLaravel Invariant] ${message}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Log info message (development only)
|
|
85
|
+
*
|
|
86
|
+
* @param {string} message - Info message
|
|
87
|
+
* @param {*} data - Optional data
|
|
88
|
+
*/
|
|
89
|
+
export function devLog(message, data = null) {
|
|
90
|
+
if (isDevelopment()) {
|
|
91
|
+
if (data) {
|
|
92
|
+
console.log(`[OneLaravel] ${message}`, data);
|
|
93
|
+
} else {
|
|
94
|
+
console.log(`[OneLaravel] ${message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Assert type at runtime (development only)
|
|
101
|
+
*
|
|
102
|
+
* @param {*} value - Value to check
|
|
103
|
+
* @param {string} expectedType - Expected type
|
|
104
|
+
* @param {string} name - Variable name
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* assertType(viewId, 'string', 'viewId');
|
|
108
|
+
* assertType(config, 'object', 'config');
|
|
109
|
+
*/
|
|
110
|
+
export function assertType(value, expectedType, name) {
|
|
111
|
+
if (!isDevelopment()) return;
|
|
112
|
+
|
|
113
|
+
const actualType = typeof value;
|
|
114
|
+
|
|
115
|
+
if (actualType !== expectedType) {
|
|
116
|
+
console.warn(
|
|
117
|
+
`[OneLaravel Type Warning] Expected ${name} to be ${expectedType}, ` +
|
|
118
|
+
`but got ${actualType}`,
|
|
119
|
+
{ value }
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Check for circular references (development only)
|
|
126
|
+
*
|
|
127
|
+
* @param {Object} obj - Object to check
|
|
128
|
+
* @param {string} path - Object path for error message
|
|
129
|
+
* @returns {boolean} True if circular reference detected
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* if (checkCircular(view, 'view')) {
|
|
133
|
+
* warn(false, 'Circular reference in view');
|
|
134
|
+
* }
|
|
135
|
+
*/
|
|
136
|
+
export function checkCircular(obj, path = 'root') {
|
|
137
|
+
if (!isDevelopment()) return false;
|
|
138
|
+
|
|
139
|
+
const seen = new WeakSet();
|
|
140
|
+
|
|
141
|
+
function detect(obj, currentPath) {
|
|
142
|
+
if (obj && typeof obj === 'object') {
|
|
143
|
+
if (seen.has(obj)) {
|
|
144
|
+
console.warn(
|
|
145
|
+
`[OneLaravel Circular Reference] Detected at ${currentPath}`
|
|
146
|
+
);
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
seen.add(obj);
|
|
151
|
+
|
|
152
|
+
for (const key in obj) {
|
|
153
|
+
if (obj.hasOwnProperty(key)) {
|
|
154
|
+
if (detect(obj[key], `${currentPath}.${key}`)) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return detect(obj, path);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Performance warning if execution time exceeds threshold
|
|
169
|
+
*
|
|
170
|
+
* @param {Function} fn - Function to measure
|
|
171
|
+
* @param {string} label - Label for measurement
|
|
172
|
+
* @param {number} threshold - Threshold in ms (default: 16ms for 60fps)
|
|
173
|
+
* @returns {*} Function result
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* warnIfSlow(() => view.render(), 'render', 16);
|
|
177
|
+
*/
|
|
178
|
+
export function warnIfSlow(fn, label, threshold = 16) {
|
|
179
|
+
if (!isDevelopment()) {
|
|
180
|
+
return fn();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const start = performance.now();
|
|
184
|
+
const result = fn();
|
|
185
|
+
const duration = performance.now() - start;
|
|
186
|
+
|
|
187
|
+
if (duration > threshold) {
|
|
188
|
+
console.warn(
|
|
189
|
+
`[OneLaravel Performance] ${label} took ${duration.toFixed(2)}ms ` +
|
|
190
|
+
`(threshold: ${threshold}ms)`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export default {
|
|
198
|
+
warn,
|
|
199
|
+
deprecate,
|
|
200
|
+
invariant,
|
|
201
|
+
devLog,
|
|
202
|
+
assertType,
|
|
203
|
+
checkCircular,
|
|
204
|
+
warnIfSlow,
|
|
205
|
+
};
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance Utilities
|
|
3
|
+
* Debounce, throttle, and other performance optimization helpers
|
|
4
|
+
*
|
|
5
|
+
* @module helpers/performance
|
|
6
|
+
* @author OneLaravel Team
|
|
7
|
+
* @since 2025-12-29
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Debounce function - delays execution until after wait time has elapsed
|
|
12
|
+
* since the last invocation
|
|
13
|
+
*
|
|
14
|
+
* @param {Function} fn - Function to debounce
|
|
15
|
+
* @param {number} delay - Delay in milliseconds (default: 300)
|
|
16
|
+
* @returns {Function} Debounced function
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* const handleSearch = debounce((query) => {
|
|
20
|
+
* api.search(query);
|
|
21
|
+
* }, 300);
|
|
22
|
+
*
|
|
23
|
+
* input.addEventListener('input', (e) => handleSearch(e.target.value));
|
|
24
|
+
*/
|
|
25
|
+
export function debounce(fn, delay = 300) {
|
|
26
|
+
let timeoutId;
|
|
27
|
+
|
|
28
|
+
return function debounced(...args) {
|
|
29
|
+
clearTimeout(timeoutId);
|
|
30
|
+
timeoutId = setTimeout(() => {
|
|
31
|
+
fn.apply(this, args);
|
|
32
|
+
}, delay);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Throttle function - ensures function is called at most once per time period
|
|
38
|
+
*
|
|
39
|
+
* @param {Function} fn - Function to throttle
|
|
40
|
+
* @param {number} limit - Time limit in milliseconds (default: 100)
|
|
41
|
+
* @returns {Function} Throttled function
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* const handleScroll = throttle(() => {
|
|
45
|
+
* updateScrollPosition();
|
|
46
|
+
* }, 100);
|
|
47
|
+
*
|
|
48
|
+
* window.addEventListener('scroll', handleScroll);
|
|
49
|
+
*/
|
|
50
|
+
export function throttle(fn, limit = 100) {
|
|
51
|
+
let inThrottle;
|
|
52
|
+
|
|
53
|
+
return function throttled(...args) {
|
|
54
|
+
if (!inThrottle) {
|
|
55
|
+
fn.apply(this, args);
|
|
56
|
+
inThrottle = true;
|
|
57
|
+
setTimeout(() => {
|
|
58
|
+
inThrottle = false;
|
|
59
|
+
}, limit);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Request Animation Frame wrapper for smooth animations
|
|
66
|
+
*
|
|
67
|
+
* @param {Function} fn - Function to execute on next frame
|
|
68
|
+
* @returns {Function} RAF-wrapped function
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* const updateAnimation = raf(() => {
|
|
72
|
+
* element.style.transform = `translateX(${x}px)`;
|
|
73
|
+
* });
|
|
74
|
+
*/
|
|
75
|
+
export function raf(fn) {
|
|
76
|
+
let rafId;
|
|
77
|
+
|
|
78
|
+
return function rafWrapped(...args) {
|
|
79
|
+
if (rafId) {
|
|
80
|
+
cancelAnimationFrame(rafId);
|
|
81
|
+
}
|
|
82
|
+
rafId = requestAnimationFrame(() => {
|
|
83
|
+
fn.apply(this, args);
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Memoize function results for performance
|
|
90
|
+
*
|
|
91
|
+
* @param {Function} fn - Function to memoize
|
|
92
|
+
* @param {Function} keyFn - Optional function to generate cache key
|
|
93
|
+
* @returns {Function} Memoized function
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* const expensiveCalc = memoize((n) => {
|
|
97
|
+
* return fibonacci(n);
|
|
98
|
+
* });
|
|
99
|
+
*/
|
|
100
|
+
export function memoize(fn, keyFn = (...args) => JSON.stringify(args)) {
|
|
101
|
+
const cache = new Map();
|
|
102
|
+
|
|
103
|
+
return function memoized(...args) {
|
|
104
|
+
const key = keyFn(...args);
|
|
105
|
+
|
|
106
|
+
if (cache.has(key)) {
|
|
107
|
+
return cache.get(key);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const result = fn.apply(this, args);
|
|
111
|
+
cache.set(key, result);
|
|
112
|
+
return result;
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Batch DOM reads and writes to prevent layout thrashing
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* const batcher = new DOMBatcher();
|
|
121
|
+
*
|
|
122
|
+
* // Schedule reads
|
|
123
|
+
* batcher.read(() => {
|
|
124
|
+
* const height = element.offsetHeight;
|
|
125
|
+
* });
|
|
126
|
+
*
|
|
127
|
+
* // Schedule writes
|
|
128
|
+
* batcher.write(() => {
|
|
129
|
+
* element.style.height = `${newHeight}px`;
|
|
130
|
+
* });
|
|
131
|
+
*/
|
|
132
|
+
export class DOMBatcher {
|
|
133
|
+
constructor() {
|
|
134
|
+
this.readQueue = [];
|
|
135
|
+
this.writeQueue = [];
|
|
136
|
+
this.scheduled = false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Schedule a DOM read operation
|
|
141
|
+
* @param {Function} fn - Read operation
|
|
142
|
+
*/
|
|
143
|
+
read(fn) {
|
|
144
|
+
this.readQueue.push(fn);
|
|
145
|
+
this.schedule();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Schedule a DOM write operation
|
|
150
|
+
* @param {Function} fn - Write operation
|
|
151
|
+
*/
|
|
152
|
+
write(fn) {
|
|
153
|
+
this.writeQueue.push(fn);
|
|
154
|
+
this.schedule();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Schedule batch execution
|
|
159
|
+
* @private
|
|
160
|
+
*/
|
|
161
|
+
schedule() {
|
|
162
|
+
if (this.scheduled) return;
|
|
163
|
+
|
|
164
|
+
this.scheduled = true;
|
|
165
|
+
requestAnimationFrame(() => {
|
|
166
|
+
this.flush();
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Execute all queued operations
|
|
172
|
+
* @private
|
|
173
|
+
*/
|
|
174
|
+
flush() {
|
|
175
|
+
// Execute all reads first
|
|
176
|
+
while (this.readQueue.length > 0) {
|
|
177
|
+
const fn = this.readQueue.shift();
|
|
178
|
+
fn();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Then execute all writes
|
|
182
|
+
while (this.writeQueue.length > 0) {
|
|
183
|
+
const fn = this.writeQueue.shift();
|
|
184
|
+
fn();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.scheduled = false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Measure and log performance of a function
|
|
193
|
+
*
|
|
194
|
+
* @param {string} label - Label for the measurement
|
|
195
|
+
* @param {Function} fn - Function to measure
|
|
196
|
+
* @returns {*} Result of the function
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* const result = measurePerformance('render', () => {
|
|
200
|
+
* return view.render();
|
|
201
|
+
* });
|
|
202
|
+
*/
|
|
203
|
+
export function measurePerformance(label, fn) {
|
|
204
|
+
const start = performance.now();
|
|
205
|
+
const result = fn();
|
|
206
|
+
const end = performance.now();
|
|
207
|
+
|
|
208
|
+
if (typeof result?.then === 'function') {
|
|
209
|
+
return result.then((value) => {
|
|
210
|
+
console.debug(`[Performance] ${label}: ${(end - start).toFixed(2)}ms (async)`);
|
|
211
|
+
return value;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
console.debug(`[Performance] ${label}: ${(end - start).toFixed(2)}ms`);
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export default {
|
|
220
|
+
debounce,
|
|
221
|
+
throttle,
|
|
222
|
+
raf,
|
|
223
|
+
memoize,
|
|
224
|
+
DOMBatcher,
|
|
225
|
+
measurePerformance,
|
|
226
|
+
};
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
export const uniqId = (prefix = '') => {
|
|
2
|
+
const id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
3
|
+
const r = Math.random() * 16 | 0;
|
|
4
|
+
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
5
|
+
return v.toString(16);
|
|
6
|
+
});
|
|
7
|
+
return prefix + id;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const emptyFn = () => { };
|
|
11
|
+
|
|
12
|
+
export const noop = () => { };
|
|
13
|
+
|
|
14
|
+
export const __hasOwnProp = (obj, key) => {
|
|
15
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const __defineProps = (obj, props, configs = {}) => {
|
|
19
|
+
Object.entries(props).forEach(([key, value]) => {
|
|
20
|
+
Object.defineProperty(obj, key, {
|
|
21
|
+
value: value,
|
|
22
|
+
writable: configs[key]?.writable ?? false,
|
|
23
|
+
configurable: configs[key]?.configurable ?? false,
|
|
24
|
+
enumerable: configs[key]?.enumerable ?? false,
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const __defineProp = (obj, key, PropertyDescriptor = {}) => {
|
|
30
|
+
if (!__hasOwnProp(PropertyDescriptor, 'value') && !(__hasOwnProp(PropertyDescriptor, 'get') && typeof PropertyDescriptor.get === 'function')) {
|
|
31
|
+
throw new Error(`Property ${key} must have a value or get method`);
|
|
32
|
+
}
|
|
33
|
+
if (__hasOwnProp(PropertyDescriptor, 'value')) {
|
|
34
|
+
if (__hasOwnProp(PropertyDescriptor, 'get')) {
|
|
35
|
+
delete PropertyDescriptor.get;
|
|
36
|
+
}
|
|
37
|
+
if (__hasOwnProp(PropertyDescriptor, 'set')) {
|
|
38
|
+
delete PropertyDescriptor.set;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!__hasOwnProp(PropertyDescriptor, 'writable')) {
|
|
42
|
+
if (!__hasOwnProp(PropertyDescriptor, 'get')) {
|
|
43
|
+
PropertyDescriptor.writable = false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!__hasOwnProp(PropertyDescriptor, 'configurable')) {
|
|
47
|
+
PropertyDescriptor.configurable = false;
|
|
48
|
+
}
|
|
49
|
+
if (!__hasOwnProp(PropertyDescriptor, 'enumerable')) {
|
|
50
|
+
PropertyDescriptor.enumerable = false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return Object.defineProperty(obj, key, PropertyDescriptor);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const __defineProperties = (obj, properties) => {
|
|
57
|
+
Object.entries(properties).forEach(([key, value]) => {
|
|
58
|
+
__defineProp(obj, key, value);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const __defineMethods = (obj, methods) => {
|
|
63
|
+
Object.entries(methods).forEach(([key, value]) => {
|
|
64
|
+
if (typeof value !== 'function') {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
Object.defineProperty(obj, key, {
|
|
68
|
+
value: value,
|
|
69
|
+
writable: false,
|
|
70
|
+
configurable: false,
|
|
71
|
+
enumerable: false,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const __defineGetters = (obj, getters) => {
|
|
77
|
+
Object.entries(getters).forEach(([key, value]) => {
|
|
78
|
+
Object.defineProperty(obj, key, {
|
|
79
|
+
get: value,
|
|
80
|
+
configurable: false,
|
|
81
|
+
enumerable: false,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
export const deleteProp = (obj, prop) => {
|
|
86
|
+
try {
|
|
87
|
+
delete obj[prop];
|
|
88
|
+
return true;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error('App.deleteProp error:', error);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export const deleteProps = (obj, props) => {
|
|
95
|
+
try {
|
|
96
|
+
props.forEach(prop => {
|
|
97
|
+
delete obj[prop];
|
|
98
|
+
});
|
|
99
|
+
return true;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error('App.deleteProps error:', error);
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Kiểm tra đối tượng có dữ liệu hay không
|
|
108
|
+
* @param {object} obj biến đối tượng
|
|
109
|
+
* @returns {boolean} true nếu có dữ liệu, false nếu rỗng
|
|
110
|
+
*/
|
|
111
|
+
export const hasData = obj => {
|
|
112
|
+
if (!obj || typeof obj !== 'object') return false;
|
|
113
|
+
for (const _ in obj) return true; // fast path
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const isEmptyObject = obj => hasData(obj) === false;
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Kiểm tra xem chuỗi đầu vào có phải là một thẻ HTML hợp lệ hoặc danh sách các thẻ HTML hợp lệ không.
|
|
124
|
+
* @param {string} input - Chuỗi cần kiểm tra
|
|
125
|
+
* @returns {boolean} true nếu hợp lệ, false nếu không hợp lệ
|
|
126
|
+
*/
|
|
127
|
+
export function isHtmlString(input) {
|
|
128
|
+
if (typeof input !== 'string') return false;
|
|
129
|
+
// Loại bỏ khoảng trắng đầu cuối
|
|
130
|
+
const str = input.trim();
|
|
131
|
+
if (!str) return false;
|
|
132
|
+
|
|
133
|
+
// Regex kiểm tra một thẻ HTML đơn
|
|
134
|
+
const singleTagRegex = /^<([a-zA-Z][\w:-]*)(\s+[^<>]*)?>.*<\/\1>$|^<([a-zA-Z][\w:-]*)(\s+[^<>]*)?\/>$/s;
|
|
135
|
+
|
|
136
|
+
// Regex kiểm tra danh sách các thẻ HTML (có thể có nhiều thẻ liền nhau)
|
|
137
|
+
// Sử dụng DOMParser để kiểm tra chắc chắn hơn
|
|
138
|
+
try {
|
|
139
|
+
const parser = new DOMParser();
|
|
140
|
+
// Bọc trong một thẻ cha để DOMParser không tự động thêm <html><body>
|
|
141
|
+
const doc = parser.parseFromString(`<div>${str}</div>`, 'text/html');
|
|
142
|
+
const wrapper = doc.body.firstElementChild;
|
|
143
|
+
if (!wrapper) return false;
|
|
144
|
+
// Nếu tất cả các node con đều là element node (thẻ html)
|
|
145
|
+
for (let node of wrapper.childNodes) {
|
|
146
|
+
if (node.nodeType !== 1) { // 1: ELEMENT_NODE
|
|
147
|
+
// Cho phép text node chỉ nếu nó là khoảng trắng
|
|
148
|
+
if (node.nodeType === 3 && !node.textContent.trim()) continue;
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Nếu có ít nhất một thẻ hợp lệ
|
|
153
|
+
return wrapper.children.length > 0;
|
|
154
|
+
} catch (e) {
|
|
155
|
+
// Nếu DOMParser không hỗ trợ hoặc lỗi, fallback về regex đơn
|
|
156
|
+
return singleTagRegex.test(str);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Escape string for HTML output
|
|
161
|
+
* @param {*} value - Value to escape
|
|
162
|
+
* @returns {string} Escaped string
|
|
163
|
+
*/
|
|
164
|
+
export function escapeString(value) {
|
|
165
|
+
if (value === null || value === undefined) {
|
|
166
|
+
return '';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const str = String(value);
|
|
170
|
+
return str
|
|
171
|
+
.replace(/&/g, '&')
|
|
172
|
+
.replace(/</g, '<')
|
|
173
|
+
.replace(/>/g, '>')
|
|
174
|
+
.replace(/"/g, '"')
|
|
175
|
+
.replace(/'/g, ''')
|
|
176
|
+
.replace(/`/g, '`');
|
|
177
|
+
;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Thay thế tất cả các chuỗi con "find" trong "input" bằng chuỗi "replace".
|
|
182
|
+
* Có 3 trường hợp:
|
|
183
|
+
* 1. find là string, replace là string: thay thế tất cả find thành replace.
|
|
184
|
+
* 2. find là object: key là chuỗi cần tìm, value là chuỗi thay thế.
|
|
185
|
+
* 3. find là object, replace là string (prefix), postfix là string:
|
|
186
|
+
* tìm tất cả prefix+key+postfix, thay thế bằng value.
|
|
187
|
+
* @param {string} input - Chuỗi gốc cần thay thế
|
|
188
|
+
* @param {string|Object} find - Chuỗi hoặc object cần tìm để thay thế
|
|
189
|
+
* @param {string|undefined} replace - Chuỗi thay thế hoặc prefix
|
|
190
|
+
* @param {string|undefined} postfix - Chuỗi hậu tố (chỉ dùng cho dạng 3)
|
|
191
|
+
* @returns {string} Chuỗi sau khi đã thay thế
|
|
192
|
+
*/
|
|
193
|
+
export function replaceAll(input, find, replace, postfix) {
|
|
194
|
+
if (typeof input !== 'string' || !find) return input;
|
|
195
|
+
|
|
196
|
+
// Trường hợp 1: find là string, replace là string
|
|
197
|
+
if (typeof find === 'string') {
|
|
198
|
+
if (find === '') return input;
|
|
199
|
+
return input.split(find).join(replace);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Trường hợp 2 & 3: find là object
|
|
203
|
+
if (typeof find === 'object' && find !== null) {
|
|
204
|
+
let result = input;
|
|
205
|
+
// Dạng 3: có prefix (replace) và postfix
|
|
206
|
+
|
|
207
|
+
if (typeof replace === 'string' && typeof postfix === 'string') {
|
|
208
|
+
for (const [key, value] of Object.entries(find)) {
|
|
209
|
+
if (key === '') continue;
|
|
210
|
+
// Escape ký tự đặc biệt trong prefix, key, postfix
|
|
211
|
+
const pattern = (replace + key + postfix).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
212
|
+
result = result.replace(new RegExp(pattern, 'g'), value);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Dạng 3: chỉ có prefix (replace), không có postfix
|
|
216
|
+
else if (typeof replace === 'string' && typeof postfix === 'undefined') {
|
|
217
|
+
for (const [key, value] of Object.entries(find)) {
|
|
218
|
+
if (key === '') continue;
|
|
219
|
+
const pattern = (replace + key).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
220
|
+
result = result.replace(new RegExp(pattern, 'g'), value);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Dạng 2: không có prefix, không có postfix
|
|
224
|
+
else {
|
|
225
|
+
for (const [key, value] of Object.entries(find)) {
|
|
226
|
+
if (key === '') continue;
|
|
227
|
+
const pattern = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
228
|
+
result = result.replace(new RegExp(pattern, 'g'), value);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Nếu không khớp trường hợp nào, trả về input gốc
|
|
235
|
+
return input;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
export function withScope(context = {}, callback) {
|
|
241
|
+
if (typeof callback !== 'function') {
|
|
242
|
+
throw new TypeError('withContext expects a function as the second argument.');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Kiểm tra context phải là object
|
|
246
|
+
if (typeof context !== 'object' || context === null) {
|
|
247
|
+
throw new TypeError('Context must be a non-null object.');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Tạo danh sách key và value
|
|
251
|
+
const keys = Object.keys(context);
|
|
252
|
+
const values = Object.values(context);
|
|
253
|
+
|
|
254
|
+
// Tạo hàm mới có các biến cục bộ tương ứng
|
|
255
|
+
const func = new Function(
|
|
256
|
+
...keys,
|
|
257
|
+
`"use strict"; return (${callback.toString()})();`
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// Gọi hàm mới trong phạm vi với context
|
|
261
|
+
return func(...values);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function withContext(context, fn, self) {
|
|
265
|
+
const keys = Object.keys(context);
|
|
266
|
+
const values = Object.values(context);
|
|
267
|
+
return function (...args) {
|
|
268
|
+
return new Function(...keys, `"use strict"; return (${fn.toString()})(...arguments)`)
|
|
269
|
+
.apply(self ?? this, values.concat(args));
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Execute function and return result
|
|
275
|
+
* @param {Function} fn - Function to execute
|
|
276
|
+
* @returns {string} Result as string
|
|
277
|
+
*/
|
|
278
|
+
export function exe(fn, defaultValue = '') {
|
|
279
|
+
try {
|
|
280
|
+
const result = typeof fn === 'function' ? fn() : fn;
|
|
281
|
+
return result !== undefined ? result : defaultValue;
|
|
282
|
+
} catch (error) {
|
|
283
|
+
console.error('App.execute error:', error);
|
|
284
|
+
return defaultValue;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|