@hyperspan/framework 1.0.26 → 1.0.28
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
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
import { Idiomorph } from './idiomorph';
|
|
2
2
|
import { lazyLoadScripts } from './hyperspan-scripts.client';
|
|
3
|
+
import type { Hyperspan as HS } from '../../types';
|
|
4
|
+
|
|
5
|
+
const HS_ACTION_BEFORE_FETCH: HS.ActionEventName = 'hs:action:before-fetch';
|
|
6
|
+
const HS_ACTION_AFTER_FETCH: HS.ActionEventName = 'hs:action:after-fetch';
|
|
7
|
+
const HS_ACTION_BEFORE_SWAP: HS.ActionEventName = 'hs:action:before-swap';
|
|
8
|
+
const HS_ACTION_AFTER_SWAP: HS.ActionEventName = 'hs:action:after-swap';
|
|
9
|
+
const HS_ACTION_BEFORE_NAVIGATE: HS.ActionEventName = 'hs:action:before-navigate';
|
|
10
|
+
|
|
11
|
+
function dispatchActionEvent<T>(
|
|
12
|
+
target: EventTarget,
|
|
13
|
+
name: HS.ActionEventName,
|
|
14
|
+
detail: T,
|
|
15
|
+
cancelable = false
|
|
16
|
+
): boolean {
|
|
17
|
+
return target.dispatchEvent(
|
|
18
|
+
new CustomEvent(name, {
|
|
19
|
+
detail,
|
|
20
|
+
bubbles: true,
|
|
21
|
+
cancelable,
|
|
22
|
+
composed: true,
|
|
23
|
+
})
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** No box by default (`display: contents`) — style `:has(hs-action-loading)` for loading UI. */
|
|
28
|
+
function ensureActionLoadingStyles() {
|
|
29
|
+
if (document.getElementById('hs-action-loading-style')) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const style = document.createElement('style');
|
|
33
|
+
style.id = 'hs-action-loading-style';
|
|
34
|
+
style.textContent = 'hs-action-loading{display:contents}';
|
|
35
|
+
document.head.appendChild(style);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class HSActionLoading extends HTMLElement {
|
|
39
|
+
connectedCallback() {
|
|
40
|
+
ensureActionLoadingStyles();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!customElements.get('hs-action-loading')) {
|
|
45
|
+
customElements.define('hs-action-loading', HSActionLoading);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function setActionLoading(hsActionTag: HTMLElement | null, loading: boolean) {
|
|
49
|
+
if (!hsActionTag) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const existing = hsActionTag.querySelector('hs-action-loading');
|
|
53
|
+
if (loading) {
|
|
54
|
+
if (!existing) {
|
|
55
|
+
hsActionTag.appendChild(document.createElement('hs-action-loading'));
|
|
56
|
+
}
|
|
57
|
+
} else if (existing) {
|
|
58
|
+
existing.remove();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
3
61
|
|
|
4
62
|
const actionFormObserver = new MutationObserver((list) => {
|
|
5
63
|
list.forEach((mutation) => {
|
|
@@ -71,7 +129,24 @@ function formSubmitToRoute(e: Event, form: HTMLFormElement, opts: TFormSubmitOpt
|
|
|
71
129
|
}
|
|
72
130
|
}
|
|
73
131
|
|
|
74
|
-
const hsActionTag = form.closest('hs-action');
|
|
132
|
+
const hsActionTag = form.closest('hs-action') as HTMLElement | null;
|
|
133
|
+
const eventTarget: EventTarget = hsActionTag || document;
|
|
134
|
+
const fetchDetail: HS.ActionFetchDetail = {
|
|
135
|
+
form,
|
|
136
|
+
action: hsActionTag,
|
|
137
|
+
url: formUrl,
|
|
138
|
+
method,
|
|
139
|
+
loadingElement: true,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
if (!dispatchActionEvent(eventTarget, HS_ACTION_BEFORE_FETCH, fetchDetail, true)) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (fetchDetail.loadingElement) {
|
|
147
|
+
setActionLoading(hsActionTag, true);
|
|
148
|
+
}
|
|
149
|
+
|
|
75
150
|
const submitBtn = form.querySelector('button[type=submit],input[type=submit]');
|
|
76
151
|
if (submitBtn) {
|
|
77
152
|
submitBtn.setAttribute('disabled', 'disabled');
|
|
@@ -82,6 +157,18 @@ function formSubmitToRoute(e: Event, form: HTMLFormElement, opts: TFormSubmitOpt
|
|
|
82
157
|
if (isFullDocument) {
|
|
83
158
|
html = html.replace(/^[\s\uFEFF]*<!DOCTYPE[^>]*>/i, '');
|
|
84
159
|
}
|
|
160
|
+
|
|
161
|
+
const swapDetail: HS.ActionSwapDetail = {
|
|
162
|
+
form,
|
|
163
|
+
action: hsActionTag,
|
|
164
|
+
html,
|
|
165
|
+
fullDocument: isFullDocument,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
if (!dispatchActionEvent(eventTarget, HS_ACTION_BEFORE_SWAP, swapDetail, true)) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
85
172
|
const target = isFullDocument ? window.document : hsActionTag || form;
|
|
86
173
|
const options = isFullDocument ? undefined : { morphStyle: 'innerHTML' };
|
|
87
174
|
|
|
@@ -96,10 +183,42 @@ function formSubmitToRoute(e: Event, form: HTMLFormElement, opts: TFormSubmitOpt
|
|
|
96
183
|
|
|
97
184
|
activateScriptsIn(isFullDocument ? document.body : (target as ParentNode));
|
|
98
185
|
|
|
186
|
+
dispatchActionEvent(eventTarget, HS_ACTION_AFTER_SWAP, swapDetail);
|
|
187
|
+
|
|
99
188
|
opts.afterResponse && opts.afterResponse();
|
|
100
189
|
lazyLoadScripts();
|
|
101
190
|
}
|
|
102
191
|
|
|
192
|
+
async function navigateTo(url: string, preferHard: boolean) {
|
|
193
|
+
const navigateDetail: HS.ActionNavigateDetail = {
|
|
194
|
+
form,
|
|
195
|
+
action: hsActionTag,
|
|
196
|
+
url,
|
|
197
|
+
hard: preferHard,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
if (!dispatchActionEvent(eventTarget, HS_ACTION_BEFORE_NAVIGATE, navigateDetail, true)) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (navigateDetail.hard) {
|
|
205
|
+
window.location.assign(navigateDetail.url);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Soft morph requires same-origin (CORS / Idiomorph are not safe across domains).
|
|
210
|
+
const resolved = new URL(navigateDetail.url, window.location.href);
|
|
211
|
+
if (resolved.origin !== window.location.origin) {
|
|
212
|
+
window.location.assign(navigateDetail.url);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const pageRes = await fetch(navigateDetail.url, {
|
|
217
|
+
headers: { Accept: 'text/html' },
|
|
218
|
+
});
|
|
219
|
+
await consumeStreamingHtmlResponse(pageRes, applyResponseHtml);
|
|
220
|
+
}
|
|
221
|
+
|
|
103
222
|
fetch(formUrl, { body: formData, method, headers })
|
|
104
223
|
.then(async (res: Response) => {
|
|
105
224
|
// Look for special header that indicates a redirect.
|
|
@@ -109,21 +228,13 @@ function formSubmitToRoute(e: Event, form: HTMLFormElement, opts: TFormSubmitOpt
|
|
|
109
228
|
if (newUrl) {
|
|
110
229
|
const resolved = new URL(newUrl, window.location.href);
|
|
111
230
|
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
resolved.origin
|
|
116
|
-
resolved.pathname
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
headers: { Accept: 'text/html' },
|
|
120
|
-
});
|
|
121
|
-
await consumeStreamingHtmlResponse(pageRes, applyResponseHtml);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// If the new URL is different, we need to redirect the user to the new URL
|
|
126
|
-
window.location.assign(newUrl);
|
|
231
|
+
// Default: same-origin + same path → soft morph; anything else → hard navigation.
|
|
232
|
+
// Listeners can flip detail.hard on hs:action:before-navigate to override.
|
|
233
|
+
const preferHard =
|
|
234
|
+
resolved.origin !== window.location.origin ||
|
|
235
|
+
resolved.pathname !== window.location.pathname;
|
|
236
|
+
|
|
237
|
+
await navigateTo(newUrl, preferHard);
|
|
127
238
|
}
|
|
128
239
|
return;
|
|
129
240
|
}
|
|
@@ -132,22 +243,18 @@ function formSubmitToRoute(e: Event, form: HTMLFormElement, opts: TFormSubmitOpt
|
|
|
132
243
|
})
|
|
133
244
|
.catch((error) => {
|
|
134
245
|
console.error('[Hyperspan] Error submitting form action:', error);
|
|
246
|
+
})
|
|
247
|
+
.finally(() => {
|
|
248
|
+
setActionLoading(hsActionTag, false);
|
|
249
|
+
dispatchActionEvent(eventTarget, HS_ACTION_AFTER_FETCH, fetchDetail);
|
|
135
250
|
});
|
|
136
251
|
}
|
|
137
252
|
|
|
138
253
|
/** Streaming async chunks use template ids ending in `_content`. */
|
|
139
254
|
const STREAM_CHUNK_MARKER = /<template id="[^"]+_content">/;
|
|
140
255
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (!match || match.index === 0) {
|
|
144
|
-
return { initial: html, streamTail: null };
|
|
145
|
-
}
|
|
146
|
-
return {
|
|
147
|
-
initial: html.slice(0, match.index),
|
|
148
|
-
streamTail: html.slice(match.index),
|
|
149
|
-
};
|
|
150
|
-
}
|
|
256
|
+
/** Marks the end of a streaming chunk boundary. */
|
|
257
|
+
const CHUNK_END = '<!--/hs:chunk-->';
|
|
151
258
|
|
|
152
259
|
/** Clone a script node so the browser will execute it (preserves module type). */
|
|
153
260
|
function cloneScriptForExecution(script: HTMLScriptElement): HTMLScriptElement {
|
|
@@ -207,8 +314,18 @@ function appendHtmlToBody(html: string) {
|
|
|
207
314
|
}
|
|
208
315
|
|
|
209
316
|
/**
|
|
210
|
-
* Read a (possibly streaming) HTML response
|
|
211
|
-
*
|
|
317
|
+
* Read a (possibly streaming) HTML response. The stream is shaped as:
|
|
318
|
+
* [full initial page HTML, including <slot> placeholders]
|
|
319
|
+
* <template id="X_content">…<!--end--></template><script>…_hsc.push({id:'X'})…</script>
|
|
320
|
+
* …one <template>/<script> pair per async chunk…
|
|
321
|
+
*
|
|
322
|
+
* Network reads do NOT align with these logical boundaries, so we buffer instead of assuming
|
|
323
|
+
* the whole initial page (or a whole chunk) arrives in a single read:
|
|
324
|
+
* 1. Accumulate until the first stream-chunk <template> marker appears (or the stream ends),
|
|
325
|
+
* then Idiomorph the everything-before-it as the initial page HTML - exactly once.
|
|
326
|
+
* 2. After that, append only COMPLETE chunks (each terminated by the server's <!--/hs:chunk-->
|
|
327
|
+
* delimiter) to document.body so their scripts run and placeholders resolve. Any partial
|
|
328
|
+
* trailing chunk stays buffered until the rest of it arrives.
|
|
212
329
|
*/
|
|
213
330
|
async function consumeStreamingHtmlResponse(
|
|
214
331
|
res: Response,
|
|
@@ -225,34 +342,59 @@ async function consumeStreamingHtmlResponse(
|
|
|
225
342
|
|
|
226
343
|
const reader = body.getReader();
|
|
227
344
|
const decoder = new TextDecoder();
|
|
228
|
-
let
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
345
|
+
let buffer = '';
|
|
346
|
+
let initialApplied = false;
|
|
347
|
+
|
|
348
|
+
function pump(isFinal: boolean) {
|
|
349
|
+
// Phase 1: split off and morph the initial page HTML (everything before the first chunk).
|
|
350
|
+
if (!initialApplied) {
|
|
351
|
+
const match = STREAM_CHUNK_MARKER.exec(buffer);
|
|
352
|
+
if (match) {
|
|
353
|
+
const initial = buffer.slice(0, match.index);
|
|
354
|
+
buffer = buffer.slice(match.index);
|
|
355
|
+
initialApplied = true;
|
|
356
|
+
if (initial) {
|
|
357
|
+
applyInitialHtml(initial);
|
|
358
|
+
}
|
|
359
|
+
} else if (isFinal) {
|
|
360
|
+
// No stream chunks at all - the whole response is the page HTML.
|
|
361
|
+
initialApplied = true;
|
|
362
|
+
if (buffer) {
|
|
363
|
+
applyInitialHtml(buffer);
|
|
364
|
+
}
|
|
365
|
+
buffer = '';
|
|
366
|
+
return;
|
|
367
|
+
} else {
|
|
368
|
+
// Marker not here yet; keep buffering the (possibly large) initial HTML.
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
233
371
|
}
|
|
234
372
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
if (streamTail) {
|
|
242
|
-
appendHtmlToBody(streamTail);
|
|
373
|
+
// Phase 2: flush complete chunks; hold back any partial trailing chunk.
|
|
374
|
+
if (isFinal) {
|
|
375
|
+
if (buffer) {
|
|
376
|
+
appendHtmlToBody(buffer);
|
|
377
|
+
buffer = '';
|
|
243
378
|
}
|
|
244
379
|
return;
|
|
245
380
|
}
|
|
246
|
-
|
|
247
|
-
|
|
381
|
+
const lastEnd = buffer.lastIndexOf(CHUNK_END);
|
|
382
|
+
if (lastEnd === -1) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const flushEnd = lastEnd + CHUNK_END.length;
|
|
386
|
+
appendHtmlToBody(buffer.slice(0, flushEnd));
|
|
387
|
+
buffer = buffer.slice(flushEnd);
|
|
248
388
|
}
|
|
249
389
|
|
|
250
390
|
while (true) {
|
|
251
391
|
const { done, value } = await reader.read();
|
|
252
392
|
if (done) {
|
|
253
|
-
|
|
393
|
+
buffer += decoder.decode();
|
|
394
|
+
pump(true);
|
|
254
395
|
break;
|
|
255
396
|
}
|
|
256
|
-
|
|
397
|
+
buffer += decoder.decode(value, { stream: true });
|
|
398
|
+
pump(false);
|
|
257
399
|
}
|
|
258
400
|
}
|
|
@@ -33,7 +33,12 @@ async function waitForContent(
|
|
|
33
33
|
function renderStreamChunk(chunk: { id: string }) {
|
|
34
34
|
const slotId = chunk.id;
|
|
35
35
|
const slotEl = document.getElementById(slotId);
|
|
36
|
-
const templateEl = document.getElementById(`${slotId}_content`) as HTMLTemplateElement;
|
|
36
|
+
const templateEl = document.getElementById(`${slotId}_content`) as HTMLTemplateElement | null;
|
|
37
|
+
|
|
38
|
+
// Content template is gone (e.g. this chunk was already rendered and re-queued). Nothing to do.
|
|
39
|
+
if (!templateEl) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
37
42
|
|
|
38
43
|
if (slotEl) {
|
|
39
44
|
// Content AND slot are present - let's insert the content into the slot
|
|
@@ -72,6 +77,8 @@ declare global {
|
|
|
72
77
|
push: (e: { id: string }) => void;
|
|
73
78
|
forEach: (callback: (e: { id: string }) => void) => void;
|
|
74
79
|
};
|
|
80
|
+
_hscInit?: boolean;
|
|
81
|
+
_hscLoading?: boolean;
|
|
75
82
|
}
|
|
76
83
|
}
|
|
77
84
|
|
package/src/layout.ts
CHANGED
|
@@ -3,7 +3,9 @@ import { JS_IMPORT_MAP, buildClientJS } from './client/js';
|
|
|
3
3
|
import { CSS_PUBLIC_PATH, CSS_ROUTE_MAP } from './client/css';
|
|
4
4
|
import type { Hyperspan as HS } from './types';
|
|
5
5
|
|
|
6
|
-
const clientStreamingJS = await buildClientJS(
|
|
6
|
+
const clientStreamingJS = await buildClientJS(
|
|
7
|
+
import.meta.resolve('./client/_hs/hyperspan-streaming.client')
|
|
8
|
+
);
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Output the importmap for the client so we can use ESModules on the client to load JS files on demand
|
|
@@ -16,14 +18,18 @@ export function hyperspanScriptTags() {
|
|
|
16
18
|
<script id="hyperspan-streaming-script">
|
|
17
19
|
// [Hyperspan] Streaming - Load the client streaming JS module only when the first chunk is loaded
|
|
18
20
|
window._hsc = window._hsc || [];
|
|
19
|
-
window.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
if (!window._hscInit) {
|
|
22
|
+
window._hscInit = true;
|
|
23
|
+
window._hsc.push = function (e) {
|
|
24
|
+
Array.prototype.push.call(window._hsc, e);
|
|
25
|
+
if (!window._hscLoading) {
|
|
26
|
+
window._hscLoading = true;
|
|
27
|
+
const script = document.createElement('script');
|
|
28
|
+
script.src = '${clientStreamingJS.publicPath}';
|
|
29
|
+
document.body.appendChild(script);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
27
33
|
</script>
|
|
28
34
|
`;
|
|
29
35
|
}
|
|
@@ -36,10 +42,8 @@ export function hyperspanStyleTags(context: HS.Context) {
|
|
|
36
42
|
const cssImports = context.route.cssImports ?? CSS_ROUTE_MAP.get(context.route.path) ?? [];
|
|
37
43
|
|
|
38
44
|
for (const cssFile of cssImports) {
|
|
39
|
-
styleTags.push(html`
|
|
40
|
-
<link rel="stylesheet" href="${CSS_PUBLIC_PATH}/${cssFile}" />
|
|
41
|
-
`);
|
|
45
|
+
styleTags.push(html` <link rel="stylesheet" href="${CSS_PUBLIC_PATH}/${cssFile}" /> `);
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
return styleTags;
|
|
45
|
-
}
|
|
49
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -538,12 +538,14 @@ export async function returnHTMLResponse(
|
|
|
538
538
|
return new StreamResponse(
|
|
539
539
|
renderStream(routeContent as HSHtml, {
|
|
540
540
|
renderChunk: (chunk) => {
|
|
541
|
+
// Trailing <!--/hs:chunk--> marks the end of a streaming chunk boundary.
|
|
541
542
|
return html`
|
|
542
543
|
<template id="${chunk.id}_content">${html.raw(chunk.content)}<!--end--></template>
|
|
543
544
|
<script>
|
|
544
545
|
window._hsc = window._hsc || [];
|
|
545
546
|
window._hsc.push({ id: '${chunk.id}' });
|
|
546
547
|
</script>
|
|
548
|
+
<!--/hs:chunk-->
|
|
547
549
|
`;
|
|
548
550
|
},
|
|
549
551
|
}),
|
package/src/types.ts
CHANGED
|
@@ -299,6 +299,56 @@ export namespace Hyperspan {
|
|
|
299
299
|
fetch: (request: Request) => Promise<Response>;
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Client-side action lifecycle events (dispatched from `<hs-action>`, bubbles to document).
|
|
304
|
+
*
|
|
305
|
+
* - `hs:action:before-fetch` — before the action request starts (cancelable).
|
|
306
|
+
* - `hs:action:after-fetch` — after the action request finishes (success or error).
|
|
307
|
+
* - `hs:action:before-swap` — before HTML morph (cancelable). Close modals here.
|
|
308
|
+
* - `hs:action:after-swap` — after HTML morph.
|
|
309
|
+
* - `hs:action:before-navigate` — before redirect soft/hard navigation (cancelable).
|
|
310
|
+
* Set `detail.hard = true` for a full page load, or `false` to fetch+morph in place.
|
|
311
|
+
*/
|
|
312
|
+
export type ActionEventName =
|
|
313
|
+
| 'hs:action:before-fetch'
|
|
314
|
+
| 'hs:action:after-fetch'
|
|
315
|
+
| 'hs:action:before-swap'
|
|
316
|
+
| 'hs:action:after-swap'
|
|
317
|
+
| 'hs:action:before-navigate';
|
|
318
|
+
|
|
319
|
+
export type ActionFetchDetail = {
|
|
320
|
+
form: HTMLFormElement;
|
|
321
|
+
/** The current `<hs-action>` element, if present. */
|
|
322
|
+
action: HTMLElement | null;
|
|
323
|
+
url: string;
|
|
324
|
+
method: string;
|
|
325
|
+
/**
|
|
326
|
+
* Mutable. When `true` (default), appends `<hs-action-loading>` inside `<hs-action>` for the
|
|
327
|
+
* duration of the request. Set to `false` in `hs:action:before-fetch` to skip it.
|
|
328
|
+
*/
|
|
329
|
+
loadingElement: boolean;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
export type ActionSwapDetail = {
|
|
333
|
+
form: HTMLFormElement;
|
|
334
|
+
/** The current `<hs-action>` element, if present. */
|
|
335
|
+
action: HTMLElement | null;
|
|
336
|
+
html: string;
|
|
337
|
+
fullDocument: boolean;
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
export type ActionNavigateDetail = {
|
|
341
|
+
form: HTMLFormElement;
|
|
342
|
+
/** The current `<hs-action>` element, if present. */
|
|
343
|
+
action: HTMLElement | null;
|
|
344
|
+
url: string;
|
|
345
|
+
/**
|
|
346
|
+
* Mutable. Default is soft (false) for same-origin+same-path redirects, hard (true) otherwise.
|
|
347
|
+
* Set to `true` for `window.location.assign`, or `false` to fetch + morph in place.
|
|
348
|
+
*/
|
|
349
|
+
hard: boolean;
|
|
350
|
+
};
|
|
351
|
+
|
|
302
352
|
/**
|
|
303
353
|
* Client JS Module = ESM Module + Public Path + Render Script Tag
|
|
304
354
|
*/
|
|
@@ -324,3 +374,20 @@ export namespace Hyperspan {
|
|
|
324
374
|
formErrors: unknown[];
|
|
325
375
|
}
|
|
326
376
|
}
|
|
377
|
+
|
|
378
|
+
declare global {
|
|
379
|
+
interface DocumentEventMap {
|
|
380
|
+
'hs:action:before-fetch': CustomEvent<Hyperspan.ActionFetchDetail>;
|
|
381
|
+
'hs:action:after-fetch': CustomEvent<Hyperspan.ActionFetchDetail>;
|
|
382
|
+
'hs:action:before-swap': CustomEvent<Hyperspan.ActionSwapDetail>;
|
|
383
|
+
'hs:action:after-swap': CustomEvent<Hyperspan.ActionSwapDetail>;
|
|
384
|
+
'hs:action:before-navigate': CustomEvent<Hyperspan.ActionNavigateDetail>;
|
|
385
|
+
}
|
|
386
|
+
interface HTMLElementEventMap {
|
|
387
|
+
'hs:action:before-fetch': CustomEvent<Hyperspan.ActionFetchDetail>;
|
|
388
|
+
'hs:action:after-fetch': CustomEvent<Hyperspan.ActionFetchDetail>;
|
|
389
|
+
'hs:action:before-swap': CustomEvent<Hyperspan.ActionSwapDetail>;
|
|
390
|
+
'hs:action:after-swap': CustomEvent<Hyperspan.ActionSwapDetail>;
|
|
391
|
+
'hs:action:before-navigate': CustomEvent<Hyperspan.ActionNavigateDetail>;
|
|
392
|
+
}
|
|
393
|
+
}
|