foldkit-primitives 0.2.1 → 0.4.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/README.md +514 -43
- package/dist/device/index.d.mts +11 -11
- package/dist/device/index.d.mts.map +1 -1
- package/dist/device/index.mjs +95 -87
- package/dist/device/index.mjs.map +1 -1
- package/dist/dom/index.d.mts +71 -1
- package/dist/dom/index.d.mts.map +1 -1
- package/dist/dom/index.mjs +303 -5
- package/dist/dom/index.mjs.map +1 -1
- package/dist/editable-text-CKkS_mWt.d.mts +235 -0
- package/dist/editable-text-CKkS_mWt.d.mts.map +1 -0
- package/dist/events/index.d.mts +10 -1
- package/dist/events/index.d.mts.map +1 -1
- package/dist/events/index.mjs +34 -18
- package/dist/events/index.mjs.map +1 -1
- package/dist/follow-tab-stop-DpxwLXvU.mjs +800 -0
- package/dist/follow-tab-stop-DpxwLXvU.mjs.map +1 -0
- package/dist/index-Cppf-lBx.d.mts +130 -0
- package/dist/index-Cppf-lBx.d.mts.map +1 -0
- package/dist/index.d.mts +8 -6
- package/dist/index.mjs +8 -6
- package/dist/input-modality-DGOhAknO.d.mts +26 -0
- package/dist/input-modality-DGOhAknO.d.mts.map +1 -0
- package/dist/input-modality-m6v7__Xs.mjs +43 -0
- package/dist/input-modality-m6v7__Xs.mjs.map +1 -0
- package/dist/interaction/index.d.mts +1017 -0
- package/dist/interaction/index.d.mts.map +1 -0
- package/dist/interaction/index.mjs +1647 -0
- package/dist/interaction/index.mjs.map +1 -0
- package/dist/internal-DP-xLKbT.mjs +37 -0
- package/dist/internal-DP-xLKbT.mjs.map +1 -0
- package/dist/media/index.d.mts.map +1 -1
- package/dist/media/index.mjs +3 -3
- package/dist/media/index.mjs.map +1 -1
- package/dist/motion/index.d.mts +2 -112
- package/dist/motion/index.mjs +2 -208
- package/dist/motion-DGGBZ0wt.mjs +240 -0
- package/dist/motion-DGGBZ0wt.mjs.map +1 -0
- package/dist/net/index.d.mts +68 -4
- package/dist/net/index.d.mts.map +1 -1
- package/dist/net/index.mjs +139 -56
- package/dist/net/index.mjs.map +1 -1
- package/dist/observers/index.d.mts +1 -1
- package/dist/observers/index.mjs +3 -0
- package/dist/observers/index.mjs.map +1 -1
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/state/index.d.mts +57 -15
- package/dist/state/index.d.mts.map +1 -1
- package/dist/state/index.mjs +198 -90
- package/dist/state/index.mjs.map +1 -1
- package/dist/time/index.d.mts +21 -1
- package/dist/time/index.d.mts.map +1 -1
- package/dist/time/index.mjs +48 -19
- package/dist/time/index.mjs.map +1 -1
- package/package.json +31 -8
- package/dist/motion/index.d.mts.map +0 -1
- package/dist/motion/index.mjs.map +0 -1
package/dist/dom/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/dom/autofocus.ts","../../src/dom/clipboard.ts","../../src/dom/input-mask.ts","../../src/dom/script-loader.ts","../../src/dom/share.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/dom/autofocus.ts","../../src/dom/clipboard.ts","../../src/dom/input-mask.ts","../../src/dom/keep-in-view.ts","../../src/dom/script-loader.ts","../../src/dom/share.ts","../../src/dom/measure.ts","../../src/dom/follow-tab-stop.ts","../../src/dom/scroll.ts"],"mappings":";;;;;qBAUa,SAAO,OAAA;YACR,iBAAiB,QAAQ;qBAExB,WAAS,MAAA;;;;;qBCJT,4CAAgB;;;sBAAhB,OAAA;;;YAID,0BAA0B,iBAAiB;qBAE1C,gDAAoB;;;;;;;;YAIrB,8BAA8B,qBAAqB;;;;;;qBAUlD,WAAY,iBAAe,QAAQ;;;;;qBAsBnC,gBAAe,QAAQ;;;qBCvCvB,OAAK,OAAA;;;;YAIN,eAAe,MAAM;qBA2BpB,YACX,eACA;WACY;WAAyB;;qBAoC1B,WAAS,MAAA;;;;;;;;;qBCtET,YAAU,MAAA;;;;;;qBCDV,yCAAa;;;sBAAb,OAAA;;;YAID,uBAAuB,cAAc;;qBASpC,aAAc,gBAAc,QAAQ;;;qBCbpC,WAAS,OAAA;;;;;YAKV,mBAAmB,UAAU;qBAE5B,wCAAY;;;;sBAAZ,OAAA;;;YAKD,sBAAsB,aAAa;;qBAUlC,QAAS,MAAM,cAAY,QAAQ;;;;qBCVnC,WAAY;;;;;;;qBASZ,SAAO,MAAA;;;;;;qBCrBP,eAAa,MAAA;;;;UCuBhB,mBAAmB;WAClB;aAAyB;;;QAE5B;YACI;aACC,aAAa;;;iBAIT;;WAEN;;WAEA;;;;;WAKA;;;;;;;;qBAqHE,aAAc,UAAS,sBAAyB,OAAO"}
|
package/dist/dom/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as MoveEnded, D as ScrollLock, E as HideOutside, O as FocusScope, S as Move, T as Moved, _ as zoneOf, a as TextCommitted, b as Targets, c as DRAG_THRESHOLD, d as DragPlace, f as DragStarted, g as boxOf, h as PointerDrag, i as TextCancelled, k as tabbableWithin, l as DragCancelled, m as DraggedOver, n as EditAsked, o as TextEdited, p as DragZone, r as EditableText, s as textOf, t as FollowTabStop, u as DragDropped, v as TargetHovered, w as MoveStarted, x as targetOf, y as TargetPressed } from "../follow-tab-stop-DpxwLXvU.mjs";
|
|
2
|
+
import { Effect, Option, Queue, Schema, Stream } from "effect";
|
|
2
3
|
import * as Mount from "foldkit/mount";
|
|
3
4
|
import { defineMessageUnion } from "foldkit/message";
|
|
4
5
|
//#region src/dom/autofocus.ts
|
|
@@ -21,14 +22,18 @@ const Autofocus = Mount.define("Autofocus", {
|
|
|
21
22
|
//#endregion
|
|
22
23
|
//#region src/dom/clipboard.ts
|
|
23
24
|
/**
|
|
24
|
-
*
|
|
25
|
-
* `Copied`
|
|
26
|
-
* clipboard is not application state.
|
|
25
|
+
* The clipboard as Commands: run in `update` beside any bundle. A copy yields
|
|
26
|
+
* `Copied` or `CopyFailed`, a read `Read` or `ReadFailed`. No Model involved:
|
|
27
|
+
* the clipboard is not application state.
|
|
27
28
|
*/
|
|
28
29
|
const ClipboardMessage = defineMessageUnion({
|
|
29
30
|
Copied: {},
|
|
30
31
|
CopyFailed: { message: Schema.String }
|
|
31
32
|
});
|
|
33
|
+
const ClipboardReadMessage = defineMessageUnion({
|
|
34
|
+
Read: { text: Schema.String },
|
|
35
|
+
ReadFailed: { message: Schema.String }
|
|
36
|
+
});
|
|
32
37
|
const failMessage$2 = (error) => error instanceof Error ? error.message : String(error);
|
|
33
38
|
/**
|
|
34
39
|
* Copies text to the clipboard. Without a clipboard API (SSR, insecure
|
|
@@ -46,6 +51,20 @@ const copyText = (text) => ({
|
|
|
46
51
|
onSuccess: () => Effect.succeed(ClipboardMessage.Copied())
|
|
47
52
|
}) : Effect.succeed(ClipboardMessage.CopyFailed({ message: "clipboard is unavailable" }))
|
|
48
53
|
});
|
|
54
|
+
/**
|
|
55
|
+
* Reads the clipboard's text. The browser may ask the user first; a refusal,
|
|
56
|
+
* an insecure context or no clipboard API yields `ReadFailed`.
|
|
57
|
+
*/
|
|
58
|
+
const readText = () => ({
|
|
59
|
+
name: "Clipboard.read",
|
|
60
|
+
effect: typeof navigator !== "undefined" && typeof navigator.clipboard?.readText === "function" ? Effect.matchEffect(Effect.tryPromise({
|
|
61
|
+
try: () => navigator.clipboard.readText(),
|
|
62
|
+
catch: failMessage$2
|
|
63
|
+
}), {
|
|
64
|
+
onFailure: (message) => Effect.succeed(ClipboardReadMessage.ReadFailed({ message })),
|
|
65
|
+
onSuccess: (text) => Effect.succeed(ClipboardReadMessage.Read({ text }))
|
|
66
|
+
}) : Effect.succeed(ClipboardReadMessage.ReadFailed({ message: "clipboard is unavailable" }))
|
|
67
|
+
});
|
|
49
68
|
//#endregion
|
|
50
69
|
//#region src/dom/input-mask.ts
|
|
51
70
|
/**
|
|
@@ -126,6 +145,40 @@ const InputMask = Mount.defineStream("InputMask", {
|
|
|
126
145
|
}))
|
|
127
146
|
});
|
|
128
147
|
//#endregion
|
|
148
|
+
//#region src/dom/keep-in-view.ts
|
|
149
|
+
/**
|
|
150
|
+
* Keeps what is marked in view as a Mount: whenever an element in the mounted
|
|
151
|
+
* one's subtree newly matches `selector`, such as the row just selected in a
|
|
152
|
+
* list, it is scrolled into view the least amount that shows it (`nearest`).
|
|
153
|
+
* The element keeps its identity and its focus: nothing is redrawn to move it.
|
|
154
|
+
*
|
|
155
|
+
* It watches with a `MutationObserver` for as long as it is mounted, which is as
|
|
156
|
+
* long as the list or canvas that can change what is marked.
|
|
157
|
+
*/
|
|
158
|
+
const KeepInView = Mount.defineStream("KeepInView", {
|
|
159
|
+
messages: [Schema.Never],
|
|
160
|
+
args: {
|
|
161
|
+
/** What is marked, as a selector within the element: `[aria-selected="true"]`. */
|
|
162
|
+
selector: Schema.String },
|
|
163
|
+
execute: ({ element, selector }) => Stream.callback(() => Effect.acquireRelease(Effect.sync(() => {
|
|
164
|
+
let marked = element.querySelector(selector);
|
|
165
|
+
const observer = new MutationObserver(() => {
|
|
166
|
+
const now = element.querySelector(selector);
|
|
167
|
+
if (now !== null && now !== marked && typeof now.scrollIntoView === "function") now.scrollIntoView({
|
|
168
|
+
block: "nearest",
|
|
169
|
+
inline: "nearest"
|
|
170
|
+
});
|
|
171
|
+
marked = now;
|
|
172
|
+
});
|
|
173
|
+
observer.observe(element, {
|
|
174
|
+
subtree: true,
|
|
175
|
+
childList: true,
|
|
176
|
+
attributes: true
|
|
177
|
+
});
|
|
178
|
+
return observer;
|
|
179
|
+
}), (observer) => Effect.sync(() => observer.disconnect())))
|
|
180
|
+
});
|
|
181
|
+
//#endregion
|
|
129
182
|
//#region src/dom/script-loader.ts
|
|
130
183
|
/**
|
|
131
184
|
* Script loading as a Command: appends a `src` script to the head unless one
|
|
@@ -213,6 +266,251 @@ const share = (data) => ({
|
|
|
213
266
|
}) : Effect.succeed(ShareMessage.ShareFailed({ message: "share is unavailable" }))
|
|
214
267
|
});
|
|
215
268
|
//#endregion
|
|
216
|
-
|
|
269
|
+
//#region src/dom/measure.ts
|
|
270
|
+
/**
|
|
271
|
+
* Measures marked elements as a Mount, for a view to draw something over them,
|
|
272
|
+
* such as an editor's selection outline, with no Message and no Model: where
|
|
273
|
+
* an element is, is presentation, known to the DOM and to no transition.
|
|
274
|
+
*
|
|
275
|
+
* For each named target, the first element in the mounted one's subtree that
|
|
276
|
+
* matches its selector is measured relative to the mounted element's scroll
|
|
277
|
+
* box, which is what an absolutely placed child of it is laid out in. The
|
|
278
|
+
* mounted element gets `--fk-<name>-x`, `-y`, `-w` and `-h` in pixels, and
|
|
279
|
+
* `--fk-<name>-display`: `block` while a target is found, `none` while none is.
|
|
280
|
+
*
|
|
281
|
+
* It measures again when the subtree changes (a selection moves), in the
|
|
282
|
+
* frame the change is drawn in; and, at most once a frame, when the mounted
|
|
283
|
+
* element scrolls, the window resizes, the mounted element or a measured one
|
|
284
|
+
* changes size, an image or a font inside loads, and on every frame of a
|
|
285
|
+
* transition or an animation inside. What moves a target by any other way,
|
|
286
|
+
* such as a stylesheet added that shifts it without resizing it, is seen at
|
|
287
|
+
* the next of these.
|
|
288
|
+
*/
|
|
289
|
+
/** The custom properties `Measure` writes for a target named `name`. */
|
|
290
|
+
const measured = (name) => ({
|
|
291
|
+
x: `--fk-${name}-x`,
|
|
292
|
+
y: `--fk-${name}-y`,
|
|
293
|
+
w: `--fk-${name}-w`,
|
|
294
|
+
h: `--fk-${name}-h`,
|
|
295
|
+
display: `--fk-${name}-display`
|
|
296
|
+
});
|
|
297
|
+
const Measure = Mount.defineStream("Measure", {
|
|
298
|
+
messages: [Schema.Never],
|
|
299
|
+
args: {
|
|
300
|
+
/** Each target by name, as a selector within the element: `{ selected: '[aria-selected="true"]' }`. */
|
|
301
|
+
targets: Schema.Record(Schema.String, Schema.String) },
|
|
302
|
+
execute: ({ element, targets }) => Stream.callback(() => Effect.acquireRelease(Effect.sync(() => {
|
|
303
|
+
const container = element;
|
|
304
|
+
const names = Object.keys(targets);
|
|
305
|
+
let watched = [];
|
|
306
|
+
let frame;
|
|
307
|
+
let animating = false;
|
|
308
|
+
const measure = () => {
|
|
309
|
+
if (frame !== void 0) cancelAnimationFrame(frame);
|
|
310
|
+
frame = void 0;
|
|
311
|
+
const box = container.getBoundingClientRect();
|
|
312
|
+
const found = [];
|
|
313
|
+
for (const name of names) {
|
|
314
|
+
const property = measured(name);
|
|
315
|
+
const target = container.querySelector(targets[name]);
|
|
316
|
+
if (target === null) {
|
|
317
|
+
container.style.setProperty(property.display, "none");
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
found.push(target);
|
|
321
|
+
const rect = target.getBoundingClientRect();
|
|
322
|
+
const x = rect.left - box.left - container.clientLeft + container.scrollLeft;
|
|
323
|
+
const y = rect.top - box.top - container.clientTop + container.scrollTop;
|
|
324
|
+
container.style.setProperty(property.x, `${x}px`);
|
|
325
|
+
container.style.setProperty(property.y, `${y}px`);
|
|
326
|
+
container.style.setProperty(property.w, `${rect.width}px`);
|
|
327
|
+
container.style.setProperty(property.h, `${rect.height}px`);
|
|
328
|
+
container.style.setProperty(property.display, "block");
|
|
329
|
+
}
|
|
330
|
+
if (found.length !== watched.length || found.some((each, at) => each !== watched[at])) {
|
|
331
|
+
for (const each of watched) sizes.unobserve(each);
|
|
332
|
+
for (const each of found) sizes.observe(each);
|
|
333
|
+
watched = found;
|
|
334
|
+
}
|
|
335
|
+
if (animating) {
|
|
336
|
+
animating = container.getAnimations({ subtree: true }).some((animation) => animation.playState === "running");
|
|
337
|
+
if (animating) soon();
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
const soon = () => {
|
|
341
|
+
if (frame === void 0) frame = requestAnimationFrame(measure);
|
|
342
|
+
};
|
|
343
|
+
const moving = () => {
|
|
344
|
+
animating = true;
|
|
345
|
+
soon();
|
|
346
|
+
};
|
|
347
|
+
const changes = new MutationObserver(measure);
|
|
348
|
+
const sizes = new ResizeObserver(soon);
|
|
349
|
+
document.fonts.addEventListener("loadingdone", soon);
|
|
350
|
+
changes.observe(container, {
|
|
351
|
+
subtree: true,
|
|
352
|
+
childList: true,
|
|
353
|
+
attributes: true
|
|
354
|
+
});
|
|
355
|
+
sizes.observe(container);
|
|
356
|
+
container.addEventListener("scroll", soon, { passive: true });
|
|
357
|
+
window.addEventListener("resize", soon);
|
|
358
|
+
container.addEventListener("load", soon, { capture: true });
|
|
359
|
+
container.addEventListener("transitionrun", moving);
|
|
360
|
+
container.addEventListener("animationstart", moving);
|
|
361
|
+
measure();
|
|
362
|
+
return () => {
|
|
363
|
+
if (frame !== void 0) cancelAnimationFrame(frame);
|
|
364
|
+
changes.disconnect();
|
|
365
|
+
sizes.disconnect();
|
|
366
|
+
container.removeEventListener("scroll", soon);
|
|
367
|
+
window.removeEventListener("resize", soon);
|
|
368
|
+
container.removeEventListener("load", soon, { capture: true });
|
|
369
|
+
document.fonts.removeEventListener("loadingdone", soon);
|
|
370
|
+
container.removeEventListener("transitionrun", moving);
|
|
371
|
+
container.removeEventListener("animationstart", moving);
|
|
372
|
+
};
|
|
373
|
+
}), (stop) => Effect.sync(stop)))
|
|
374
|
+
});
|
|
375
|
+
//#endregion
|
|
376
|
+
//#region src/dom/scroll.ts
|
|
377
|
+
/**
|
|
378
|
+
* Where the window is scrolled, across an application's own navigations, as
|
|
379
|
+
* a stream. Left to itself, a browser keeps the scroll of the entry it
|
|
380
|
+
* leaves when a link pushes a new one, and on Back restores the old offset
|
|
381
|
+
* before the application has drawn the entry it returns to, so the offset is
|
|
382
|
+
* clamped to a page still loading. Here a pushed entry starts at the top,
|
|
383
|
+
* and Back, Forward or a reload return where that entry was, once the page
|
|
384
|
+
* is tall enough to hold it.
|
|
385
|
+
*
|
|
386
|
+
* Three rules, each found the slow way:
|
|
387
|
+
*
|
|
388
|
+
* 1. The offset is taken when the reader acts. The next screen is drawn
|
|
389
|
+
* before a Navigate Command pushes the address, so by the time the URL
|
|
390
|
+
* changes the window is already clamped to the new, shorter page. The
|
|
391
|
+
* scroll is recorded at the reader's click or key (capture phase), and at
|
|
392
|
+
* the start of Back and Forward, which begin in the browser before
|
|
393
|
+
* anything is drawn.
|
|
394
|
+
* 2. A restore holds its place while the next screen settles. A restore made
|
|
395
|
+
* when the entry changes is undone by the loading state drawn right after
|
|
396
|
+
* it, so the target is held on each frame for a moment, until the reader
|
|
397
|
+
* scrolls, touches or types, or another navigation starts.
|
|
398
|
+
* 3. Entries are keyed by the Navigation API's entry key, which a reload and
|
|
399
|
+
* a full navigation keep; offsets rest in `sessionStorage`, so Back into
|
|
400
|
+
* another document restores too. Where the browser has no Navigation API
|
|
401
|
+
* the stream attaches nothing and leaves the browser's own behaviour
|
|
402
|
+
* alone.
|
|
403
|
+
*
|
|
404
|
+
* Lift with `Subscription.persistent`; the stream sends no Messages. Only
|
|
405
|
+
* the window scrolls: a nested container is the route graph's to know, when
|
|
406
|
+
* `Site.transition` arrives.
|
|
407
|
+
*/
|
|
408
|
+
const navigationTypeOf = (event) => "navigationType" in event ? Option.some(event.navigationType) : Option.none();
|
|
409
|
+
const readSaved = (storageKey) => {
|
|
410
|
+
try {
|
|
411
|
+
const stored = JSON.parse(sessionStorage.getItem(storageKey) ?? "[]");
|
|
412
|
+
return new Map(Array.isArray(stored) ? stored.filter((pair) => Array.isArray(pair) && typeof pair[0] === "string" && typeof pair[1] === "number") : []);
|
|
413
|
+
} catch {
|
|
414
|
+
return /* @__PURE__ */ new Map();
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
/** Starts keeping the scroll; the release undoes everything it installed. */
|
|
418
|
+
const attach = (storageKey, kept, patience) => {
|
|
419
|
+
const navigation = window.navigation;
|
|
420
|
+
if (navigation === void 0) return () => {};
|
|
421
|
+
const restoration = window.history.scrollRestoration;
|
|
422
|
+
window.history.scrollRestoration = "manual";
|
|
423
|
+
const saved = readSaved(storageKey);
|
|
424
|
+
const detaches = [];
|
|
425
|
+
const on = (target, type, listener, capture) => {
|
|
426
|
+
target.addEventListener(type, listener, capture);
|
|
427
|
+
detaches.push(() => target.removeEventListener(type, listener, capture));
|
|
428
|
+
};
|
|
429
|
+
const keyOf = () => Option.fromNullOr(navigation.currentEntry).pipe(Option.map((entry) => entry.key));
|
|
430
|
+
/** Where the current entry was left, if it has been. */
|
|
431
|
+
const savedHere = () => Option.flatMap(keyOf(), (key) => Option.fromUndefinedOr(saved.get(key)));
|
|
432
|
+
const remember = () => {
|
|
433
|
+
const key = keyOf();
|
|
434
|
+
if (Option.isNone(key)) return;
|
|
435
|
+
saved.delete(key.value);
|
|
436
|
+
saved.set(key.value, window.scrollY);
|
|
437
|
+
for (const old of saved.keys()) {
|
|
438
|
+
if (saved.size <= kept) break;
|
|
439
|
+
saved.delete(old);
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
let settling = new AbortController();
|
|
443
|
+
/**
|
|
444
|
+
* Holds the window at `top` while the next screen settles, whenever the page
|
|
445
|
+
* can hold it, until the reader scrolls, touches or types, or another
|
|
446
|
+
* navigation starts.
|
|
447
|
+
*/
|
|
448
|
+
const settle = (top) => {
|
|
449
|
+
settling.abort();
|
|
450
|
+
settling = new AbortController();
|
|
451
|
+
const { signal } = settling;
|
|
452
|
+
for (const input of [
|
|
453
|
+
"wheel",
|
|
454
|
+
"touchstart",
|
|
455
|
+
"keydown",
|
|
456
|
+
"pointerdown"
|
|
457
|
+
]) window.addEventListener(input, () => settling.abort(), {
|
|
458
|
+
passive: true,
|
|
459
|
+
signal
|
|
460
|
+
});
|
|
461
|
+
const until = performance.now() + patience;
|
|
462
|
+
const step = () => {
|
|
463
|
+
if (signal.aborted) return;
|
|
464
|
+
if (document.documentElement.scrollHeight - window.innerHeight >= top && Math.round(window.scrollY) !== top) window.scrollTo({
|
|
465
|
+
top,
|
|
466
|
+
behavior: "instant"
|
|
467
|
+
});
|
|
468
|
+
if (performance.now() < until) requestAnimationFrame(step);
|
|
469
|
+
else settling.abort();
|
|
470
|
+
};
|
|
471
|
+
step();
|
|
472
|
+
};
|
|
473
|
+
const acted = () => remember();
|
|
474
|
+
for (const input of ["click", "keydown"]) on(window, input, acted, true);
|
|
475
|
+
const traversing = (event) => {
|
|
476
|
+
if (Option.contains(navigationTypeOf(event), "traverse")) remember();
|
|
477
|
+
};
|
|
478
|
+
on(navigation, "navigate", traversing);
|
|
479
|
+
const changed = (event) => {
|
|
480
|
+
const type = navigationTypeOf(event);
|
|
481
|
+
if (Option.contains(type, "push")) settle(0);
|
|
482
|
+
else if (Option.contains(type, "traverse")) settle(Option.getOrElse(savedHere(), () => 0));
|
|
483
|
+
};
|
|
484
|
+
on(navigation, "currententrychange", changed);
|
|
485
|
+
const hidden = () => {
|
|
486
|
+
remember();
|
|
487
|
+
try {
|
|
488
|
+
sessionStorage.setItem(storageKey, JSON.stringify([...saved]));
|
|
489
|
+
} catch {}
|
|
490
|
+
};
|
|
491
|
+
on(window, "pagehide", hidden);
|
|
492
|
+
const here = savedHere();
|
|
493
|
+
if (Option.isSome(here)) settle(here.value);
|
|
494
|
+
return () => {
|
|
495
|
+
settling.abort();
|
|
496
|
+
for (const detach of detaches) detach();
|
|
497
|
+
window.history.scrollRestoration = restoration;
|
|
498
|
+
};
|
|
499
|
+
};
|
|
500
|
+
/**
|
|
501
|
+
* Keeps the window's scroll across same-document navigations: a pushed entry
|
|
502
|
+
* starts at the top, and Back, Forward or a reload return where that entry
|
|
503
|
+
* was. Without a window the stream is empty instead of throwing. Lift with
|
|
504
|
+
* `Subscription.persistent`; the stream sends no Messages.
|
|
505
|
+
*/
|
|
506
|
+
const keepScroll = (options = {}) => {
|
|
507
|
+
if (typeof window === "undefined") return Stream.empty;
|
|
508
|
+
const storageKey = options.storageKey ?? "foldkit:scroll";
|
|
509
|
+
const kept = options.kept ?? 50;
|
|
510
|
+
const patience = options.patience ?? 2e3;
|
|
511
|
+
return Stream.callback(() => Effect.acquireRelease(Effect.sync(() => attach(storageKey, kept, patience)), (detach) => Effect.sync(detach)));
|
|
512
|
+
};
|
|
513
|
+
//#endregion
|
|
514
|
+
export { Autofocus, ClipboardMessage, ClipboardReadMessage, DRAG_THRESHOLD, DragCancelled, DragDropped, DragPlace, DragStarted, DragZone, DraggedOver, EditAsked, EditableText, FocusScope, Focused, FollowTabStop, HideOutside, Input, InputMask, KeepInView, Measure, Move, MoveEnded, MoveStarted, Moved, PointerDrag, ScriptMessage, ScrollLock, ShareData, ShareMessage, TargetHovered, TargetPressed, Targets, TextCancelled, TextCommitted, TextEdited, applyMask, boxOf, copyText, keepScroll, loadScript, measured, readText, share, tabbableWithin, targetOf, textOf, zoneOf };
|
|
217
515
|
|
|
218
516
|
//# sourceMappingURL=index.mjs.map
|
package/dist/dom/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["failMessage","failMessage"],"sources":["../../src/dom/autofocus.ts","../../src/dom/clipboard.ts","../../src/dom/input-mask.ts","../../src/dom/script-loader.ts","../../src/dom/share.ts"],"sourcesContent":["/**\n * Autofocus as a Mount: focuses the element on insert, then emits `Focused`.\n * The message means focus was requested — a non-focusable element or a\n * hidden tab may decline, and the browser does not report that. Plain\n * `focus()`, no scroll options: scroll behavior stays the application's\n * choice.\n */\nimport { Effect, Schema } from 'effect'\nimport * as Mount from 'foldkit/mount'\n\nexport const Focused = Schema.TaggedStruct('Focused', {})\nexport type Focused = typeof Focused.Type\n\nexport const Autofocus = Mount.define('Autofocus', {\n messages: [Focused],\n execute: ({ element }) =>\n Effect.sync(() => {\n const focusable = element as unknown as { readonly focus?: unknown }\n if (typeof focusable.focus === 'function') {\n ;(focusable.focus as () => void).call(element)\n }\n return Focused.make({})\n }),\n})\n","/**\n * Clipboard copy as a Command: runs in `update` beside any bundle, yields\n * `Copied` on success and `CopyFailed` otherwise. No Model involved: the\n * clipboard is not application state.\n */\nimport { Effect, Schema } from 'effect'\nimport type { Command } from 'foldkit/command'\nimport { defineMessageUnion } from 'foldkit/message'\n\nexport const ClipboardMessage = defineMessageUnion({\n Copied: {},\n CopyFailed: { message: Schema.String },\n})\nexport type ClipboardMessage = typeof ClipboardMessage.Type\n\nconst failMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error)\n\n/**\n * Copies text to the clipboard. Without a clipboard API (SSR, insecure\n * context, denied permission) the command yields `CopyFailed` instead of\n * throwing, so `update` handles both outcomes as Messages.\n */\nexport const copyText = (text: string): Command<ClipboardMessage, never, never> => ({\n name: 'Clipboard.copy',\n args: { text },\n effect:\n typeof navigator !== 'undefined' && typeof navigator.clipboard?.writeText === 'function'\n ? Effect.matchEffect(\n Effect.tryPromise({\n try: () => navigator.clipboard.writeText(text),\n catch: failMessage,\n }),\n {\n onFailure: message => Effect.succeed(ClipboardMessage.CopyFailed({ message })),\n onSuccess: () => Effect.succeed(ClipboardMessage.Copied()),\n },\n )\n : Effect.succeed(ClipboardMessage.CopyFailed({ message: 'clipboard is unavailable' })),\n})\n","/**\n * An input mask as a Mount plus a pure mask function. The pattern language\n * is three placeholders — `#` digit, `A` letter, `*` either, all ASCII —\n * with every other character literal. Literals appear only once a later placeholder\n * consumes input, so partial values never dangle a separator. The Mount\n * rewrites the field, restores the caret approximately, and emits `Input`\n * with both the masked and raw values; the parent owns the state, like any\n * controlled input.\n */\nimport { Effect, Queue, Schema, Stream } from 'effect'\nimport * as Mount from 'foldkit/mount'\n\nexport const Input = Schema.TaggedStruct('Input', {\n value: Schema.String,\n raw: Schema.String,\n})\nexport type Input = typeof Input.Type\n\nconst isDigit = (char: string): boolean => char >= '0' && char <= '9'\nconst isLetter = (char: string): boolean =>\n (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z')\n\nconst matches = (placeholder: string, char: string): boolean =>\n placeholder === '#'\n ? isDigit(char)\n : placeholder === 'A'\n ? isLetter(char)\n : isDigit(char) || isLetter(char)\n\nconst isPlaceholder = (char: string): boolean => char === '#' || char === 'A' || char === '*'\n\n/** Whether any placeholder at or after `from` consumes any input at or after `at`. */\nconst fillable = (pattern: string, from: number, value: string, at: number): boolean => {\n for (let i = from; i < pattern.length; i++) {\n const placeholder = pattern[i]!\n if (!isPlaceholder(placeholder)) continue\n for (let j = at; j < value.length; j++) {\n if (matches(placeholder, value[j]!)) return true\n }\n }\n return false\n}\n\nexport const applyMask = (\n value: string,\n pattern: string,\n): { readonly masked: string; readonly raw: string } => {\n let masked = ''\n let raw = ''\n let at = 0\n for (let i = 0; i < pattern.length; i++) {\n const slot = pattern[i]!\n if (isPlaceholder(slot)) {\n while (at < value.length && !matches(slot, value[at]!)) at++\n if (at >= value.length) break\n masked += value[at]!\n raw += value[at]!\n at++\n } else if (fillable(pattern, i + 1, value, at)) {\n masked += slot\n } else {\n break\n }\n }\n return { masked, raw }\n}\n\ninterface MaskableField {\n value: string\n selectionStart: number | null\n addEventListener(type: string, listener: () => void): void\n removeEventListener(type: string, listener: () => void): void\n setSelectionRange?(start: number, end: number): void\n}\n\nconst asField = (element: Element): MaskableField | null => {\n const field = element as unknown as Partial<MaskableField>\n return typeof field.value === 'string' && typeof field.addEventListener === 'function'\n ? (field as MaskableField)\n : null\n}\n\nexport const InputMask = Mount.defineStream('InputMask', {\n messages: [Input],\n args: { pattern: Schema.String },\n execute: ({ element, pattern }) =>\n Stream.callback<typeof Input.Type>(queue =>\n Effect.gen(function* () {\n const field = asField(element)\n if (field === null) return\n const onInput = () => {\n const { masked, raw } = applyMask(field.value, pattern)\n if (masked !== field.value) {\n const caret = field.selectionStart ?? masked.length\n field.value = masked\n const at = Math.min(caret, masked.length)\n try {\n field.setSelectionRange?.(at, at)\n } catch {\n // Select-all style inputs reject ranges; the value still stands.\n }\n }\n Queue.offerUnsafe(queue, Input.make({ value: masked, raw }))\n }\n yield* Effect.acquireRelease(\n Effect.sync(() => {\n field.addEventListener('input', onInput)\n return field\n }),\n current => Effect.sync(() => current.removeEventListener('input', onInput)),\n )\n }),\n ),\n})\n","/**\n * Script loading as a Command: appends a `src` script to the head unless one\n * is already there. Idempotent by URL — concurrent placements collapse onto\n * the first tag instead of fetching twice. Yields `Loaded` on success and\n * `LoadFailed` otherwise (network error, no document), so `update` handles\n * both outcomes as Messages.\n */\nimport { Effect, Schema } from 'effect'\nimport type { Command } from 'foldkit/command'\nimport { defineMessageUnion } from 'foldkit/message'\n\nexport const ScriptMessage = defineMessageUnion({\n Loaded: {},\n LoadFailed: { message: Schema.String },\n})\nexport type ScriptMessage = typeof ScriptMessage.Type\n\nconst failMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error)\n\n// Tags still fetching, by URL: concurrent loads share one tag and its fate.\nconst inflight = new Map<string, Promise<void>>()\n\n/** Loads a script by URL, once per URL. */\nexport const loadScript = (src: string): Command<ScriptMessage, never, never> => ({\n name: 'Script.load',\n args: { src },\n effect: Effect.matchEffect(\n Effect.tryPromise({\n try: () => {\n if (typeof document === 'undefined') {\n return Promise.reject(new Error('script loading is unavailable'))\n }\n // Concurrent loads share one tag and its fate, instead of racing\n // past the presence check and fetching twice: the in-flight entry\n // is checked before presence, in the same synchronous block that\n // appends, so a second load cannot slip between them.\n const running = inflight.get(src)\n if (running !== undefined) return running\n // getAttribute, not .src: the property resolves to an absolute URL\n // and would never equal the given string.\n const present = Array.from(document.getElementsByTagName('script')).some(\n element => element.getAttribute('src') === src,\n )\n if (present) return Promise.resolve()\n const completion = new Promise<void>((resolve, reject) => {\n const element = document.createElement('script')\n element.src = src\n element.async = true\n element.onload = () => resolve()\n element.onerror = () => {\n // A dead tag must not satisfy later presence checks.\n element.remove()\n reject(new Error(`failed to load script: ${src}`))\n }\n document.head.appendChild(element)\n })\n const tracked = completion.then(\n () => {\n inflight.delete(src)\n },\n (error: unknown) => {\n inflight.delete(src)\n throw error\n },\n )\n inflight.set(src, tracked)\n return tracked\n },\n catch: (error: unknown) => error,\n }),\n {\n onFailure: error => Effect.succeed(ScriptMessage.LoadFailed({ message: failMessage(error) })),\n onSuccess: () => Effect.succeed(ScriptMessage.Loaded()),\n },\n ),\n})\n","/**\n * Web Share as a Command: runs in `update` beside any bundle, yields\n * `Shared` on success. Dismissal is its own outcome, not a failure — the\n * application tells a cancelled sheet apart from a broken one. Without a\n * share API (desktop, insecure context) it yields `ShareFailed` instead of\n * throwing, so `update` handles every outcome as a Message.\n */\nimport { Effect, Schema } from 'effect'\nimport type { Command } from 'foldkit/command'\nimport { defineMessageUnion } from 'foldkit/message'\n\nexport const ShareData = Schema.Struct({\n title: Schema.optional(Schema.String),\n text: Schema.optional(Schema.String),\n url: Schema.optional(Schema.String),\n})\nexport type ShareData = typeof ShareData.Type\n\nexport const ShareMessage = defineMessageUnion({\n Shared: {},\n Dismissed: {},\n ShareFailed: { message: Schema.String },\n})\nexport type ShareMessage = typeof ShareMessage.Type\n\nconst failMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error)\n\nconst isDismissal = (error: unknown): boolean =>\n typeof (error as { readonly name?: unknown } | null)?.name === 'string' &&\n (error as { readonly name: string }).name === 'AbortError'\n\n/** Shares data through the platform sheet. */\nexport const share = (data: ShareData): Command<ShareMessage, never, never> => ({\n name: 'Share.share',\n args: { data },\n effect:\n typeof navigator !== 'undefined' && typeof navigator.share === 'function'\n ? Effect.matchEffect(\n Effect.tryPromise({\n // Omit absent fields: the platform type rejects explicit\n // undefined under exactOptionalPropertyTypes.\n try: () =>\n navigator.share({\n ...(data.title !== undefined ? { title: data.title } : {}),\n ...(data.text !== undefined ? { text: data.text } : {}),\n ...(data.url !== undefined ? { url: data.url } : {}),\n }),\n catch: (error: unknown) => error,\n }),\n {\n onFailure: error =>\n Effect.succeed(\n isDismissal(error)\n ? ShareMessage.Dismissed()\n : ShareMessage.ShareFailed({ message: failMessage(error) }),\n ),\n onSuccess: () => Effect.succeed(ShareMessage.Shared()),\n },\n )\n : Effect.succeed(ShareMessage.ShareFailed({ message: 'share is unavailable' })),\n})\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["failMessage","failMessage"],"sources":["../../src/dom/autofocus.ts","../../src/dom/clipboard.ts","../../src/dom/input-mask.ts","../../src/dom/keep-in-view.ts","../../src/dom/script-loader.ts","../../src/dom/share.ts","../../src/dom/measure.ts","../../src/dom/scroll.ts"],"sourcesContent":["/**\n * Autofocus as a Mount: focuses the element on insert, then emits `Focused`.\n * The message means focus was requested — a non-focusable element or a\n * hidden tab may decline, and the browser does not report that. Plain\n * `focus()`, no scroll options: scroll behavior stays the application's\n * choice.\n */\nimport { Effect, Schema } from 'effect'\nimport * as Mount from 'foldkit/mount'\n\nexport const Focused = Schema.TaggedStruct('Focused', {})\nexport type Focused = typeof Focused.Type\n\nexport const Autofocus = Mount.define('Autofocus', {\n messages: [Focused],\n execute: ({ element }) =>\n Effect.sync(() => {\n const focusable = element as unknown as { readonly focus?: unknown }\n if (typeof focusable.focus === 'function') {\n ;(focusable.focus as () => void).call(element)\n }\n return Focused.make({})\n }),\n})\n","/**\n * The clipboard as Commands: run in `update` beside any bundle. A copy yields\n * `Copied` or `CopyFailed`, a read `Read` or `ReadFailed`. No Model involved:\n * the clipboard is not application state.\n */\nimport { Effect, Schema } from 'effect'\nimport type { Command } from 'foldkit/command'\nimport { defineMessageUnion } from 'foldkit/message'\n\nexport const ClipboardMessage = defineMessageUnion({\n Copied: {},\n CopyFailed: { message: Schema.String },\n})\nexport type ClipboardMessage = typeof ClipboardMessage.Type\n\nexport const ClipboardReadMessage = defineMessageUnion({\n Read: { text: Schema.String },\n ReadFailed: { message: Schema.String },\n})\nexport type ClipboardReadMessage = typeof ClipboardReadMessage.Type\n\nconst failMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error)\n\n/**\n * Copies text to the clipboard. Without a clipboard API (SSR, insecure\n * context, denied permission) the command yields `CopyFailed` instead of\n * throwing, so `update` handles both outcomes as Messages.\n */\nexport const copyText = (text: string): Command<ClipboardMessage, never, never> => ({\n name: 'Clipboard.copy',\n args: { text },\n effect:\n typeof navigator !== 'undefined' && typeof navigator.clipboard?.writeText === 'function'\n ? Effect.matchEffect(\n Effect.tryPromise({\n try: () => navigator.clipboard.writeText(text),\n catch: failMessage,\n }),\n {\n onFailure: message => Effect.succeed(ClipboardMessage.CopyFailed({ message })),\n onSuccess: () => Effect.succeed(ClipboardMessage.Copied()),\n },\n )\n : Effect.succeed(ClipboardMessage.CopyFailed({ message: 'clipboard is unavailable' })),\n})\n\n/**\n * Reads the clipboard's text. The browser may ask the user first; a refusal,\n * an insecure context or no clipboard API yields `ReadFailed`.\n */\nexport const readText = (): Command<ClipboardReadMessage, never, never> => ({\n name: 'Clipboard.read',\n effect:\n typeof navigator !== 'undefined' && typeof navigator.clipboard?.readText === 'function'\n ? Effect.matchEffect(\n Effect.tryPromise({ try: () => navigator.clipboard.readText(), catch: failMessage }),\n {\n onFailure: message => Effect.succeed(ClipboardReadMessage.ReadFailed({ message })),\n onSuccess: text => Effect.succeed(ClipboardReadMessage.Read({ text })),\n },\n )\n : Effect.succeed(ClipboardReadMessage.ReadFailed({ message: 'clipboard is unavailable' })),\n})\n","/**\n * An input mask as a Mount plus a pure mask function. The pattern language\n * is three placeholders — `#` digit, `A` letter, `*` either, all ASCII —\n * with every other character literal. Literals appear only once a later placeholder\n * consumes input, so partial values never dangle a separator. The Mount\n * rewrites the field, restores the caret approximately, and emits `Input`\n * with both the masked and raw values; the parent owns the state, like any\n * controlled input.\n */\nimport { Effect, Queue, Schema, Stream } from 'effect'\nimport * as Mount from 'foldkit/mount'\n\nexport const Input = Schema.TaggedStruct('Input', {\n value: Schema.String,\n raw: Schema.String,\n})\nexport type Input = typeof Input.Type\n\nconst isDigit = (char: string): boolean => char >= '0' && char <= '9'\nconst isLetter = (char: string): boolean =>\n (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z')\n\nconst matches = (placeholder: string, char: string): boolean =>\n placeholder === '#'\n ? isDigit(char)\n : placeholder === 'A'\n ? isLetter(char)\n : isDigit(char) || isLetter(char)\n\nconst isPlaceholder = (char: string): boolean => char === '#' || char === 'A' || char === '*'\n\n/** Whether any placeholder at or after `from` consumes any input at or after `at`. */\nconst fillable = (pattern: string, from: number, value: string, at: number): boolean => {\n for (let i = from; i < pattern.length; i++) {\n const placeholder = pattern[i]!\n if (!isPlaceholder(placeholder)) continue\n for (let j = at; j < value.length; j++) {\n if (matches(placeholder, value[j]!)) return true\n }\n }\n return false\n}\n\nexport const applyMask = (\n value: string,\n pattern: string,\n): { readonly masked: string; readonly raw: string } => {\n let masked = ''\n let raw = ''\n let at = 0\n for (let i = 0; i < pattern.length; i++) {\n const slot = pattern[i]!\n if (isPlaceholder(slot)) {\n while (at < value.length && !matches(slot, value[at]!)) at++\n if (at >= value.length) break\n masked += value[at]!\n raw += value[at]!\n at++\n } else if (fillable(pattern, i + 1, value, at)) {\n masked += slot\n } else {\n break\n }\n }\n return { masked, raw }\n}\n\ninterface MaskableField {\n value: string\n selectionStart: number | null\n addEventListener(type: string, listener: () => void): void\n removeEventListener(type: string, listener: () => void): void\n setSelectionRange?(start: number, end: number): void\n}\n\nconst asField = (element: Element): MaskableField | null => {\n const field = element as unknown as Partial<MaskableField>\n return typeof field.value === 'string' && typeof field.addEventListener === 'function'\n ? (field as MaskableField)\n : null\n}\n\nexport const InputMask = Mount.defineStream('InputMask', {\n messages: [Input],\n args: { pattern: Schema.String },\n execute: ({ element, pattern }) =>\n Stream.callback<typeof Input.Type>(queue =>\n Effect.gen(function* () {\n const field = asField(element)\n if (field === null) return\n const onInput = () => {\n const { masked, raw } = applyMask(field.value, pattern)\n if (masked !== field.value) {\n const caret = field.selectionStart ?? masked.length\n field.value = masked\n const at = Math.min(caret, masked.length)\n try {\n field.setSelectionRange?.(at, at)\n } catch {\n // Select-all style inputs reject ranges; the value still stands.\n }\n }\n Queue.offerUnsafe(queue, Input.make({ value: masked, raw }))\n }\n yield* Effect.acquireRelease(\n Effect.sync(() => {\n field.addEventListener('input', onInput)\n return field\n }),\n current => Effect.sync(() => current.removeEventListener('input', onInput)),\n )\n }),\n ),\n})\n","/**\n * Keeps what is marked in view as a Mount: whenever an element in the mounted\n * one's subtree newly matches `selector`, such as the row just selected in a\n * list, it is scrolled into view the least amount that shows it (`nearest`).\n * The element keeps its identity and its focus: nothing is redrawn to move it.\n *\n * It watches with a `MutationObserver` for as long as it is mounted, which is as\n * long as the list or canvas that can change what is marked.\n */\nimport { Effect, Schema, Stream } from 'effect'\nimport * as Mount from 'foldkit/mount'\n\nexport const KeepInView = Mount.defineStream('KeepInView', {\n messages: [Schema.Never],\n args: {\n /** What is marked, as a selector within the element: `[aria-selected=\"true\"]`. */\n selector: Schema.String,\n },\n execute: ({ element, selector }) =>\n Stream.callback<never>(() =>\n Effect.acquireRelease(\n Effect.sync(() => {\n let marked = element.querySelector(selector)\n const observer = new MutationObserver(() => {\n const now = element.querySelector(selector)\n // Only a change of what is marked scrolls: the author's own scrolling stays.\n // A DOM with no layout (jsdom, in a test) has no scrollIntoView and nothing to scroll.\n if (now !== null && now !== marked && typeof now.scrollIntoView === 'function')\n now.scrollIntoView({ block: 'nearest', inline: 'nearest' })\n marked = now\n })\n observer.observe(element, { subtree: true, childList: true, attributes: true })\n return observer\n }),\n observer => Effect.sync(() => observer.disconnect()),\n ),\n ),\n})\n","/**\n * Script loading as a Command: appends a `src` script to the head unless one\n * is already there. Idempotent by URL — concurrent placements collapse onto\n * the first tag instead of fetching twice. Yields `Loaded` on success and\n * `LoadFailed` otherwise (network error, no document), so `update` handles\n * both outcomes as Messages.\n */\nimport { Effect, Schema } from 'effect'\nimport type { Command } from 'foldkit/command'\nimport { defineMessageUnion } from 'foldkit/message'\n\nexport const ScriptMessage = defineMessageUnion({\n Loaded: {},\n LoadFailed: { message: Schema.String },\n})\nexport type ScriptMessage = typeof ScriptMessage.Type\n\nconst failMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error)\n\n// Tags still fetching, by URL: concurrent loads share one tag and its fate.\nconst inflight = new Map<string, Promise<void>>()\n\n/** Loads a script by URL, once per URL. */\nexport const loadScript = (src: string): Command<ScriptMessage, never, never> => ({\n name: 'Script.load',\n args: { src },\n effect: Effect.matchEffect(\n Effect.tryPromise({\n try: () => {\n if (typeof document === 'undefined') {\n return Promise.reject(new Error('script loading is unavailable'))\n }\n // Concurrent loads share one tag and its fate, instead of racing\n // past the presence check and fetching twice: the in-flight entry\n // is checked before presence, in the same synchronous block that\n // appends, so a second load cannot slip between them.\n const running = inflight.get(src)\n if (running !== undefined) return running\n // getAttribute, not .src: the property resolves to an absolute URL\n // and would never equal the given string.\n const present = Array.from(document.getElementsByTagName('script')).some(\n element => element.getAttribute('src') === src,\n )\n if (present) return Promise.resolve()\n const completion = new Promise<void>((resolve, reject) => {\n const element = document.createElement('script')\n element.src = src\n element.async = true\n element.onload = () => resolve()\n element.onerror = () => {\n // A dead tag must not satisfy later presence checks.\n element.remove()\n reject(new Error(`failed to load script: ${src}`))\n }\n document.head.appendChild(element)\n })\n const tracked = completion.then(\n () => {\n inflight.delete(src)\n },\n (error: unknown) => {\n inflight.delete(src)\n throw error\n },\n )\n inflight.set(src, tracked)\n return tracked\n },\n catch: (error: unknown) => error,\n }),\n {\n onFailure: error => Effect.succeed(ScriptMessage.LoadFailed({ message: failMessage(error) })),\n onSuccess: () => Effect.succeed(ScriptMessage.Loaded()),\n },\n ),\n})\n","/**\n * Web Share as a Command: runs in `update` beside any bundle, yields\n * `Shared` on success. Dismissal is its own outcome, not a failure — the\n * application tells a cancelled sheet apart from a broken one. Without a\n * share API (desktop, insecure context) it yields `ShareFailed` instead of\n * throwing, so `update` handles every outcome as a Message.\n */\nimport { Effect, Schema } from 'effect'\nimport type { Command } from 'foldkit/command'\nimport { defineMessageUnion } from 'foldkit/message'\n\nexport const ShareData = Schema.Struct({\n title: Schema.optional(Schema.String),\n text: Schema.optional(Schema.String),\n url: Schema.optional(Schema.String),\n})\nexport type ShareData = typeof ShareData.Type\n\nexport const ShareMessage = defineMessageUnion({\n Shared: {},\n Dismissed: {},\n ShareFailed: { message: Schema.String },\n})\nexport type ShareMessage = typeof ShareMessage.Type\n\nconst failMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error)\n\nconst isDismissal = (error: unknown): boolean =>\n typeof (error as { readonly name?: unknown } | null)?.name === 'string' &&\n (error as { readonly name: string }).name === 'AbortError'\n\n/** Shares data through the platform sheet. */\nexport const share = (data: ShareData): Command<ShareMessage, never, never> => ({\n name: 'Share.share',\n args: { data },\n effect:\n typeof navigator !== 'undefined' && typeof navigator.share === 'function'\n ? Effect.matchEffect(\n Effect.tryPromise({\n // Omit absent fields: the platform type rejects explicit\n // undefined under exactOptionalPropertyTypes.\n try: () =>\n navigator.share({\n ...(data.title !== undefined ? { title: data.title } : {}),\n ...(data.text !== undefined ? { text: data.text } : {}),\n ...(data.url !== undefined ? { url: data.url } : {}),\n }),\n catch: (error: unknown) => error,\n }),\n {\n onFailure: error =>\n Effect.succeed(\n isDismissal(error)\n ? ShareMessage.Dismissed()\n : ShareMessage.ShareFailed({ message: failMessage(error) }),\n ),\n onSuccess: () => Effect.succeed(ShareMessage.Shared()),\n },\n )\n : Effect.succeed(ShareMessage.ShareFailed({ message: 'share is unavailable' })),\n})\n","/**\n * Measures marked elements as a Mount, for a view to draw something over them,\n * such as an editor's selection outline, with no Message and no Model: where\n * an element is, is presentation, known to the DOM and to no transition.\n *\n * For each named target, the first element in the mounted one's subtree that\n * matches its selector is measured relative to the mounted element's scroll\n * box, which is what an absolutely placed child of it is laid out in. The\n * mounted element gets `--fk-<name>-x`, `-y`, `-w` and `-h` in pixels, and\n * `--fk-<name>-display`: `block` while a target is found, `none` while none is.\n *\n * It measures again when the subtree changes (a selection moves), in the\n * frame the change is drawn in; and, at most once a frame, when the mounted\n * element scrolls, the window resizes, the mounted element or a measured one\n * changes size, an image or a font inside loads, and on every frame of a\n * transition or an animation inside. What moves a target by any other way,\n * such as a stylesheet added that shifts it without resizing it, is seen at\n * the next of these.\n */\nimport { Effect, Schema, Stream } from 'effect'\nimport * as Mount from 'foldkit/mount'\n\n/** The custom properties `Measure` writes for a target named `name`. */\nexport const measured = (name: string) =>\n ({\n x: `--fk-${name}-x`,\n y: `--fk-${name}-y`,\n w: `--fk-${name}-w`,\n h: `--fk-${name}-h`,\n display: `--fk-${name}-display`,\n }) as const\n\nexport const Measure = Mount.defineStream('Measure', {\n messages: [Schema.Never],\n args: {\n /** Each target by name, as a selector within the element: `{ selected: '[aria-selected=\"true\"]' }`. */\n targets: Schema.Record(Schema.String, Schema.String),\n },\n execute: ({ element, targets }) =>\n Stream.callback<never>(() =>\n Effect.acquireRelease(\n Effect.sync(() => {\n const container = element as HTMLElement\n const names = Object.keys(targets)\n let watched: ReadonlyArray<Element> = []\n let frame: number | undefined\n // Whether a transition or an animation may be moving something, until one frame finds none.\n let animating = false\n\n const measure = () => {\n if (frame !== undefined) cancelAnimationFrame(frame)\n frame = undefined\n const box = container.getBoundingClientRect()\n const found: Array<Element> = []\n for (const name of names) {\n const property = measured(name)\n const target = container.querySelector(targets[name]!)\n if (target === null) {\n container.style.setProperty(property.display, 'none')\n continue\n }\n found.push(target)\n const rect = target.getBoundingClientRect()\n // Relative to the scroll box: where the content is, not where the viewport shows it.\n const x = rect.left - box.left - container.clientLeft + container.scrollLeft\n const y = rect.top - box.top - container.clientTop + container.scrollTop\n container.style.setProperty(property.x, `${x}px`)\n container.style.setProperty(property.y, `${y}px`)\n container.style.setProperty(property.w, `${rect.width}px`)\n container.style.setProperty(property.h, `${rect.height}px`)\n container.style.setProperty(property.display, 'block')\n }\n // A measured element that changes size, as an image loading does, is measured again.\n if (found.length !== watched.length || found.some((each, at) => each !== watched[at])) {\n for (const each of watched) sizes.unobserve(each)\n for (const each of found) sizes.observe(each)\n watched = found\n }\n if (animating) {\n // Asked each frame rather than counted: an element removed mid-transition ends none.\n animating = container\n .getAnimations({ subtree: true })\n .some(animation => animation.playState === 'running')\n if (animating) soon()\n }\n }\n const soon = () => {\n if (frame === undefined) frame = requestAnimationFrame(measure)\n }\n const moving = () => {\n animating = true\n soon()\n }\n\n // A change is measured as its records arrive, before the frame it is drawn in is\n // painted, so a box never shows one frame on the element it has left.\n const changes = new MutationObserver(measure)\n // Made, and the fonts listened to, before anything observes: where one is\n // missing (jsdom has neither a ResizeObserver nor `document.fonts`), the Mount\n // fails with nothing attached to call `measure`.\n const sizes = new ResizeObserver(soon)\n document.fonts.addEventListener('loadingdone', soon)\n // Its own properties change the container's style too, but writing the same\n // values again records nothing, so a measure that changed nothing rests.\n changes.observe(container, { subtree: true, childList: true, attributes: true })\n sizes.observe(container)\n container.addEventListener('scroll', soon, { passive: true })\n window.addEventListener('resize', soon)\n // `load` does not bubble, so it is caught on the way down.\n container.addEventListener('load', soon, { capture: true })\n container.addEventListener('transitionrun', moving)\n container.addEventListener('animationstart', moving)\n measure()\n return () => {\n if (frame !== undefined) cancelAnimationFrame(frame)\n changes.disconnect()\n sizes.disconnect()\n container.removeEventListener('scroll', soon)\n window.removeEventListener('resize', soon)\n container.removeEventListener('load', soon, { capture: true })\n document.fonts.removeEventListener('loadingdone', soon)\n container.removeEventListener('transitionrun', moving)\n container.removeEventListener('animationstart', moving)\n }\n }),\n stop => Effect.sync(stop),\n ),\n ),\n})\n","/**\n * Where the window is scrolled, across an application's own navigations, as\n * a stream. Left to itself, a browser keeps the scroll of the entry it\n * leaves when a link pushes a new one, and on Back restores the old offset\n * before the application has drawn the entry it returns to, so the offset is\n * clamped to a page still loading. Here a pushed entry starts at the top,\n * and Back, Forward or a reload return where that entry was, once the page\n * is tall enough to hold it.\n *\n * Three rules, each found the slow way:\n *\n * 1. The offset is taken when the reader acts. The next screen is drawn\n * before a Navigate Command pushes the address, so by the time the URL\n * changes the window is already clamped to the new, shorter page. The\n * scroll is recorded at the reader's click or key (capture phase), and at\n * the start of Back and Forward, which begin in the browser before\n * anything is drawn.\n * 2. A restore holds its place while the next screen settles. A restore made\n * when the entry changes is undone by the loading state drawn right after\n * it, so the target is held on each frame for a moment, until the reader\n * scrolls, touches or types, or another navigation starts.\n * 3. Entries are keyed by the Navigation API's entry key, which a reload and\n * a full navigation keep; offsets rest in `sessionStorage`, so Back into\n * another document restores too. Where the browser has no Navigation API\n * the stream attaches nothing and leaves the browser's own behaviour\n * alone.\n *\n * Lift with `Subscription.persistent`; the stream sends no Messages. Only\n * the window scrolls: a nested container is the route graph's to know, when\n * `Site.transition` arrives.\n */\nimport { Effect, Option, Stream } from 'effect'\n\n/** The part of the Navigation API used here: TypeScript's DOM library does not declare it yet. */\ninterface Navigation extends EventTarget {\n readonly currentEntry: { readonly key: string } | null\n}\ndeclare global {\n interface Window {\n readonly navigation?: Navigation\n }\n}\n\nexport interface KeepScrollOptions {\n /** The `sessionStorage` key holding entry offsets. Default `'foldkit:scroll'`. */\n readonly storageKey?: string | undefined\n /** Entries remembered at most: the oldest are forgotten first. Default 50. */\n readonly kept?: number | undefined\n /**\n * How long a restore holds its place while the next screen settles, in\n * milliseconds. Default 2000.\n */\n readonly patience?: number | undefined\n}\n\nconst navigationTypeOf = (event: Event): Option.Option<unknown> =>\n 'navigationType' in event ? Option.some(event.navigationType) : Option.none()\n\nconst readSaved = (storageKey: string): Map<string, number> => {\n try {\n const stored: unknown = JSON.parse(sessionStorage.getItem(storageKey) ?? '[]')\n return new Map(\n Array.isArray(stored)\n ? stored.filter(\n (pair): pair is [string, number] =>\n Array.isArray(pair) && typeof pair[0] === 'string' && typeof pair[1] === 'number',\n )\n : [],\n )\n } catch {\n // Storage blocked, or something else wrote the key: nothing is remembered.\n return new Map()\n }\n}\n\n/** Starts keeping the scroll; the release undoes everything it installed. */\nconst attach = (storageKey: string, kept: number, patience: number): (() => void) => {\n const navigation = window.navigation\n if (navigation === undefined) return () => {}\n const restoration = window.history.scrollRestoration\n window.history.scrollRestoration = 'manual'\n const saved = readSaved(storageKey)\n const detaches: Array<() => void> = []\n const on = (target: EventTarget, type: string, listener: EventListener, capture?: boolean) => {\n target.addEventListener(type, listener, capture)\n detaches.push(() => target.removeEventListener(type, listener, capture))\n }\n const keyOf = () =>\n Option.fromNullOr(navigation.currentEntry).pipe(Option.map(entry => entry.key))\n /** Where the current entry was left, if it has been. */\n const savedHere = () => Option.flatMap(keyOf(), key => Option.fromUndefinedOr(saved.get(key)))\n const remember = () => {\n const key = keyOf()\n if (Option.isNone(key)) return\n // Moved to the end, so it is the last to be forgotten.\n saved.delete(key.value)\n saved.set(key.value, window.scrollY)\n for (const old of saved.keys()) {\n if (saved.size <= kept) break\n saved.delete(old)\n }\n }\n\n let settling = new AbortController()\n /**\n * Holds the window at `top` while the next screen settles, whenever the page\n * can hold it, until the reader scrolls, touches or types, or another\n * navigation starts.\n */\n const settle = (top: number) => {\n settling.abort()\n settling = new AbortController()\n const { signal } = settling\n for (const input of ['wheel', 'touchstart', 'keydown', 'pointerdown'])\n window.addEventListener(input, () => settling.abort(), { passive: true, signal })\n const until = performance.now() + patience\n const step = () => {\n if (signal.aborted) return\n const room = document.documentElement.scrollHeight - window.innerHeight\n if (room >= top && Math.round(window.scrollY) !== top)\n window.scrollTo({ top, behavior: 'instant' })\n if (performance.now() < until) requestAnimationFrame(step)\n else settling.abort()\n }\n step()\n }\n\n // Remembered where the reader acts, before the application does: by the time\n // it pushes an address it has drawn the next screen, and the window is\n // already clamped to it. Back and Forward start in the browser, before\n // anything is drawn, so they are remembered as they begin.\n const acted = () => remember()\n for (const input of ['click', 'keydown']) on(window, input, acted, true)\n const traversing = (event: Event) => {\n if (Option.contains(navigationTypeOf(event), 'traverse')) remember()\n }\n on(navigation, 'navigate', traversing)\n const changed = (event: Event) => {\n const type = navigationTypeOf(event)\n if (Option.contains(type, 'push')) settle(0)\n else if (Option.contains(type, 'traverse')) settle(Option.getOrElse(savedHere(), () => 0))\n // A replace is the same entry, narrowed or selected in place: it stays where it is.\n }\n on(navigation, 'currententrychange', changed)\n const hidden = () => {\n remember()\n try {\n sessionStorage.setItem(storageKey, JSON.stringify([...saved]))\n } catch {\n // Storage blocked: the next document starts at the top.\n }\n }\n on(window, 'pagehide', hidden)\n // A reload, or Back into this document from another: where this entry was.\n const here = savedHere()\n if (Option.isSome(here)) settle(here.value)\n return () => {\n settling.abort()\n for (const detach of detaches) detach()\n window.history.scrollRestoration = restoration\n }\n}\n\n/**\n * Keeps the window's scroll across same-document navigations: a pushed entry\n * starts at the top, and Back, Forward or a reload return where that entry\n * was. Without a window the stream is empty instead of throwing. Lift with\n * `Subscription.persistent`; the stream sends no Messages.\n */\nexport const keepScroll = (options: KeepScrollOptions = {}): Stream.Stream<never> => {\n if (typeof window === 'undefined') return Stream.empty\n const storageKey = options.storageKey ?? 'foldkit:scroll'\n const kept = options.kept ?? 50\n const patience = options.patience ?? 2000\n return Stream.callback<never>(() =>\n Effect.acquireRelease(\n Effect.sync(() => attach(storageKey, kept, patience)),\n detach => Effect.sync(detach),\n ),\n )\n}\n"],"mappings":"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"}
|