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/state/index.d.mts
CHANGED
|
@@ -6,11 +6,53 @@ export declare const HistoryModel: <Value>(value: Schema.Codec<Value, unknown>)
|
|
|
6
6
|
readonly past: Schema.$Array<Schema.Codec<Value, unknown, never, never>>;
|
|
7
7
|
readonly present: Schema.Codec<Value, unknown, never, never>;
|
|
8
8
|
readonly future: Schema.$Array<Schema.Codec<Value, unknown, never, never>>;
|
|
9
|
+
/** The group of the last push, so the next push of the same group joins it; `null` for none. */
|
|
10
|
+
readonly group: Schema.NullOr<Schema.String>;
|
|
9
11
|
}>;
|
|
10
12
|
export type HistoryModel<Value> = {
|
|
11
13
|
readonly past: ReadonlyArray<Value>;
|
|
12
14
|
readonly present: Value;
|
|
13
15
|
readonly future: ReadonlyArray<Value>;
|
|
16
|
+
readonly group: string | null;
|
|
17
|
+
};
|
|
18
|
+
/** Whether Undo would move: the past is non-empty. */
|
|
19
|
+
export declare const canUndo: <Value>(model: HistoryModel<Value>) => boolean;
|
|
20
|
+
/** Whether Redo would move: the future is non-empty. */
|
|
21
|
+
export declare const canRedo: <Value>(model: HistoryModel<Value>) => boolean;
|
|
22
|
+
/** The steps of a history as pure functions of its Model. */
|
|
23
|
+
export declare const History: {
|
|
24
|
+
/** A history whose present is `value`, with nothing to undo. */
|
|
25
|
+
start: <Value>(present: Value) => HistoryModel<Value>;
|
|
26
|
+
/** Records `value` as the present; the same `group` as the last push joins its step. */
|
|
27
|
+
push: <Value>(model: HistoryModel<Value>, value: Value, options: {
|
|
28
|
+
readonly capacity: number;
|
|
29
|
+
readonly group?: string | null | undefined;
|
|
30
|
+
}) => HistoryModel<Value>;
|
|
31
|
+
/** One step back, or the Model as it was when there is none. */
|
|
32
|
+
undo: <Value>(model: HistoryModel<Value>) => HistoryModel<Value>;
|
|
33
|
+
/** One step forward, or the Model as it was when there is none. */
|
|
34
|
+
redo: <Value>(model: HistoryModel<Value>) => HistoryModel<Value>;
|
|
35
|
+
/**
|
|
36
|
+
* The value at `step` made the present, counting every kept value from the
|
|
37
|
+
* oldest (0), so the present is at `past.length`: what a click on a step's
|
|
38
|
+
* thumbnail does. The Model as it was for the present or a step not kept.
|
|
39
|
+
*/
|
|
40
|
+
goTo: <Value>(model: HistoryModel<Value>, step: number) => HistoryModel<Value>;
|
|
41
|
+
/** Nothing to undo or redo, the present kept. */
|
|
42
|
+
clear: <Value>(model: HistoryModel<Value>) => HistoryModel<Value>;
|
|
43
|
+
/** The next push starts a step of its own, whatever its group: a group ends here. */
|
|
44
|
+
close: <Value>(model: HistoryModel<Value>) => HistoryModel<Value>;
|
|
45
|
+
/**
|
|
46
|
+
* The step `group` is making taken back, and not redoable: what a cancelled
|
|
47
|
+
* edit, or one that ended where it began, leaves. The Model as it was when
|
|
48
|
+
* the last push was not of `group`. What the group's first push dropped
|
|
49
|
+
* stays dropped: the redo future it cleared, and an entry capacity evicted;
|
|
50
|
+
* with a capacity of 0 nothing was kept, so the present stays and only the
|
|
51
|
+
* group ends.
|
|
52
|
+
*/
|
|
53
|
+
revert: <Value>(model: HistoryModel<Value>, group: string) => HistoryModel<Value>;
|
|
54
|
+
canUndo: <Value>(model: HistoryModel<Value>) => boolean;
|
|
55
|
+
canRedo: <Value>(model: HistoryModel<Value>) => boolean;
|
|
14
56
|
};
|
|
15
57
|
export declare const history: <const Name extends string, Value>(config: {
|
|
16
58
|
readonly name: Name;
|
|
@@ -18,33 +60,33 @@ export declare const history: <const Name extends string, Value>(config: {
|
|
|
18
60
|
readonly capacity?: number | undefined;
|
|
19
61
|
}) => Bundle.Bundle<Name, {
|
|
20
62
|
readonly initial: Value;
|
|
21
|
-
}, {
|
|
22
|
-
readonly
|
|
23
|
-
|
|
24
|
-
readonly future: readonly Value[];
|
|
25
|
-
}, {
|
|
63
|
+
}, HistoryModel<Value>, {
|
|
64
|
+
readonly _tag: "Clear";
|
|
65
|
+
} | {
|
|
26
66
|
readonly _tag: "Push";
|
|
27
67
|
readonly value: Value;
|
|
68
|
+
readonly group?: string | undefined;
|
|
28
69
|
} | {
|
|
29
70
|
readonly _tag: "Undo";
|
|
30
71
|
} | {
|
|
31
72
|
readonly _tag: "Redo";
|
|
32
73
|
} | {
|
|
33
|
-
readonly _tag: "
|
|
74
|
+
readonly _tag: "GoTo";
|
|
75
|
+
readonly step: number;
|
|
34
76
|
}, never, never, never, void, {}, {}> & {
|
|
35
77
|
Message: import("foldkit/message").MessageUnion<{
|
|
36
78
|
readonly Push: {
|
|
37
79
|
readonly value: Schema.Codec<Value, unknown, never, never>;
|
|
80
|
+
readonly group: Schema.optional<Schema.String>;
|
|
38
81
|
};
|
|
39
82
|
readonly Undo: {};
|
|
40
83
|
readonly Redo: {};
|
|
84
|
+
readonly GoTo: {
|
|
85
|
+
readonly step: Schema.Number;
|
|
86
|
+
};
|
|
41
87
|
readonly Clear: {};
|
|
42
88
|
}>;
|
|
43
89
|
};
|
|
44
|
-
/** Whether Undo would move: the past is non-empty. */
|
|
45
|
-
export declare const canUndo: <Value>(model: HistoryModel<Value>) => boolean;
|
|
46
|
-
/** Whether Redo would move: the future is non-empty. */
|
|
47
|
-
export declare const canRedo: <Value>(model: HistoryModel<Value>) => boolean;
|
|
48
90
|
//#endregion
|
|
49
91
|
//#region src/state/locale.d.ts
|
|
50
92
|
export declare const LocaleModel: Schema.Struct<{
|
|
@@ -149,8 +191,6 @@ export declare const isSelected: (model: SelectionSetModel, id: string) => boole
|
|
|
149
191
|
export declare const SelectionSet: Bundle.Bundle<"SelectionSet", void, {
|
|
150
192
|
readonly selected: readonly string[];
|
|
151
193
|
}, {
|
|
152
|
-
readonly _tag: "Clear";
|
|
153
|
-
} | {
|
|
154
194
|
readonly _tag: "Select";
|
|
155
195
|
readonly id: string;
|
|
156
196
|
} | {
|
|
@@ -162,6 +202,8 @@ export declare const SelectionSet: Bundle.Bundle<"SelectionSet", void, {
|
|
|
162
202
|
} | {
|
|
163
203
|
readonly _tag: "ReplaceAll";
|
|
164
204
|
readonly ids: readonly string[];
|
|
205
|
+
} | {
|
|
206
|
+
readonly _tag: "Clear";
|
|
165
207
|
}, never, never, never, void, {}, {}>;
|
|
166
208
|
//#endregion
|
|
167
209
|
//#region src/state/virtual.d.ts
|
|
@@ -269,15 +311,15 @@ export declare const Virtual: Bundle.Bundle<"Virtual", {
|
|
|
269
311
|
readonly _tag: "Measured";
|
|
270
312
|
readonly key: string;
|
|
271
313
|
readonly height: number;
|
|
272
|
-
} | {
|
|
273
|
-
readonly _tag: "Settled";
|
|
274
|
-
readonly generation: number;
|
|
275
314
|
} | {
|
|
276
315
|
readonly _tag: "Scrolled";
|
|
277
316
|
readonly top: number;
|
|
278
317
|
} | {
|
|
279
318
|
readonly _tag: "Prune";
|
|
280
319
|
readonly keys: readonly string[];
|
|
320
|
+
} | {
|
|
321
|
+
readonly _tag: "Settled";
|
|
322
|
+
readonly generation: number;
|
|
281
323
|
}, never, never, never, void, {}, {}>;
|
|
282
324
|
export declare const ViewportScrolled: Schema.TaggedStruct<"ViewportScrolled", {
|
|
283
325
|
readonly top: Schema.Number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/state/history.ts","../../src/state/locale.ts","../../src/state/pagination.ts","../../src/state/range.ts","../../src/state/selection.ts","../../src/state/virtual.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/state/history.ts","../../src/state/locale.ts","../../src/state/pagination.ts","../../src/state/range.ts","../../src/state/selection.ts","../../src/state/virtual.ts"],"mappings":";;;;qBAiBa,eAAgB,OAAO,OAAO,OAAO,MAAM,oBAAe,OAAA;;;;;;;YAQ3D,aAAa;WACd,MAAM,cAAc;WACpB,SAAS;WACT,QAAQ,cAAc;WACtB;;;qBA2FE,UAAW,OAAO,OAAO,aAAa;;qBAGtC,UAAW,OAAO,OAAO,aAAa;;qBAGtC;;EA9FE,QAAA,OAAK,SAAW,UAAQ,aAAa;;EAetC,OAAA,OAAK,OACV,aAAa,QAAM,OACnB,OAAK;aACQ;aAA2B;QAC9C,aAAa;;EAcF,OAAA,OAAK,OAAS,aAAa,WAAS,aAAa;;EAWjD,OAAA,OAAK,OAAS,aAAa,WAAS,aAAa;;;;;;EAWjD,OAAA,OAAK,OAAS,aAAa,QAAM,iBAAiB,aAAa;;EAY9D,QAAA,OAAK,OAAS,aAAa,WAAS,aAAa;;EAKjD,QAAA,OAAK,OAAS,aAAa,WAAS,aAAa;;;;;;;;;EAGhD,SAAA,OAAK,OAAS,aAAa,QAAM,kBAAkB,aAAa;EAaxD,UAAA,OAAK,OAAS,aAAa;EAG3B,UAAA,OAAK,OAAS,aAAa;;qBAmCtC,gBAAiB,qBAAqB,OAAO;WAC/C,MAAM;WACN,OAAO,OAAO,MAAM;WACpB;MACV,OAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBCxJY,aAAW,OAAA;;;YACZ,qBAAqB,YAAY;qBAEhC,yCAAa;;qBAAb,OAAA;;;YACD,uBAAuB,cAAc;qBAEpC,QAAM,OAAA;;;;;;;;;;qBCNN,iBAAe,OAAA;;;;;YAKhB,yBAAyB,gBAAgB;qBAExC,6CAAiB;;;;;;;;;;;;;YAOlB,2BAA2B,kBAAkB;;qBAU5C,YAAa,OAAO;;qBAIpB,SAAU,OAAO;qBAEjB,YAAU,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBCjCV,QAAS,eAAe,aAAa,kBAAW;;;qBCGhD,mBAAiB,OAAA;;;YAClB,2BAA2B,kBAAkB;qBAE5C,+CAAmB;;;;;;;;;;;;;;;YAOpB,6BAA6B,oBAAoB;;qBAMhD,aAAc,OAAO,mBAAmB;qBAGxC,cAAY,OAAA;;;;;;;;;;;;;;;;;;;iBCTR;;WAEN;;WAEA;;;iBAIM;WACN;WACA;WACA;WACA;;;qBAmEE,cACX,MAAM,uBACN,SAAS,SAAS,yBAClB,QAAQ;;qBAcG,YACX,eACA,MAAM,uBACN,SAAS,SAAS,yBAClB,QAAQ;;;;;;;;qBAUG,eACX,MAAM,uBACN,SAAS,SAAS,yBAClB,QAAQ,eACR,mBACA,wBACA,qBACC;qBAmBU,cAAY,OAAA;;;;;;;;;;;YAWb,sBAAsB,aAAa;qBAElC,0CAAc;;;;;;;;;;;;;;;;;;;YAUf,wBAAwB,eAAe;;;;;;;qBAetC,YACX,OAAO,cACP,MAAM,uBACN,2BACC;WAA2B;;;;;;;qBAkBjB,gBACX,OAAO,cACP,MAAM,uBACN;;;;;;;qBAYW,UACX,OAAO,cACP,MAAM,uBACN,wBACA;qBAYW,SAAO,OAAA;WAKU;WACP;WACL;WACS;WACF;WACD;WACQ;WACF,iBAAA,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAyG1B,kBAAgB,OAAA;;;YACjB,0BAA0B,iBAAiB;;;;;;qBAO1C,UAAQ,MAAA;;;;qBAeR,aAAW,OAAA;;;;YAIZ,qBAAqB,YAAY;;;;;;qBAShC,YAAU,MAAA;;;;;;;;;;;;;;qBAgCV,eACX,UAAU;WAAyB;WAAsB;IACzD;iBAae;WACN;WACA;WACA;WACA;WACA;WACA;;;;;;;;;qBAUE,UACX,MAAM,uBACN,SAAS,SAAS,yBAClB;WACW;WACA;WACA;WACA;;WAEC,YAAY,cAAc;WAA4B"}
|
package/dist/state/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { i as unlessSame, r as sameIds } from "../internal-DP-xLKbT.mjs";
|
|
1
2
|
import { Effect, Queue, Schema, Stream } from "effect";
|
|
2
3
|
import * as Mount from "foldkit/mount";
|
|
3
4
|
import { defineMessageUnion } from "foldkit/message";
|
|
@@ -7,67 +8,162 @@ import { Bundle } from "foldkit-bundle";
|
|
|
7
8
|
* Undo/redo as a bundle factory: the past, present, and future of one value
|
|
8
9
|
* live in the Model, so every step replays. Pushing records, even an unchanged
|
|
9
10
|
* value; skipping duplicates is the application's policy, not the stack's.
|
|
11
|
+
*
|
|
12
|
+
* A push may name a `group`. Consecutive pushes of the same group are one step:
|
|
13
|
+
* the present moves and nothing new is recorded, so typing a word undoes as a
|
|
14
|
+
* whole. Grouping needs no clock; the application names what belongs together.
|
|
15
|
+
*
|
|
16
|
+
* The steps are also pure functions (`History.push`, `undo`, `redo`, `goTo`, `clear`),
|
|
17
|
+
* for a parent that records an edit in the same transition that makes it
|
|
18
|
+
* rather than dispatching a Message to a placed bundle.
|
|
10
19
|
*/
|
|
11
20
|
const HistoryModel = (value) => Schema.Struct({
|
|
12
21
|
past: Schema.Array(value),
|
|
13
22
|
present: value,
|
|
14
|
-
future: Schema.Array(value)
|
|
23
|
+
future: Schema.Array(value),
|
|
24
|
+
/** The group of the last push, so the next push of the same group joins it; `null` for none. */
|
|
25
|
+
group: Schema.NullOr(Schema.String)
|
|
26
|
+
});
|
|
27
|
+
const start = (present) => ({
|
|
28
|
+
past: [],
|
|
29
|
+
present,
|
|
30
|
+
future: [],
|
|
31
|
+
group: null
|
|
15
32
|
});
|
|
33
|
+
/**
|
|
34
|
+
* No redo steps, keeping an already empty future: a view that reads the
|
|
35
|
+
* future compares it by identity, so a push per keystroke or stroke cell
|
|
36
|
+
* must not hand it a new empty array each time.
|
|
37
|
+
*/
|
|
38
|
+
const noFuture = (model) => model.future.length === 0 ? model.future : [];
|
|
39
|
+
const push = (model, value, options) => {
|
|
40
|
+
const group = options.group ?? null;
|
|
41
|
+
if (group !== null && group === model.group && model.past.length > 0) return {
|
|
42
|
+
...model,
|
|
43
|
+
present: value,
|
|
44
|
+
future: noFuture(model)
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
past: options.capacity === 0 ? [] : [...model.past, model.present].slice(-options.capacity),
|
|
48
|
+
present: value,
|
|
49
|
+
future: noFuture(model),
|
|
50
|
+
group
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
const undo = (model) => {
|
|
54
|
+
const previous = model.past[model.past.length - 1];
|
|
55
|
+
if (previous === void 0) return model;
|
|
56
|
+
return {
|
|
57
|
+
past: model.past.slice(0, -1),
|
|
58
|
+
present: previous,
|
|
59
|
+
future: [model.present, ...model.future],
|
|
60
|
+
group: null
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
const redo = (model) => {
|
|
64
|
+
const next = model.future[0];
|
|
65
|
+
if (next === void 0) return model;
|
|
66
|
+
return {
|
|
67
|
+
past: [...model.past, model.present],
|
|
68
|
+
present: next,
|
|
69
|
+
future: model.future.slice(1),
|
|
70
|
+
group: null
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
const goTo = (model, step) => {
|
|
74
|
+
const steps = [
|
|
75
|
+
...model.past,
|
|
76
|
+
model.present,
|
|
77
|
+
...model.future
|
|
78
|
+
];
|
|
79
|
+
if (!Number.isInteger(step) || step < 0 || step >= steps.length || step === model.past.length) return model;
|
|
80
|
+
return {
|
|
81
|
+
past: steps.slice(0, step),
|
|
82
|
+
present: steps[step],
|
|
83
|
+
future: steps.slice(step + 1),
|
|
84
|
+
group: null
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
const clear = (model) => model.past.length === 0 && model.future.length === 0 && model.group === null ? model : start(model.present);
|
|
88
|
+
const close = (model) => model.group === null ? model : {
|
|
89
|
+
...model,
|
|
90
|
+
group: null
|
|
91
|
+
};
|
|
92
|
+
const revert = (model, group) => model.group === group ? {
|
|
93
|
+
...undo(model),
|
|
94
|
+
future: [],
|
|
95
|
+
group: null
|
|
96
|
+
} : model;
|
|
97
|
+
const checkCapacity = (name, capacity) => {
|
|
98
|
+
if (!Number.isInteger(capacity) || capacity < 0) throw new Error(`History "${name}": capacity must be a non-negative integer`);
|
|
99
|
+
return capacity;
|
|
100
|
+
};
|
|
101
|
+
/** Whether Undo would move: the past is non-empty. */
|
|
102
|
+
const canUndo = (model) => model.past.length > 0;
|
|
103
|
+
/** Whether Redo would move: the future is non-empty. */
|
|
104
|
+
const canRedo = (model) => model.future.length > 0;
|
|
105
|
+
/** The steps of a history as pure functions of its Model. */
|
|
106
|
+
const History = {
|
|
107
|
+
/** A history whose present is `value`, with nothing to undo. */
|
|
108
|
+
start,
|
|
109
|
+
/** Records `value` as the present; the same `group` as the last push joins its step. */
|
|
110
|
+
push,
|
|
111
|
+
/** One step back, or the Model as it was when there is none. */
|
|
112
|
+
undo,
|
|
113
|
+
/** One step forward, or the Model as it was when there is none. */
|
|
114
|
+
redo,
|
|
115
|
+
/**
|
|
116
|
+
* The value at `step` made the present, counting every kept value from the
|
|
117
|
+
* oldest (0), so the present is at `past.length`: what a click on a step's
|
|
118
|
+
* thumbnail does. The Model as it was for the present or a step not kept.
|
|
119
|
+
*/
|
|
120
|
+
goTo,
|
|
121
|
+
/** Nothing to undo or redo, the present kept. */
|
|
122
|
+
clear,
|
|
123
|
+
/** The next push starts a step of its own, whatever its group: a group ends here. */
|
|
124
|
+
close,
|
|
125
|
+
/**
|
|
126
|
+
* The step `group` is making taken back, and not redoable: what a cancelled
|
|
127
|
+
* edit, or one that ended where it began, leaves. The Model as it was when
|
|
128
|
+
* the last push was not of `group`. What the group's first push dropped
|
|
129
|
+
* stays dropped: the redo future it cleared, and an entry capacity evicted;
|
|
130
|
+
* with a capacity of 0 nothing was kept, so the present stays and only the
|
|
131
|
+
* group ends.
|
|
132
|
+
*/
|
|
133
|
+
revert,
|
|
134
|
+
canUndo,
|
|
135
|
+
canRedo
|
|
136
|
+
};
|
|
16
137
|
const history = (config) => {
|
|
17
|
-
const capacity = config.capacity ?? 100;
|
|
18
|
-
if (!Number.isInteger(capacity) || capacity < 0) throw new Error(`History "${config.name}": capacity must be a non-negative integer`);
|
|
138
|
+
const capacity = checkCapacity(config.name, config.capacity ?? 100);
|
|
19
139
|
const Model = HistoryModel(config.value);
|
|
20
140
|
const Message = defineMessageUnion({
|
|
21
|
-
Push: {
|
|
141
|
+
Push: {
|
|
142
|
+
value: config.value,
|
|
143
|
+
group: Schema.optional(Schema.String)
|
|
144
|
+
},
|
|
22
145
|
Undo: {},
|
|
23
146
|
Redo: {},
|
|
147
|
+
GoTo: { step: Schema.Number },
|
|
24
148
|
Clear: {}
|
|
25
149
|
});
|
|
26
150
|
return Object.assign(Bundle.make(config.name, {
|
|
27
151
|
Model,
|
|
28
152
|
Message,
|
|
29
153
|
args: Schema.Struct({ initial: config.value }),
|
|
30
|
-
init: (args) => ({ model:
|
|
31
|
-
past: [],
|
|
32
|
-
present: args.initial,
|
|
33
|
-
future: []
|
|
34
|
-
} }),
|
|
154
|
+
init: (args) => ({ model: start(args.initial) }),
|
|
35
155
|
update: (model, message) => Message.match(message, {
|
|
36
|
-
Push: ({ value }) => ({ model: {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return { model: {
|
|
45
|
-
past: model.past.slice(0, -1),
|
|
46
|
-
present: previous,
|
|
47
|
-
future: [model.present, ...model.future]
|
|
48
|
-
} };
|
|
49
|
-
},
|
|
50
|
-
Redo: () => {
|
|
51
|
-
const next = model.future[0];
|
|
52
|
-
if (next === void 0) return { model };
|
|
53
|
-
return { model: {
|
|
54
|
-
past: [...model.past, model.present],
|
|
55
|
-
present: next,
|
|
56
|
-
future: model.future.slice(1)
|
|
57
|
-
} };
|
|
58
|
-
},
|
|
59
|
-
Clear: () => ({ model: {
|
|
60
|
-
past: [],
|
|
61
|
-
present: model.present,
|
|
62
|
-
future: []
|
|
63
|
-
} })
|
|
156
|
+
Push: ({ value, group }) => ({ model: push(model, value, {
|
|
157
|
+
capacity,
|
|
158
|
+
group
|
|
159
|
+
}) }),
|
|
160
|
+
Undo: () => ({ model: undo(model) }),
|
|
161
|
+
Redo: () => ({ model: redo(model) }),
|
|
162
|
+
GoTo: ({ step }) => ({ model: goTo(model, step) }),
|
|
163
|
+
Clear: () => ({ model: clear(model) })
|
|
64
164
|
})
|
|
65
165
|
}), { Message });
|
|
66
166
|
};
|
|
67
|
-
/** Whether Undo would move: the past is non-empty. */
|
|
68
|
-
const canUndo = (model) => model.past.length > 0;
|
|
69
|
-
/** Whether Redo would move: the future is non-empty. */
|
|
70
|
-
const canRedo = (model) => model.future.length > 0;
|
|
71
167
|
//#endregion
|
|
72
168
|
//#region src/state/locale.ts
|
|
73
169
|
/**
|
|
@@ -83,10 +179,10 @@ const Locale = Bundle.make("Locale", {
|
|
|
83
179
|
Message: LocaleMessage,
|
|
84
180
|
args: Schema.Struct({ default: Schema.String }),
|
|
85
181
|
init: (args) => ({ model: { locale: typeof navigator !== "undefined" && typeof navigator.language === "string" ? navigator.language : args.default } }),
|
|
86
|
-
update: (model, message) => LocaleMessage.match(message, { SetLocale: ({ locale }) =>
|
|
182
|
+
update: (model, message) => LocaleMessage.match(message, { SetLocale: ({ locale }) => locale === model.locale ? { model } : { model: {
|
|
87
183
|
...model,
|
|
88
184
|
locale
|
|
89
|
-
} }
|
|
185
|
+
} } })
|
|
90
186
|
});
|
|
91
187
|
//#endregion
|
|
92
188
|
//#region src/state/pagination.ts
|
|
@@ -130,28 +226,28 @@ const Pagination = Bundle.make("Pagination", {
|
|
|
130
226
|
total: args.total ?? null
|
|
131
227
|
} }),
|
|
132
228
|
update: (model, message) => PaginationMessage.match(message, {
|
|
133
|
-
GoToPage: ({ page }) => ({ model: {
|
|
229
|
+
GoToPage: ({ page }) => ({ model: unlessSame(model, {
|
|
134
230
|
...model,
|
|
135
231
|
page: clampPage(page, model.perPage, model.total)
|
|
136
|
-
} }),
|
|
137
|
-
NextPage: () => ({ model: {
|
|
232
|
+
}) }),
|
|
233
|
+
NextPage: () => ({ model: unlessSame(model, {
|
|
138
234
|
...model,
|
|
139
235
|
page: clampPage(model.page + 1, model.perPage, model.total)
|
|
140
|
-
} }),
|
|
141
|
-
PrevPage: () => ({ model: {
|
|
236
|
+
}) }),
|
|
237
|
+
PrevPage: () => ({ model: unlessSame(model, {
|
|
142
238
|
...model,
|
|
143
239
|
page: clampPage(model.page - 1, model.perPage, model.total)
|
|
144
|
-
} }),
|
|
145
|
-
SetPerPage: ({ perPage }) => perPage > 0 ? { model: {
|
|
240
|
+
}) }),
|
|
241
|
+
SetPerPage: ({ perPage }) => perPage > 0 ? { model: unlessSame(model, {
|
|
146
242
|
...model,
|
|
147
243
|
perPage,
|
|
148
244
|
page: clampPage(model.page, perPage, model.total)
|
|
149
|
-
} } : { model },
|
|
150
|
-
SetTotal: ({ total }) => total === null || total >= 0 ? { model: {
|
|
245
|
+
}) } : { model },
|
|
246
|
+
SetTotal: ({ total }) => total === null || total >= 0 ? { model: unlessSame(model, {
|
|
151
247
|
...model,
|
|
152
248
|
total,
|
|
153
249
|
page: clampPage(model.page, model.perPage, total)
|
|
154
|
-
} } : { model }
|
|
250
|
+
}) } : { model }
|
|
155
251
|
})
|
|
156
252
|
});
|
|
157
253
|
//#endregion
|
|
@@ -194,10 +290,13 @@ const SelectionSet = Bundle.make("SelectionSet", {
|
|
|
194
290
|
init: () => ({ model: { selected: [] } }),
|
|
195
291
|
update: (model, message) => SelectionSetMessage.match(message, {
|
|
196
292
|
Select: ({ id }) => isSelected(model, id) ? { model } : { model: { selected: [...model.selected, id] } },
|
|
197
|
-
Deselect: ({ id }) => ({ model: { selected: without(model.selected, id) } }
|
|
293
|
+
Deselect: ({ id }) => isSelected(model, id) ? { model: { selected: without(model.selected, id) } } : { model },
|
|
198
294
|
Toggle: ({ id }) => isSelected(model, id) ? { model: { selected: without(model.selected, id) } } : { model: { selected: [...model.selected, id] } },
|
|
199
|
-
ReplaceAll: ({ ids }) =>
|
|
200
|
-
|
|
295
|
+
ReplaceAll: ({ ids }) => {
|
|
296
|
+
const selected = [...new Set(ids)];
|
|
297
|
+
return sameIds(selected, model.selected) ? { model } : { model: { selected } };
|
|
298
|
+
},
|
|
299
|
+
Clear: () => model.selected.length === 0 ? { model } : { model: { selected: [] } }
|
|
201
300
|
})
|
|
202
301
|
});
|
|
203
302
|
//#endregion
|
|
@@ -222,22 +321,40 @@ const heightAt = (index, heights, keys, layout) => {
|
|
|
222
321
|
if (key === void 0) return layout.estimatedHeight;
|
|
223
322
|
return saneHeight(heights[key] ?? layout.estimatedHeight, layout.estimatedHeight);
|
|
224
323
|
};
|
|
324
|
+
const offsetsCache = /* @__PURE__ */ new WeakMap();
|
|
325
|
+
const sameLayout = (a, b) => a.estimatedHeight === b.estimatedHeight && a.gap === b.gap && a.paddingStart === b.paddingStart && a.paddingEnd === b.paddingEnd;
|
|
326
|
+
const topsFor = (keys, heights, layout) => {
|
|
327
|
+
const cached = offsetsCache.get(keys);
|
|
328
|
+
if (cached !== void 0 && cached.heights === heights && sameLayout(cached.layout, layout)) return cached.tops;
|
|
329
|
+
const tops = new Float64Array(keys.length + 1);
|
|
330
|
+
tops[0] = layout.paddingStart;
|
|
331
|
+
for (let index = 0; index < keys.length; index++) tops[index + 1] = tops[index] + heightAt(index, heights, keys, layout) + layout.gap;
|
|
332
|
+
offsetsCache.set(keys, {
|
|
333
|
+
heights,
|
|
334
|
+
layout: { ...layout },
|
|
335
|
+
tops
|
|
336
|
+
});
|
|
337
|
+
return tops;
|
|
338
|
+
};
|
|
339
|
+
/** The first index in `[from, to)` whose value passes `test`, or `to`; `test` must be monotonic. */
|
|
340
|
+
const firstIndex = (from, to, test) => {
|
|
341
|
+
let low = from;
|
|
342
|
+
let high = to;
|
|
343
|
+
while (low < high) {
|
|
344
|
+
const middle = low + high >>> 1;
|
|
345
|
+
if (test(middle)) high = middle;
|
|
346
|
+
else low = middle + 1;
|
|
347
|
+
}
|
|
348
|
+
return low;
|
|
349
|
+
};
|
|
225
350
|
/** Total scrollable height: padding, measured rows, estimates, and gaps. */
|
|
226
351
|
const totalHeight = (keys, heights, layout) => {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (index < keys.length - 1) total += layout.gap;
|
|
231
|
-
}
|
|
232
|
-
return total;
|
|
352
|
+
const last = keys.length - 1;
|
|
353
|
+
if (last === -1) return layout.paddingStart + layout.paddingEnd;
|
|
354
|
+
return topsFor(keys, heights, layout)[last] + heightAt(last, heights, keys, layout) + layout.paddingEnd;
|
|
233
355
|
};
|
|
234
356
|
/** Pixel offset of a row's top edge: padding plus the prefix before it. */
|
|
235
|
-
const offsetFor = (index, keys, heights, layout) =>
|
|
236
|
-
const clamped = Math.max(0, Math.min(index, keys.length));
|
|
237
|
-
let offset = layout.paddingStart;
|
|
238
|
-
for (let i = 0; i < clamped; i++) offset += heightAt(i, heights, keys, layout) + layout.gap;
|
|
239
|
-
return offset;
|
|
240
|
-
};
|
|
357
|
+
const offsetFor = (index, keys, heights, layout) => topsFor(keys, heights, layout)[Math.max(0, Math.min(index, keys.length))];
|
|
241
358
|
/**
|
|
242
359
|
* Which rows to render for a scroll position: the rows intersecting
|
|
243
360
|
* `[scrollTop, scrollTop + viewportHeight)`, widened by `overscan` on both
|
|
@@ -251,23 +368,11 @@ const visibleRange = (keys, heights, layout, scrollTop, viewportHeight, overscan
|
|
|
251
368
|
start: 0,
|
|
252
369
|
end: 0
|
|
253
370
|
};
|
|
371
|
+
const tops = topsFor(keys, heights, layout);
|
|
254
372
|
const top = Math.max(0, scrollTop);
|
|
255
373
|
const bottom = top + viewportHeight;
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
for (let index = 0; index < count; index++) {
|
|
259
|
-
const height = heightAt(index, heights, keys, layout);
|
|
260
|
-
if (offset + height <= top) {
|
|
261
|
-
start = index + 1;
|
|
262
|
-
offset += height + layout.gap;
|
|
263
|
-
} else break;
|
|
264
|
-
}
|
|
265
|
-
let end = start;
|
|
266
|
-
let cursor = offset;
|
|
267
|
-
while (end < count && cursor < bottom) {
|
|
268
|
-
cursor += heightAt(end, heights, keys, layout) + layout.gap;
|
|
269
|
-
end += 1;
|
|
270
|
-
}
|
|
374
|
+
const start = firstIndex(0, count, (index) => tops[index] + heightAt(index, heights, keys, layout) > top);
|
|
375
|
+
const end = firstIndex(start, count, (index) => tops[index] >= bottom);
|
|
271
376
|
return {
|
|
272
377
|
start: Math.max(0, start - Math.max(0, overscan)),
|
|
273
378
|
end: Math.min(count, end + Math.max(0, overscan))
|
|
@@ -331,8 +436,9 @@ const distanceToEnd = (model, keys, viewportHeight) => {
|
|
|
331
436
|
* to zero.
|
|
332
437
|
*/
|
|
333
438
|
const isAtEnd = (model, keys, viewportHeight, threshold = 0) => {
|
|
334
|
-
|
|
335
|
-
|
|
439
|
+
const total = totalHeight(keys, model.heights, layoutOf(model));
|
|
440
|
+
if (total <= 0) return true;
|
|
441
|
+
return total - (model.scrollTop + viewportHeight) <= Math.max(0, threshold);
|
|
336
442
|
};
|
|
337
443
|
const saneTop = (top) => Number.isFinite(top) ? Math.max(0, top) : null;
|
|
338
444
|
const saneMeasured = (height) => Number.isFinite(height) && height >= 0 ? height : null;
|
|
@@ -370,7 +476,7 @@ const Virtual = Bundle.make("Virtual", {
|
|
|
370
476
|
update: (model, message, args) => VirtualMessage.match(message, {
|
|
371
477
|
Scrolled: ({ top }) => {
|
|
372
478
|
const sane = saneTop(top);
|
|
373
|
-
if (sane === null) return { model };
|
|
479
|
+
if (sane === null || top === model.scrollTop) return { model };
|
|
374
480
|
const generation = model.generation + 1;
|
|
375
481
|
return {
|
|
376
482
|
model: {
|
|
@@ -388,7 +494,7 @@ const Virtual = Bundle.make("Virtual", {
|
|
|
388
494
|
},
|
|
389
495
|
Measured: ({ key, height }) => {
|
|
390
496
|
const sane = saneMeasured(height);
|
|
391
|
-
return sane === null ? { model } : { model: {
|
|
497
|
+
return sane === null || Object.hasOwn(model.heights, key) && model.heights[key] === sane ? { model } : { model: {
|
|
392
498
|
...model,
|
|
393
499
|
heights: {
|
|
394
500
|
...model.heights,
|
|
@@ -399,11 +505,13 @@ const Virtual = Bundle.make("Virtual", {
|
|
|
399
505
|
Prune: ({ keys }) => {
|
|
400
506
|
const kept = new Set(keys);
|
|
401
507
|
const heights = {};
|
|
508
|
+
let dropped = false;
|
|
402
509
|
for (const [key, height] of Object.entries(model.heights)) if (kept.has(key)) heights[key] = height;
|
|
403
|
-
|
|
510
|
+
else dropped = true;
|
|
511
|
+
return dropped ? { model: {
|
|
404
512
|
...model,
|
|
405
513
|
heights
|
|
406
|
-
} };
|
|
514
|
+
} } : { model };
|
|
407
515
|
},
|
|
408
516
|
Settled: ({ generation }) => generation === model.generation ? { model: {
|
|
409
517
|
...model,
|
|
@@ -506,6 +614,6 @@ const masonry = (keys, heights, options) => {
|
|
|
506
614
|
};
|
|
507
615
|
};
|
|
508
616
|
//#endregion
|
|
509
|
-
export { HistoryModel, Locale, LocaleMessage, LocaleModel, MeasureRow, Pagination, PaginationMessage, PaginationModel, RowMeasured, SelectionSet, SelectionSetMessage, SelectionSetModel, Viewport, ViewportScrolled, Virtual, VirtualMessage, VirtualModel, canRedo, canUndo, distanceToEnd, history, isAtEnd, isSelected, masonry, offset, offsetFor, pageCount, range, stickyHeader, totalHeight, visibleRange, windowFor };
|
|
617
|
+
export { History, HistoryModel, Locale, LocaleMessage, LocaleModel, MeasureRow, Pagination, PaginationMessage, PaginationModel, RowMeasured, SelectionSet, SelectionSetMessage, SelectionSetModel, Viewport, ViewportScrolled, Virtual, VirtualMessage, VirtualModel, canRedo, canUndo, distanceToEnd, history, isAtEnd, isSelected, masonry, offset, offsetFor, pageCount, range, stickyHeader, totalHeight, visibleRange, windowFor };
|
|
510
618
|
|
|
511
619
|
//# sourceMappingURL=index.mjs.map
|