ezfw-core 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/components/EzBaseComponent.ts +648 -0
- package/components/EzComponent.ts +89 -0
- package/components/EzInput.module.scss +183 -0
- package/components/EzInput.ts +104 -0
- package/components/EzLabel.ts +22 -0
- package/components/EzOutlet.ts +181 -0
- package/components/HtmlWrapper.ts +305 -0
- package/components/avatar/EzAvatar.module.scss +200 -0
- package/components/avatar/EzAvatar.ts +130 -0
- package/components/badge/EzBadge.module.scss +202 -0
- package/components/badge/EzBadge.ts +77 -0
- package/components/button/EzButton.module.scss +402 -0
- package/components/button/EzButton.ts +175 -0
- package/components/button/EzButtonGroup.ts +48 -0
- package/components/card/EzCard.module.scss +71 -0
- package/components/card/EzCard.ts +120 -0
- package/components/chart/EzBarChart.ts +47 -0
- package/components/chart/EzChart.module.scss +14 -0
- package/components/chart/EzChart.ts +279 -0
- package/components/chart/EzDoughnutChart.ts +47 -0
- package/components/chart/EzLineChart.ts +53 -0
- package/components/checkbox/EzCheckbox.module.scss +145 -0
- package/components/checkbox/EzCheckbox.ts +115 -0
- package/components/dataview/EzDataView.module.scss +115 -0
- package/components/dataview/EzDataView.ts +355 -0
- package/components/dataview/modes/EzDataViewCards.ts +322 -0
- package/components/dataview/modes/EzDataViewGrid.ts +76 -0
- package/components/datepicker/EzDatePicker.module.scss +348 -0
- package/components/datepicker/EzDatePicker.ts +519 -0
- package/components/dialog/EzDialog.module.scss +180 -0
- package/components/dropdown/EzDropdown.module.scss +107 -0
- package/components/dropdown/EzDropdown.ts +235 -0
- package/components/feed/EzActivityFeed.module.scss +90 -0
- package/components/feed/EzActivityFeed.ts +78 -0
- package/components/form/EzForm.ts +364 -0
- package/components/form/EzValidators.test.js +421 -0
- package/components/form/EzValidators.ts +202 -0
- package/components/grid/EzGrid.scss +88 -0
- package/components/grid/EzGrid.ts +1085 -0
- package/components/grid/EzGridContainer.ts +104 -0
- package/components/grid/body/EzGridBody.scss +283 -0
- package/components/grid/body/EzGridBody.ts +549 -0
- package/components/grid/body/EzGridCell.ts +211 -0
- package/components/grid/body/EzGridRow.ts +196 -0
- package/components/grid/filter/EzGridFilters.scss +78 -0
- package/components/grid/filter/EzGridFilters.ts +285 -0
- package/components/grid/footer/EzGridFooter.scss +136 -0
- package/components/grid/footer/EzGridFooter.ts +448 -0
- package/components/grid/header/EzGridHeader.scss +199 -0
- package/components/grid/header/EzGridHeader.ts +430 -0
- package/components/grid/query/EzGridQuery.ts +81 -0
- package/components/grid/state/EzGridColumns.ts +155 -0
- package/components/grid/state/EzGridController.ts +470 -0
- package/components/grid/state/EzGridLifecycle.ts +136 -0
- package/components/grid/state/EzGridNormalizers.test.js +273 -0
- package/components/grid/state/EzGridNormalizers.ts +162 -0
- package/components/grid/state/EzGridParts.ts +233 -0
- package/components/grid/state/EzGridPersistence.ts +140 -0
- package/components/grid/state/EzGridRemote.test.js +573 -0
- package/components/grid/state/EzGridRemote.ts +335 -0
- package/components/grid/state/EzGridSelection.ts +231 -0
- package/components/grid/state/EzGridSort.ts +286 -0
- package/components/grid/title/EzGridActionBar.ts +98 -0
- package/components/grid/title/EzGridTitle.ts +114 -0
- package/components/grid/title/EzGridTitleBar.scss +65 -0
- package/components/grid/title/EzGridTitleBar.ts +87 -0
- package/components/grid/types.ts +607 -0
- package/components/panel/EzPanel.module.scss +133 -0
- package/components/panel/EzPanel.ts +147 -0
- package/components/radio/EzRadio.module.scss +190 -0
- package/components/radio/EzRadio.ts +149 -0
- package/components/select/EzSelect.module.scss +153 -0
- package/components/select/EzSelect.ts +238 -0
- package/components/skeleton/EzSkeleton.module.scss +95 -0
- package/components/skeleton/EzSkeleton.ts +70 -0
- package/components/store/EzStore.ts +344 -0
- package/components/switch/EzSwitch.module.scss +164 -0
- package/components/switch/EzSwitch.ts +117 -0
- package/components/tabs/EzTabPanel.module.scss +181 -0
- package/components/tabs/EzTabPanel.ts +402 -0
- package/components/textarea/EzTextarea.module.scss +131 -0
- package/components/textarea/EzTextarea.ts +161 -0
- package/components/timepicker/EzTimePicker.module.scss +282 -0
- package/components/timepicker/EzTimePicker.ts +540 -0
- package/components/toast/EzToast.module.scss +291 -0
- package/components/tooltip/EzTooltip.module.scss +124 -0
- package/components/tooltip/EzTooltip.ts +153 -0
- package/core/EzComponentTypes.ts +693 -0
- package/core/EzError.ts +63 -0
- package/core/EzModel.ts +268 -0
- package/core/EzTypes.ts +328 -0
- package/core/eventBus.ts +284 -0
- package/core/ez.ts +617 -0
- package/core/loader.ts +725 -0
- package/core/renderer.ts +1010 -0
- package/core/router.ts +490 -0
- package/core/services.ts +124 -0
- package/core/state.ts +142 -0
- package/core/utils.ts +81 -0
- package/package.json +51 -0
- package/services/RouteUI.js +17 -0
- package/services/crypto.js +64 -0
- package/services/dialog.js +222 -0
- package/services/fetchApi.js +63 -0
- package/services/firebase.js +30 -0
- package/services/toast.js +214 -0
- package/template/doc/EzDocs.js +15 -0
- package/template/doc/EzDocs.module.scss +627 -0
- package/template/doc/EzDocsController.js +164 -0
- package/template/doc/data/activityfeed/EzActivityFeedDoc.js +42 -0
- package/template/doc/data/avatar/EzAvatarDoc.js +71 -0
- package/template/doc/data/badge/EzBadgeDoc.js +92 -0
- package/template/doc/data/button/EzButtonDoc.js +77 -0
- package/template/doc/data/buttongroup/EzButtonGroupDoc.js +102 -0
- package/template/doc/data/card/EzCardDoc.js +39 -0
- package/template/doc/data/chart/EzChartDoc.js +60 -0
- package/template/doc/data/checkbox/EzCheckboxDoc.js +67 -0
- package/template/doc/data/component/EzComponentDoc.js +34 -0
- package/template/doc/data/cssmodules/CSSModulesDoc.js +70 -0
- package/template/doc/data/datepicker/EzDatePickerDoc.js +126 -0
- package/template/doc/data/dialog/EzDialogDoc.js +217 -0
- package/template/doc/data/dropdown/EzDropdownDoc.js +178 -0
- package/template/doc/data/form/EzFormDoc.js +90 -0
- package/template/doc/data/grid/EzGridDoc.js +99 -0
- package/template/doc/data/input/EzInputDoc.js +92 -0
- package/template/doc/data/label/EzLabelDoc.js +40 -0
- package/template/doc/data/model/EzModelDoc.js +53 -0
- package/template/doc/data/outlet/EzOutletDoc.js +63 -0
- package/template/doc/data/panel/EzPanelDoc.js +214 -0
- package/template/doc/data/radio/EzRadioDoc.js +174 -0
- package/template/doc/data/router/EzRouterDoc.js +75 -0
- package/template/doc/data/select/EzSelectDoc.js +37 -0
- package/template/doc/data/skeleton/EzSkeletonDoc.js +149 -0
- package/template/doc/data/switch/EzSwitchDoc.js +82 -0
- package/template/doc/data/tabpanel/EzTabPanelDoc.js +44 -0
- package/template/doc/data/textarea/EzTextareaDoc.js +131 -0
- package/template/doc/data/timepicker/EzTimePickerDoc.js +107 -0
- package/template/doc/data/tooltip/EzTooltipDoc.js +193 -0
- package/template/doc/data/validators/EzValidatorsDoc.js +37 -0
- package/template/doc/sidebar/EzDocsSidebar.js +32 -0
- package/template/doc/sidebar/category/EzDocsCategory.js +33 -0
- package/template/doc/sidebar/item/EzDocsComponentItem.js +24 -0
- package/template/doc/viewer/EzDocsViewer.js +18 -0
- package/template/doc/viewer/codepanel/EzDocsCodePanel.js +51 -0
- package/template/doc/viewer/content/EzDocsContent.js +315 -0
- package/template/doc/viewer/header/EzDocsViewerHeader.js +46 -0
- package/template/doc/viewer/showcase/EzDocsShowcase.js +59 -0
- package/template/doc/viewer/showcase/EzDocsShowcaseSection.js +25 -0
- package/template/doc/viewer/showcase/EzDocsVariantItem.js +29 -0
- package/template/doc/welcome/EzDocsWelcome.js +48 -0
- package/themes/ez-theme.scss +179 -0
- package/themes/nature-fresh.scss +169 -0
- package/types/global.d.ts +21 -0
- package/utils/cssModules.js +81 -0
package/core/router.ts
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import { EzError } from './EzError.js';
|
|
2
|
+
|
|
3
|
+
export interface NavigationContext {
|
|
4
|
+
from: string;
|
|
5
|
+
to: string;
|
|
6
|
+
params: Record<string, string>;
|
|
7
|
+
match: RouteMatch | null;
|
|
8
|
+
meta: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type MiddlewareFunction = (context: NavigationContext, next: () => Promise<void>) => Promise<void> | void;
|
|
12
|
+
|
|
13
|
+
export interface RouteDefinition {
|
|
14
|
+
path: string;
|
|
15
|
+
view: string;
|
|
16
|
+
protected?: (string | (() => boolean | Promise<boolean>))[];
|
|
17
|
+
children?: RouteDefinition[];
|
|
18
|
+
middleware?: MiddlewareFunction[];
|
|
19
|
+
meta?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface NormalizedRoute {
|
|
23
|
+
path: string;
|
|
24
|
+
view: string;
|
|
25
|
+
protected: (string | (() => boolean | Promise<boolean>))[];
|
|
26
|
+
children: NormalizedRoute[];
|
|
27
|
+
middleware: MiddlewareFunction[];
|
|
28
|
+
meta: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RouteChainItem {
|
|
32
|
+
path: string;
|
|
33
|
+
view: string;
|
|
34
|
+
params: Record<string, string>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface RouteMatch {
|
|
38
|
+
chain: RouteChainItem[];
|
|
39
|
+
params: Record<string, string>;
|
|
40
|
+
fullPath: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface RouteInfo {
|
|
44
|
+
path: string;
|
|
45
|
+
params: Record<string, string>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type ErrorHandler = (error: Error, context: NavigationContext) => void;
|
|
49
|
+
|
|
50
|
+
interface ControllerInstance {
|
|
51
|
+
onRouteChange?: (info: { fullPath: string; params: Record<string, string>; chain: RouteChainItem[] }) => Promise<void>;
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface EzInstance {
|
|
56
|
+
_context: {
|
|
57
|
+
route?: string | null;
|
|
58
|
+
view?: string | null;
|
|
59
|
+
controller?: string | null;
|
|
60
|
+
};
|
|
61
|
+
_loader: {
|
|
62
|
+
getModules(): Record<string, unknown>;
|
|
63
|
+
loadModule(path: string): Promise<void>;
|
|
64
|
+
resolveEzType(name: string): Promise<void>;
|
|
65
|
+
prefetchAll(views: string[]): Promise<void>;
|
|
66
|
+
};
|
|
67
|
+
_registry: Record<string, { controller?: string; [key: string]: unknown }>;
|
|
68
|
+
_eventBus?: {
|
|
69
|
+
emit(event: string, data: unknown): void;
|
|
70
|
+
};
|
|
71
|
+
get(name: string): unknown;
|
|
72
|
+
getController(name: string): Promise<ControllerInstance>;
|
|
73
|
+
_createElement(config: unknown): Promise<HTMLElement>;
|
|
74
|
+
handleFrameworkError(error: Error): void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class EzRouter {
|
|
78
|
+
private ez: EzInstance;
|
|
79
|
+
private _routes: NormalizedRoute[] = [];
|
|
80
|
+
private _currentLayoutKey: string | null = null;
|
|
81
|
+
private _currentPath: string = '/';
|
|
82
|
+
private _beforeMiddleware: MiddlewareFunction[] = [];
|
|
83
|
+
private _afterMiddleware: MiddlewareFunction[] = [];
|
|
84
|
+
private _errorHandlers: ErrorHandler[] = [];
|
|
85
|
+
private _navigating: boolean = false;
|
|
86
|
+
|
|
87
|
+
constructor(ez: EzInstance) {
|
|
88
|
+
this.ez = ez;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
routes(routeDefs: RouteDefinition[]): void {
|
|
92
|
+
if (!Array.isArray(routeDefs)) {
|
|
93
|
+
console.error('[ez.routes] Must receive an array of route definitions.');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const normalize = (r: RouteDefinition): NormalizedRoute => ({
|
|
98
|
+
path: r.path || '/',
|
|
99
|
+
view: r.view,
|
|
100
|
+
protected: r.protected || [],
|
|
101
|
+
children: Array.isArray(r.children)
|
|
102
|
+
? r.children.map(normalize)
|
|
103
|
+
: [],
|
|
104
|
+
middleware: r.middleware || [],
|
|
105
|
+
meta: r.meta || {}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
this._routes = routeDefs.map(normalize);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
beforeEach(fn: MiddlewareFunction): () => void {
|
|
112
|
+
if (typeof fn !== 'function') {
|
|
113
|
+
throw new Error('[EzRouter] Middleware must be a function');
|
|
114
|
+
}
|
|
115
|
+
this._beforeMiddleware.push(fn);
|
|
116
|
+
return () => {
|
|
117
|
+
const idx = this._beforeMiddleware.indexOf(fn);
|
|
118
|
+
if (idx > -1) this._beforeMiddleware.splice(idx, 1);
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
afterEach(fn: MiddlewareFunction): () => void {
|
|
123
|
+
if (typeof fn !== 'function') {
|
|
124
|
+
throw new Error('[EzRouter] Middleware must be a function');
|
|
125
|
+
}
|
|
126
|
+
this._afterMiddleware.push(fn);
|
|
127
|
+
return () => {
|
|
128
|
+
const idx = this._afterMiddleware.indexOf(fn);
|
|
129
|
+
if (idx > -1) this._afterMiddleware.splice(idx, 1);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
onError(fn: ErrorHandler): () => void {
|
|
134
|
+
if (typeof fn !== 'function') {
|
|
135
|
+
throw new Error('[EzRouter] Error handler must be a function');
|
|
136
|
+
}
|
|
137
|
+
this._errorHandlers.push(fn);
|
|
138
|
+
return () => {
|
|
139
|
+
const idx = this._errorHandlers.indexOf(fn);
|
|
140
|
+
if (idx > -1) this._errorHandlers.splice(idx, 1);
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private async _runMiddlewareChain(middlewares: MiddlewareFunction[], context: NavigationContext): Promise<boolean> {
|
|
145
|
+
if (!middlewares.length) return true;
|
|
146
|
+
|
|
147
|
+
let index = 0;
|
|
148
|
+
let completed = false;
|
|
149
|
+
|
|
150
|
+
const next = async (): Promise<void> => {
|
|
151
|
+
if (index >= middlewares.length) {
|
|
152
|
+
completed = true;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const middleware = middlewares[index++];
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
await middleware(context, next);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
this._handleNavigationError(err as Error, context);
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
await next();
|
|
168
|
+
return completed;
|
|
169
|
+
} catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private _handleNavigationError(error: Error, context: NavigationContext): void {
|
|
175
|
+
if (this._errorHandlers.length) {
|
|
176
|
+
for (const handler of this._errorHandlers) {
|
|
177
|
+
try {
|
|
178
|
+
handler(error, context);
|
|
179
|
+
} catch (e) {
|
|
180
|
+
console.error('[EzRouter] Error in error handler:', e);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
this.ez.handleFrameworkError(error);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
go(path: string): void {
|
|
189
|
+
const clean = path || '/';
|
|
190
|
+
|
|
191
|
+
window.history.pushState({}, '', clean);
|
|
192
|
+
|
|
193
|
+
this._onLocationChange().catch(err => {
|
|
194
|
+
this.ez.handleFrameworkError(err);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async navigate(viewName: string, route: RouteInfo | null = null): Promise<void> {
|
|
199
|
+
this.ez._context.view = viewName;
|
|
200
|
+
this.ez._context.route = route?.path ?? null;
|
|
201
|
+
this.ez._context.controller = null;
|
|
202
|
+
|
|
203
|
+
const modules = this.ez._loader.getModules();
|
|
204
|
+
const path = Object.keys(modules).find(p =>
|
|
205
|
+
p.toLowerCase().includes(`/${viewName.toLowerCase()}/`)
|
|
206
|
+
|| p.toLowerCase().endsWith(`/${viewName.toLowerCase()}.js`)
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
if (!path) {
|
|
210
|
+
throw new EzError({
|
|
211
|
+
code: 'EZ_VIEW_404',
|
|
212
|
+
source: 'router',
|
|
213
|
+
message: `View "${viewName}" not found`,
|
|
214
|
+
route: this.ez._context.route,
|
|
215
|
+
view: viewName
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
await this.ez._loader.loadModule(path);
|
|
220
|
+
|
|
221
|
+
const def = this.ez.get(viewName);
|
|
222
|
+
if (!def) {
|
|
223
|
+
throw new EzError({
|
|
224
|
+
code: 'EZ_VIEW_404',
|
|
225
|
+
source: 'router',
|
|
226
|
+
message: `View "${viewName}" not found`,
|
|
227
|
+
route: this.ez._context.route,
|
|
228
|
+
view: viewName
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const config = {
|
|
233
|
+
eztype: viewName,
|
|
234
|
+
route,
|
|
235
|
+
routeParams: route?.params
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const root = document.getElementById('app');
|
|
239
|
+
if (!root) return;
|
|
240
|
+
|
|
241
|
+
root.querySelectorAll('*').forEach(node => {
|
|
242
|
+
const inst = (node as unknown as { __ezInstance?: { destroy?: () => void } }).__ezInstance;
|
|
243
|
+
if (inst && typeof inst.destroy === 'function') {
|
|
244
|
+
inst.destroy();
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
root.innerHTML = '';
|
|
249
|
+
|
|
250
|
+
const el = await this.ez._createElement(config);
|
|
251
|
+
root.appendChild(el);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private _matchRoute(path: string): RouteMatch | null {
|
|
255
|
+
const clean = path.replace(/\/+$/, '') || '/';
|
|
256
|
+
const segments = clean.split('/').filter(Boolean);
|
|
257
|
+
|
|
258
|
+
for (const route of this._routes) {
|
|
259
|
+
const match = this._matchRouteNode(route, segments, []);
|
|
260
|
+
if (match) return match;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private _matchRouteNode(
|
|
267
|
+
route: NormalizedRoute,
|
|
268
|
+
segments: string[],
|
|
269
|
+
chain: RouteChainItem[],
|
|
270
|
+
parentParams: Record<string, string> = {}
|
|
271
|
+
): RouteMatch | null {
|
|
272
|
+
const routeSegments = route.path
|
|
273
|
+
.replace(/^\/+/, '')
|
|
274
|
+
.split('/')
|
|
275
|
+
.filter(Boolean);
|
|
276
|
+
|
|
277
|
+
if (routeSegments.length > segments.length) return null;
|
|
278
|
+
|
|
279
|
+
const params: Record<string, string> = { ...parentParams };
|
|
280
|
+
|
|
281
|
+
for (let i = 0; i < routeSegments.length; i++) {
|
|
282
|
+
const r = routeSegments[i];
|
|
283
|
+
const s = segments[i];
|
|
284
|
+
|
|
285
|
+
if (r.startsWith(':')) {
|
|
286
|
+
params[r.slice(1)] = decodeURIComponent(s);
|
|
287
|
+
} else if (r.toLowerCase() !== s.toLowerCase()) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const consumed = routeSegments.length;
|
|
293
|
+
const nextSegments = segments.slice(consumed);
|
|
294
|
+
|
|
295
|
+
const nextChain: RouteChainItem[] = [
|
|
296
|
+
...chain,
|
|
297
|
+
{ path: route.path, view: route.view, params }
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
if (!nextSegments.length) {
|
|
301
|
+
return {
|
|
302
|
+
chain: nextChain,
|
|
303
|
+
params,
|
|
304
|
+
fullPath: '/' + segments.join('/')
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (route.children) {
|
|
309
|
+
for (const child of route.children) {
|
|
310
|
+
const childMatch = this._matchRouteNode(
|
|
311
|
+
child,
|
|
312
|
+
nextSegments,
|
|
313
|
+
nextChain,
|
|
314
|
+
params
|
|
315
|
+
);
|
|
316
|
+
if (childMatch) return childMatch;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private async _onLocationChange(): Promise<void> {
|
|
324
|
+
if (this._navigating) return;
|
|
325
|
+
this._navigating = true;
|
|
326
|
+
|
|
327
|
+
const fromPath = this._currentPath;
|
|
328
|
+
const toPath = window.location.pathname || '/';
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
const match = this._matchRoute(toPath);
|
|
332
|
+
if (!match) {
|
|
333
|
+
throw new EzError({
|
|
334
|
+
code: 'EZ_ROUTER_404',
|
|
335
|
+
source: 'router',
|
|
336
|
+
message: `No route matched "${toPath}"`,
|
|
337
|
+
route: toPath
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const { chain } = match;
|
|
342
|
+
if (!chain.length) return;
|
|
343
|
+
|
|
344
|
+
const root = chain[0];
|
|
345
|
+
const children = chain.slice(1);
|
|
346
|
+
|
|
347
|
+
const routeDef = this._routes.find(r => r.view === root.view);
|
|
348
|
+
|
|
349
|
+
const context: NavigationContext = {
|
|
350
|
+
from: fromPath,
|
|
351
|
+
to: toPath,
|
|
352
|
+
params: match.params,
|
|
353
|
+
match,
|
|
354
|
+
meta: routeDef?.meta || {}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const beforeOk = await this._runMiddlewareChain(
|
|
358
|
+
this._beforeMiddleware,
|
|
359
|
+
context
|
|
360
|
+
);
|
|
361
|
+
if (!beforeOk) return;
|
|
362
|
+
|
|
363
|
+
if (routeDef?.middleware?.length) {
|
|
364
|
+
const routeOk = await this._runMiddlewareChain(
|
|
365
|
+
routeDef.middleware,
|
|
366
|
+
context
|
|
367
|
+
);
|
|
368
|
+
if (!routeOk) return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
this.ez._context.route = toPath;
|
|
372
|
+
this.ez._context.view = root.view;
|
|
373
|
+
|
|
374
|
+
if (routeDef?.protected?.length) {
|
|
375
|
+
for (const rule of routeDef.protected) {
|
|
376
|
+
const ok = await this._resolveProtectedCheck(rule);
|
|
377
|
+
if (!ok) {
|
|
378
|
+
this.go('/');
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (this._currentLayoutKey !== root.view) {
|
|
385
|
+
this._currentLayoutKey = root.view;
|
|
386
|
+
await this.navigate(root.view, {
|
|
387
|
+
path: root.path,
|
|
388
|
+
params: root.params
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const controllerName = await this._resolveControllerFromView(root.view);
|
|
393
|
+
if (controllerName) {
|
|
394
|
+
const ctrl = await this.ez.getController(controllerName);
|
|
395
|
+
await ctrl?.onRouteChange?.({
|
|
396
|
+
fullPath: toPath,
|
|
397
|
+
params: match.params,
|
|
398
|
+
chain: children
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
this._currentPath = toPath;
|
|
403
|
+
|
|
404
|
+
await this._runMiddlewareChain(this._afterMiddleware, context);
|
|
405
|
+
|
|
406
|
+
this.ez._eventBus?.emit('route:change', context);
|
|
407
|
+
|
|
408
|
+
} finally {
|
|
409
|
+
this._navigating = false;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private async _resolveProtectedCheck(str: string | (() => boolean | Promise<boolean>)): Promise<boolean> {
|
|
414
|
+
if (typeof str === 'function') {
|
|
415
|
+
return str();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const parts = str.split(':');
|
|
419
|
+
if (parts.length < 2) return false;
|
|
420
|
+
|
|
421
|
+
const controllerName = parts[0];
|
|
422
|
+
const methodAndArgs = parts[1].split('.');
|
|
423
|
+
|
|
424
|
+
const method = methodAndArgs[0];
|
|
425
|
+
const args = methodAndArgs.slice(1);
|
|
426
|
+
|
|
427
|
+
const controller = await this.ez.getController(controllerName);
|
|
428
|
+
if (!controller) return false;
|
|
429
|
+
|
|
430
|
+
const fn = controller[method];
|
|
431
|
+
if (typeof fn !== 'function') {
|
|
432
|
+
throw new EzError({
|
|
433
|
+
code: 'EZ_GUARD_001',
|
|
434
|
+
source: 'router',
|
|
435
|
+
message: `Guard method "${method}" not found on controller "${controllerName}"`,
|
|
436
|
+
route: this.ez._context.route,
|
|
437
|
+
view: this.ez._context.view,
|
|
438
|
+
details: {
|
|
439
|
+
controller: controllerName,
|
|
440
|
+
method,
|
|
441
|
+
available: Object.keys(controller)
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return await (fn as (...args: string[]) => Promise<boolean>).apply(controller, args);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private async _resolveControllerFromView(viewName: string): Promise<string | null> {
|
|
450
|
+
if (!viewName) return null;
|
|
451
|
+
|
|
452
|
+
await this.ez._loader.resolveEzType(viewName);
|
|
453
|
+
const def = this.ez._registry[viewName];
|
|
454
|
+
if (!def || typeof def !== 'object') return null;
|
|
455
|
+
|
|
456
|
+
return def.controller || null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
installPopStateListener(): void {
|
|
460
|
+
window.addEventListener('popstate', () => {
|
|
461
|
+
this._onLocationChange().catch(err => {
|
|
462
|
+
this.ez.handleFrameworkError(err);
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async prefetch(path: string): Promise<void> {
|
|
468
|
+
const match = this._matchRoute(path);
|
|
469
|
+
if (!match || !match.chain.length) return;
|
|
470
|
+
|
|
471
|
+
const views = match.chain.map(r => r.view).filter(Boolean);
|
|
472
|
+
await this.ez._loader.prefetchAll(views);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
enableLinkPrefetch(): void {
|
|
476
|
+
document.addEventListener('pointerenter', (e) => {
|
|
477
|
+
const link = (e.target as HTMLElement).closest('a[href]');
|
|
478
|
+
if (!link) return;
|
|
479
|
+
|
|
480
|
+
const href = link.getAttribute('href');
|
|
481
|
+
if (!href) return;
|
|
482
|
+
|
|
483
|
+
if (!href.startsWith('/')) return;
|
|
484
|
+
|
|
485
|
+
if (link.hasAttribute('data-no-prefetch')) return;
|
|
486
|
+
|
|
487
|
+
this.prefetch(href);
|
|
488
|
+
}, { capture: true, passive: true });
|
|
489
|
+
}
|
|
490
|
+
}
|
package/core/services.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { RouteUIService } from '../services/RouteUI.js';
|
|
2
|
+
import { FetchApi } from '../services/fetchApi.js';
|
|
3
|
+
import { EzDialogService } from '../services/dialog.js';
|
|
4
|
+
import { EzToastService } from '../services/toast.js';
|
|
5
|
+
|
|
6
|
+
interface FirebaseConfig {
|
|
7
|
+
apiKey: string;
|
|
8
|
+
authDomain: string;
|
|
9
|
+
projectId: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface FirebaseService {
|
|
13
|
+
init(): Promise<void>;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface CryptoServiceType {
|
|
18
|
+
init(secret: string): Promise<void>;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface EzInstance {
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class EzServices {
|
|
27
|
+
private ez: EzInstance;
|
|
28
|
+
private _firebase: FirebaseService | null = null;
|
|
29
|
+
private _api: FetchApi | null = null;
|
|
30
|
+
private _crypto: CryptoServiceType | null = null;
|
|
31
|
+
private _routeUI: RouteUIService | null = null;
|
|
32
|
+
private _dialog: EzDialogService | null = null;
|
|
33
|
+
private _toast: EzToastService | null = null;
|
|
34
|
+
private _initialized: boolean = false;
|
|
35
|
+
|
|
36
|
+
constructor(ez: EzInstance) {
|
|
37
|
+
this.ez = ez;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async init(): Promise<void> {
|
|
41
|
+
if (this._initialized) return;
|
|
42
|
+
|
|
43
|
+
await Promise.all([
|
|
44
|
+
this._initRouteUI(),
|
|
45
|
+
this._initApi(),
|
|
46
|
+
this._initCrypto(),
|
|
47
|
+
this._initFirebase(),
|
|
48
|
+
this._initDialog(),
|
|
49
|
+
this._initToast()
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
this._initialized = true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private _initRouteUI(): void {
|
|
56
|
+
this._routeUI = new RouteUIService();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get routeUI(): RouteUIService | null {
|
|
60
|
+
return this._routeUI;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private _initApi(): void {
|
|
64
|
+
this._api = new FetchApi({
|
|
65
|
+
baseUrl: (import.meta as unknown as { env: Record<string, string> }).env.VITE_API_BASE_URL || ''
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get api(): FetchApi | null {
|
|
70
|
+
return this._api;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private async _initCrypto(): Promise<void> {
|
|
74
|
+
const env = (import.meta as unknown as { env: Record<string, string> }).env;
|
|
75
|
+
if (!env.VITE_CRYPTO_SECRET) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const { CryptoService } = await import('../services/crypto.js');
|
|
80
|
+
await CryptoService.init(env.VITE_CRYPTO_SECRET);
|
|
81
|
+
this._crypto = CryptoService;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get crypto(): CryptoServiceType | null {
|
|
85
|
+
return this._crypto;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private async _initFirebase(): Promise<void> {
|
|
89
|
+
const env = (import.meta as unknown as { env: Record<string, string> }).env;
|
|
90
|
+
if (env.VITE_USE_FIREBASE !== 'true') {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const firebaseConfig: FirebaseConfig = {
|
|
95
|
+
apiKey: env.VITE_FIREBASE_API_KEY,
|
|
96
|
+
authDomain: env.VITE_FIREBASE_AUTH_DOMAIN,
|
|
97
|
+
projectId: env.VITE_FIREBASE_PROJECT_ID,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const { FirebaseService } = await import('../services/firebase.js');
|
|
101
|
+
this._firebase = new FirebaseService(firebaseConfig) as unknown as FirebaseService;
|
|
102
|
+
await this._firebase!.init();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get firebase(): FirebaseService | null {
|
|
106
|
+
return this._firebase;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private _initDialog(): void {
|
|
110
|
+
this._dialog = new EzDialogService();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get dialog(): EzDialogService | null {
|
|
114
|
+
return this._dialog;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private _initToast(): void {
|
|
118
|
+
this._toast = new EzToastService();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
get toast(): EzToastService | null {
|
|
122
|
+
return this._toast;
|
|
123
|
+
}
|
|
124
|
+
}
|