wyreframe 0.4.3 → 0.5.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/package.json +1 -1
- package/src/renderer/Renderer.gen.tsx +17 -0
- package/src/renderer/Renderer.mjs +67 -23
- package/src/renderer/Renderer.res +104 -12
package/package.json
CHANGED
|
@@ -19,6 +19,22 @@ export abstract class DomBindings_element { protected opaque!: any }; /* simulat
|
|
|
19
19
|
* @param toScene The scene ID navigating to */
|
|
20
20
|
export type onSceneChangeCallback = (_1:(undefined | string), _2:string) => void;
|
|
21
21
|
|
|
22
|
+
/** * Dead end click info type.
|
|
23
|
+
* Contains information about the clicked element that has no navigation target. */
|
|
24
|
+
export type deadEndClickInfo = {
|
|
25
|
+
readonly sceneId: string;
|
|
26
|
+
readonly elementId: string;
|
|
27
|
+
readonly elementText: string;
|
|
28
|
+
readonly elementType:
|
|
29
|
+
"link"
|
|
30
|
+
| "button"
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** * Dead end click callback type.
|
|
34
|
+
* Called when a button or link without navigation target is clicked.
|
|
35
|
+
* @param info Information about the clicked element and current scene */
|
|
36
|
+
export type onDeadEndClickCallback = (_1:deadEndClickInfo) => void;
|
|
37
|
+
|
|
22
38
|
/** * Configuration for the rendering process. */
|
|
23
39
|
export type renderOptions = {
|
|
24
40
|
readonly theme: (undefined | string);
|
|
@@ -26,6 +42,7 @@ export type renderOptions = {
|
|
|
26
42
|
readonly injectStyles: boolean;
|
|
27
43
|
readonly containerClass: (undefined | string);
|
|
28
44
|
readonly onSceneChange: (undefined | onSceneChangeCallback);
|
|
45
|
+
readonly onDeadEndClick: (undefined | onDeadEndClickCallback);
|
|
29
46
|
readonly device: (undefined | Types_deviceType)
|
|
30
47
|
};
|
|
31
48
|
|
|
@@ -14,6 +14,7 @@ let defaultOptions = {
|
|
|
14
14
|
injectStyles: true,
|
|
15
15
|
containerClass: undefined,
|
|
16
16
|
onSceneChange: undefined,
|
|
17
|
+
onDeadEndClick: undefined,
|
|
17
18
|
device: undefined
|
|
18
19
|
};
|
|
19
20
|
|
|
@@ -134,7 +135,24 @@ function deviceTypeToClass(device) {
|
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
137
|
|
|
137
|
-
function
|
|
138
|
+
function isNavigationAction(action) {
|
|
139
|
+
if (typeof action !== "object") {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
switch (action.TAG) {
|
|
143
|
+
case "Validate" :
|
|
144
|
+
case "Call" :
|
|
145
|
+
return false;
|
|
146
|
+
default:
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function hasNavigationAction(actions) {
|
|
152
|
+
return actions.some(isNavigationAction);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function renderElement(_elem, onAction, onDeadEnd) {
|
|
138
156
|
while (true) {
|
|
139
157
|
let elem = _elem;
|
|
140
158
|
if (isInputOnlyBox(elem)) {
|
|
@@ -156,7 +174,7 @@ function renderElement(_elem, onAction) {
|
|
|
156
174
|
div.dataset["name"] = name;
|
|
157
175
|
}
|
|
158
176
|
elem.children.forEach(child => {
|
|
159
|
-
let el = renderElement(child, onAction);
|
|
177
|
+
let el = renderElement(child, onAction, onDeadEnd);
|
|
160
178
|
if (el !== undefined) {
|
|
161
179
|
div.appendChild(Primitive_option.valFromOption(el));
|
|
162
180
|
return;
|
|
@@ -165,18 +183,25 @@ function renderElement(_elem, onAction) {
|
|
|
165
183
|
return Primitive_option.some(div);
|
|
166
184
|
case "Button" :
|
|
167
185
|
let actions = elem.actions;
|
|
186
|
+
let text = elem.text;
|
|
187
|
+
let id = elem.id;
|
|
168
188
|
let btn = document.createElement("button");
|
|
169
189
|
btn.className = "wf-button";
|
|
170
|
-
btn.id =
|
|
171
|
-
btn.textContent =
|
|
190
|
+
btn.id = id;
|
|
191
|
+
btn.textContent = text;
|
|
172
192
|
applyAlignment(btn, elem.align);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
193
|
+
let hasNavigation = actions.some(isNavigationAction);
|
|
194
|
+
if (hasNavigation) {
|
|
195
|
+
if (onAction !== undefined) {
|
|
196
|
+
btn.addEventListener("click", _event => {
|
|
197
|
+
let action = actions[0];
|
|
198
|
+
if (action !== undefined) {
|
|
199
|
+
return onAction(action);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
} else if (onDeadEnd !== undefined) {
|
|
204
|
+
btn.addEventListener("click", _event => onDeadEnd(id, text, "button"));
|
|
180
205
|
}
|
|
181
206
|
return Primitive_option.some(btn);
|
|
182
207
|
case "Input" :
|
|
@@ -190,19 +215,29 @@ function renderElement(_elem, onAction) {
|
|
|
190
215
|
return Primitive_option.some(input$1);
|
|
191
216
|
case "Link" :
|
|
192
217
|
let actions$1 = elem.actions;
|
|
218
|
+
let text$1 = elem.text;
|
|
219
|
+
let id$1 = elem.id;
|
|
193
220
|
let link = document.createElement("a");
|
|
194
221
|
link.className = "wf-link";
|
|
195
|
-
link.id =
|
|
222
|
+
link.id = id$1;
|
|
196
223
|
link.href = "#";
|
|
197
|
-
link.textContent =
|
|
224
|
+
link.textContent = text$1;
|
|
198
225
|
applyAlignment(link, elem.align);
|
|
199
|
-
|
|
226
|
+
let hasNavigation$1 = actions$1.some(isNavigationAction);
|
|
227
|
+
if (hasNavigation$1) {
|
|
228
|
+
if (onAction !== undefined) {
|
|
229
|
+
link.addEventListener("click", event => {
|
|
230
|
+
event.preventDefault();
|
|
231
|
+
let action = actions$1[0];
|
|
232
|
+
if (action !== undefined) {
|
|
233
|
+
return onAction(action);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
} else if (onDeadEnd !== undefined) {
|
|
200
238
|
link.addEventListener("click", event => {
|
|
201
239
|
event.preventDefault();
|
|
202
|
-
|
|
203
|
-
if (action !== undefined) {
|
|
204
|
-
return onAction(action);
|
|
205
|
-
}
|
|
240
|
+
onDeadEnd(id$1, text$1, "link");
|
|
206
241
|
});
|
|
207
242
|
}
|
|
208
243
|
return Primitive_option.some(link);
|
|
@@ -239,7 +274,7 @@ function renderElement(_elem, onAction) {
|
|
|
239
274
|
row.className = "wf-row";
|
|
240
275
|
applyAlignment(row, elem.align);
|
|
241
276
|
elem.children.forEach(child => {
|
|
242
|
-
let el = renderElement(child, onAction);
|
|
277
|
+
let el = renderElement(child, onAction, onDeadEnd);
|
|
243
278
|
if (el !== undefined) {
|
|
244
279
|
row.appendChild(Primitive_option.valFromOption(el));
|
|
245
280
|
return;
|
|
@@ -255,7 +290,7 @@ function renderElement(_elem, onAction) {
|
|
|
255
290
|
let contentEl = document.createElement("div");
|
|
256
291
|
contentEl.className = "wf-section-content";
|
|
257
292
|
elem.children.forEach(child => {
|
|
258
|
-
let el = renderElement(child, onAction);
|
|
293
|
+
let el = renderElement(child, onAction, onDeadEnd);
|
|
259
294
|
if (el !== undefined) {
|
|
260
295
|
contentEl.appendChild(Primitive_option.valFromOption(el));
|
|
261
296
|
return;
|
|
@@ -268,12 +303,12 @@ function renderElement(_elem, onAction) {
|
|
|
268
303
|
};
|
|
269
304
|
}
|
|
270
305
|
|
|
271
|
-
function renderScene(scene, onAction) {
|
|
306
|
+
function renderScene(scene, onAction, onDeadEnd) {
|
|
272
307
|
let sceneEl = document.createElement("div");
|
|
273
308
|
sceneEl.className = "wf-scene";
|
|
274
309
|
sceneEl.dataset["scene"] = scene.id;
|
|
275
310
|
scene.elements.forEach(elem => {
|
|
276
|
-
let el = renderElement(elem, onAction);
|
|
311
|
+
let el = renderElement(elem, onAction, onDeadEnd);
|
|
277
312
|
if (el !== undefined) {
|
|
278
313
|
sceneEl.appendChild(Primitive_option.valFromOption(el));
|
|
279
314
|
return;
|
|
@@ -465,7 +500,14 @@ function render(ast, options) {
|
|
|
465
500
|
};
|
|
466
501
|
let sceneMap = new Map();
|
|
467
502
|
ast.scenes.forEach(scene => {
|
|
468
|
-
let
|
|
503
|
+
let callback = opts.onDeadEndClick;
|
|
504
|
+
let handleDeadEnd = callback !== undefined ? (elementId, elementText, elementType) => callback({
|
|
505
|
+
sceneId: scene.id,
|
|
506
|
+
elementId: elementId,
|
|
507
|
+
elementText: elementText,
|
|
508
|
+
elementType: elementType
|
|
509
|
+
}) : undefined;
|
|
510
|
+
let sceneEl = renderScene(scene, handleAction, handleDeadEnd);
|
|
469
511
|
app.appendChild(sceneEl);
|
|
470
512
|
sceneMap.set(scene.id, sceneEl);
|
|
471
513
|
});
|
|
@@ -538,6 +580,8 @@ export {
|
|
|
538
580
|
alignmentToClass,
|
|
539
581
|
applyAlignment,
|
|
540
582
|
deviceTypeToClass,
|
|
583
|
+
isNavigationAction,
|
|
584
|
+
hasNavigationAction,
|
|
541
585
|
renderElement,
|
|
542
586
|
renderScene,
|
|
543
587
|
createSceneManager,
|
|
@@ -56,6 +56,24 @@ module DomBindings = {
|
|
|
56
56
|
*/
|
|
57
57
|
type onSceneChangeCallback = (option<string>, string) => unit
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Dead end click info type.
|
|
61
|
+
* Contains information about the clicked element that has no navigation target.
|
|
62
|
+
*/
|
|
63
|
+
type deadEndClickInfo = {
|
|
64
|
+
sceneId: string,
|
|
65
|
+
elementId: string,
|
|
66
|
+
elementText: string,
|
|
67
|
+
elementType: [#button | #link],
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Dead end click callback type.
|
|
72
|
+
* Called when a button or link without navigation target is clicked.
|
|
73
|
+
* @param info Information about the clicked element and current scene
|
|
74
|
+
*/
|
|
75
|
+
type onDeadEndClickCallback = deadEndClickInfo => unit
|
|
76
|
+
|
|
59
77
|
/**
|
|
60
78
|
* Configuration for the rendering process.
|
|
61
79
|
*/
|
|
@@ -65,6 +83,7 @@ type renderOptions = {
|
|
|
65
83
|
injectStyles: bool,
|
|
66
84
|
containerClass: option<string>,
|
|
67
85
|
onSceneChange: option<onSceneChangeCallback>,
|
|
86
|
+
onDeadEndClick: option<onDeadEndClickCallback>,
|
|
68
87
|
device: option<deviceType>,
|
|
69
88
|
}
|
|
70
89
|
|
|
@@ -77,6 +96,7 @@ let defaultOptions: renderOptions = {
|
|
|
77
96
|
injectStyles: true,
|
|
78
97
|
containerClass: None,
|
|
79
98
|
onSceneChange: None,
|
|
99
|
+
onDeadEndClick: None,
|
|
80
100
|
device: None,
|
|
81
101
|
}
|
|
82
102
|
|
|
@@ -230,17 +250,44 @@ let deviceTypeToClass = (device: deviceType): string => {
|
|
|
230
250
|
*/
|
|
231
251
|
type actionHandler = interactionAction => unit
|
|
232
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Dead end handler function type - called when an element without navigation is clicked.
|
|
255
|
+
* Receives element ID, text, and type.
|
|
256
|
+
*/
|
|
257
|
+
type deadEndHandler = (string, string, [#button | #link]) => unit
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Check if an action is a navigation action (Goto, Back, Forward).
|
|
261
|
+
*/
|
|
262
|
+
let isNavigationAction = (action: interactionAction): bool => {
|
|
263
|
+
switch action {
|
|
264
|
+
| Goto(_) | Back | Forward => true
|
|
265
|
+
| Validate(_) | Call(_) => false
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Check if actions array has any navigation actions.
|
|
271
|
+
*/
|
|
272
|
+
let hasNavigationAction = (actions: array<interactionAction>): bool => {
|
|
273
|
+
actions->Array.some(isNavigationAction)
|
|
274
|
+
}
|
|
275
|
+
|
|
233
276
|
// ============================================================================
|
|
234
277
|
// Element Rendering
|
|
235
278
|
// ============================================================================
|
|
236
279
|
|
|
237
|
-
let rec renderElement = (
|
|
280
|
+
let rec renderElement = (
|
|
281
|
+
elem: element,
|
|
282
|
+
~onAction: option<actionHandler>=?,
|
|
283
|
+
~onDeadEnd: option<deadEndHandler>=?,
|
|
284
|
+
): option<DomBindings.element> => {
|
|
238
285
|
// Handle input-only boxes by rendering children directly in a wrapper
|
|
239
286
|
if isInputOnlyBox(elem) {
|
|
240
287
|
let inputs = getInputsFromBox(elem)
|
|
241
288
|
// If only one input, render it directly
|
|
242
289
|
switch inputs->Array.get(0) {
|
|
243
|
-
| Some(input) => renderElement(input, ~onAction?)
|
|
290
|
+
| Some(input) => renderElement(input, ~onAction?, ~onDeadEnd?)
|
|
244
291
|
| None => None
|
|
245
292
|
}
|
|
246
293
|
} else {
|
|
@@ -258,7 +305,7 @@ let rec renderElement = (elem: element, ~onAction: option<actionHandler>=?): opt
|
|
|
258
305
|
}
|
|
259
306
|
|
|
260
307
|
children->Array.forEach(child => {
|
|
261
|
-
switch renderElement(child, ~onAction?) {
|
|
308
|
+
switch renderElement(child, ~onAction?, ~onDeadEnd?) {
|
|
262
309
|
| Some(el) => div->DomBindings.appendChild(el)
|
|
263
310
|
| None => ()
|
|
264
311
|
}
|
|
@@ -274,8 +321,11 @@ let rec renderElement = (elem: element, ~onAction: option<actionHandler>=?): opt
|
|
|
274
321
|
btn->DomBindings.setTextContent(text)
|
|
275
322
|
applyAlignment(btn, align)
|
|
276
323
|
|
|
277
|
-
//
|
|
278
|
-
|
|
324
|
+
// Check if button has navigation actions
|
|
325
|
+
let hasNavigation = hasNavigationAction(actions)
|
|
326
|
+
|
|
327
|
+
if hasNavigation {
|
|
328
|
+
// Attach click handler for navigation actions
|
|
279
329
|
switch onAction {
|
|
280
330
|
| Some(handler) => {
|
|
281
331
|
btn->DomBindings.addEventListener("click", _event => {
|
|
@@ -288,6 +338,16 @@ let rec renderElement = (elem: element, ~onAction: option<actionHandler>=?): opt
|
|
|
288
338
|
}
|
|
289
339
|
| None => ()
|
|
290
340
|
}
|
|
341
|
+
} else {
|
|
342
|
+
// No navigation - call dead end handler
|
|
343
|
+
switch onDeadEnd {
|
|
344
|
+
| Some(handler) => {
|
|
345
|
+
btn->DomBindings.addEventListener("click", _event => {
|
|
346
|
+
handler(id, text, #button)
|
|
347
|
+
})
|
|
348
|
+
}
|
|
349
|
+
| None => ()
|
|
350
|
+
}
|
|
291
351
|
}
|
|
292
352
|
|
|
293
353
|
Some(btn)
|
|
@@ -312,8 +372,11 @@ let rec renderElement = (elem: element, ~onAction: option<actionHandler>=?): opt
|
|
|
312
372
|
link->DomBindings.setTextContent(text)
|
|
313
373
|
applyAlignment(link, align)
|
|
314
374
|
|
|
315
|
-
//
|
|
316
|
-
|
|
375
|
+
// Check if link has navigation actions
|
|
376
|
+
let hasNavigation = hasNavigationAction(actions)
|
|
377
|
+
|
|
378
|
+
if hasNavigation {
|
|
379
|
+
// Attach click handler for navigation actions
|
|
317
380
|
switch onAction {
|
|
318
381
|
| Some(handler) => {
|
|
319
382
|
link->DomBindings.addEventListener("click", event => {
|
|
@@ -327,6 +390,17 @@ let rec renderElement = (elem: element, ~onAction: option<actionHandler>=?): opt
|
|
|
327
390
|
}
|
|
328
391
|
| None => ()
|
|
329
392
|
}
|
|
393
|
+
} else {
|
|
394
|
+
// No navigation - call dead end handler
|
|
395
|
+
switch onDeadEnd {
|
|
396
|
+
| Some(handler) => {
|
|
397
|
+
link->DomBindings.addEventListener("click", event => {
|
|
398
|
+
DomBindings.preventDefault(event)
|
|
399
|
+
handler(id, text, #link)
|
|
400
|
+
})
|
|
401
|
+
}
|
|
402
|
+
| None => ()
|
|
403
|
+
}
|
|
330
404
|
}
|
|
331
405
|
|
|
332
406
|
Some(link)
|
|
@@ -376,7 +450,7 @@ let rec renderElement = (elem: element, ~onAction: option<actionHandler>=?): opt
|
|
|
376
450
|
applyAlignment(row, align)
|
|
377
451
|
|
|
378
452
|
children->Array.forEach(child => {
|
|
379
|
-
switch renderElement(child, ~onAction?) {
|
|
453
|
+
switch renderElement(child, ~onAction?, ~onDeadEnd?) {
|
|
380
454
|
| Some(el) => row->DomBindings.appendChild(el)
|
|
381
455
|
| None => ()
|
|
382
456
|
}
|
|
@@ -397,7 +471,7 @@ let rec renderElement = (elem: element, ~onAction: option<actionHandler>=?): opt
|
|
|
397
471
|
contentEl->DomBindings.setClassName("wf-section-content")
|
|
398
472
|
|
|
399
473
|
children->Array.forEach(child => {
|
|
400
|
-
switch renderElement(child, ~onAction?) {
|
|
474
|
+
switch renderElement(child, ~onAction?, ~onDeadEnd?) {
|
|
401
475
|
| Some(el) => contentEl->DomBindings.appendChild(el)
|
|
402
476
|
| None => ()
|
|
403
477
|
}
|
|
@@ -411,13 +485,17 @@ let rec renderElement = (elem: element, ~onAction: option<actionHandler>=?): opt
|
|
|
411
485
|
}
|
|
412
486
|
}
|
|
413
487
|
|
|
414
|
-
let renderScene = (
|
|
488
|
+
let renderScene = (
|
|
489
|
+
scene: scene,
|
|
490
|
+
~onAction: option<actionHandler>=?,
|
|
491
|
+
~onDeadEnd: option<deadEndHandler>=?,
|
|
492
|
+
): DomBindings.element => {
|
|
415
493
|
let sceneEl = DomBindings.document->DomBindings.createElement("div")
|
|
416
494
|
sceneEl->DomBindings.setClassName("wf-scene")
|
|
417
495
|
sceneEl->DomBindings.dataset->DomBindings.setDataAttr("scene", scene.id)
|
|
418
496
|
|
|
419
497
|
scene.elements->Array.forEach(elem => {
|
|
420
|
-
switch renderElement(elem, ~onAction?) {
|
|
498
|
+
switch renderElement(elem, ~onAction?, ~onDeadEnd?) {
|
|
421
499
|
| Some(el) => sceneEl->DomBindings.appendChild(el)
|
|
422
500
|
| None => ()
|
|
423
501
|
}
|
|
@@ -670,7 +748,21 @@ let render = (ast: ast, options: option<renderOptions>): renderResult => {
|
|
|
670
748
|
let sceneMap = Map.make()
|
|
671
749
|
|
|
672
750
|
ast.scenes->Array.forEach(scene => {
|
|
673
|
-
|
|
751
|
+
// Create a scene-specific dead end handler that includes the scene ID
|
|
752
|
+
let handleDeadEnd = switch opts.onDeadEndClick {
|
|
753
|
+
| Some(callback) =>
|
|
754
|
+
Some((elementId: string, elementText: string, elementType: [#button | #link]) => {
|
|
755
|
+
callback({
|
|
756
|
+
sceneId: scene.id,
|
|
757
|
+
elementId,
|
|
758
|
+
elementText,
|
|
759
|
+
elementType,
|
|
760
|
+
})
|
|
761
|
+
})
|
|
762
|
+
| None => None
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
let sceneEl = renderScene(scene, ~onAction=handleAction, ~onDeadEnd=?handleDeadEnd)
|
|
674
766
|
app->DomBindings.appendChild(sceneEl)
|
|
675
767
|
sceneMap->Map.set(scene.id, sceneEl)
|
|
676
768
|
})
|