@hawsen-the-first/interactiv 0.0.1
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 +326 -0
- package/dist/animations.css +160 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/src/animationBus.d.ts +30 -0
- package/dist/src/animationBus.d.ts.map +1 -0
- package/dist/src/animationBus.js +125 -0
- package/dist/src/appBuilder.d.ts +173 -0
- package/dist/src/appBuilder.d.ts.map +1 -0
- package/dist/src/appBuilder.js +957 -0
- package/dist/src/eventBus.d.ts +100 -0
- package/dist/src/eventBus.d.ts.map +1 -0
- package/dist/src/eventBus.js +326 -0
- package/dist/src/eventManager.d.ts +87 -0
- package/dist/src/eventManager.d.ts.map +1 -0
- package/dist/src/eventManager.js +455 -0
- package/dist/src/garbageCollector.d.ts +68 -0
- package/dist/src/garbageCollector.d.ts.map +1 -0
- package/dist/src/garbageCollector.js +169 -0
- package/dist/src/logger.d.ts +11 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +15 -0
- package/dist/src/navigationManager.d.ts +105 -0
- package/dist/src/navigationManager.d.ts.map +1 -0
- package/dist/src/navigationManager.js +533 -0
- package/dist/src/screensaverManager.d.ts +66 -0
- package/dist/src/screensaverManager.d.ts.map +1 -0
- package/dist/src/screensaverManager.js +417 -0
- package/dist/src/settingsManager.d.ts +48 -0
- package/dist/src/settingsManager.d.ts.map +1 -0
- package/dist/src/settingsManager.js +317 -0
- package/dist/src/stateManager.d.ts +58 -0
- package/dist/src/stateManager.d.ts.map +1 -0
- package/dist/src/stateManager.js +278 -0
- package/dist/src/types.d.ts +32 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +1 -0
- package/dist/utils/generateGuid.d.ts +2 -0
- package/dist/utils/generateGuid.d.ts.map +1 -0
- package/dist/utils/generateGuid.js +19 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +42 -0
- package/dist/utils/template-helpers.d.ts +32 -0
- package/dist/utils/template-helpers.d.ts.map +1 -0
- package/dist/utils/template-helpers.js +24 -0
- package/package.json +59 -0
|
@@ -0,0 +1,957 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-this-alias */
|
|
2
|
+
import { EventBus } from "./eventBus";
|
|
3
|
+
import { NavigationManager } from "./navigationManager";
|
|
4
|
+
import { ScreensaverManager, } from "./screensaverManager";
|
|
5
|
+
import { SettingsManager } from "./settingsManager";
|
|
6
|
+
import { EventManager } from "./eventManager";
|
|
7
|
+
import { ComponentStateManager } from "./stateManager";
|
|
8
|
+
import { logger } from "./logger";
|
|
9
|
+
class RenderableComponent {
|
|
10
|
+
componentId;
|
|
11
|
+
state;
|
|
12
|
+
renderBus;
|
|
13
|
+
shadowRoot;
|
|
14
|
+
bubbleChanges;
|
|
15
|
+
children = [];
|
|
16
|
+
parent;
|
|
17
|
+
template = "";
|
|
18
|
+
styles = "";
|
|
19
|
+
lastRenderedHTML = "";
|
|
20
|
+
properties = new Map();
|
|
21
|
+
hostElement;
|
|
22
|
+
eventManager;
|
|
23
|
+
stateManager;
|
|
24
|
+
orchestrator;
|
|
25
|
+
globalSheets;
|
|
26
|
+
constructor(id, orchestrator, bubbleChanges = false) {
|
|
27
|
+
this.componentId = id;
|
|
28
|
+
this.bubbleChanges = bubbleChanges;
|
|
29
|
+
this.orchestrator = orchestrator;
|
|
30
|
+
this.globalSheets = [];
|
|
31
|
+
this.renderBus = orchestrator.registerEventBus(`render-${id}`);
|
|
32
|
+
this.setupRenderListeners();
|
|
33
|
+
this.attachShadowDOM();
|
|
34
|
+
this.initializeEventManager();
|
|
35
|
+
this.initializeStateManager();
|
|
36
|
+
}
|
|
37
|
+
setupRenderListeners() {
|
|
38
|
+
this.renderBus.on("render-request", (e) => {
|
|
39
|
+
const { changeType, source } = e.detail;
|
|
40
|
+
if (this.shouldRerender(changeType, source)) {
|
|
41
|
+
this.performRender();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
this.renderBus.on("child-changed", (e) => {
|
|
45
|
+
const { childId, changeType } = e.detail;
|
|
46
|
+
this.handleChildChange(childId, changeType);
|
|
47
|
+
});
|
|
48
|
+
this.renderBus.on("parent-changed", (e) => {
|
|
49
|
+
const { changeType } = e.detail;
|
|
50
|
+
this.handleParentChange(changeType);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
attachShadowDOM() {
|
|
54
|
+
this.hostElement = document.createElement("div");
|
|
55
|
+
this.hostElement.id = this.componentId;
|
|
56
|
+
this.shadowRoot = this.hostElement.attachShadow({ mode: "open" });
|
|
57
|
+
if (this.globalSheets?.length > 0) {
|
|
58
|
+
this.shadowRoot.adoptedStyleSheets = [...this.shadowRoot.adoptedStyleSheets, ...this.globalSheets];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async useGlobalStyles(text) {
|
|
62
|
+
const sheet = new CSSStyleSheet();
|
|
63
|
+
await sheet.replace(text);
|
|
64
|
+
this.globalSheets.push(sheet);
|
|
65
|
+
}
|
|
66
|
+
initializeEventManager() {
|
|
67
|
+
this.eventManager = new EventManager(this.shadowRoot, this.componentId);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Clean up event listeners before re-render to prevent duplicates
|
|
71
|
+
*/
|
|
72
|
+
// private cleanupEventListeners(): void {
|
|
73
|
+
// // Clear all selector-based listeners to prevent duplicates
|
|
74
|
+
// // The EventManager will handle this automatically now with deduplication
|
|
75
|
+
// // but we can add explicit cleanup if needed
|
|
76
|
+
// }
|
|
77
|
+
initializeStateManager() {
|
|
78
|
+
this.stateManager = new ComponentStateManager(this.componentId, (key, value, isLocal) => {
|
|
79
|
+
this.handleStateChange(key, value, isLocal);
|
|
80
|
+
});
|
|
81
|
+
// Attach the state proxy to this component
|
|
82
|
+
this.state = this.stateManager.getStateProxy();
|
|
83
|
+
}
|
|
84
|
+
handleStateChange(key, value, isLocal) {
|
|
85
|
+
// Update properties map for template compilation
|
|
86
|
+
this.properties.set(key, value);
|
|
87
|
+
// Trigger re-render directly instead of through event bus to avoid loops
|
|
88
|
+
this.performRender();
|
|
89
|
+
// Handle bubbling for local state changes
|
|
90
|
+
if (isLocal && this.bubbleChanges && this.parent) {
|
|
91
|
+
this.parent.renderBus.emit("child-changed", {
|
|
92
|
+
childId: this.componentId,
|
|
93
|
+
changeType: "state-change",
|
|
94
|
+
stateKey: key,
|
|
95
|
+
isLocal: true,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
performRender(options) {
|
|
100
|
+
const fadeConfig = options?.fade;
|
|
101
|
+
if (fadeConfig?.enabled) {
|
|
102
|
+
this.performRenderWithFade(fadeConfig.duration ?? 300, options);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
this.performRenderImmediate(options);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async performRenderWithFade(duration, options) {
|
|
109
|
+
// Fade out
|
|
110
|
+
this.hostElement.style.transition = `opacity ${duration}ms ease`;
|
|
111
|
+
this.hostElement.style.opacity = '0';
|
|
112
|
+
// Wait for fade out to complete
|
|
113
|
+
await new Promise(resolve => setTimeout(resolve, duration));
|
|
114
|
+
// Perform actual render
|
|
115
|
+
this.performRenderImmediate(options);
|
|
116
|
+
// Fade in
|
|
117
|
+
this.hostElement.style.opacity = '1';
|
|
118
|
+
}
|
|
119
|
+
performRenderImmediate(options) {
|
|
120
|
+
this.onBeforeRender();
|
|
121
|
+
const compiledHTML = this.compileTemplate();
|
|
122
|
+
if (compiledHTML !== this.lastRenderedHTML) {
|
|
123
|
+
this.updateShadowDOM(compiledHTML);
|
|
124
|
+
this.lastRenderedHTML = compiledHTML;
|
|
125
|
+
this.attachEventListeners();
|
|
126
|
+
this.renderChildren(options);
|
|
127
|
+
}
|
|
128
|
+
this.onAfterRender();
|
|
129
|
+
}
|
|
130
|
+
compileTemplate() {
|
|
131
|
+
let compiled = this.template;
|
|
132
|
+
// Handle ifnot/else blocks first (inverted if) - process these before regular if blocks
|
|
133
|
+
compiled = compiled.replace(/\{\{#ifnot\s+(\w+)\s*\}\}([\s\S]*?)(?:\{\{else\}\}([\s\S]*?))?\{\{\/ifnot\}\}/g, (_, condition, ifnotContent, elseContent) => {
|
|
134
|
+
const conditionValue = this.getProperty(condition);
|
|
135
|
+
if (!conditionValue) {
|
|
136
|
+
return ifnotContent || "";
|
|
137
|
+
}
|
|
138
|
+
else if (elseContent !== undefined) {
|
|
139
|
+
return elseContent;
|
|
140
|
+
}
|
|
141
|
+
return "";
|
|
142
|
+
});
|
|
143
|
+
// Handle if/else blocks
|
|
144
|
+
compiled = compiled.replace(/\{\{#if\s+(\w+)\s*\}\}([\s\S]*?)(?:\{\{else\}\}([\s\S]*?))?\{\{\/if\}\}/g, (_, condition, ifContent, elseContent) => {
|
|
145
|
+
const conditionValue = this.getProperty(condition);
|
|
146
|
+
if (conditionValue) {
|
|
147
|
+
return ifContent || "";
|
|
148
|
+
}
|
|
149
|
+
else if (elseContent !== undefined) {
|
|
150
|
+
return elseContent;
|
|
151
|
+
}
|
|
152
|
+
return "";
|
|
153
|
+
});
|
|
154
|
+
// Handle property interpolation last
|
|
155
|
+
compiled = compiled.replace(/\{\{(\w+)\}\}/g, (_, propName) => {
|
|
156
|
+
return this.getProperty(propName) || "";
|
|
157
|
+
});
|
|
158
|
+
return compiled;
|
|
159
|
+
}
|
|
160
|
+
updateShadowDOM(html) {
|
|
161
|
+
this.shadowRoot.innerHTML = `
|
|
162
|
+
<style>${this.styles}</style>
|
|
163
|
+
${html}
|
|
164
|
+
`;
|
|
165
|
+
}
|
|
166
|
+
renderChildren(options) {
|
|
167
|
+
const childrenContainer = this.shadowRoot.querySelector(".children-container");
|
|
168
|
+
if (childrenContainer && this.children.length > 0) {
|
|
169
|
+
childrenContainer.innerHTML = "";
|
|
170
|
+
this.children.forEach((child) => {
|
|
171
|
+
// Ensure child is rendered before appending
|
|
172
|
+
// Pass fade options to children so they fade together
|
|
173
|
+
child.performRender(options);
|
|
174
|
+
// Ensure child host element is visible
|
|
175
|
+
//child.hostElement.style.display = "block";
|
|
176
|
+
childrenContainer.appendChild(child.hostElement);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
getProperty(key) {
|
|
181
|
+
return this.properties.get(key);
|
|
182
|
+
}
|
|
183
|
+
getPublicProperty(key) {
|
|
184
|
+
return this.properties.get(key);
|
|
185
|
+
}
|
|
186
|
+
setProperty(key, value) {
|
|
187
|
+
const oldValue = this.properties.get(key);
|
|
188
|
+
if (oldValue !== value) {
|
|
189
|
+
this.properties.set(key, value);
|
|
190
|
+
// Special handling for template changes
|
|
191
|
+
if (key === "template") {
|
|
192
|
+
this.template = value;
|
|
193
|
+
this.performRender();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
// Special handling for styles changes
|
|
197
|
+
if (key === "styles") {
|
|
198
|
+
this.styles = value;
|
|
199
|
+
this.performRender();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
this.renderBus.emit("render-request", {
|
|
203
|
+
changeType: "property-change",
|
|
204
|
+
source: this.componentId,
|
|
205
|
+
property: key,
|
|
206
|
+
oldValue,
|
|
207
|
+
newValue: value,
|
|
208
|
+
});
|
|
209
|
+
if (this.bubbleChanges && this.parent) {
|
|
210
|
+
this.parent.renderBus.emit("child-changed", {
|
|
211
|
+
childId: this.componentId,
|
|
212
|
+
changeType: "property-change",
|
|
213
|
+
property: key,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
addChild(child) {
|
|
219
|
+
child.parent = this;
|
|
220
|
+
this.children.push(child);
|
|
221
|
+
// Force immediate re-render when child is added
|
|
222
|
+
this.updateShadowDOM(this.compileTemplate());
|
|
223
|
+
this.attachEventListeners();
|
|
224
|
+
this.renderChildren();
|
|
225
|
+
this.notifyChildrenOfStructureChange();
|
|
226
|
+
}
|
|
227
|
+
removeChild(childId) {
|
|
228
|
+
const childIndex = this.children.findIndex((c) => c.componentId === childId);
|
|
229
|
+
if (childIndex !== -1) {
|
|
230
|
+
const child = this.children[childIndex];
|
|
231
|
+
child.parent = undefined;
|
|
232
|
+
this.children.splice(childIndex, 1);
|
|
233
|
+
this.renderBus.emit("render-request", {
|
|
234
|
+
changeType: "child-removed",
|
|
235
|
+
source: this.componentId,
|
|
236
|
+
childId: childId,
|
|
237
|
+
});
|
|
238
|
+
this.notifyChildrenOfStructureChange();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
addSibling(sibling) {
|
|
242
|
+
sibling.parent = this.parent;
|
|
243
|
+
this.parent?.children.push(sibling);
|
|
244
|
+
// Force immediate re-render when child is added
|
|
245
|
+
this.updateShadowDOM(this.compileTemplate());
|
|
246
|
+
this.attachEventListeners();
|
|
247
|
+
this.renderChildren();
|
|
248
|
+
this.notifyChildrenOfStructureChange();
|
|
249
|
+
}
|
|
250
|
+
removeSibling(siblingId) {
|
|
251
|
+
const me = this;
|
|
252
|
+
if (me.parent) {
|
|
253
|
+
const siblingIndex = me.parent?.children.findIndex((c) => c.componentId === siblingId);
|
|
254
|
+
if (siblingIndex !== -1) {
|
|
255
|
+
const sibling = me.parent?.children[siblingIndex];
|
|
256
|
+
if (sibling) {
|
|
257
|
+
sibling.parent = undefined;
|
|
258
|
+
me.parent.children.splice(siblingIndex, 1);
|
|
259
|
+
}
|
|
260
|
+
this.renderBus.emit("render-request", {
|
|
261
|
+
changeType: "child-removed",
|
|
262
|
+
source: me.componentId,
|
|
263
|
+
childId: siblingId,
|
|
264
|
+
});
|
|
265
|
+
this.notifyChildrenOfStructureChange();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
notifyChildrenOfStructureChange() {
|
|
270
|
+
this.children.forEach((child) => {
|
|
271
|
+
child.renderBus.emit("parent-changed", {
|
|
272
|
+
changeType: "structure-change",
|
|
273
|
+
parentId: this.componentId,
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
attachEventListeners() {
|
|
278
|
+
const actionElements = this.shadowRoot.querySelectorAll("[data-action]");
|
|
279
|
+
actionElements.forEach((element) => {
|
|
280
|
+
const action = element.getAttribute("data-action");
|
|
281
|
+
element.addEventListener("click", (e) => {
|
|
282
|
+
this.handleAction(action, e);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
handleAction(action, event) {
|
|
287
|
+
this.renderBus.emit("action-triggered", {
|
|
288
|
+
action,
|
|
289
|
+
componentId: this.componentId,
|
|
290
|
+
event,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
onBeforeRender() { }
|
|
294
|
+
onAfterRender() { }
|
|
295
|
+
shouldRerender(changeType, source) {
|
|
296
|
+
if (changeType && source) {
|
|
297
|
+
return true; // Default: always re-render
|
|
298
|
+
}
|
|
299
|
+
return true; // Default: always re-render
|
|
300
|
+
}
|
|
301
|
+
handleParentChange(changeType) {
|
|
302
|
+
// Default: re-render when parent changes
|
|
303
|
+
if (changeType) {
|
|
304
|
+
this.performRender();
|
|
305
|
+
}
|
|
306
|
+
this.performRender();
|
|
307
|
+
}
|
|
308
|
+
handleChildChange(childId, changeType) {
|
|
309
|
+
// Default: re-render when children change
|
|
310
|
+
if (childId && changeType) {
|
|
311
|
+
this.performRender();
|
|
312
|
+
}
|
|
313
|
+
this.performRender();
|
|
314
|
+
}
|
|
315
|
+
// Initial render method
|
|
316
|
+
performInitialRender() {
|
|
317
|
+
this.defineTemplate();
|
|
318
|
+
this.defineStyles();
|
|
319
|
+
this.performRender();
|
|
320
|
+
}
|
|
321
|
+
getHostElement() {
|
|
322
|
+
return this.hostElement;
|
|
323
|
+
}
|
|
324
|
+
getChildrenCount() {
|
|
325
|
+
return this.children.length;
|
|
326
|
+
}
|
|
327
|
+
// Event Manager convenience methods
|
|
328
|
+
/**
|
|
329
|
+
* Add a point interaction (click + tap) to elements matching the selector
|
|
330
|
+
*/
|
|
331
|
+
point(selector, callback) {
|
|
332
|
+
this.eventManager.point(selector, callback);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Add an un-point interaction (click + tap) to elements matching the selector
|
|
336
|
+
*/
|
|
337
|
+
unpoint(selector, callback) {
|
|
338
|
+
this.eventManager.unpoint(selector, callback);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Add drag interaction (mouse drag + touch drag) to elements matching the selector
|
|
342
|
+
*/
|
|
343
|
+
drag(selector, callbacks) {
|
|
344
|
+
this.eventManager.drag(selector, callbacks);
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Add hover interaction (mouse enter/leave + touch fallback) to elements matching the selector
|
|
348
|
+
*/
|
|
349
|
+
hover(selector, callbacks) {
|
|
350
|
+
this.eventManager.hover(selector, callbacks);
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Add long press interaction to elements matching the selector
|
|
354
|
+
*/
|
|
355
|
+
longPress(selector, callback, duration = 500) {
|
|
356
|
+
this.eventManager.longPress(selector, callback, duration);
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Add swipe gesture detection to elements matching the selector
|
|
360
|
+
*/
|
|
361
|
+
swipe(selector, callbacks, threshold = 50) {
|
|
362
|
+
this.eventManager.swipe(selector, callbacks, threshold);
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Add a custom event listener with automatic cleanup
|
|
366
|
+
*/
|
|
367
|
+
addEventListener(element, type, listener, options) {
|
|
368
|
+
this.eventManager.addEventListener(element, type, listener, options);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Remove a specific event listener
|
|
372
|
+
*/
|
|
373
|
+
removeEventListener(element, type, listener) {
|
|
374
|
+
this.eventManager.removeEventListener(element, type, listener);
|
|
375
|
+
}
|
|
376
|
+
// State Management convenience methods
|
|
377
|
+
/**
|
|
378
|
+
* Create and manage local component state
|
|
379
|
+
*/
|
|
380
|
+
useState(key, initialValue) {
|
|
381
|
+
return this.stateManager.useState(key, initialValue);
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Subscribe to and manage global application state
|
|
385
|
+
*/
|
|
386
|
+
useGlobalState(key, initialValue) {
|
|
387
|
+
return this.stateManager.useGlobalState(key, initialValue);
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Get current local state value without subscribing
|
|
391
|
+
*/
|
|
392
|
+
getLocalState(key) {
|
|
393
|
+
return this.stateManager.getLocalState(key);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Define multiple state properties at once with initial values
|
|
397
|
+
* This creates reactive state properties accessible via this.state
|
|
398
|
+
*/
|
|
399
|
+
defineState(initialState) {
|
|
400
|
+
this.stateManager.defineState(initialState);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Destroy the component and clean up all event listeners
|
|
404
|
+
*/
|
|
405
|
+
destroy() {
|
|
406
|
+
if (this.stateManager) {
|
|
407
|
+
this.stateManager.destroy();
|
|
408
|
+
}
|
|
409
|
+
if (this.eventManager) {
|
|
410
|
+
this.eventManager.destroy();
|
|
411
|
+
}
|
|
412
|
+
// Clean up children
|
|
413
|
+
this.children.forEach((child) => child.destroy());
|
|
414
|
+
this.children.length = 0;
|
|
415
|
+
// Remove from parent
|
|
416
|
+
if (this.parent) {
|
|
417
|
+
this.parent.removeChild(this.componentId);
|
|
418
|
+
}
|
|
419
|
+
// Remove from DOM
|
|
420
|
+
if (this.hostElement && this.hostElement.parentNode) {
|
|
421
|
+
this.hostElement.parentNode.removeChild(this.hostElement);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// AppBuilder - Root of the hierarchy
|
|
426
|
+
class AppBuilder extends RenderableComponent {
|
|
427
|
+
pages = [];
|
|
428
|
+
navigationManager;
|
|
429
|
+
constructor(orchestrator) {
|
|
430
|
+
super("app-builder", orchestrator, false); // Don't bubble by default
|
|
431
|
+
this.navigationManager = new NavigationManager(orchestrator);
|
|
432
|
+
new ScreensaverManager(orchestrator, this.navigationManager);
|
|
433
|
+
new SettingsManager(orchestrator, this.navigationManager);
|
|
434
|
+
this.performInitialRender();
|
|
435
|
+
this.setupNavigationContainer();
|
|
436
|
+
}
|
|
437
|
+
setupNavigationContainer() {
|
|
438
|
+
// Make the app content container a navigation container
|
|
439
|
+
const appContent = this.shadowRoot.querySelector(".app-content");
|
|
440
|
+
if (appContent) {
|
|
441
|
+
appContent.classList.add("nav-container");
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
defineTemplate() {
|
|
445
|
+
this.template = /* html */ `
|
|
446
|
+
<div class="app">
|
|
447
|
+
<main class="app-content">
|
|
448
|
+
<div class="children-container">
|
|
449
|
+
<!-- Pages will be rendered here -->
|
|
450
|
+
</div>
|
|
451
|
+
</main>
|
|
452
|
+
</div>
|
|
453
|
+
`;
|
|
454
|
+
}
|
|
455
|
+
defineStyles() {
|
|
456
|
+
this.styles = `
|
|
457
|
+
:host {
|
|
458
|
+
display: block;
|
|
459
|
+
width: 100%;
|
|
460
|
+
height: 100vh;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.app {
|
|
464
|
+
display: flex;
|
|
465
|
+
flex-direction: column;
|
|
466
|
+
height: 100%;
|
|
467
|
+
font-family: Arial, sans-serif;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
`;
|
|
471
|
+
}
|
|
472
|
+
addPage(page) {
|
|
473
|
+
this.pages.push(page);
|
|
474
|
+
this.addChild(page);
|
|
475
|
+
// Register page with navigation manager
|
|
476
|
+
this.navigationManager.registerPage(page);
|
|
477
|
+
}
|
|
478
|
+
removePage(pageId) {
|
|
479
|
+
const pageIndex = this.pages.findIndex((p) => p.componentId === pageId);
|
|
480
|
+
if (pageIndex !== -1) {
|
|
481
|
+
this.pages.splice(pageIndex, 1);
|
|
482
|
+
this.removeChild(pageId);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
navigateToPage(pageId, config) {
|
|
486
|
+
this.orchestrator.navigateToPage(pageId, config);
|
|
487
|
+
}
|
|
488
|
+
navigateToView(viewId, config) {
|
|
489
|
+
this.orchestrator.navigateToView(viewId, config);
|
|
490
|
+
}
|
|
491
|
+
getCurrentPageId() {
|
|
492
|
+
return this.navigationManager.getCurrentPageId();
|
|
493
|
+
}
|
|
494
|
+
getCurrentViewId() {
|
|
495
|
+
return this.navigationManager.getCurrentViewId();
|
|
496
|
+
}
|
|
497
|
+
isTransitioning() {
|
|
498
|
+
return this.navigationManager.isTransitioning();
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Add a screensaver page with the specified configuration
|
|
502
|
+
* The screensaver page can contain multiple views for rich interactive experiences
|
|
503
|
+
* @param screensaverPage The page to use as the screensaver (can contain multiple views)
|
|
504
|
+
* @param config Configuration object for the screensaver (excluding 'page' property)
|
|
505
|
+
*/
|
|
506
|
+
addScreensaver(screensaverPage, config) {
|
|
507
|
+
// Add the screensaver page to the app
|
|
508
|
+
this.addPage(screensaverPage);
|
|
509
|
+
// Register the screensaver with the screensaver manager via event bus
|
|
510
|
+
const screensaverBus = this.orchestrator.getEventBus("screensaver-manager");
|
|
511
|
+
if (screensaverBus) {
|
|
512
|
+
screensaverBus.emit("register-screensaver", {
|
|
513
|
+
config: {
|
|
514
|
+
...config,
|
|
515
|
+
page: screensaverPage,
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
throw new Error("Screensaver manager not found. Ensure AppBuilder is properly initialized.");
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
attachToDom() {
|
|
524
|
+
const appContainer = document.getElementById("app");
|
|
525
|
+
if (appContainer) {
|
|
526
|
+
appContainer.appendChild(this.getHostElement());
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
// Fallback to body if #app doesn't exist
|
|
530
|
+
document.body.appendChild(this.getHostElement());
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
// Page - Contains Views
|
|
535
|
+
class Page extends RenderableComponent {
|
|
536
|
+
views = [];
|
|
537
|
+
constructor(id, orchestrator, bubbleChanges = true, customTemplate, customStyles) {
|
|
538
|
+
super(id, orchestrator, bubbleChanges);
|
|
539
|
+
// Set custom template/styles before initial render if provided
|
|
540
|
+
if (customTemplate) {
|
|
541
|
+
this.properties.set("template", customTemplate);
|
|
542
|
+
}
|
|
543
|
+
if (customStyles) {
|
|
544
|
+
this.properties.set("styles", customStyles);
|
|
545
|
+
}
|
|
546
|
+
this.performInitialRender();
|
|
547
|
+
}
|
|
548
|
+
defineTemplate() {
|
|
549
|
+
// Check if a custom template was set via setProperty
|
|
550
|
+
const customTemplate = this.getProperty("template");
|
|
551
|
+
if (customTemplate) {
|
|
552
|
+
this.template = customTemplate;
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
// Use default template
|
|
556
|
+
this.template = `
|
|
557
|
+
<div class="page" data-page-id="${this.componentId}">
|
|
558
|
+
<div class="page-header">
|
|
559
|
+
<h2>{{pageTitle}}</h2>
|
|
560
|
+
<div class="page-actions">
|
|
561
|
+
{{#if showActions}}
|
|
562
|
+
<button data-action="edit" class="btn">Edit Page</button>
|
|
563
|
+
<button data-action="delete" class="btn danger">Delete Page</button>
|
|
564
|
+
{{/if}}
|
|
565
|
+
</div>
|
|
566
|
+
</div>
|
|
567
|
+
<div class="page-content">
|
|
568
|
+
<div class="children-container">
|
|
569
|
+
<!-- Views will be rendered here -->
|
|
570
|
+
</div>
|
|
571
|
+
</div>
|
|
572
|
+
</div>
|
|
573
|
+
`;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
defineStyles() {
|
|
577
|
+
// Check if custom styles were set via setProperty
|
|
578
|
+
const customStyles = this.getProperty("styles");
|
|
579
|
+
if (customStyles) {
|
|
580
|
+
this.styles = customStyles;
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
// Use default styles
|
|
584
|
+
this.styles = `
|
|
585
|
+
:host {
|
|
586
|
+
display: block;
|
|
587
|
+
margin-bottom: 2rem;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.page {
|
|
591
|
+
border: 2px solid #3498db;
|
|
592
|
+
border-radius: 8px;
|
|
593
|
+
padding: 1rem;
|
|
594
|
+
background: #ecf0f1;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.page-header {
|
|
598
|
+
display: flex;
|
|
599
|
+
justify-content: space-between;
|
|
600
|
+
align-items: center;
|
|
601
|
+
margin-bottom: 1rem;
|
|
602
|
+
padding-bottom: 0.5rem;
|
|
603
|
+
border-bottom: 1px solid #bdc3c7;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
.page-actions {
|
|
607
|
+
display: flex;
|
|
608
|
+
gap: 0.5rem;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.btn {
|
|
612
|
+
padding: 0.5rem 1rem;
|
|
613
|
+
border: none;
|
|
614
|
+
border-radius: 4px;
|
|
615
|
+
cursor: pointer;
|
|
616
|
+
background: #3498db;
|
|
617
|
+
color: white;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
.btn.danger {
|
|
621
|
+
background: #e74c3c;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
.btn:hover {
|
|
625
|
+
opacity: 0.8;
|
|
626
|
+
}
|
|
627
|
+
`;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
addView(view) {
|
|
631
|
+
this.views.push(view);
|
|
632
|
+
this.addChild(view);
|
|
633
|
+
// Register view with navigation manager via event bus
|
|
634
|
+
const navBus = this.orchestrator.getEventBus("navigation-manager");
|
|
635
|
+
if (navBus) {
|
|
636
|
+
navBus.emit("register-view", { view });
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
removeView(viewId) {
|
|
640
|
+
const viewIndex = this.views.findIndex((v) => v.componentId === viewId);
|
|
641
|
+
if (viewIndex !== -1) {
|
|
642
|
+
this.views.splice(viewIndex, 1);
|
|
643
|
+
this.removeChild(viewId);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Add a screensaver page with the specified configuration
|
|
648
|
+
* @param screensaverPage The page to use as the screensaver (can contain multiple views)
|
|
649
|
+
* @param config Configuration object for the screensaver (excluding 'page' property)
|
|
650
|
+
*/
|
|
651
|
+
addScreensaver(screensaverPage, config) {
|
|
652
|
+
// Register the screensaver with the screensaver manager via event bus
|
|
653
|
+
const screensaverBus = this.orchestrator.getEventBus("screensaver-manager");
|
|
654
|
+
if (screensaverBus) {
|
|
655
|
+
screensaverBus.emit("register-screensaver", {
|
|
656
|
+
config: {
|
|
657
|
+
...config,
|
|
658
|
+
page: screensaverPage,
|
|
659
|
+
},
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
throw new Error("Screensaver manager not found. Ensure AppBuilder is properly initialized.");
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Add a hidden settings page to this page with the specified configuration
|
|
668
|
+
* The settings page is activated by touching corners in sequence: top-left, top-right, bottom-right
|
|
669
|
+
* @param view The view to use as the settings page
|
|
670
|
+
* @param config Configuration object for the settings page
|
|
671
|
+
*/
|
|
672
|
+
addSettings(view, config) {
|
|
673
|
+
// First add the view to this page (but don't register it with navigation yet)
|
|
674
|
+
this.views.push(view);
|
|
675
|
+
this.addChild(view);
|
|
676
|
+
// Register the settings with the settings manager via event bus
|
|
677
|
+
const settingsBus = this.orchestrator.getEventBus("settings-manager");
|
|
678
|
+
if (settingsBus) {
|
|
679
|
+
settingsBus.emit("register-settings", {
|
|
680
|
+
config: {
|
|
681
|
+
...config,
|
|
682
|
+
view: view,
|
|
683
|
+
},
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
throw new Error("Settings manager not found. Ensure AppBuilder is properly initialized.");
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
// View - Contains Components
|
|
692
|
+
class View extends RenderableComponent {
|
|
693
|
+
components = [];
|
|
694
|
+
constructor(id, orchestrator, bubbleChanges = false, customTemplate, customStyles) {
|
|
695
|
+
super(id, orchestrator, bubbleChanges);
|
|
696
|
+
// Set custom template/styles before initial render if provided
|
|
697
|
+
if (customTemplate) {
|
|
698
|
+
this.properties.set("template", customTemplate);
|
|
699
|
+
}
|
|
700
|
+
if (customStyles) {
|
|
701
|
+
this.properties.set("styles", customStyles);
|
|
702
|
+
}
|
|
703
|
+
this.performInitialRender();
|
|
704
|
+
}
|
|
705
|
+
defineTemplate() {
|
|
706
|
+
// Check if a custom template was set via setProperty
|
|
707
|
+
const customTemplate = this.getProperty("template");
|
|
708
|
+
if (customTemplate) {
|
|
709
|
+
this.template = customTemplate;
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
// Use default template
|
|
713
|
+
this.template = `
|
|
714
|
+
<div class="view" data-view-id="${this.componentId}">
|
|
715
|
+
<div class="view-header">
|
|
716
|
+
<h3>{{viewTitle}}</h3>
|
|
717
|
+
<span class="view-type">{{viewType}}</span>
|
|
718
|
+
</div>
|
|
719
|
+
<div class="view-content">
|
|
720
|
+
<div class="children-container">
|
|
721
|
+
<!-- Components will be rendered here -->
|
|
722
|
+
</div>
|
|
723
|
+
</div>
|
|
724
|
+
</div>
|
|
725
|
+
`;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
defineStyles() {
|
|
729
|
+
// Check if custom styles were set via setProperty
|
|
730
|
+
const customStyles = this.getProperty("styles");
|
|
731
|
+
if (customStyles) {
|
|
732
|
+
this.styles = customStyles;
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
// Use default styles
|
|
736
|
+
this.styles = `
|
|
737
|
+
:host {
|
|
738
|
+
display: block;
|
|
739
|
+
margin-bottom: 1rem;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
.view {
|
|
743
|
+
border: 1px solid #95a5a6;
|
|
744
|
+
border-radius: 6px;
|
|
745
|
+
padding: 0.75rem;
|
|
746
|
+
background: white;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
.view-header {
|
|
750
|
+
display: flex;
|
|
751
|
+
justify-content: space-between;
|
|
752
|
+
align-items: center;
|
|
753
|
+
margin-bottom: 0.75rem;
|
|
754
|
+
padding-bottom: 0.25rem;
|
|
755
|
+
border-bottom: 1px solid #ecf0f1;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
.view-type {
|
|
759
|
+
background: #95a5a6;
|
|
760
|
+
color: white;
|
|
761
|
+
padding: 0.25rem 0.5rem;
|
|
762
|
+
border-radius: 3px;
|
|
763
|
+
font-size: 0.8rem;
|
|
764
|
+
}
|
|
765
|
+
`;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
addComponent(component) {
|
|
769
|
+
this.components.push(component);
|
|
770
|
+
this.addChild(component);
|
|
771
|
+
}
|
|
772
|
+
// TODO: Make this actually remove the component from the DOM.
|
|
773
|
+
removeComponent(componentId) {
|
|
774
|
+
logger.trace(`Removing component with ID: ${componentId}`);
|
|
775
|
+
const componentIndex = this.components.findIndex((c) => {
|
|
776
|
+
c.componentId === componentId;
|
|
777
|
+
});
|
|
778
|
+
if (componentIndex !== -1) {
|
|
779
|
+
this.components.splice(componentIndex, 1);
|
|
780
|
+
this.removeChild(componentId);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
// Component - Leaf nodes in the hierarchy
|
|
785
|
+
class Component extends RenderableComponent {
|
|
786
|
+
constructor(id, orchestrator, bubbleChanges = false, customTemplate, customStyles) {
|
|
787
|
+
super(id, orchestrator, bubbleChanges);
|
|
788
|
+
if (customTemplate) {
|
|
789
|
+
this.properties.set("template", customTemplate);
|
|
790
|
+
}
|
|
791
|
+
if (customStyles) {
|
|
792
|
+
this.properties.set("styles", customStyles);
|
|
793
|
+
}
|
|
794
|
+
this.performInitialRender();
|
|
795
|
+
}
|
|
796
|
+
defineTemplate() {
|
|
797
|
+
// Check if a custom template was set via setProperty
|
|
798
|
+
const customTemplate = this.getProperty("template");
|
|
799
|
+
if (customTemplate) {
|
|
800
|
+
this.template = customTemplate;
|
|
801
|
+
}
|
|
802
|
+
else {
|
|
803
|
+
// Use default template
|
|
804
|
+
this.template = `
|
|
805
|
+
<div class="component-container" data-component-id="${this.componentId}">
|
|
806
|
+
<div class="component-header">
|
|
807
|
+
<h4>{{title}}</h4>
|
|
808
|
+
<span class="component-status {{statusClass}}">{{status}}</span>
|
|
809
|
+
</div>
|
|
810
|
+
<div class="component-body">
|
|
811
|
+
<p class="description">{{description}}</p>
|
|
812
|
+
{{#if showContent}}
|
|
813
|
+
<div class="dynamic-content">
|
|
814
|
+
{{content}}
|
|
815
|
+
</div>
|
|
816
|
+
{{else}}
|
|
817
|
+
<div class="no-content-message">
|
|
818
|
+
No content available
|
|
819
|
+
</div>
|
|
820
|
+
{{/if}}
|
|
821
|
+
{{#ifnot isDisabled}}
|
|
822
|
+
<div class="component-controls">
|
|
823
|
+
<button class="control-btn" data-action="toggle">Toggle</button>
|
|
824
|
+
</div>
|
|
825
|
+
{{else}}
|
|
826
|
+
<div class="disabled-message">
|
|
827
|
+
Component is disabled
|
|
828
|
+
</div>
|
|
829
|
+
{{/ifnot}}
|
|
830
|
+
</div>
|
|
831
|
+
<div class="component-actions">
|
|
832
|
+
<button class="action-btn" data-action="{{buttonAction}}">
|
|
833
|
+
{{buttonText}}
|
|
834
|
+
</button>
|
|
835
|
+
</div>
|
|
836
|
+
<div class="children-container">
|
|
837
|
+
<!-- Child components will be inserted here -->
|
|
838
|
+
</div>
|
|
839
|
+
</div>
|
|
840
|
+
`;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
defineStyles() {
|
|
844
|
+
// Check if custom styles were set via setProperty
|
|
845
|
+
const customStyles = this.getProperty("styles");
|
|
846
|
+
if (customStyles) {
|
|
847
|
+
this.styles = customStyles;
|
|
848
|
+
}
|
|
849
|
+
else {
|
|
850
|
+
// Use default styles
|
|
851
|
+
this.styles = `
|
|
852
|
+
:host {
|
|
853
|
+
display: block;
|
|
854
|
+
margin-bottom: 0.5rem;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
.component-container {
|
|
858
|
+
border: 1px solid #ddd;
|
|
859
|
+
border-radius: 4px;
|
|
860
|
+
padding: 0.5rem;
|
|
861
|
+
background: #f9f9f9;
|
|
862
|
+
font-family: Arial, sans-serif;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
.component-header {
|
|
866
|
+
display: flex;
|
|
867
|
+
justify-content: space-between;
|
|
868
|
+
align-items: center;
|
|
869
|
+
margin-bottom: 0.5rem;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
.component-header h4 {
|
|
873
|
+
margin: 0;
|
|
874
|
+
color: #2c3e50;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
.component-status {
|
|
878
|
+
padding: 0.2rem 0.4rem;
|
|
879
|
+
border-radius: 3px;
|
|
880
|
+
font-size: 0.7rem;
|
|
881
|
+
font-weight: bold;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
.component-status.active {
|
|
885
|
+
background: #2ecc71;
|
|
886
|
+
color: white;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
.component-status.inactive {
|
|
890
|
+
background: #95a5a6;
|
|
891
|
+
color: white;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
.dynamic-content {
|
|
895
|
+
background: #f5f5f5;
|
|
896
|
+
padding: 0.5rem;
|
|
897
|
+
border-radius: 4px;
|
|
898
|
+
margin: 0.5rem 0;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
.action-btn {
|
|
902
|
+
background: #3498db;
|
|
903
|
+
color: white;
|
|
904
|
+
border: none;
|
|
905
|
+
padding: 0.4rem 0.8rem;
|
|
906
|
+
border-radius: 4px;
|
|
907
|
+
cursor: pointer;
|
|
908
|
+
font-size: 0.8rem;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
.action-btn:hover {
|
|
912
|
+
background: #2980b9;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
.no-content-message {
|
|
916
|
+
background: #f8f9fa;
|
|
917
|
+
color: #6c757d;
|
|
918
|
+
padding: 0.5rem;
|
|
919
|
+
border-radius: 4px;
|
|
920
|
+
font-style: italic;
|
|
921
|
+
text-align: center;
|
|
922
|
+
margin: 0.5rem 0;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
.component-controls {
|
|
926
|
+
margin: 0.5rem 0;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
.control-btn {
|
|
930
|
+
background: #28a745;
|
|
931
|
+
color: white;
|
|
932
|
+
border: none;
|
|
933
|
+
padding: 0.3rem 0.6rem;
|
|
934
|
+
border-radius: 3px;
|
|
935
|
+
cursor: pointer;
|
|
936
|
+
font-size: 0.75rem;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
.control-btn:hover {
|
|
940
|
+
background: #218838;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
.disabled-message {
|
|
944
|
+
background: #f8d7da;
|
|
945
|
+
color: #721c24;
|
|
946
|
+
padding: 0.5rem;
|
|
947
|
+
border-radius: 4px;
|
|
948
|
+
font-style: italic;
|
|
949
|
+
text-align: center;
|
|
950
|
+
margin: 0.5rem 0;
|
|
951
|
+
border: 1px solid #f5c6cb;
|
|
952
|
+
}
|
|
953
|
+
`;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
export { AppBuilder, Page, View, Component, RenderableComponent, EventBus };
|