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,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Boundary System
|
|
3
|
+
* Provides error handling and fallback UI for views
|
|
4
|
+
*
|
|
5
|
+
* @module core/ErrorBoundary
|
|
6
|
+
* @author OneLaravel Team
|
|
7
|
+
* @since 2025-12-29
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import logger from './services/LoggerService.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Error Boundary for Views
|
|
14
|
+
* Catches errors during view lifecycle and provides fallback UI
|
|
15
|
+
*
|
|
16
|
+
* @class ViewErrorBoundary
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* const errorBoundary = new ViewErrorBoundary(view);
|
|
20
|
+
* const result = errorBoundary.wrap(() => view.render());
|
|
21
|
+
*/
|
|
22
|
+
export class ViewErrorBoundary {
|
|
23
|
+
/**
|
|
24
|
+
* Create error boundary
|
|
25
|
+
* @param {ViewController} controller - View controller instance
|
|
26
|
+
*/
|
|
27
|
+
constructor(controller) {
|
|
28
|
+
this.controller = controller;
|
|
29
|
+
this.view = controller.view;
|
|
30
|
+
this.hasError = false;
|
|
31
|
+
this.error = null;
|
|
32
|
+
this.errorInfo = null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Wrap a function with error handling
|
|
37
|
+
*
|
|
38
|
+
* @param {Function} fn - Function to wrap
|
|
39
|
+
* @param {string} phase - Lifecycle phase name
|
|
40
|
+
* @returns {*} Function result or error fallback
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* const html = errorBoundary.wrap(() => view.render(), 'render');
|
|
44
|
+
*/
|
|
45
|
+
wrap(fn, phase = 'execution') {
|
|
46
|
+
try {
|
|
47
|
+
const result = fn();
|
|
48
|
+
|
|
49
|
+
// Handle async functions
|
|
50
|
+
if (result && typeof result.then === 'function') {
|
|
51
|
+
return result.catch(error => {
|
|
52
|
+
return this.handleError(error, { phase, async: true });
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return result;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return this.handleError(error, { phase, async: false });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Handle caught error
|
|
64
|
+
*
|
|
65
|
+
* @param {Error} error - The error object
|
|
66
|
+
* @param {Object} errorInfo - Additional error information
|
|
67
|
+
* @returns {string} Error fallback HTML
|
|
68
|
+
*
|
|
69
|
+
* @private
|
|
70
|
+
*/
|
|
71
|
+
handleError(error, errorInfo = {}) {
|
|
72
|
+
this.hasError = true;
|
|
73
|
+
this.error = error;
|
|
74
|
+
this.errorInfo = errorInfo;
|
|
75
|
+
|
|
76
|
+
// Log error
|
|
77
|
+
logger.error('View Error Boundary:', {
|
|
78
|
+
view: this.controller.path,
|
|
79
|
+
viewId: this.controller.id,
|
|
80
|
+
phase: errorInfo.phase,
|
|
81
|
+
error: error.message,
|
|
82
|
+
stack: error.stack,
|
|
83
|
+
...errorInfo
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Call error lifecycle hook if exists
|
|
87
|
+
if (typeof this.controller.onError === 'function') {
|
|
88
|
+
try {
|
|
89
|
+
this.controller.onError(error, errorInfo);
|
|
90
|
+
} catch (hookError) {
|
|
91
|
+
logger.error('Error in onError hook:', hookError);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Return fallback UI
|
|
96
|
+
return this.renderErrorFallback(error, errorInfo);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Render error fallback UI
|
|
101
|
+
*
|
|
102
|
+
* @param {Error} error - The error object
|
|
103
|
+
* @param {Object} errorInfo - Additional error information
|
|
104
|
+
* @returns {string} Error HTML
|
|
105
|
+
*
|
|
106
|
+
* @private
|
|
107
|
+
*/
|
|
108
|
+
renderErrorFallback(error, errorInfo) {
|
|
109
|
+
const isDev = this.controller.App?.env?.debug || false;
|
|
110
|
+
const viewPath = this.controller.path || 'unknown';
|
|
111
|
+
const phase = errorInfo.phase || 'unknown';
|
|
112
|
+
|
|
113
|
+
if (isDev) {
|
|
114
|
+
// Development mode - show detailed error
|
|
115
|
+
return `
|
|
116
|
+
<div class="error-boundary dev-error" style="
|
|
117
|
+
padding: 20px;
|
|
118
|
+
margin: 20px 0;
|
|
119
|
+
background: #fee;
|
|
120
|
+
border: 2px solid #c33;
|
|
121
|
+
border-radius: 8px;
|
|
122
|
+
font-family: monospace;
|
|
123
|
+
">
|
|
124
|
+
<h3 style="color: #c33; margin: 0 0 10px 0;">
|
|
125
|
+
⚠️ Error in View: ${viewPath}
|
|
126
|
+
</h3>
|
|
127
|
+
<p style="margin: 5px 0;"><strong>Phase:</strong> ${phase}</p>
|
|
128
|
+
<p style="margin: 5px 0;"><strong>Message:</strong> ${error.message}</p>
|
|
129
|
+
${error.stack ? `
|
|
130
|
+
<details style="margin-top: 10px;">
|
|
131
|
+
<summary style="cursor: pointer; color: #c33;">
|
|
132
|
+
Stack Trace
|
|
133
|
+
</summary>
|
|
134
|
+
<pre style="
|
|
135
|
+
background: #f5f5f5;
|
|
136
|
+
padding: 10px;
|
|
137
|
+
overflow-x: auto;
|
|
138
|
+
margin-top: 10px;
|
|
139
|
+
">${error.stack}</pre>
|
|
140
|
+
</details>
|
|
141
|
+
` : ''}
|
|
142
|
+
<p style="margin-top: 15px; color: #666; font-size: 0.9em;">
|
|
143
|
+
This detailed error is only visible in development mode.
|
|
144
|
+
</p>
|
|
145
|
+
</div>
|
|
146
|
+
`;
|
|
147
|
+
} else {
|
|
148
|
+
// Production mode - show generic error
|
|
149
|
+
return `
|
|
150
|
+
<div class="error-boundary prod-error" style="
|
|
151
|
+
padding: 20px;
|
|
152
|
+
margin: 20px 0;
|
|
153
|
+
background: #f9f9f9;
|
|
154
|
+
border: 1px solid #ddd;
|
|
155
|
+
border-radius: 8px;
|
|
156
|
+
text-align: center;
|
|
157
|
+
">
|
|
158
|
+
<h3 style="color: #666; margin: 0 0 10px 0;">
|
|
159
|
+
Something went wrong
|
|
160
|
+
</h3>
|
|
161
|
+
<p style="color: #999;">
|
|
162
|
+
We're sorry, but something went wrong while loading this content.
|
|
163
|
+
Please try refreshing the page.
|
|
164
|
+
</p>
|
|
165
|
+
</div>
|
|
166
|
+
`;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Reset error boundary state
|
|
172
|
+
*/
|
|
173
|
+
reset() {
|
|
174
|
+
this.hasError = false;
|
|
175
|
+
this.error = null;
|
|
176
|
+
this.errorInfo = null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Check if boundary has caught an error
|
|
181
|
+
* @returns {boolean}
|
|
182
|
+
*/
|
|
183
|
+
get hasCaughtError() {
|
|
184
|
+
return this.hasError;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Global error handler for uncaught errors
|
|
190
|
+
*
|
|
191
|
+
* @param {Application} App - App instance
|
|
192
|
+
* @param {Function} customHandler - Custom error handler
|
|
193
|
+
*/
|
|
194
|
+
export function setupGlobalErrorHandler(App, customHandler = null) {
|
|
195
|
+
// Handle uncaught errors
|
|
196
|
+
window.addEventListener('error', (event) => {
|
|
197
|
+
logger.error('Uncaught Error:', {
|
|
198
|
+
message: event.message,
|
|
199
|
+
filename: event.filename,
|
|
200
|
+
lineno: event.lineno,
|
|
201
|
+
colno: event.colno,
|
|
202
|
+
error: event.error
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (customHandler) {
|
|
206
|
+
customHandler(event.error, { type: 'uncaught', event });
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Handle unhandled promise rejections
|
|
211
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
212
|
+
logger.error('Unhandled Promise Rejection:', {
|
|
213
|
+
reason: event.reason,
|
|
214
|
+
promise: event.promise
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
if (customHandler) {
|
|
218
|
+
customHandler(event.reason, { type: 'rejection', event });
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export default {
|
|
224
|
+
ViewErrorBoundary,
|
|
225
|
+
setupGlobalErrorHandler,
|
|
226
|
+
};
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventDelegator
|
|
3
|
+
*
|
|
4
|
+
* Tối ưu hóa event handling bằng cách delegate events lên root element
|
|
5
|
+
* thay vì attach listener riêng cho từng element.
|
|
6
|
+
*
|
|
7
|
+
* Lợi ích hiệu suất:
|
|
8
|
+
* - Giảm số lượng event listeners từ O(n) xuống O(1)
|
|
9
|
+
* - Tự động hỗ trợ dynamic content (không cần re-attach)
|
|
10
|
+
* - Giảm memory footprint
|
|
11
|
+
* - Cải thiện performance khi có nhiều elements
|
|
12
|
+
*
|
|
13
|
+
* Cách hoạt động:
|
|
14
|
+
* - Một listener duy nhất trên root element (document hoặc container)
|
|
15
|
+
* - Sử dụng event bubbling để bắt events từ children
|
|
16
|
+
* - Match target element với selector để gọi đúng handler
|
|
17
|
+
*
|
|
18
|
+
* @class EventDelegator
|
|
19
|
+
* @version 1.0.0
|
|
20
|
+
* @since 2025-01-06
|
|
21
|
+
*/
|
|
22
|
+
class EventDelegator {
|
|
23
|
+
constructor() {
|
|
24
|
+
/**
|
|
25
|
+
* Map lưu delegated handlers theo event type
|
|
26
|
+
* @type {Map<string, Map<string, Array<{selector: string, handler: Function, options: Object}>>>}
|
|
27
|
+
*/
|
|
28
|
+
this.delegatedHandlers = new Map();
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Map lưu root listeners đã attach
|
|
32
|
+
* @type {Map<HTMLElement, Map<string, Function>>}
|
|
33
|
+
*/
|
|
34
|
+
this.rootListeners = new Map();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Root element mặc định cho delegation
|
|
38
|
+
* @type {HTMLElement}
|
|
39
|
+
*/
|
|
40
|
+
this.defaultRoot = document;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Cache selector matches để improve performance
|
|
44
|
+
* @type {WeakMap<HTMLElement, Map<string, boolean>>}
|
|
45
|
+
*/
|
|
46
|
+
this.matchCache = new WeakMap();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Đăng ký delegated event handler
|
|
51
|
+
*
|
|
52
|
+
* @param {string} eventType - Loại event (click, input, etc.)
|
|
53
|
+
* @param {string} selector - CSS selector để match elements
|
|
54
|
+
* @param {Function} handler - Handler function
|
|
55
|
+
* @param {Object} options - Tùy chọn
|
|
56
|
+
* @param {HTMLElement} options.root - Root element để attach listener (default: document)
|
|
57
|
+
* @param {boolean} options.capture - Sử dụng capture phase
|
|
58
|
+
* @param {boolean} options.once - Chỉ chạy một lần
|
|
59
|
+
* @returns {Function} Unsubscribe function
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* const unsubscribe = EventDelegator.on('click', '.btn-submit', (e) => {
|
|
63
|
+
* console.log('Button clicked:', e.target);
|
|
64
|
+
* });
|
|
65
|
+
*/
|
|
66
|
+
on(eventType, selector, handler, options = {}) {
|
|
67
|
+
const root = options.root || this.defaultRoot;
|
|
68
|
+
const capture = options.capture || false;
|
|
69
|
+
|
|
70
|
+
// Tạo structure nếu chưa tồn tại
|
|
71
|
+
if (!this.delegatedHandlers.has(eventType)) {
|
|
72
|
+
this.delegatedHandlers.set(eventType, new Map());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const eventMap = this.delegatedHandlers.get(eventType);
|
|
76
|
+
const rootKey = this._getRootKey(root);
|
|
77
|
+
|
|
78
|
+
if (!eventMap.has(rootKey)) {
|
|
79
|
+
eventMap.set(rootKey, []);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const handlers = eventMap.get(rootKey);
|
|
83
|
+
|
|
84
|
+
// Wrap handler nếu có option once
|
|
85
|
+
const wrappedHandler = options.once
|
|
86
|
+
? (...args) => {
|
|
87
|
+
handler(...args);
|
|
88
|
+
this.off(eventType, selector, wrappedHandler, { root });
|
|
89
|
+
}
|
|
90
|
+
: handler;
|
|
91
|
+
|
|
92
|
+
// Lưu handler info
|
|
93
|
+
const handlerInfo = { selector, handler: wrappedHandler, options };
|
|
94
|
+
handlers.push(handlerInfo);
|
|
95
|
+
|
|
96
|
+
// Attach root listener nếu chưa có
|
|
97
|
+
this._ensureRootListener(root, eventType, capture);
|
|
98
|
+
|
|
99
|
+
// Return unsubscribe function
|
|
100
|
+
return () => this.off(eventType, selector, wrappedHandler, { root });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Gỡ bỏ delegated event handler
|
|
105
|
+
*
|
|
106
|
+
* @param {string} eventType - Loại event
|
|
107
|
+
* @param {string} selector - CSS selector
|
|
108
|
+
* @param {Function} handler - Handler function (optional, nếu không có sẽ xóa tất cả)
|
|
109
|
+
* @param {Object} options - Tùy chọn
|
|
110
|
+
* @param {HTMLElement} options.root - Root element
|
|
111
|
+
*/
|
|
112
|
+
off(eventType, selector, handler = null, options = {}) {
|
|
113
|
+
const root = options.root || this.defaultRoot;
|
|
114
|
+
const eventMap = this.delegatedHandlers.get(eventType);
|
|
115
|
+
|
|
116
|
+
if (!eventMap) return;
|
|
117
|
+
|
|
118
|
+
const rootKey = this._getRootKey(root);
|
|
119
|
+
const handlers = eventMap.get(rootKey);
|
|
120
|
+
|
|
121
|
+
if (!handlers) return;
|
|
122
|
+
|
|
123
|
+
// Lọc handlers
|
|
124
|
+
const filtered = handlers.filter(info => {
|
|
125
|
+
if (info.selector !== selector) return true;
|
|
126
|
+
if (handler && info.handler !== handler) return true;
|
|
127
|
+
return false;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
eventMap.set(rootKey, filtered);
|
|
131
|
+
|
|
132
|
+
// Nếu không còn handlers cho root này, remove root listener
|
|
133
|
+
if (filtered.length === 0) {
|
|
134
|
+
this._removeRootListener(root, eventType);
|
|
135
|
+
eventMap.delete(rootKey);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Nếu không còn roots cho event type này, xóa event type
|
|
139
|
+
if (eventMap.size === 0) {
|
|
140
|
+
this.delegatedHandlers.delete(eventType);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Xóa tất cả delegated handlers cho một root
|
|
146
|
+
*
|
|
147
|
+
* @param {HTMLElement} root - Root element
|
|
148
|
+
*/
|
|
149
|
+
clearRoot(root = this.defaultRoot) {
|
|
150
|
+
const rootKey = this._getRootKey(root);
|
|
151
|
+
|
|
152
|
+
this.delegatedHandlers.forEach((eventMap, eventType) => {
|
|
153
|
+
if (eventMap.has(rootKey)) {
|
|
154
|
+
this._removeRootListener(root, eventType);
|
|
155
|
+
eventMap.delete(rootKey);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (eventMap.size === 0) {
|
|
159
|
+
this.delegatedHandlers.delete(eventType);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
this.rootListeners.delete(root);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Xóa tất cả delegated handlers
|
|
168
|
+
*/
|
|
169
|
+
clearAll() {
|
|
170
|
+
this.rootListeners.forEach((listenerMap, root) => {
|
|
171
|
+
listenerMap.forEach((listener, eventType) => {
|
|
172
|
+
root.removeEventListener(eventType, listener, true);
|
|
173
|
+
root.removeEventListener(eventType, listener, false);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
this.delegatedHandlers.clear();
|
|
178
|
+
this.rootListeners.clear();
|
|
179
|
+
this.matchCache = new WeakMap();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Kiểm tra element có match selector không (với cache)
|
|
184
|
+
*
|
|
185
|
+
* @param {HTMLElement} element - Element cần kiểm tra
|
|
186
|
+
* @param {string} selector - CSS selector
|
|
187
|
+
* @returns {boolean}
|
|
188
|
+
* @private
|
|
189
|
+
*/
|
|
190
|
+
_matchesSelector(element, selector) {
|
|
191
|
+
if (!element || !element.matches) return false;
|
|
192
|
+
|
|
193
|
+
// Check cache
|
|
194
|
+
let cache = this.matchCache.get(element);
|
|
195
|
+
if (!cache) {
|
|
196
|
+
cache = new Map();
|
|
197
|
+
this.matchCache.set(element, cache);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (cache.has(selector)) {
|
|
201
|
+
return cache.get(selector);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Compute and cache
|
|
205
|
+
const matches = element.matches(selector);
|
|
206
|
+
cache.set(selector, matches);
|
|
207
|
+
|
|
208
|
+
return matches;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Tìm matching element trong event path
|
|
213
|
+
*
|
|
214
|
+
* @param {Event} event - DOM event
|
|
215
|
+
* @param {string} selector - CSS selector
|
|
216
|
+
* @param {HTMLElement} root - Root element
|
|
217
|
+
* @returns {HTMLElement|null}
|
|
218
|
+
* @private
|
|
219
|
+
*/
|
|
220
|
+
_findMatchingElement(event, selector, root) {
|
|
221
|
+
let element = event.target;
|
|
222
|
+
|
|
223
|
+
// Traverse lên parent tree cho đến root
|
|
224
|
+
while (element && element !== root) {
|
|
225
|
+
if (this._matchesSelector(element, selector)) {
|
|
226
|
+
return element;
|
|
227
|
+
}
|
|
228
|
+
element = element.parentElement;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Check root itself
|
|
232
|
+
if (element === root && this._matchesSelector(element, selector)) {
|
|
233
|
+
return element;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Tạo root listener cho event type
|
|
241
|
+
*
|
|
242
|
+
* @param {HTMLElement} root - Root element
|
|
243
|
+
* @param {string} eventType - Event type
|
|
244
|
+
* @param {boolean} capture - Use capture phase
|
|
245
|
+
* @private
|
|
246
|
+
*/
|
|
247
|
+
_ensureRootListener(root, eventType, capture = false) {
|
|
248
|
+
// Check đã có listener chưa
|
|
249
|
+
let listenerMap = this.rootListeners.get(root);
|
|
250
|
+
if (!listenerMap) {
|
|
251
|
+
listenerMap = new Map();
|
|
252
|
+
this.rootListeners.set(root, listenerMap);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const listenerKey = `${eventType}:${capture}`;
|
|
256
|
+
if (listenerMap.has(listenerKey)) {
|
|
257
|
+
return; // Đã có listener
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Tạo delegating listener
|
|
261
|
+
const listener = (event) => {
|
|
262
|
+
const eventMap = this.delegatedHandlers.get(eventType);
|
|
263
|
+
if (!eventMap) return;
|
|
264
|
+
|
|
265
|
+
const rootKey = this._getRootKey(root);
|
|
266
|
+
const handlers = eventMap.get(rootKey);
|
|
267
|
+
if (!handlers || handlers.length === 0) return;
|
|
268
|
+
|
|
269
|
+
// 1. Collect all matches
|
|
270
|
+
const matchedHandlers = [];
|
|
271
|
+
for (const handlerInfo of handlers) {
|
|
272
|
+
const matchedElement = this._findMatchingElement(event, handlerInfo.selector, root);
|
|
273
|
+
if (matchedElement) {
|
|
274
|
+
matchedHandlers.push({
|
|
275
|
+
...handlerInfo,
|
|
276
|
+
element: matchedElement
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// 2. Sort by depth (deepest first = child before parent)
|
|
282
|
+
matchedHandlers.sort((a, b) => {
|
|
283
|
+
if (a.element === b.element) return 0;
|
|
284
|
+
// If a contains b, a is parent/ancestor of b. We want b (child) first.
|
|
285
|
+
return a.element.contains(b.element) ? 1 : -1;
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// 3. Shim stopPropagation to track status
|
|
289
|
+
let isPropagationStopped = false;
|
|
290
|
+
const originalStopPropagation = event.stopPropagation;
|
|
291
|
+
event.stopPropagation = function () {
|
|
292
|
+
isPropagationStopped = true;
|
|
293
|
+
if (originalStopPropagation) {
|
|
294
|
+
originalStopPropagation.apply(this, arguments);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// 4. Execute handlers
|
|
299
|
+
for (const { selector, handler, options, element } of matchedHandlers) {
|
|
300
|
+
if (isPropagationStopped) break;
|
|
301
|
+
|
|
302
|
+
// Add delegateTarget property to original event
|
|
303
|
+
// KHÔNG tạo Object.create(event) vì sẽ mất native Event methods
|
|
304
|
+
Object.defineProperty(event, 'delegateTarget', {
|
|
305
|
+
value: element,
|
|
306
|
+
writable: false,
|
|
307
|
+
enumerable: true,
|
|
308
|
+
configurable: true
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
// Gọi handler với event gốc, KHÔNG thay đổi context
|
|
313
|
+
// Handler giữ nguyên context của nó (controller/view)
|
|
314
|
+
const result = handler(event);
|
|
315
|
+
|
|
316
|
+
// Xử lý return value
|
|
317
|
+
if (result === false) {
|
|
318
|
+
event.preventDefault();
|
|
319
|
+
event.stopPropagation();
|
|
320
|
+
}
|
|
321
|
+
} catch (error) {
|
|
322
|
+
console.error('[EventDelegator] Handler error:', error, {
|
|
323
|
+
eventType,
|
|
324
|
+
selector,
|
|
325
|
+
element
|
|
326
|
+
});
|
|
327
|
+
} finally {
|
|
328
|
+
// Cleanup delegateTarget property
|
|
329
|
+
delete event.delegateTarget;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// Attach listener
|
|
335
|
+
root.addEventListener(eventType, listener, capture);
|
|
336
|
+
listenerMap.set(listenerKey, listener);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Gỡ bỏ root listener
|
|
341
|
+
*
|
|
342
|
+
* @param {HTMLElement} root - Root element
|
|
343
|
+
* @param {string} eventType - Event type
|
|
344
|
+
* @private
|
|
345
|
+
*/
|
|
346
|
+
_removeRootListener(root, eventType) {
|
|
347
|
+
const listenerMap = this.rootListeners.get(root);
|
|
348
|
+
if (!listenerMap) return;
|
|
349
|
+
|
|
350
|
+
// Remove both capture and non-capture listeners
|
|
351
|
+
['true', 'false'].forEach(captureStr => {
|
|
352
|
+
const capture = captureStr === 'true';
|
|
353
|
+
const listenerKey = `${eventType}:${capture}`;
|
|
354
|
+
const listener = listenerMap.get(listenerKey);
|
|
355
|
+
|
|
356
|
+
if (listener) {
|
|
357
|
+
root.removeEventListener(eventType, listener, capture);
|
|
358
|
+
listenerMap.delete(listenerKey);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Nếu không còn listeners, xóa map
|
|
363
|
+
if (listenerMap.size === 0) {
|
|
364
|
+
this.rootListeners.delete(root);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Tạo unique key cho root element
|
|
370
|
+
*
|
|
371
|
+
* @param {HTMLElement} root - Root element
|
|
372
|
+
* @returns {string}
|
|
373
|
+
* @private
|
|
374
|
+
*/
|
|
375
|
+
_getRootKey(root) {
|
|
376
|
+
if (root === document) return 'document';
|
|
377
|
+
if (root === window) return 'window';
|
|
378
|
+
if (root.id) return `#${root.id}`;
|
|
379
|
+
|
|
380
|
+
// Fallback: use WeakMap hoặc generate unique ID
|
|
381
|
+
if (!root._delegatorKey) {
|
|
382
|
+
root._delegatorKey = `root_${Math.random().toString(36).substr(2, 9)}`;
|
|
383
|
+
}
|
|
384
|
+
return root._delegatorKey;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Lấy thống kê về delegated handlers
|
|
389
|
+
* Hữu ích cho debugging và monitoring
|
|
390
|
+
*
|
|
391
|
+
* @returns {Object}
|
|
392
|
+
*/
|
|
393
|
+
getStats() {
|
|
394
|
+
const stats = {
|
|
395
|
+
eventTypes: this.delegatedHandlers.size,
|
|
396
|
+
roots: this.rootListeners.size,
|
|
397
|
+
totalHandlers: 0,
|
|
398
|
+
byEventType: {}
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
this.delegatedHandlers.forEach((eventMap, eventType) => {
|
|
402
|
+
let count = 0;
|
|
403
|
+
eventMap.forEach(handlers => {
|
|
404
|
+
count += handlers.length;
|
|
405
|
+
});
|
|
406
|
+
stats.totalHandlers += count;
|
|
407
|
+
stats.byEventType[eventType] = count;
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
return stats;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Export singleton instance
|
|
415
|
+
const delegator = new EventDelegator();
|
|
416
|
+
export default delegator;
|