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/renderer.ts
ADDED
|
@@ -0,0 +1,1010 @@
|
|
|
1
|
+
import { EzError } from './EzError.js';
|
|
2
|
+
import { EzBaseComponent, EzBaseComponentConfig } from '../components/EzBaseComponent.js';
|
|
3
|
+
import tooltipStyles from '../components/tooltip/EzTooltip.module.scss';
|
|
4
|
+
import { cx } from '../utils/cssModules.js';
|
|
5
|
+
|
|
6
|
+
const tooltipCls = cx(tooltipStyles);
|
|
7
|
+
|
|
8
|
+
export type LazyMode = 'visible' | 'idle';
|
|
9
|
+
|
|
10
|
+
export interface ComponentConfig extends EzBaseComponentConfig {
|
|
11
|
+
eztype?: string;
|
|
12
|
+
controller?: string;
|
|
13
|
+
ref?: string;
|
|
14
|
+
bind?: Record<string, unknown> | string;
|
|
15
|
+
cls?: string | string[];
|
|
16
|
+
flex?: number | string;
|
|
17
|
+
width?: number | string;
|
|
18
|
+
height?: number | string;
|
|
19
|
+
css?: string;
|
|
20
|
+
onLoad?: string | ((config: ComponentConfig, state: unknown) => Promise<void>);
|
|
21
|
+
fallback?: ComponentConfig | string | ((state: unknown) => Promise<ComponentConfig>);
|
|
22
|
+
skipFallback?: boolean;
|
|
23
|
+
skeleton?: boolean | ComponentConfig;
|
|
24
|
+
init?: string | ((config: ComponentConfig) => Promise<void>);
|
|
25
|
+
skipInit?: boolean;
|
|
26
|
+
name?: string;
|
|
27
|
+
items?: ComponentConfig[];
|
|
28
|
+
template?: (props: unknown, controller?: unknown, state?: unknown) => ComponentConfig;
|
|
29
|
+
props?: Record<string, unknown>;
|
|
30
|
+
_ezAfterRender?: (el: Node, instance: EzBaseComponent | null, config: ComponentConfig) => void;
|
|
31
|
+
_ezSourcePath?: string;
|
|
32
|
+
_onLoadComplete?: boolean;
|
|
33
|
+
_styleModule?: Record<string, string>;
|
|
34
|
+
lazy?: LazyMode;
|
|
35
|
+
lazyThreshold?: number;
|
|
36
|
+
lazyRootMargin?: string;
|
|
37
|
+
priority?: 'high' | 'low' | 'idle';
|
|
38
|
+
tooltip?: string | TooltipConfig;
|
|
39
|
+
store?: string | unknown;
|
|
40
|
+
routeParams?: Record<string, string>;
|
|
41
|
+
layout?: string;
|
|
42
|
+
style?: Record<string, string>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface TooltipConfig {
|
|
46
|
+
text: string;
|
|
47
|
+
position?: 'top' | 'bottom' | 'left' | 'right';
|
|
48
|
+
variant?: 'dark' | 'light';
|
|
49
|
+
delay?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface LazyQueueData {
|
|
53
|
+
config: ComponentConfig;
|
|
54
|
+
controllerName: string | null;
|
|
55
|
+
inheritedState: unknown;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface ControllerInstance {
|
|
59
|
+
state?: Record<string, unknown>;
|
|
60
|
+
[key: string]: unknown;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface RegisteredComponent {
|
|
64
|
+
prototype?: { render?: () => HTMLElement | Promise<HTMLElement> };
|
|
65
|
+
controller?: string;
|
|
66
|
+
template?: (props: unknown, controller?: unknown, state?: unknown) => ComponentConfig;
|
|
67
|
+
onLoad?: string | ((config: ComponentConfig, state: unknown) => Promise<void>);
|
|
68
|
+
fallback?: ComponentConfig | string | ((state: unknown) => Promise<ComponentConfig>);
|
|
69
|
+
skipFallback?: boolean;
|
|
70
|
+
skeleton?: boolean | ComponentConfig;
|
|
71
|
+
css?: string;
|
|
72
|
+
cls?: string | string[];
|
|
73
|
+
layout?: string;
|
|
74
|
+
style?: Record<string, string>;
|
|
75
|
+
items?: ComponentConfig[];
|
|
76
|
+
[key: string]: unknown;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface EzInstance {
|
|
80
|
+
_registry: Record<string, RegisteredComponent>;
|
|
81
|
+
_registryMeta?: Record<string, { definedIn?: string }>;
|
|
82
|
+
_controllers: Record<string, ControllerInstance>;
|
|
83
|
+
_refs: Record<string, EzBaseComponent>;
|
|
84
|
+
_context: {
|
|
85
|
+
route?: string | null;
|
|
86
|
+
view?: string | null;
|
|
87
|
+
controller?: string | null;
|
|
88
|
+
};
|
|
89
|
+
_loader: {
|
|
90
|
+
resolveController(name: string): Promise<unknown>;
|
|
91
|
+
resolveStore(name: string): Promise<void>;
|
|
92
|
+
resolveEzType(name: string): Promise<void>;
|
|
93
|
+
loadWithPriority(name: string, priority: string): Promise<void>;
|
|
94
|
+
getCurrentModulePath(): string | null;
|
|
95
|
+
};
|
|
96
|
+
getController(name: string): Promise<ControllerInstance>;
|
|
97
|
+
getStore(name: string): unknown;
|
|
98
|
+
registerComponent(name: string, component: unknown): void;
|
|
99
|
+
hasStyles(name: string): boolean;
|
|
100
|
+
resolveStyles(name: string): Promise<Record<string, string> | null>;
|
|
101
|
+
handleFrameworkError(error: Error): void;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export class EzRenderer {
|
|
105
|
+
private ez: EzInstance;
|
|
106
|
+
private _lazyQueue: Map<Element, LazyQueueData> = new Map();
|
|
107
|
+
private _lazyObserver: IntersectionObserver | null = null;
|
|
108
|
+
|
|
109
|
+
constructor(ez: EzInstance) {
|
|
110
|
+
this.ez = ez;
|
|
111
|
+
this._initLazyObserver();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private _initLazyObserver(): void {
|
|
115
|
+
if (typeof IntersectionObserver === 'undefined') return;
|
|
116
|
+
|
|
117
|
+
this._lazyObserver = new IntersectionObserver(
|
|
118
|
+
(entries) => {
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
if (!entry.isIntersecting) continue;
|
|
121
|
+
|
|
122
|
+
const placeholder = entry.target;
|
|
123
|
+
const data = this._lazyQueue.get(placeholder);
|
|
124
|
+
|
|
125
|
+
if (!data) continue;
|
|
126
|
+
|
|
127
|
+
this._lazyObserver!.unobserve(placeholder);
|
|
128
|
+
this._lazyQueue.delete(placeholder);
|
|
129
|
+
|
|
130
|
+
this._renderLazyComponent(placeholder as HTMLElement, data);
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
rootMargin: '50px',
|
|
135
|
+
threshold: 0.1
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private async _renderLazyComponent(placeholder: HTMLElement, data: LazyQueueData): Promise<void> {
|
|
141
|
+
const { config, controllerName, inheritedState } = data;
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const renderConfig = { ...config, lazy: undefined };
|
|
145
|
+
const el = await this.createElement(renderConfig, controllerName, inheritedState);
|
|
146
|
+
|
|
147
|
+
if (placeholder.isConnected) {
|
|
148
|
+
placeholder.replaceWith(el);
|
|
149
|
+
}
|
|
150
|
+
} catch (err) {
|
|
151
|
+
this.ez.handleFrameworkError(err as Error);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private _createLazyPlaceholder(config: ComponentConfig, controllerName: string | null, inheritedState: unknown): HTMLElement {
|
|
156
|
+
const placeholder = document.createElement('div');
|
|
157
|
+
placeholder.className = 'ez-lazy-placeholder';
|
|
158
|
+
placeholder.setAttribute('data-lazy-eztype', config.eztype || 'EzComponent');
|
|
159
|
+
|
|
160
|
+
if (config.width) {
|
|
161
|
+
placeholder.style.width = typeof config.width === 'number'
|
|
162
|
+
? `${config.width}px`
|
|
163
|
+
: config.width;
|
|
164
|
+
}
|
|
165
|
+
if (config.height) {
|
|
166
|
+
placeholder.style.height = typeof config.height === 'number'
|
|
167
|
+
? `${config.height}px`
|
|
168
|
+
: config.height;
|
|
169
|
+
}
|
|
170
|
+
if (config.flex) {
|
|
171
|
+
placeholder.style.flex = config.flex.toString();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this._lazyQueue.set(placeholder, { config, controllerName, inheritedState });
|
|
175
|
+
|
|
176
|
+
if (config.lazyThreshold !== undefined || config.lazyRootMargin !== undefined) {
|
|
177
|
+
const customObserver = new IntersectionObserver(
|
|
178
|
+
(entries) => {
|
|
179
|
+
for (const entry of entries) {
|
|
180
|
+
if (!entry.isIntersecting) continue;
|
|
181
|
+
|
|
182
|
+
customObserver.unobserve(entry.target);
|
|
183
|
+
const data = this._lazyQueue.get(entry.target);
|
|
184
|
+
if (data) {
|
|
185
|
+
this._lazyQueue.delete(entry.target);
|
|
186
|
+
this._renderLazyComponent(entry.target as HTMLElement, data);
|
|
187
|
+
}
|
|
188
|
+
customObserver.disconnect();
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
rootMargin: config.lazyRootMargin || '50px',
|
|
193
|
+
threshold: config.lazyThreshold ?? 0.1
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
customObserver.observe(placeholder);
|
|
197
|
+
} else {
|
|
198
|
+
this._lazyObserver?.observe(placeholder);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return placeholder;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
cloneConfig<T>(input: T, seen: WeakSet<object> = new WeakSet()): T {
|
|
205
|
+
if (input == null) return input;
|
|
206
|
+
|
|
207
|
+
const t = typeof input;
|
|
208
|
+
if (t !== 'object') return input;
|
|
209
|
+
|
|
210
|
+
if (input instanceof Node) return input;
|
|
211
|
+
|
|
212
|
+
if ((input as object).constructor !== Object && !Array.isArray(input)) {
|
|
213
|
+
return input;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (seen.has(input as object)) {
|
|
217
|
+
return input;
|
|
218
|
+
}
|
|
219
|
+
seen.add(input as object);
|
|
220
|
+
|
|
221
|
+
if (input instanceof Set) return input;
|
|
222
|
+
if (input instanceof Map) return input;
|
|
223
|
+
if (input instanceof Date) return input;
|
|
224
|
+
|
|
225
|
+
if ((input as { _isEzStore?: boolean })._isEzStore) return input;
|
|
226
|
+
|
|
227
|
+
if (Array.isArray(input)) {
|
|
228
|
+
return input.map(v => this.cloneConfig(v, seen)) as T;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const out: Record<string, unknown> = {};
|
|
232
|
+
for (const k in input as Record<string, unknown>) {
|
|
233
|
+
out[k] = this.cloneConfig((input as Record<string, unknown>)[k], seen);
|
|
234
|
+
}
|
|
235
|
+
return out as T;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
applyControllerBindings(config: ComponentConfig, controllerName: string): void {
|
|
239
|
+
if (!config || typeof config !== 'object') return;
|
|
240
|
+
|
|
241
|
+
const controller = this.ez._controllers[controllerName];
|
|
242
|
+
|
|
243
|
+
if (!controller) {
|
|
244
|
+
for (const key in config) {
|
|
245
|
+
if (key.startsWith('on') && typeof (config as Record<string, unknown>)[key] === 'string') {
|
|
246
|
+
const src =
|
|
247
|
+
config._ezSourcePath
|
|
248
|
+
|| this.ez._registryMeta?.[this.ez._context.view || '']?.definedIn
|
|
249
|
+
|| this.ez._loader?.getCurrentModulePath()
|
|
250
|
+
|| null;
|
|
251
|
+
|
|
252
|
+
throw new EzError({
|
|
253
|
+
code: 'EZ_HANDLER_NO_CONTROLLER',
|
|
254
|
+
source: 'event',
|
|
255
|
+
message: `Handler "${(config as Record<string, unknown>)[key]}" requires a controller but none is available`,
|
|
256
|
+
route: this.ez._context.route,
|
|
257
|
+
view: this.ez._context.view,
|
|
258
|
+
details: {
|
|
259
|
+
eztype: config.eztype,
|
|
260
|
+
handler: key,
|
|
261
|
+
method: (config as Record<string, unknown>)[key],
|
|
262
|
+
route: src
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
for (const key in config) {
|
|
271
|
+
const value = (config as Record<string, unknown>)[key];
|
|
272
|
+
|
|
273
|
+
if (typeof value === 'string' && typeof controller[value] === 'function') {
|
|
274
|
+
(config as Record<string, unknown>)[key] = (controller[value] as Function).bind(controller);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (Array.isArray(config.items)) {
|
|
279
|
+
config.items.forEach(child => this.applyControllerBindings(child, controllerName));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async executeOnLoad(config: ComponentConfig, controllerName: string): Promise<void> {
|
|
284
|
+
if (!config.onLoad) return;
|
|
285
|
+
|
|
286
|
+
const controllerObj = await this.ez.getController(controllerName);
|
|
287
|
+
|
|
288
|
+
if (!controllerName || !controllerObj) {
|
|
289
|
+
throw new EzError({
|
|
290
|
+
code: 'EZ_ONLOAD_002',
|
|
291
|
+
source: 'component',
|
|
292
|
+
message: 'onLoad requires a valid controller',
|
|
293
|
+
route: this.ez._context.route,
|
|
294
|
+
view: this.ez._context.view,
|
|
295
|
+
details: {
|
|
296
|
+
controller: controllerName
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const controllerState = controllerObj.state;
|
|
302
|
+
|
|
303
|
+
let fn: unknown = config.onLoad;
|
|
304
|
+
|
|
305
|
+
if (typeof fn === 'string') {
|
|
306
|
+
const fnName = fn;
|
|
307
|
+
fn = controllerObj?.[fnName];
|
|
308
|
+
if (typeof fn !== 'function') {
|
|
309
|
+
const sourcePath =
|
|
310
|
+
config._ezSourcePath
|
|
311
|
+
|| this.ez._registryMeta?.[this.ez._context.view || '']?.definedIn
|
|
312
|
+
|| this.ez._loader?.getCurrentModulePath()
|
|
313
|
+
|| null;
|
|
314
|
+
|
|
315
|
+
throw new EzError({
|
|
316
|
+
code: 'EZ_ONLOAD_002',
|
|
317
|
+
source: 'component',
|
|
318
|
+
message: 'onLoad requires a valid controller',
|
|
319
|
+
route: this.ez._context.route,
|
|
320
|
+
view: this.ez._context.view,
|
|
321
|
+
details: {
|
|
322
|
+
controller: controllerName,
|
|
323
|
+
file: sourcePath
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (typeof fn === 'function') {
|
|
330
|
+
await fn.call(controllerObj, config, controllerState);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async executeOnInit(config: ComponentConfig, controllerName: string | null): Promise<void> {
|
|
335
|
+
let controller = this.ez._controllers[controllerName || ''];
|
|
336
|
+
|
|
337
|
+
if (!controller && config.controller) {
|
|
338
|
+
const ctrlName = typeof config.controller === 'string'
|
|
339
|
+
? config.controller
|
|
340
|
+
: null;
|
|
341
|
+
if (ctrlName) {
|
|
342
|
+
controller = this.ez._controllers[ctrlName]
|
|
343
|
+
|| this.ez._controllers[`${ctrlName}Controller`];
|
|
344
|
+
} else if (typeof config.controller === 'object') {
|
|
345
|
+
controller = config.controller as ControllerInstance;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (!controller) return;
|
|
350
|
+
|
|
351
|
+
let fn: unknown = config.init;
|
|
352
|
+
if (typeof fn === 'string') {
|
|
353
|
+
fn = controller[fn];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (typeof fn === 'function') {
|
|
357
|
+
await fn.call(controller, config);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
collectControllersFromBind(config: ComponentConfig): Set<string> {
|
|
362
|
+
const out: Set<string> = new Set();
|
|
363
|
+
|
|
364
|
+
const bind = config.bind;
|
|
365
|
+
if (!bind) return out;
|
|
366
|
+
|
|
367
|
+
const scan = (val: unknown): void => {
|
|
368
|
+
if (typeof val !== 'string') return;
|
|
369
|
+
|
|
370
|
+
const idx = val.indexOf(':');
|
|
371
|
+
if (idx > 0) {
|
|
372
|
+
out.add(val.slice(0, idx));
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
if (typeof bind === 'string') scan(bind);
|
|
377
|
+
else {
|
|
378
|
+
for (const key in bind) {
|
|
379
|
+
scan(bind[key]);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return out;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async createElement(config: ComponentConfig | ComponentConfig[] | null, controllerName: string | null = null, inheritedState: unknown = null): Promise<Node> {
|
|
387
|
+
if (!config) return document.createTextNode('');
|
|
388
|
+
|
|
389
|
+
if (Array.isArray(config)) {
|
|
390
|
+
const frag = document.createDocumentFragment();
|
|
391
|
+
for (const child of config) {
|
|
392
|
+
frag.appendChild(await this.createElement(child, controllerName, inheritedState));
|
|
393
|
+
}
|
|
394
|
+
return frag;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (config.lazy === 'visible' && this._lazyObserver) {
|
|
398
|
+
return this._createLazyPlaceholder(config, controllerName, inheritedState);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const controllers: Set<string> = new Set();
|
|
402
|
+
|
|
403
|
+
if (config.controller) {
|
|
404
|
+
controllers.add(config.controller);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
for (const c of this.collectControllersFromBind(config)) {
|
|
408
|
+
controllers.add(c);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
for (const c of controllers) {
|
|
412
|
+
await this.ez._loader.resolveController(c);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (typeof config.store === 'string') {
|
|
416
|
+
await this.ez._loader.resolveStore(config.store);
|
|
417
|
+
config = this.cloneConfig(config);
|
|
418
|
+
config.store = this.ez.getStore(config.store as string);
|
|
419
|
+
} else {
|
|
420
|
+
config = this.cloneConfig(config);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const { eztype = 'EzComponent', ref } = config;
|
|
424
|
+
|
|
425
|
+
let Registered = this.ez._registry[eztype] as RegisteredComponent | undefined;
|
|
426
|
+
|
|
427
|
+
const isFrameworkComponent = eztype.startsWith('Ez') || eztype[0] === eztype[0].toUpperCase();
|
|
428
|
+
|
|
429
|
+
if (!Registered && typeof eztype === 'string' && !isFrameworkComponent) {
|
|
430
|
+
const testEl = document.createElement(eztype);
|
|
431
|
+
if (testEl instanceof HTMLElement && !(eztype in this.ez._registry)) {
|
|
432
|
+
const { HtmlWrapper } = await import('../components/HtmlWrapper.js');
|
|
433
|
+
|
|
434
|
+
this.ez.registerComponent(eztype, HtmlWrapper);
|
|
435
|
+
Registered = HtmlWrapper as unknown as RegisteredComponent;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (!Registered) {
|
|
440
|
+
const priority = config.priority || 'high';
|
|
441
|
+
await this.ez._loader.loadWithPriority(eztype, priority);
|
|
442
|
+
Registered = this.ez._registry[eztype] as RegisteredComponent | undefined;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (!Registered) {
|
|
446
|
+
throw new EzError({
|
|
447
|
+
code: 'EZ_COMPONENT_500',
|
|
448
|
+
source: 'component',
|
|
449
|
+
message: `Invalid component type for eztype "${eztype}"`,
|
|
450
|
+
route: this.ez._context.route,
|
|
451
|
+
view: this.ez._context.view,
|
|
452
|
+
details: { eztype }
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
this.ez._context.controller = null;
|
|
457
|
+
|
|
458
|
+
const registeredController = typeof Registered === 'object' ? Registered.controller : null;
|
|
459
|
+
const activeController = config.controller || registeredController || controllerName || '';
|
|
460
|
+
await this.ez._loader.resolveController(activeController);
|
|
461
|
+
|
|
462
|
+
this.ez._context.controller = activeController;
|
|
463
|
+
|
|
464
|
+
if (activeController && !config.controller) {
|
|
465
|
+
config.controller = activeController;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const controllerState = this.ez._controllers[activeController]?.state;
|
|
469
|
+
|
|
470
|
+
this.applyControllerBindings(config, activeController);
|
|
471
|
+
|
|
472
|
+
const onLoadMethod = config._onLoadComplete ? null : (config.onLoad || (typeof Registered === 'object' ? Registered.onLoad : null));
|
|
473
|
+
let fallbackDef = config.fallback || (typeof Registered === 'object' ? Registered.fallback : null);
|
|
474
|
+
const skipFallback = config.skipFallback || (typeof Registered === 'object' ? Registered.skipFallback : false);
|
|
475
|
+
const skeletonMode = config.skeleton || (typeof Registered === 'object' ? Registered.skeleton : false);
|
|
476
|
+
|
|
477
|
+
if (skeletonMode && !fallbackDef && onLoadMethod) {
|
|
478
|
+
const skeletonCss = config.css || (typeof Registered === 'object' ? Registered.css : null);
|
|
479
|
+
|
|
480
|
+
if (skeletonCss) {
|
|
481
|
+
await this.ez.resolveStyles(skeletonCss);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const itemRenderFn = (config as { itemRender?: (data: unknown) => ComponentConfig }).itemRender || (typeof Registered === 'object' ? (Registered as { itemRender?: (data: unknown) => ComponentConfig }).itemRender : null);
|
|
485
|
+
if (itemRenderFn) {
|
|
486
|
+
try {
|
|
487
|
+
const mockData = this._createMockData();
|
|
488
|
+
const templateConfig = itemRenderFn(mockData);
|
|
489
|
+
if (templateConfig) {
|
|
490
|
+
fallbackDef = await this._buildSkeletonConfig(templateConfig, skeletonCss || undefined);
|
|
491
|
+
}
|
|
492
|
+
} catch {
|
|
493
|
+
// itemRender failed
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (!fallbackDef) {
|
|
498
|
+
const templateFn = config.template || (typeof Registered === 'object' ? Registered.template : null);
|
|
499
|
+
if (templateFn) {
|
|
500
|
+
try {
|
|
501
|
+
const templateConfig = templateFn({});
|
|
502
|
+
if (templateConfig) {
|
|
503
|
+
fallbackDef = await this._buildSkeletonConfig(templateConfig, skeletonCss || undefined);
|
|
504
|
+
}
|
|
505
|
+
} catch {
|
|
506
|
+
// template failed
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (!fallbackDef) {
|
|
512
|
+
const items = config.items || (typeof Registered === 'object' ? Registered.items : null);
|
|
513
|
+
if (items && Array.isArray(items)) {
|
|
514
|
+
fallbackDef = await this._buildSkeletonConfig({
|
|
515
|
+
eztype: 'EzComponent',
|
|
516
|
+
cls: config.cls || (typeof Registered === 'object' ? Registered.cls : undefined),
|
|
517
|
+
css: skeletonCss || undefined,
|
|
518
|
+
layout: config.layout || (typeof Registered === 'object' ? Registered.layout : undefined),
|
|
519
|
+
style: config.style || (typeof Registered === 'object' ? Registered.style : undefined),
|
|
520
|
+
items: items
|
|
521
|
+
}, skeletonCss || undefined);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (!fallbackDef) {
|
|
526
|
+
fallbackDef = { eztype: 'EzSkeleton', variant: 'rect', css: skeletonCss || undefined } as unknown as ComponentConfig;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (onLoadMethod) {
|
|
531
|
+
let fallbackConfig: ComponentConfig = {};
|
|
532
|
+
|
|
533
|
+
if (!fallbackDef && !skipFallback) {
|
|
534
|
+
throw new EzError({
|
|
535
|
+
code: 'EZ_ONLOAD_001',
|
|
536
|
+
source: 'component',
|
|
537
|
+
message: 'onLoad requires a fallback component',
|
|
538
|
+
route: this.ez._context.route,
|
|
539
|
+
view: this.ez._context.view,
|
|
540
|
+
details: {
|
|
541
|
+
eztype,
|
|
542
|
+
hint: 'Define a fallback property or use skeleton: true'
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (fallbackDef && typeof fallbackDef === 'object') {
|
|
548
|
+
fallbackConfig = fallbackDef as ComponentConfig;
|
|
549
|
+
} else if (fallbackDef && typeof fallbackDef === 'function') {
|
|
550
|
+
fallbackConfig = await (fallbackDef as (state: unknown) => Promise<ComponentConfig>)(this.ez._controllers[activeController]?.state);
|
|
551
|
+
} else if (fallbackDef && typeof fallbackDef === 'string') {
|
|
552
|
+
let registered = this.ez._registry[fallbackDef];
|
|
553
|
+
|
|
554
|
+
if (!registered) {
|
|
555
|
+
await this.ez._loader.resolveEzType(fallbackDef);
|
|
556
|
+
registered = this.ez._registry[fallbackDef];
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (!registered) {
|
|
560
|
+
throw new EzError({
|
|
561
|
+
code: 'EZ_COMPONENT_500',
|
|
562
|
+
source: 'component',
|
|
563
|
+
message: `Invalid component type for eztype "${eztype}"`,
|
|
564
|
+
route: this.ez._context.route,
|
|
565
|
+
view: this.ez._context.view,
|
|
566
|
+
details: { eztype }
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
fallbackConfig = registered as unknown as ComponentConfig;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const fallbackEl = await this.createElement(fallbackConfig, activeController, inheritedState);
|
|
574
|
+
|
|
575
|
+
this.executeOnLoad({ ...config, onLoad: onLoadMethod }, activeController)
|
|
576
|
+
.then(async () => {
|
|
577
|
+
if (!(fallbackEl as HTMLElement).isConnected) return;
|
|
578
|
+
|
|
579
|
+
const rendered = await this.createElement({
|
|
580
|
+
...config,
|
|
581
|
+
_onLoadComplete: true
|
|
582
|
+
}, activeController, inheritedState);
|
|
583
|
+
|
|
584
|
+
(fallbackEl as HTMLElement).replaceWith(rendered);
|
|
585
|
+
})
|
|
586
|
+
.catch(err => {
|
|
587
|
+
this.ez.handleFrameworkError(err);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
return fallbackEl;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
let el: Node;
|
|
594
|
+
let instance: EzBaseComponent | null = null;
|
|
595
|
+
|
|
596
|
+
let styleModule: Record<string, string> | null = null;
|
|
597
|
+
if (config.css) {
|
|
598
|
+
styleModule = await this.ez.resolveStyles(config.css);
|
|
599
|
+
config._styleModule = styleModule || undefined;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (Registered.prototype?.render) {
|
|
603
|
+
instance = new (Registered as unknown as new (config: ComponentConfig) => EzBaseComponent)(config);
|
|
604
|
+
const rendered = (instance as unknown as { render(): Node | Promise<Node> }).render();
|
|
605
|
+
|
|
606
|
+
if (rendered instanceof Promise) {
|
|
607
|
+
el = await rendered;
|
|
608
|
+
} else {
|
|
609
|
+
el = rendered;
|
|
610
|
+
}
|
|
611
|
+
(el as { __ezInstance?: EzBaseComponent }).__ezInstance = instance;
|
|
612
|
+
instance.el = el as HTMLElement;
|
|
613
|
+
|
|
614
|
+
if (typeof config._ezAfterRender === 'function') {
|
|
615
|
+
queueMicrotask(() => {
|
|
616
|
+
try {
|
|
617
|
+
config._ezAfterRender!(el, instance, config);
|
|
618
|
+
} catch (err) {
|
|
619
|
+
this.ez.handleFrameworkError(err as Error);
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (config.name && el instanceof HTMLElement) {
|
|
625
|
+
el.setAttribute('data-ez-field', config.name);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (inheritedState instanceof EzBaseComponent) {
|
|
629
|
+
(inheritedState as unknown as { _children?: EzBaseComponent[] })._children ??= [];
|
|
630
|
+
(inheritedState as unknown as { _children: EzBaseComponent[] })._children.push(instance);
|
|
631
|
+
}
|
|
632
|
+
} else if (typeof Registered === 'object') {
|
|
633
|
+
const { eztype: _, ...rest } = config;
|
|
634
|
+
|
|
635
|
+
let merged: ComponentConfig = { ...Registered as ComponentConfig, ...rest };
|
|
636
|
+
|
|
637
|
+
if (!merged.css && this.ez.hasStyles(eztype)) {
|
|
638
|
+
merged.css = eztype;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (merged.css && !merged._styleModule) {
|
|
642
|
+
merged._styleModule = (await this.ez.resolveStyles(merged.css)) || undefined;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
let targetIsClassComponent = false;
|
|
646
|
+
if (merged.eztype && merged.eztype !== 'EzComponent') {
|
|
647
|
+
const targetRegistered = this.ez._registry[merged.eztype];
|
|
648
|
+
if (targetRegistered?.prototype?.render) {
|
|
649
|
+
targetIsClassComponent = true;
|
|
650
|
+
} else if (!targetRegistered) {
|
|
651
|
+
try {
|
|
652
|
+
await this.ez._loader.resolveEzType(merged.eztype);
|
|
653
|
+
const resolved = this.ez._registry[merged.eztype];
|
|
654
|
+
if (resolved?.prototype?.render) {
|
|
655
|
+
targetIsClassComponent = true;
|
|
656
|
+
}
|
|
657
|
+
} catch {
|
|
658
|
+
// Not found
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (typeof merged.template === 'function' && !targetIsClassComponent) {
|
|
664
|
+
const inheritedCss = merged.css;
|
|
665
|
+
const inheritedStyleModule = merged._styleModule;
|
|
666
|
+
|
|
667
|
+
const tpl = merged.template(config.props || {}, this.ez._controllers[activeController], controllerState);
|
|
668
|
+
if (!tpl || typeof tpl !== 'object') {
|
|
669
|
+
throw new EzError({
|
|
670
|
+
code: 'EZ_TEMPLATE_001',
|
|
671
|
+
source: 'component',
|
|
672
|
+
message: 'template() must return a component config object',
|
|
673
|
+
view: this.ez._context.view,
|
|
674
|
+
route: this.ez._context.route
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
merged = tpl;
|
|
679
|
+
|
|
680
|
+
if (inheritedCss && !merged.css) {
|
|
681
|
+
merged.css = inheritedCss;
|
|
682
|
+
merged._styleModule = inheritedStyleModule;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
el = await this.createElement(merged, activeController, controllerState);
|
|
687
|
+
} else {
|
|
688
|
+
throw new EzError({
|
|
689
|
+
code: 'EZ_COMPONENT_500',
|
|
690
|
+
source: 'component',
|
|
691
|
+
message: `Invalid component type for eztype "${eztype}"`,
|
|
692
|
+
route: this.ez._context.route,
|
|
693
|
+
view: this.ez._context.view,
|
|
694
|
+
details: { eztype }
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (config.cls && el instanceof HTMLElement) {
|
|
699
|
+
if (Array.isArray(config.cls)) {
|
|
700
|
+
const resolvedClasses = config.cls.map(c => {
|
|
701
|
+
if (styleModule && typeof c === 'string' && styleModule[c]) {
|
|
702
|
+
return styleModule[c];
|
|
703
|
+
}
|
|
704
|
+
return c;
|
|
705
|
+
}).filter(Boolean);
|
|
706
|
+
if (resolvedClasses.length) {
|
|
707
|
+
el.classList.add(...resolvedClasses);
|
|
708
|
+
}
|
|
709
|
+
} else if (typeof config.cls === 'string') {
|
|
710
|
+
const classes = config.cls.split(' ').filter(Boolean).map(c => {
|
|
711
|
+
if (styleModule && styleModule[c]) {
|
|
712
|
+
return styleModule[c];
|
|
713
|
+
}
|
|
714
|
+
return c;
|
|
715
|
+
});
|
|
716
|
+
if (classes.length) {
|
|
717
|
+
el.classList.add(...classes);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (config.tooltip && el instanceof HTMLElement) {
|
|
723
|
+
this._applyTooltip(el, config.tooltip, instance);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (config.flex) {
|
|
727
|
+
(el as HTMLElement).style.flex = config.flex.toString();
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (ref && instance) {
|
|
731
|
+
this.ez._refs[ref] = instance;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (!Registered.prototype?.render && config.items) {
|
|
735
|
+
const children = await this.createChildElements(
|
|
736
|
+
config.items,
|
|
737
|
+
activeController,
|
|
738
|
+
controllerState,
|
|
739
|
+
config.css || null
|
|
740
|
+
);
|
|
741
|
+
children.forEach(child => (el as HTMLElement).appendChild(child));
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (config.init && config.skipInit !== true) {
|
|
745
|
+
queueMicrotask(() => {
|
|
746
|
+
this.executeOnInit(config, controllerName)
|
|
747
|
+
.catch(err => this.ez.handleFrameworkError(err));
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return el;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
async createChildElements(items: ComponentConfig[], controllerName: string, inheritedState: unknown, parentCss: string | null = null): Promise<Node[]> {
|
|
755
|
+
const out: Node[] = [];
|
|
756
|
+
if (!Array.isArray(items)) return out;
|
|
757
|
+
|
|
758
|
+
const parentSource = (inheritedState as { _ezSourcePath?: string })?._ezSourcePath || null;
|
|
759
|
+
|
|
760
|
+
for (const item of items) {
|
|
761
|
+
const localItem = item?.controller
|
|
762
|
+
? this.cloneConfig(item)
|
|
763
|
+
: { ...this.cloneConfig(item), controller: controllerName };
|
|
764
|
+
|
|
765
|
+
if (!localItem._ezSourcePath) {
|
|
766
|
+
localItem._ezSourcePath = parentSource || undefined;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (parentCss && !localItem.css) {
|
|
770
|
+
localItem.css = parentCss;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const child = await this.createElement(localItem, controllerName, inheritedState);
|
|
774
|
+
|
|
775
|
+
if (child instanceof Node) {
|
|
776
|
+
if (localItem.flex !== undefined) (child as HTMLElement).style.flex = localItem.flex.toString();
|
|
777
|
+
if (localItem.width !== undefined) (child as HTMLElement).style.width = typeof localItem.width === 'number' ? `${localItem.width}px` : localItem.width;
|
|
778
|
+
if (localItem.height !== undefined) (child as HTMLElement).style.height = typeof localItem.height === 'number' ? `${localItem.height}px` : localItem.height;
|
|
779
|
+
out.push(child);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
return out;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
_createMockData(): Record<string, unknown> {
|
|
787
|
+
return {
|
|
788
|
+
id: 1,
|
|
789
|
+
user_id: 1,
|
|
790
|
+
name: 'Loading...',
|
|
791
|
+
user_first_name: 'Loading',
|
|
792
|
+
user_last_name: '...',
|
|
793
|
+
user_full_name: 'Loading...',
|
|
794
|
+
title: 'Loading...',
|
|
795
|
+
email: 'loading@example.com',
|
|
796
|
+
phone: '000-000-0000',
|
|
797
|
+
avatar: '',
|
|
798
|
+
image: '',
|
|
799
|
+
description: 'Loading content...',
|
|
800
|
+
status: 'active',
|
|
801
|
+
created_at: new Date().toISOString(),
|
|
802
|
+
updated_at: new Date().toISOString(),
|
|
803
|
+
user_last_login: new Date().toISOString(),
|
|
804
|
+
role: { role_name: 'Loading...' }
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async _buildSkeletonConfig(config: ComponentConfig, parentCss?: string): Promise<ComponentConfig> {
|
|
809
|
+
if (!config) return { eztype: 'EzSkeleton', variant: 'text' } as unknown as ComponentConfig;
|
|
810
|
+
|
|
811
|
+
const eztype = config.eztype || 'EzComponent';
|
|
812
|
+
const css = config.css || parentCss;
|
|
813
|
+
|
|
814
|
+
const contentElements: Record<string, string> = {
|
|
815
|
+
'h1': 'title', 'h2': 'title', 'h3': 'title',
|
|
816
|
+
'h4': 'text', 'h5': 'text', 'h6': 'text',
|
|
817
|
+
'p': 'text', 'span': 'text', 'label': 'text',
|
|
818
|
+
'img': 'rect',
|
|
819
|
+
'EzAvatar': 'circle',
|
|
820
|
+
'EzButton': 'rect',
|
|
821
|
+
'EzInput': 'rect',
|
|
822
|
+
'EzBadge': 'rect',
|
|
823
|
+
'StatCard': 'rect'
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
if (contentElements[eztype]) {
|
|
827
|
+
return {
|
|
828
|
+
eztype: 'EzSkeleton',
|
|
829
|
+
variant: contentElements[eztype],
|
|
830
|
+
cls: config.cls,
|
|
831
|
+
css: css,
|
|
832
|
+
style: config.style,
|
|
833
|
+
flex: config.flex,
|
|
834
|
+
rounded: eztype !== 'EzAvatar'
|
|
835
|
+
} as unknown as ComponentConfig;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (eztype === 'EzComponent' || eztype === 'div' || eztype === 'section' || eztype === 'article') {
|
|
839
|
+
const result: ComponentConfig = {
|
|
840
|
+
eztype: eztype,
|
|
841
|
+
cls: config.cls,
|
|
842
|
+
css: css,
|
|
843
|
+
layout: config.layout,
|
|
844
|
+
flex: config.flex,
|
|
845
|
+
style: config.style
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
if (config.items && Array.isArray(config.items)) {
|
|
849
|
+
result.items = await Promise.all(config.items.map(item => this._buildSkeletonConfig(item, css)));
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
return result;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (eztype[0] === eztype[0].toUpperCase()) {
|
|
856
|
+
await this.ez._loader.resolveEzType(eztype);
|
|
857
|
+
const registered = this.ez._registry[eztype];
|
|
858
|
+
|
|
859
|
+
const componentCss = (registered as { css?: string })?.css || parentCss;
|
|
860
|
+
if (componentCss) {
|
|
861
|
+
await this.ez.resolveStyles(componentCss);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (registered && typeof registered === 'object') {
|
|
865
|
+
if ((registered as { skeleton?: ComponentConfig }).skeleton && typeof (registered as { skeleton?: ComponentConfig }).skeleton === 'object') {
|
|
866
|
+
return await this._buildSkeletonConfig((registered as { skeleton: ComponentConfig }).skeleton, componentCss);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (typeof (registered as { template?: (props: unknown) => ComponentConfig }).template === 'function') {
|
|
870
|
+
try {
|
|
871
|
+
const templateResult = (registered as { template: (props: unknown) => ComponentConfig }).template(config.props || {});
|
|
872
|
+
if (templateResult) {
|
|
873
|
+
return await this._buildSkeletonConfig(templateResult, componentCss);
|
|
874
|
+
}
|
|
875
|
+
} catch {
|
|
876
|
+
// Template failed
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if ((registered as { items?: ComponentConfig[] }).items && Array.isArray((registered as { items?: ComponentConfig[] }).items)) {
|
|
881
|
+
return {
|
|
882
|
+
eztype: 'EzComponent',
|
|
883
|
+
cls: (registered as { cls?: string | string[] }).cls,
|
|
884
|
+
css: componentCss,
|
|
885
|
+
layout: (registered as { layout?: string }).layout,
|
|
886
|
+
style: (registered as { style?: Record<string, string> }).style,
|
|
887
|
+
flex: config.flex,
|
|
888
|
+
items: await Promise.all((registered as { items: ComponentConfig[] }).items.map(item => this._buildSkeletonConfig(item, componentCss)))
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
return {
|
|
894
|
+
eztype: 'EzSkeleton',
|
|
895
|
+
variant: 'rect',
|
|
896
|
+
cls: config.cls,
|
|
897
|
+
css: css,
|
|
898
|
+
flex: config.flex,
|
|
899
|
+
rounded: true
|
|
900
|
+
} as unknown as ComponentConfig;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
return {
|
|
904
|
+
eztype: 'EzSkeleton',
|
|
905
|
+
variant: 'rect',
|
|
906
|
+
cls: config.cls,
|
|
907
|
+
css: css,
|
|
908
|
+
flex: config.flex
|
|
909
|
+
} as unknown as ComponentConfig;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
_applyTooltip(el: HTMLElement, tipConfig: string | TooltipConfig, instance: EzBaseComponent | null): void {
|
|
913
|
+
const text = typeof tipConfig === 'string' ? tipConfig : tipConfig.text;
|
|
914
|
+
const position = (typeof tipConfig === 'object' ? tipConfig.position : null) || 'top';
|
|
915
|
+
const variant = (typeof tipConfig === 'object' ? tipConfig.variant : null) || 'dark';
|
|
916
|
+
const delay = (typeof tipConfig === 'object' ? tipConfig.delay : null) ?? 200;
|
|
917
|
+
|
|
918
|
+
if (!text) return;
|
|
919
|
+
|
|
920
|
+
const tooltip = document.createElement('div');
|
|
921
|
+
tooltip.className = tooltipCls('tooltip', variant);
|
|
922
|
+
tooltip.textContent = text;
|
|
923
|
+
tooltip.style.opacity = '0';
|
|
924
|
+
tooltip.style.visibility = 'hidden';
|
|
925
|
+
|
|
926
|
+
const arrow = document.createElement('div');
|
|
927
|
+
arrow.className = tooltipCls('arrow');
|
|
928
|
+
tooltip.appendChild(arrow);
|
|
929
|
+
|
|
930
|
+
document.body.appendChild(tooltip);
|
|
931
|
+
|
|
932
|
+
let showTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
933
|
+
let hideTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
934
|
+
|
|
935
|
+
const showTooltip = (): void => {
|
|
936
|
+
if (hideTimeout) clearTimeout(hideTimeout);
|
|
937
|
+
showTimeout = setTimeout(() => {
|
|
938
|
+
const rect = el.getBoundingClientRect();
|
|
939
|
+
const tooltipRect = tooltip.getBoundingClientRect();
|
|
940
|
+
|
|
941
|
+
let top: number, left: number;
|
|
942
|
+
|
|
943
|
+
switch (position) {
|
|
944
|
+
case 'top':
|
|
945
|
+
top = rect.top - tooltipRect.height - 8;
|
|
946
|
+
left = rect.left + (rect.width - tooltipRect.width) / 2;
|
|
947
|
+
break;
|
|
948
|
+
case 'bottom':
|
|
949
|
+
top = rect.bottom + 8;
|
|
950
|
+
left = rect.left + (rect.width - tooltipRect.width) / 2;
|
|
951
|
+
break;
|
|
952
|
+
case 'left':
|
|
953
|
+
top = rect.top + (rect.height - tooltipRect.height) / 2;
|
|
954
|
+
left = rect.left - tooltipRect.width - 8;
|
|
955
|
+
break;
|
|
956
|
+
case 'right':
|
|
957
|
+
top = rect.top + (rect.height - tooltipRect.height) / 2;
|
|
958
|
+
left = rect.right + 8;
|
|
959
|
+
break;
|
|
960
|
+
default:
|
|
961
|
+
top = rect.top - tooltipRect.height - 8;
|
|
962
|
+
left = rect.left + (rect.width - tooltipRect.width) / 2;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const padding = 8;
|
|
966
|
+
if (left < padding) left = padding;
|
|
967
|
+
if (left + tooltipRect.width > window.innerWidth - padding) {
|
|
968
|
+
left = window.innerWidth - tooltipRect.width - padding;
|
|
969
|
+
}
|
|
970
|
+
if (top < padding) top = padding;
|
|
971
|
+
if (top + tooltipRect.height > window.innerHeight - padding) {
|
|
972
|
+
top = window.innerHeight - tooltipRect.height - padding;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
tooltip.style.top = `${top + window.scrollY}px`;
|
|
976
|
+
tooltip.style.left = `${left + window.scrollX}px`;
|
|
977
|
+
tooltip.className = tooltipCls('tooltip', variant, position);
|
|
978
|
+
tooltip.style.opacity = '1';
|
|
979
|
+
tooltip.style.visibility = 'visible';
|
|
980
|
+
}, delay);
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
const hideTooltip = (): void => {
|
|
984
|
+
if (showTimeout) clearTimeout(showTimeout);
|
|
985
|
+
hideTimeout = setTimeout(() => {
|
|
986
|
+
tooltip.style.opacity = '0';
|
|
987
|
+
tooltip.style.visibility = 'hidden';
|
|
988
|
+
}, 0);
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
el.addEventListener('mouseenter', showTooltip);
|
|
992
|
+
el.addEventListener('mouseleave', hideTooltip);
|
|
993
|
+
el.addEventListener('focus', showTooltip);
|
|
994
|
+
el.addEventListener('blur', hideTooltip);
|
|
995
|
+
|
|
996
|
+
if (instance) {
|
|
997
|
+
const originalDestroy = instance.destroy?.bind(instance);
|
|
998
|
+
instance.destroy = (): void => {
|
|
999
|
+
if (showTimeout) clearTimeout(showTimeout);
|
|
1000
|
+
if (hideTimeout) clearTimeout(hideTimeout);
|
|
1001
|
+
el.removeEventListener('mouseenter', showTooltip);
|
|
1002
|
+
el.removeEventListener('mouseleave', hideTooltip);
|
|
1003
|
+
el.removeEventListener('focus', showTooltip);
|
|
1004
|
+
el.removeEventListener('blur', hideTooltip);
|
|
1005
|
+
if (tooltip.parentNode) tooltip.parentNode.removeChild(tooltip);
|
|
1006
|
+
if (originalDestroy) originalDestroy();
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|