@farm.js/islx 0.0.0-stage → 0.0.2
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/CHANGELOG.md +55 -0
- package/LICENSE +21 -0
- package/README.md +198 -2
- package/SPEC.md +220 -0
- package/dist/_chunks/actions-2sTj0A2T.mjs +306 -0
- package/dist/_chunks/actions-B8dJ8f7L.mjs +615 -0
- package/dist/_chunks/actions-BCtIGvrP.d.mts +58 -0
- package/dist/_chunks/actions-Eutk6UzT.mjs +326 -0
- package/dist/_chunks/build-COIcs7wm.mjs +9 -0
- package/dist/_chunks/codec-CK8kZp4u.d.mts +64 -0
- package/dist/_chunks/codec-DqfVYKsx.mjs +279 -0
- package/dist/_chunks/handle-C0N9ZyNV.d.mts +35 -0
- package/dist/_chunks/hooks-3VZV8f_d.d.mts +30 -0
- package/dist/_chunks/hooks-De0gNpOn.d.mts +64 -0
- package/dist/_chunks/prehydration-B1GLTLdk.mjs +36 -0
- package/dist/_chunks/schedule-BFoGKRrU.mjs +216 -0
- package/dist/_chunks/schedule-Dw14dW0H.d.mts +17 -0
- package/dist/_chunks/tree-BDB5VEao.mjs +777 -0
- package/dist/_chunks/tree-fID6sz25.d.mts +102 -0
- package/dist/_chunks/value-store-Dm-7xJ6W.d.mts +19 -0
- package/dist/_chunks/wire-C3Rpbwl6.d.mts +98 -0
- package/dist/_chunks/wire-CXOodE06.mjs +121 -0
- package/dist/client.d.mts +58 -0
- package/dist/client.mjs +197 -0
- package/dist/compiler.d.mts +2 -0
- package/dist/compiler.mjs +1 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +3 -0
- package/dist/react/client.d.mts +49 -0
- package/dist/react/client.mjs +456 -0
- package/dist/react/server.d.mts +51 -0
- package/dist/react/server.mjs +84 -0
- package/dist/react.d.mts +3 -0
- package/dist/react.mjs +1 -0
- package/dist/server.d.mts +200 -0
- package/dist/server.mjs +378 -0
- package/dist/vite.d.mts +57 -0
- package/dist/vite.mjs +497 -0
- package/package.json +121 -4
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** React's form state: the result, the hook it belongs to, the action id, its bound count. */
|
|
2
|
+
type ReactFormState = [state: unknown, key: string, referenceId: string, boundCount: number];
|
|
3
|
+
interface ActionErrorOutcome {
|
|
4
|
+
/** Send the browser here: a 303 for forms, `{"redirect"}` for calls. */
|
|
5
|
+
redirect?: string;
|
|
6
|
+
/** Answer with this status (and no details). */
|
|
7
|
+
status?: number;
|
|
8
|
+
}
|
|
9
|
+
interface HandleActionOptions {
|
|
10
|
+
/** Origins or hosts, besides the request's own, that may post actions: `*.example.com`. */
|
|
11
|
+
allowedOrigins?: readonly string[];
|
|
12
|
+
/** The largest body accepted, in bytes. Default 1 MB. */
|
|
13
|
+
bodySizeLimit?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Runs the action, so the framework can give it the request's context (cookies, headers,
|
|
16
|
+
* an abort signal).
|
|
17
|
+
*/
|
|
18
|
+
run?<T>(request: Request, action: () => Promise<T>): Promise<T>;
|
|
19
|
+
/**
|
|
20
|
+
* Maps an error the action threw to a redirect or a status: the framework's `redirect()` and
|
|
21
|
+
* `notFound()`. Anything unmapped is a 500, logged with `onError`.
|
|
22
|
+
*/
|
|
23
|
+
mapError?(error: unknown): ActionErrorOutcome | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Renders the page after a form posted from a `useActionState` hook, with the action's result
|
|
26
|
+
* as React's `formState` (`renderToReadableStream(…, { formState })`, and the same to
|
|
27
|
+
* `hydrateRoot`). Without it, the form redirects back and the result is not shown.
|
|
28
|
+
*/
|
|
29
|
+
renderFormState?(formState: ReactFormState, request: Request): Response | Promise<Response>;
|
|
30
|
+
/** Reports an action that failed. Default `console.error`. */
|
|
31
|
+
onError?(error: unknown): void;
|
|
32
|
+
}
|
|
33
|
+
/** Runs the server action a request posts, or returns null when it posts none. */
|
|
34
|
+
declare function handleAction(request: Request, options?: HandleActionOptions): Promise<Response | null>;
|
|
35
|
+
export { handleAction as i, HandleActionOptions as n, ReactFormState as r, ActionErrorOutcome as t };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { t as ActionErrorOutcome } from "./handle-C0N9ZyNV.mjs";
|
|
2
|
+
|
|
3
|
+
// src/server/hooks.d.ts
|
|
4
|
+
|
|
5
|
+
interface ActionInfo {
|
|
6
|
+
request: Request;
|
|
7
|
+
/** The action's id. */
|
|
8
|
+
id: string;
|
|
9
|
+
/** Its export name, or the inline function's name. */
|
|
10
|
+
name: string;
|
|
11
|
+
/** Its module, from the app root. */
|
|
12
|
+
module: string;
|
|
13
|
+
}
|
|
14
|
+
interface ServerHooks {
|
|
15
|
+
/**
|
|
16
|
+
* Wraps every action: check the caller, rate-limit, log, time. Call `run` to run the action,
|
|
17
|
+
* and return what it returns; throw to refuse (`actionError` maps the error to a response).
|
|
18
|
+
*/
|
|
19
|
+
action?(info: ActionInfo, run: () => Promise<unknown>): Promise<unknown>;
|
|
20
|
+
/** Maps an error an action threw to a redirect or a status: a framework's `redirect()`. */
|
|
21
|
+
actionError?(error: unknown, info: ActionInfo): ActionErrorOutcome | undefined;
|
|
22
|
+
/** Reports an action that failed with an error nothing mapped. */
|
|
23
|
+
error?(error: unknown, info: ActionInfo): void;
|
|
24
|
+
}
|
|
25
|
+
/** Types an app's server hooks. */
|
|
26
|
+
declare function defineServerHooks(hooks: ServerHooks): ServerHooks;
|
|
27
|
+
/** Installs the app's hooks; the plugin's server runtime calls it. */
|
|
28
|
+
declare function setServerHooks(hooks: ServerHooks | undefined): void;
|
|
29
|
+
declare function getServerHooks(): ServerHooks;
|
|
30
|
+
export { setServerHooks as a, getServerHooks as i, ServerHooks as n, defineServerHooks as r, ActionInfo as t };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { n as IslandStrategy } from "./schedule-Dw14dW0H.mjs";
|
|
2
|
+
|
|
3
|
+
// src/client/hooks.d.ts
|
|
4
|
+
|
|
5
|
+
interface ClientHookEvents {
|
|
6
|
+
/** Before the next page is fetched. `preventDefault()` stays on this page. */
|
|
7
|
+
navigate: {
|
|
8
|
+
url: URL;
|
|
9
|
+
from: URL;
|
|
10
|
+
kind: "link" | "history" | "code";
|
|
11
|
+
};
|
|
12
|
+
/** The next page is in place. */
|
|
13
|
+
navigated: {
|
|
14
|
+
url: URL;
|
|
15
|
+
duration: number;
|
|
16
|
+
};
|
|
17
|
+
/** A navigation fell back to loading the page the ordinary way. */
|
|
18
|
+
navigateError: {
|
|
19
|
+
url: URL;
|
|
20
|
+
reason: string;
|
|
21
|
+
};
|
|
22
|
+
/** An island became interactive: `duration` runs from scheduling to React's first commit. */
|
|
23
|
+
hydrate: {
|
|
24
|
+
element: Element;
|
|
25
|
+
src: string;
|
|
26
|
+
export: string;
|
|
27
|
+
strategy: IslandStrategy;
|
|
28
|
+
duration: number;
|
|
29
|
+
};
|
|
30
|
+
/** An island failed; its server HTML was restored. */
|
|
31
|
+
islandError: {
|
|
32
|
+
element: Element;
|
|
33
|
+
src: string;
|
|
34
|
+
export: string;
|
|
35
|
+
error: unknown;
|
|
36
|
+
};
|
|
37
|
+
/** A server action call starts. `name` is the export (or inline function) name when known. */
|
|
38
|
+
action: {
|
|
39
|
+
id: string;
|
|
40
|
+
name?: string;
|
|
41
|
+
};
|
|
42
|
+
/** A server action call finished. */
|
|
43
|
+
actionEnd: {
|
|
44
|
+
id: string;
|
|
45
|
+
name?: string;
|
|
46
|
+
ok: boolean;
|
|
47
|
+
duration: number;
|
|
48
|
+
redirect?: string;
|
|
49
|
+
error?: unknown;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
type ClientHookEvent<Name extends keyof ClientHookEvents> = ClientHookEvents[Name] & {
|
|
53
|
+
/** Only `navigate` can be cancelled. */
|
|
54
|
+
preventDefault(): void;
|
|
55
|
+
readonly defaultPrevented: boolean;
|
|
56
|
+
};
|
|
57
|
+
type ClientHooks = { [Name in keyof ClientHookEvents]?: (event: ClientHookEvent<Name>) => void };
|
|
58
|
+
/** Types an app's hooks module. */
|
|
59
|
+
declare function defineClientHooks(hooks: ClientHooks): ClientHooks;
|
|
60
|
+
/** Installs the app's hooks; the plugin's browser entry calls it. */
|
|
61
|
+
declare function setClientHooks(hooks: ClientHooks | undefined): void;
|
|
62
|
+
/** The DOM event name of a hook: `navigateError` → `f:navigate-error`. */
|
|
63
|
+
declare function hookEventName(name: keyof ClientHookEvents): string;
|
|
64
|
+
export { hookEventName as a, defineClientHooks as i, ClientHookEvents as n, setClientHooks as o, ClientHooks as r, ClientHookEvent as t };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// src/client/prehydration.ts
|
|
2
|
+
/**
|
|
3
|
+
* Holding interactions that happen before the islx script has run. An inline script in the page
|
|
4
|
+
* captures a click or a submit on an island that has not hydrated (it is prevented and queued),
|
|
5
|
+
* and announces a pointerdown or a focus (which go through). The runtime then hydrates those
|
|
6
|
+
* islands first and replays what was held.
|
|
7
|
+
*
|
|
8
|
+
* The script is inlined into every page, so it stays hand-minified and imports nothing.
|
|
9
|
+
*/
|
|
10
|
+
/** Where the inline script queues held interactions: `{ target, kind }`. */
|
|
11
|
+
const PENDING_INTERACTIONS = "__islxPending";
|
|
12
|
+
/** The DOM event the script dispatches for each interaction it sees. */
|
|
13
|
+
const INTERACTION_EVENT = "f:island-interaction";
|
|
14
|
+
/** Elements whose click is held and replayed rather than merely observed. */
|
|
15
|
+
const REPLAYABLE_SELECTOR = "button,[role=\"button\"],input[type=\"button\"],input[type=\"submit\"],input[type=\"reset\"]";
|
|
16
|
+
/** The inline script's source (no `<script>` tag): pass it to the page's head or bootstrap. */
|
|
17
|
+
const PREHYDRATION_SCRIPT = `(function(){if(window.${PENDING_INTERACTIONS})return;var q=window.${PENDING_INTERACTIONS}=[];function island(n){var i=n&&n.closest&&n.closest("f-island");return i&&!i.hasAttribute("hydrated")?i:null}function tell(t,k){document.dispatchEvent(new CustomEvent("${INTERACTION_EVENT}",{detail:{target:t,kind:k}}))}function hold(e,t,k){if(!q.some(function(p){return p.target===t})){q.push({target:t,kind:k});tell(t,k)}e.preventDefault();e.stopImmediatePropagation()}document.addEventListener("click",function(e){if(e.defaultPrevented||e.button!==0||e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)return;var t=e.target&&e.target.closest&&e.target.closest(${JSON.stringify(REPLAYABLE_SELECTOR)});if(!t||t.closest("a[href]")||!island(t))return;hold(e,t,"click")},true);document.addEventListener("submit",function(e){var f=e.target;if(e.defaultPrevented||!f||f.nodeName!=="FORM"||!island(f))return;hold(e,f,"submit")},true);function seen(e){if(island(e.target))tell(e.target,null)}document.addEventListener("pointerdown",seen,true);document.addEventListener("focusin",seen,true)})();`;
|
|
18
|
+
/** Takes the interactions the inline script held inside `container`, oldest first. */
|
|
19
|
+
function takePendingInteractions(container) {
|
|
20
|
+
const queue = globalThis[PENDING_INTERACTIONS];
|
|
21
|
+
if (!Array.isArray(queue)) return [];
|
|
22
|
+
const taken = [];
|
|
23
|
+
for (let index = queue.length - 1; index >= 0; index--) {
|
|
24
|
+
const item = queue[index];
|
|
25
|
+
if (!(item?.target instanceof Element) || !container.contains(item.target)) continue;
|
|
26
|
+
queue.splice(index, 1);
|
|
27
|
+
taken.unshift(item);
|
|
28
|
+
}
|
|
29
|
+
return taken;
|
|
30
|
+
}
|
|
31
|
+
/** Whether the inline script holds an interaction inside `container`. */
|
|
32
|
+
function hasPendingInteraction(container) {
|
|
33
|
+
const queue = globalThis[PENDING_INTERACTIONS];
|
|
34
|
+
return Array.isArray(queue) && queue.some((item) => item?.target instanceof Element && container.contains(item.target));
|
|
35
|
+
}
|
|
36
|
+
export { hasPendingInteraction as a, REPLAYABLE_SELECTOR as i, PENDING_INTERACTIONS as n, takePendingInteractions as o, PREHYDRATION_SCRIPT as r, INTERACTION_EVENT as t };
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { a as hasPendingInteraction, o as takePendingInteractions, t as INTERACTION_EVENT } from "./prehydration-B1GLTLdk.mjs";
|
|
2
|
+
import { m as HYDRATED_ATTRIBUTE } from "./wire-CXOodE06.mjs";
|
|
3
|
+
// src/client/schedule.ts
|
|
4
|
+
/**
|
|
5
|
+
* When an island hydrates. Renderer-neutral: every adapter schedules through this.
|
|
6
|
+
*
|
|
7
|
+
* - `load`: as soon as the runtime starts (the default).
|
|
8
|
+
* - `idle`: when the browser is idle, or after 2 seconds.
|
|
9
|
+
* - `visible`: when the island comes within 200px of the viewport.
|
|
10
|
+
* - `interaction`: on the first click, submit, pointerdown or focus inside it. A click or a
|
|
11
|
+
* submit is held while the island's code loads, then replayed on the hydrated island, so the
|
|
12
|
+
* user's first action is not lost.
|
|
13
|
+
*
|
|
14
|
+
* Until it hydrates, an island is its server HTML: links and native forms keep working.
|
|
15
|
+
*/
|
|
16
|
+
const ISLAND_STRATEGIES = [
|
|
17
|
+
"load",
|
|
18
|
+
"idle",
|
|
19
|
+
"visible",
|
|
20
|
+
"interaction"
|
|
21
|
+
];
|
|
22
|
+
function isIslandStrategy(value) {
|
|
23
|
+
return ISLAND_STRATEGIES.includes(value);
|
|
24
|
+
}
|
|
25
|
+
/** Elements whose click is held and replayed rather than merely observed. */
|
|
26
|
+
const REPLAYABLE = "button,[role=\"button\"],input[type=\"button\"],input[type=\"submit\"],input[type=\"reset\"]";
|
|
27
|
+
function whenIdle(callback) {
|
|
28
|
+
const scope = window;
|
|
29
|
+
if (typeof scope.requestIdleCallback === "function") {
|
|
30
|
+
const handle = scope.requestIdleCallback(callback, { timeout: 2e3 });
|
|
31
|
+
return () => scope.cancelIdleCallback?.(handle);
|
|
32
|
+
}
|
|
33
|
+
const handle = window.setTimeout(callback, 1);
|
|
34
|
+
return () => window.clearTimeout(handle);
|
|
35
|
+
}
|
|
36
|
+
const visibility = /* @__PURE__ */ new WeakMap();
|
|
37
|
+
/** One observer per document, shared by every island; targets are released once they fire. */
|
|
38
|
+
function whenVisible(document, targets, start) {
|
|
39
|
+
let registry = visibility.get(document);
|
|
40
|
+
if (!registry) {
|
|
41
|
+
const subscriptions = /* @__PURE__ */ new Map();
|
|
42
|
+
registry = {
|
|
43
|
+
observer: new IntersectionObserver((entries) => {
|
|
44
|
+
const pending = /* @__PURE__ */ new Set();
|
|
45
|
+
for (const entry of entries) if (entry.isIntersecting) for (const callback of subscriptions.get(entry.target) ?? []) pending.add(callback);
|
|
46
|
+
for (const callback of pending) callback();
|
|
47
|
+
}, { rootMargin: "200px" }),
|
|
48
|
+
targets: subscriptions
|
|
49
|
+
};
|
|
50
|
+
visibility.set(document, registry);
|
|
51
|
+
}
|
|
52
|
+
const current = registry;
|
|
53
|
+
for (const target of targets) {
|
|
54
|
+
let callbacks = current.targets.get(target);
|
|
55
|
+
if (!callbacks) {
|
|
56
|
+
callbacks = /* @__PURE__ */ new Set();
|
|
57
|
+
current.targets.set(target, callbacks);
|
|
58
|
+
current.observer.observe(target);
|
|
59
|
+
}
|
|
60
|
+
callbacks.add(start);
|
|
61
|
+
}
|
|
62
|
+
let active = true;
|
|
63
|
+
return () => {
|
|
64
|
+
if (!active) return;
|
|
65
|
+
active = false;
|
|
66
|
+
for (const target of targets) {
|
|
67
|
+
const callbacks = current.targets.get(target);
|
|
68
|
+
callbacks?.delete(start);
|
|
69
|
+
if (callbacks?.size === 0) {
|
|
70
|
+
current.targets.delete(target);
|
|
71
|
+
current.observer.unobserve(target);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (current.targets.size === 0) {
|
|
75
|
+
current.observer.disconnect();
|
|
76
|
+
visibility.delete(document);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Replays a held interaction. `requestSubmit()` dispatches a real submit event, so handlers bound
|
|
82
|
+
* during hydration see it and validation runs; `form.submit()` would bypass both.
|
|
83
|
+
*/
|
|
84
|
+
function replay({ target, kind }) {
|
|
85
|
+
if (kind === "submit") {
|
|
86
|
+
const form = target;
|
|
87
|
+
if (typeof form.requestSubmit === "function") form.requestSubmit();
|
|
88
|
+
else target.dispatchEvent(new Event("submit", {
|
|
89
|
+
bubbles: true,
|
|
90
|
+
cancelable: true
|
|
91
|
+
}));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const clickable = target;
|
|
95
|
+
if (typeof clickable.click === "function") clickable.click();
|
|
96
|
+
else target.dispatchEvent(new MouseEvent("click", {
|
|
97
|
+
bubbles: true,
|
|
98
|
+
cancelable: true
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
function finish(container, held) {
|
|
102
|
+
const interactions = /* @__PURE__ */ new Map();
|
|
103
|
+
if (held) interactions.set(held.target, held);
|
|
104
|
+
for (const pending of takePendingInteractions(container)) if (pending.kind) interactions.set(pending.target, {
|
|
105
|
+
target: pending.target,
|
|
106
|
+
kind: pending.kind
|
|
107
|
+
});
|
|
108
|
+
if (!container.isConnected) return;
|
|
109
|
+
container.setAttribute(HYDRATED_ATTRIBUTE, "");
|
|
110
|
+
for (const interaction of interactions.values()) window.setTimeout(() => {
|
|
111
|
+
if (container.isConnected && interaction.target.isConnected && container.contains(interaction.target)) replay(interaction);
|
|
112
|
+
}, 0);
|
|
113
|
+
}
|
|
114
|
+
/** Runs `hydrate` when the island's strategy says; resolves with its result, or undefined if aborted. */
|
|
115
|
+
function scheduleHydration({ container, strategy, signal, hydrate }) {
|
|
116
|
+
const resolved = strategy ?? "load";
|
|
117
|
+
if (signal?.aborted) return Promise.resolve(void 0);
|
|
118
|
+
if (resolved === "load") return Promise.resolve().then(() => signal?.aborted ? void 0 : hydrate()).then((value) => {
|
|
119
|
+
if (signal?.aborted) return void 0;
|
|
120
|
+
finish(container, null);
|
|
121
|
+
return value;
|
|
122
|
+
});
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
let started = false;
|
|
125
|
+
let cancelled = false;
|
|
126
|
+
let held = null;
|
|
127
|
+
const triggers = /* @__PURE__ */ new Set();
|
|
128
|
+
const captures = /* @__PURE__ */ new Set();
|
|
129
|
+
let removeAbort = null;
|
|
130
|
+
const cleanupTriggers = () => {
|
|
131
|
+
for (const dispose of triggers) dispose();
|
|
132
|
+
triggers.clear();
|
|
133
|
+
};
|
|
134
|
+
const cleanup = () => {
|
|
135
|
+
cleanupTriggers();
|
|
136
|
+
for (const dispose of captures) dispose();
|
|
137
|
+
captures.clear();
|
|
138
|
+
removeAbort?.();
|
|
139
|
+
removeAbort = null;
|
|
140
|
+
};
|
|
141
|
+
const start = () => {
|
|
142
|
+
if (started || cancelled) return;
|
|
143
|
+
started = true;
|
|
144
|
+
cleanupTriggers();
|
|
145
|
+
Promise.resolve().then(hydrate).then((value) => {
|
|
146
|
+
cleanup();
|
|
147
|
+
if (cancelled || signal?.aborted) return;
|
|
148
|
+
finish(container, held);
|
|
149
|
+
resolve(value);
|
|
150
|
+
}, (error) => {
|
|
151
|
+
cleanup();
|
|
152
|
+
reject(error);
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
if (signal) {
|
|
156
|
+
const abort = () => {
|
|
157
|
+
cancelled = true;
|
|
158
|
+
cleanup();
|
|
159
|
+
resolve(void 0);
|
|
160
|
+
};
|
|
161
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
162
|
+
removeAbort = () => signal.removeEventListener("abort", abort);
|
|
163
|
+
}
|
|
164
|
+
if (hasPendingInteraction(container)) return start();
|
|
165
|
+
const onAnnounced = (event) => {
|
|
166
|
+
const detail = event.detail;
|
|
167
|
+
if (detail?.target instanceof Element && container.contains(detail.target)) start();
|
|
168
|
+
};
|
|
169
|
+
document.addEventListener(INTERACTION_EVENT, onAnnounced);
|
|
170
|
+
captures.add(() => document.removeEventListener(INTERACTION_EVENT, onAnnounced));
|
|
171
|
+
if (resolved === "idle") {
|
|
172
|
+
triggers.add(whenIdle(start));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (resolved === "visible") {
|
|
176
|
+
if (typeof IntersectionObserver !== "function") return start();
|
|
177
|
+
const targets = container.getClientRects().length > 0 ? [container] : Array.from(container.children);
|
|
178
|
+
if (targets.length === 0) return start();
|
|
179
|
+
triggers.add(whenVisible(container.ownerDocument, targets, start));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const hold = (event, target, kind) => {
|
|
183
|
+
held ??= {
|
|
184
|
+
target,
|
|
185
|
+
kind
|
|
186
|
+
};
|
|
187
|
+
event.preventDefault();
|
|
188
|
+
event.stopImmediatePropagation();
|
|
189
|
+
start();
|
|
190
|
+
};
|
|
191
|
+
const onClick = (event) => {
|
|
192
|
+
const mouse = event;
|
|
193
|
+
const target = event.target instanceof Element ? event.target.closest(REPLAYABLE) : null;
|
|
194
|
+
if (!target || !container.contains(target) || target.closest("a[href]") || mouse.button !== 0 || mouse.metaKey || mouse.altKey || mouse.ctrlKey || mouse.shiftKey) return;
|
|
195
|
+
hold(event, target, "click");
|
|
196
|
+
};
|
|
197
|
+
const onSubmit = (event) => {
|
|
198
|
+
const form = event.target;
|
|
199
|
+
if (!(form instanceof Element) || form.nodeName !== "FORM") return;
|
|
200
|
+
if (!container.contains(form) || event.defaultPrevented) return;
|
|
201
|
+
hold(event, form, "submit");
|
|
202
|
+
};
|
|
203
|
+
const onActivate = (event) => {
|
|
204
|
+
if (event.target instanceof Element && container.contains(event.target)) start();
|
|
205
|
+
};
|
|
206
|
+
const listen = (type, handler) => {
|
|
207
|
+
document.addEventListener(type, handler, true);
|
|
208
|
+
captures.add(() => document.removeEventListener(type, handler, true));
|
|
209
|
+
};
|
|
210
|
+
listen("click", onClick);
|
|
211
|
+
listen("submit", onSubmit);
|
|
212
|
+
listen("pointerdown", onActivate);
|
|
213
|
+
listen("focusin", onActivate);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
export { isIslandStrategy as n, scheduleHydration as r, ISLAND_STRATEGIES as t };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type IslandStrategy = "load" | "idle" | "visible" | "interaction";
|
|
2
|
+
declare const ISLAND_STRATEGIES: readonly IslandStrategy[];
|
|
3
|
+
declare function isIslandStrategy(value: unknown): value is IslandStrategy;
|
|
4
|
+
interface ScheduleOptions<T> {
|
|
5
|
+
container: Element;
|
|
6
|
+
strategy?: IslandStrategy | null;
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
hydrate(): T | Promise<T>;
|
|
9
|
+
}
|
|
10
|
+
/** Runs `hydrate` when the island's strategy says; resolves with its result, or undefined if aborted. */
|
|
11
|
+
declare function scheduleHydration<T>({
|
|
12
|
+
container,
|
|
13
|
+
strategy,
|
|
14
|
+
signal,
|
|
15
|
+
hydrate
|
|
16
|
+
}: ScheduleOptions<T>): Promise<T | undefined>;
|
|
17
|
+
export { scheduleHydration as a, isIslandStrategy as i, IslandStrategy as n, ScheduleOptions as r, ISLAND_STRATEGIES as t };
|