@nanahoshi/mona-ui 0.24.0 → 0.24.1
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 +1 -0
- package/fesm2022/nanahoshi-mona-ui-common.mjs +265 -159
- package/package.json +1 -1
- package/types/nanahoshi-mona-ui-common.d.ts +116 -73
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ A modern, high-performance, accessible UI component library built for Angular an
|
|
|
18
18
|
## Documentation
|
|
19
19
|
|
|
20
20
|
- [Internationalization (i18n) & RTL Guide](docs/i18n.md)
|
|
21
|
+
- [Patch Stores & Merge Utilities](docs/patch-store.md)
|
|
21
22
|
- [Component Migration Inventory](docs/internal/i18n-rtl-migration-inventory.md)
|
|
22
23
|
|
|
23
24
|
---
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Pipe, linkedSignal, computed, signal } from '@angular/core';
|
|
3
|
-
import { any, contains, all,
|
|
4
|
-
import { DateTime } from 'luxon';
|
|
2
|
+
import { Pipe, linkedSignal, computed, signal, untracked } from '@angular/core';
|
|
3
|
+
import { any, contains, all, firstOrDefault, ImmutableDictionary } from '@mirei/ts-collections';
|
|
5
4
|
|
|
6
5
|
class PreventableEvent {
|
|
7
6
|
#event;
|
|
@@ -80,11 +79,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImpor
|
|
|
80
79
|
}] });
|
|
81
80
|
|
|
82
81
|
class SlicePipe {
|
|
82
|
+
/**
|
|
83
|
+
* Returns the elements from `start` up to, but not including, `end`, with the semantics of `Array.prototype.slice`:
|
|
84
|
+
* negative indices count from the end, and an omitted `end` slices to the end. Returns an empty array for
|
|
85
|
+
* `null` or `undefined`, such as the first value of the `async` pipe.
|
|
86
|
+
*/
|
|
83
87
|
transform(value, start, end) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
if (value == null) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
return Array.from(value).slice(start, end);
|
|
88
92
|
}
|
|
89
93
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SlicePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
90
94
|
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.6", ngImport: i0, type: SlicePipe, isStandalone: true, name: "monaSlice" }); }
|
|
@@ -127,8 +131,14 @@ function deepMerge(base, patch, options = defaultOptions, path = []) {
|
|
|
127
131
|
const out = isPlainObject(base) ? { ...base } : {};
|
|
128
132
|
const keys = Object.keys(patch);
|
|
129
133
|
for (const key of keys) {
|
|
134
|
+
if (key === "__proto__") {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
130
137
|
const patchValue = patch[key];
|
|
131
138
|
if (patchValue === undefined) {
|
|
139
|
+
if (options.undefinedValues === "assign") {
|
|
140
|
+
out[key] = undefined;
|
|
141
|
+
}
|
|
132
142
|
continue;
|
|
133
143
|
}
|
|
134
144
|
const baseValue = out[key];
|
|
@@ -146,28 +156,35 @@ function deepMerge(base, patch, options = defaultOptions, path = []) {
|
|
|
146
156
|
];
|
|
147
157
|
}
|
|
148
158
|
else {
|
|
149
|
-
// unionBy
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
159
|
+
// unionBy strategy: a patch item is merged into the base item with the same key; items without a
|
|
160
|
+
// key are kept as they are.
|
|
161
|
+
const identify = createUnionIdentifier(strategy.key);
|
|
162
|
+
const items = [];
|
|
163
|
+
const indexById = new Map();
|
|
164
|
+
const add = (item, merge) => {
|
|
165
|
+
const id = identify(item);
|
|
166
|
+
const index = id === undefined ? undefined : indexById.get(id);
|
|
167
|
+
if (index === undefined) {
|
|
168
|
+
if (id !== undefined) {
|
|
169
|
+
indexById.set(id, items.length);
|
|
170
|
+
}
|
|
171
|
+
items.push(item);
|
|
172
|
+
return;
|
|
155
173
|
}
|
|
156
|
-
|
|
174
|
+
const existing = items[index];
|
|
175
|
+
items[index] = !merge
|
|
176
|
+
? item
|
|
177
|
+
: isPlainObject(item) && isPlainObject(existing)
|
|
178
|
+
? deepMerge(existing, item, options, currentPath)
|
|
179
|
+
: { ...existing, ...item };
|
|
157
180
|
};
|
|
158
181
|
for (const item of baseValue) {
|
|
159
|
-
|
|
182
|
+
add(item, false);
|
|
160
183
|
}
|
|
161
184
|
for (const item of patchValue) {
|
|
162
|
-
|
|
163
|
-
const existing = map.get(id);
|
|
164
|
-
const mergedItem = isPlainObject(item) && isPlainObject(existing)
|
|
165
|
-
? // @ts-ignore // TODO: Remove this when it is no longer flagged as error
|
|
166
|
-
deepMerge(existing, item, options)
|
|
167
|
-
: { ...existing, ...item };
|
|
168
|
-
map.set(id, mergedItem);
|
|
185
|
+
add(item, true);
|
|
169
186
|
}
|
|
170
|
-
out[key] =
|
|
187
|
+
out[key] = items;
|
|
171
188
|
}
|
|
172
189
|
continue;
|
|
173
190
|
}
|
|
@@ -185,23 +202,24 @@ function mergeInto(base, patch, options) {
|
|
|
185
202
|
function mergePatchInto(base, patch, options) {
|
|
186
203
|
return deepMerge(base, patch, options);
|
|
187
204
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
205
|
+
/**
|
|
206
|
+
* Builds a patch that sets `leaf` at `keys`. When `current` (the value the patch will be merged into) holds an
|
|
207
|
+
* array along the path, the patch carries a copy of that array with the one element replaced, since arrays are
|
|
208
|
+
* replaced rather than merged. Without `current`, every level is an object.
|
|
209
|
+
*/
|
|
210
|
+
function buildNested(keys, leaf, current) {
|
|
211
|
+
if (keys.length === 0) {
|
|
191
212
|
return {};
|
|
192
213
|
}
|
|
193
|
-
|
|
194
|
-
return { [head]: leaf };
|
|
195
|
-
}
|
|
196
|
-
return { [head]: buildNested(rest, leaf) };
|
|
214
|
+
return buildLevel(keys, leaf, current, false);
|
|
197
215
|
}
|
|
198
|
-
function patchAt(path, value) {
|
|
216
|
+
function patchAt(path, value, current) {
|
|
199
217
|
const keys = path;
|
|
200
|
-
const nested = buildNested(keys, value);
|
|
218
|
+
const nested = buildNested(keys, value, current);
|
|
201
219
|
return nested;
|
|
202
220
|
}
|
|
203
221
|
function setAt(obj, path, value, options) {
|
|
204
|
-
return deepMerge(obj, patchAt(path, value), options);
|
|
222
|
+
return deepMerge(obj, patchAt(path, value, obj), options);
|
|
205
223
|
}
|
|
206
224
|
function prunePatch(base, patch) {
|
|
207
225
|
if (patch === null || typeof patch !== "object" || base === null || typeof base !== "object") {
|
|
@@ -211,7 +229,15 @@ function prunePatch(base, patch) {
|
|
|
211
229
|
const patchObj = patch;
|
|
212
230
|
const result = { ...patchObj };
|
|
213
231
|
for (const key of Object.keys(patchObj)) {
|
|
232
|
+
if (key === "__proto__") {
|
|
233
|
+
delete result[key];
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
214
236
|
if (!(key in baseObj)) {
|
|
237
|
+
// An undefined field the base doesn't have is no change.
|
|
238
|
+
if (patchObj[key] === undefined) {
|
|
239
|
+
delete result[key];
|
|
240
|
+
}
|
|
215
241
|
continue;
|
|
216
242
|
}
|
|
217
243
|
const baseValue = baseObj[key];
|
|
@@ -231,39 +257,131 @@ function prunePatch(base, patch) {
|
|
|
231
257
|
}
|
|
232
258
|
return Object.keys(result).length === 0 ? undefined : result;
|
|
233
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Structural equality for plain objects, arrays, `Date`, `RegExp`, `Map` and `Set`. Primitives are equal when
|
|
262
|
+
* `===` says so, and `NaN` equals `NaN`. Dates are equal when they hold the same time (two invalid dates are
|
|
263
|
+
* equal). Map keys and set members are matched by `SameValueZero`, like `Map.prototype.has`. Cyclic values are
|
|
264
|
+
* supported.
|
|
265
|
+
*/
|
|
234
266
|
function deepEquals(o1, o2) {
|
|
235
|
-
|
|
267
|
+
return valuesEqual(o1, o2, new Map());
|
|
268
|
+
}
|
|
269
|
+
function valuesEqual(o1, o2, visited) {
|
|
270
|
+
if (o1 === o2 || (Number.isNaN(o1) && Number.isNaN(o2))) {
|
|
236
271
|
return true;
|
|
237
272
|
}
|
|
238
|
-
if (
|
|
273
|
+
if (o1 === null || o2 === null || typeof o1 !== "object" || typeof o2 !== "object") {
|
|
239
274
|
return false;
|
|
240
275
|
}
|
|
241
|
-
if (o1 instanceof Date
|
|
242
|
-
return
|
|
276
|
+
if (o1 instanceof Date || o2 instanceof Date) {
|
|
277
|
+
return o1 instanceof Date && o2 instanceof Date && Object.is(o1.getTime(), o2.getTime());
|
|
278
|
+
}
|
|
279
|
+
if (o1 instanceof RegExp || o2 instanceof RegExp) {
|
|
280
|
+
return o1 instanceof RegExp && o2 instanceof RegExp && o1.source === o2.source && o1.flags === o2.flags;
|
|
243
281
|
}
|
|
244
|
-
|
|
282
|
+
// A pair already being compared further up is assumed equal; any difference shows up there.
|
|
283
|
+
let seen = visited.get(o1);
|
|
284
|
+
if (seen?.has(o2)) {
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
if (!seen) {
|
|
288
|
+
seen = new Set();
|
|
289
|
+
visited.set(o1, seen);
|
|
290
|
+
}
|
|
291
|
+
seen.add(o2);
|
|
292
|
+
if (o1 instanceof Map || o2 instanceof Map) {
|
|
293
|
+
return o1 instanceof Map && o2 instanceof Map && mapsEqual(o1, o2, visited);
|
|
294
|
+
}
|
|
295
|
+
if (o1 instanceof Set || o2 instanceof Set) {
|
|
296
|
+
return o1 instanceof Set && o2 instanceof Set && setsEqual(o1, o2);
|
|
297
|
+
}
|
|
298
|
+
if (Array.isArray(o1) || Array.isArray(o2)) {
|
|
299
|
+
return (Array.isArray(o1) &&
|
|
300
|
+
Array.isArray(o2) &&
|
|
301
|
+
o1.length === o2.length &&
|
|
302
|
+
o1.every((value, index) => valuesEqual(value, o2[index], visited)));
|
|
303
|
+
}
|
|
304
|
+
const keys1 = Object.keys(o1);
|
|
305
|
+
if (keys1.length !== Object.keys(o2).length) {
|
|
245
306
|
return false;
|
|
246
307
|
}
|
|
247
|
-
|
|
248
|
-
|
|
308
|
+
const record1 = o1;
|
|
309
|
+
const record2 = o2;
|
|
310
|
+
return keys1.every(key => Object.hasOwn(record2, key) && valuesEqual(record1[key], record2[key], visited));
|
|
311
|
+
}
|
|
312
|
+
/** Maps are equal when they have the same keys (by `SameValueZero`) and deeply equal values. */
|
|
313
|
+
function mapsEqual(m1, m2, visited) {
|
|
314
|
+
if (m1.size !== m2.size) {
|
|
315
|
+
return false;
|
|
249
316
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const keys2 = Object.keys(o2);
|
|
253
|
-
if (keys1.length !== keys2.length) {
|
|
317
|
+
for (const [key, value] of m1) {
|
|
318
|
+
if (!m2.has(key) || !valuesEqual(value, m2.get(key), visited)) {
|
|
254
319
|
return false;
|
|
255
320
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
321
|
+
}
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
/** Sets are equal when they have the same members (by `SameValueZero`). */
|
|
325
|
+
function setsEqual(s1, s2) {
|
|
326
|
+
if (s1.size !== s2.size) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
for (const value of s1) {
|
|
330
|
+
if (!s2.has(value)) {
|
|
331
|
+
return false;
|
|
261
332
|
}
|
|
262
|
-
return true;
|
|
263
333
|
}
|
|
264
|
-
return
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* One level of `buildNested`. Inside an array (`whole`), each level must be the whole value, since the array
|
|
338
|
+
* element replaces the old one; outside, a level holds only the key on the path.
|
|
339
|
+
*/
|
|
340
|
+
function buildLevel(keys, leaf, current, whole) {
|
|
341
|
+
const [head, ...rest] = keys;
|
|
342
|
+
const isArray = Array.isArray(current);
|
|
343
|
+
const child = current != null && typeof current === "object"
|
|
344
|
+
? current[head]
|
|
345
|
+
: undefined;
|
|
346
|
+
const value = rest.length === 0 ? leaf : buildLevel(rest, leaf, child, whole || isArray);
|
|
347
|
+
if (isArray) {
|
|
348
|
+
const copy = [...current];
|
|
349
|
+
copy[Number(head)] = value;
|
|
350
|
+
return copy;
|
|
351
|
+
}
|
|
352
|
+
if (whole && isPlainObject(current)) {
|
|
353
|
+
return { ...current, [head]: value };
|
|
354
|
+
}
|
|
355
|
+
return { [head]: value };
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Returns a function that tells `unionBy` items apart by the values of their key fields, or returns `undefined` for
|
|
359
|
+
* an item that lacks one. Values keep their type (`1` and `"1"` differ), and composite keys can't collide.
|
|
360
|
+
*/
|
|
361
|
+
function createUnionIdentifier(by) {
|
|
362
|
+
const keys = Array.isArray(by) ? by : [by];
|
|
363
|
+
return item => {
|
|
364
|
+
const values = keys.map(key => item != null && typeof item === "object" ? item[key] : undefined);
|
|
365
|
+
return values.some(value => value == null) ? undefined : JSON.stringify(values, unionKeyReplacer);
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function unionKeyReplacer(_key, value) {
|
|
369
|
+
return typeof value === "bigint" ? { bigint: String(value) } : value;
|
|
265
370
|
}
|
|
266
371
|
|
|
372
|
+
/**
|
|
373
|
+
* Drafts hold final values: an `undefined` in a patch clears the field, and the draft is applied over the base
|
|
374
|
+
* with `replace`, so an array strategy only ever applies once, to the patch that introduced it.
|
|
375
|
+
*/
|
|
376
|
+
const STORE_MERGE_OPTIONS = { undefinedValues: "assign" };
|
|
377
|
+
/**
|
|
378
|
+
* Applies `patch` to the view of `base` with `draft`, and returns the fields that still differ from `base`.
|
|
379
|
+
*/
|
|
380
|
+
function applyPatch(base, draft, patch, options) {
|
|
381
|
+
const currentView = deepMerge(base, draft, STORE_MERGE_OPTIONS);
|
|
382
|
+
const nextView = deepMerge(currentView, patch, { ...STORE_MERGE_OPTIONS, ...options });
|
|
383
|
+
return prunePatch(base, nextView) ?? {};
|
|
384
|
+
}
|
|
267
385
|
class ObjectPatchStore {
|
|
268
386
|
#base;
|
|
269
387
|
#committed;
|
|
@@ -274,8 +392,8 @@ class ObjectPatchStore {
|
|
|
274
392
|
computation: (newBase, previous) => {
|
|
275
393
|
const strategy = this.#options.baseChangeStrategy ?? "wipe";
|
|
276
394
|
if (!previous) {
|
|
277
|
-
// First computation
|
|
278
|
-
return
|
|
395
|
+
// First computation; the constructor then sets the initial draft.
|
|
396
|
+
return {};
|
|
279
397
|
}
|
|
280
398
|
switch (strategy) {
|
|
281
399
|
case "preserve":
|
|
@@ -301,7 +419,7 @@ class ObjectPatchStore {
|
|
|
301
419
|
return deepEquals(prunedDraft, prunedCommitted);
|
|
302
420
|
}, /* @ts-ignore */
|
|
303
421
|
...(ngDevMode ? [{ debugName: "pristine" }] : /* istanbul ignore next */ []));
|
|
304
|
-
this.view = computed(() => deepMerge(this.#base(), this.#draft()), /* @ts-ignore */
|
|
422
|
+
this.view = computed(() => deepMerge(this.#base(), this.#draft(), STORE_MERGE_OPTIONS), /* @ts-ignore */
|
|
305
423
|
...(ngDevMode ? [{ debugName: "view" }] : /* istanbul ignore next */ []));
|
|
306
424
|
this.#options = options;
|
|
307
425
|
this.#base = options.source();
|
|
@@ -325,32 +443,25 @@ class ObjectPatchStore {
|
|
|
325
443
|
}
|
|
326
444
|
patch(patch, options) {
|
|
327
445
|
const effectiveOptions = options ?? this.#options.mergeOptions;
|
|
328
|
-
this.#draft.update(currentDraft =>
|
|
329
|
-
const currentFullView = deepMerge(this.#base(), currentDraft, effectiveOptions);
|
|
330
|
-
const updatedFullView = deepMerge(currentFullView, patch, effectiveOptions);
|
|
331
|
-
const nextDraft = prunePatch(this.#base(), updatedFullView);
|
|
332
|
-
return nextDraft ?? {};
|
|
333
|
-
});
|
|
446
|
+
this.#draft.update(currentDraft => applyPatch(this.#base(), currentDraft, patch, effectiveOptions));
|
|
334
447
|
}
|
|
335
448
|
patchPath(path, value, options) {
|
|
336
|
-
|
|
449
|
+
// Arrays along the path are copied from the current view (see buildNested).
|
|
450
|
+
const patch = buildNested(path, value, untracked(this.view));
|
|
337
451
|
this.patch(patch, options);
|
|
338
452
|
}
|
|
339
453
|
reset() {
|
|
340
454
|
this.#draft.set({});
|
|
341
455
|
this.#committed.set({});
|
|
342
456
|
}
|
|
457
|
+
select(projector, equal = deepEquals) {
|
|
458
|
+
return computed(() => projector(this.view()), { equal });
|
|
459
|
+
}
|
|
343
460
|
setDraft(patch) {
|
|
344
461
|
this.#draft.set(patch);
|
|
345
462
|
}
|
|
346
463
|
setPath(path, value, options) {
|
|
347
|
-
|
|
348
|
-
// preserving any unrelated in-progress changes.
|
|
349
|
-
const patch = buildNested(path, value);
|
|
350
|
-
this.patch(patch, options);
|
|
351
|
-
}
|
|
352
|
-
select(projector, equal = deepEquals) {
|
|
353
|
-
return computed(() => projector(this.view()), { equal });
|
|
464
|
+
this.patchPath(path, value, options);
|
|
354
465
|
}
|
|
355
466
|
}
|
|
356
467
|
class IterablePatchStore {
|
|
@@ -365,8 +476,9 @@ class IterablePatchStore {
|
|
|
365
476
|
#options;
|
|
366
477
|
/**
|
|
367
478
|
* Memoisation cache for the `view` computed. Keyed by item id; each
|
|
368
|
-
* entry stores the draft
|
|
369
|
-
* can skip deepMerge when neither
|
|
479
|
+
* entry stores the base item and draft references that produced the merged
|
|
480
|
+
* result, so we can skip deepMerge when neither of them changed. Rebuilt on
|
|
481
|
+
* every computation, so ids that leave the base or lose their draft drop out.
|
|
370
482
|
*/
|
|
371
483
|
#viewCache;
|
|
372
484
|
constructor(options) {
|
|
@@ -376,8 +488,8 @@ class IterablePatchStore {
|
|
|
376
488
|
*/
|
|
377
489
|
this.#baseMap = computed(() => new Map(Array.from(this.#base()).map(item => [this.#options.idOf(item), item])), /* @ts-ignore */
|
|
378
490
|
...(ngDevMode ? [{ debugName: "#baseMap" }] : /* istanbul ignore next */ []));
|
|
379
|
-
this.#drafts = linkedSignal({ ...(ngDevMode ? { debugName: "#drafts" } : /* istanbul ignore next */ {}), source: () => this.#
|
|
380
|
-
computation: (
|
|
491
|
+
this.#drafts = linkedSignal({ ...(ngDevMode ? { debugName: "#drafts" } : /* istanbul ignore next */ {}), source: () => this.#base(),
|
|
492
|
+
computation: (newBase, previous) => {
|
|
381
493
|
const strategy = this.#options.baseChangeStrategy ?? "wipe";
|
|
382
494
|
if (!previous) {
|
|
383
495
|
return ImmutableDictionary.create();
|
|
@@ -386,10 +498,10 @@ class IterablePatchStore {
|
|
|
386
498
|
case "preserve":
|
|
387
499
|
return previous.value;
|
|
388
500
|
case "rebase": {
|
|
389
|
-
const
|
|
501
|
+
const baseMap = new Map(Array.from(newBase, item => [this.#options.idOf(item), item]));
|
|
390
502
|
let rebased = ImmutableDictionary.create();
|
|
391
503
|
for (const [id, draft] of previous.value.entries()) {
|
|
392
|
-
const baseItem =
|
|
504
|
+
const baseItem = baseMap.get(id);
|
|
393
505
|
if (!baseItem) {
|
|
394
506
|
// Item no longer exists in base — drop the draft.
|
|
395
507
|
continue;
|
|
@@ -408,30 +520,32 @@ class IterablePatchStore {
|
|
|
408
520
|
} });
|
|
409
521
|
/**
|
|
410
522
|
* Memoisation cache for the `view` computed. Keyed by item id; each
|
|
411
|
-
* entry stores the draft
|
|
412
|
-
* can skip deepMerge when neither
|
|
523
|
+
* entry stores the base item and draft references that produced the merged
|
|
524
|
+
* result, so we can skip deepMerge when neither of them changed. Rebuilt on
|
|
525
|
+
* every computation, so ids that leave the base or lose their draft drop out.
|
|
413
526
|
*/
|
|
414
527
|
this.#viewCache = new Map();
|
|
415
528
|
this.changes = computed(() => {
|
|
416
|
-
const baseArray = Array.from(this.#base());
|
|
417
529
|
const drafts = this.#drafts();
|
|
418
|
-
|
|
419
|
-
|
|
530
|
+
const changes = [];
|
|
531
|
+
for (const b of this.#base()) {
|
|
420
532
|
const draft = drafts.get(this.#options.idOf(b));
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
533
|
+
if (draft != null && !isEmptyObject(draft)) {
|
|
534
|
+
changes.push(deepMerge(b, draft, STORE_MERGE_OPTIONS));
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return changes;
|
|
424
538
|
}, /* @ts-ignore */
|
|
425
539
|
...(ngDevMode ? [{ debugName: "changes" }] : /* istanbul ignore next */ []));
|
|
426
540
|
this.pristine = computed(() => {
|
|
427
|
-
const
|
|
541
|
+
const baseMap = this.#baseMap();
|
|
428
542
|
const drafts = this.#drafts();
|
|
429
543
|
const committed = this.#committed();
|
|
430
544
|
const allIds = new Set([...drafts.keys(), ...committed.keys()]);
|
|
431
545
|
for (const id of allIds) {
|
|
432
546
|
const draft = drafts.get(id) ?? {};
|
|
433
547
|
const committedDraft = committed.get(id) ?? {};
|
|
434
|
-
const originalItem =
|
|
548
|
+
const originalItem = baseMap.get(id);
|
|
435
549
|
const prunedDraft = originalItem ? (prunePatch(originalItem, draft) ?? {}) : draft;
|
|
436
550
|
const prunedCommitted = originalItem
|
|
437
551
|
? (prunePatch(originalItem, committedDraft) ?? {})
|
|
@@ -445,52 +559,45 @@ class IterablePatchStore {
|
|
|
445
559
|
...(ngDevMode ? [{ debugName: "pristine" }] : /* istanbul ignore next */ []));
|
|
446
560
|
this.view = computed(() => {
|
|
447
561
|
const drafts = this.#drafts();
|
|
448
|
-
|
|
562
|
+
const previousCache = this.#viewCache;
|
|
563
|
+
const nextCache = new Map();
|
|
564
|
+
const view = Array.from(this.#base()).map(b => {
|
|
449
565
|
const id = this.#options.idOf(b);
|
|
450
566
|
const patch = drafts.get(id);
|
|
451
567
|
if (!patch || isEmptyObject(patch)) {
|
|
452
|
-
// No draft — return base item directly
|
|
453
|
-
this.#viewCache.delete(id);
|
|
568
|
+
// No draft — return base item directly.
|
|
454
569
|
return b;
|
|
455
570
|
}
|
|
456
|
-
const cached =
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
this.#viewCache.set(id, { patchRef: patch, merged });
|
|
571
|
+
const cached = previousCache.get(id);
|
|
572
|
+
// Reuse the merged value only when neither the base item nor its
|
|
573
|
+
// draft changed, to avoid an unnecessary deepMerge and allocation.
|
|
574
|
+
const merged = cached && cached.baseRef === b && cached.patchRef === patch
|
|
575
|
+
? cached.merged
|
|
576
|
+
: deepMerge(b, patch, STORE_MERGE_OPTIONS);
|
|
577
|
+
nextCache.set(id, { baseRef: b, merged, patchRef: patch });
|
|
464
578
|
return merged;
|
|
465
579
|
});
|
|
580
|
+
this.#viewCache = nextCache;
|
|
581
|
+
return view;
|
|
466
582
|
}, /* @ts-ignore */
|
|
467
583
|
...(ngDevMode ? [{ debugName: "view" }] : /* istanbul ignore next */ []));
|
|
468
584
|
this.#options = options;
|
|
469
585
|
this.#base = options.source();
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
}
|
|
475
|
-
else {
|
|
476
|
-
initial = ImmutableDictionary.create(options.initial());
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
else {
|
|
480
|
-
initial = ImmutableDictionary.create();
|
|
481
|
-
}
|
|
586
|
+
const initialValue = options.initial?.();
|
|
587
|
+
const initial = initialValue instanceof ImmutableDictionary
|
|
588
|
+
? initialValue
|
|
589
|
+
: ImmutableDictionary.create(initialValue ?? []);
|
|
482
590
|
this.#committed = signal(initial, /* @ts-ignore */
|
|
483
591
|
...(ngDevMode ? [{ debugName: "#committed" }] : /* istanbul ignore next */ []));
|
|
484
|
-
if (
|
|
485
|
-
this.#drafts.set(
|
|
592
|
+
if (initialValue) {
|
|
593
|
+
this.#drafts.set(initial);
|
|
486
594
|
}
|
|
487
595
|
}
|
|
488
596
|
clear(id) {
|
|
489
597
|
this.#drafts.update(drafts => {
|
|
490
598
|
if (!drafts.containsKey(id))
|
|
491
599
|
return drafts;
|
|
492
|
-
|
|
493
|
-
return newDrafts.remove(id);
|
|
600
|
+
return drafts.remove(id);
|
|
494
601
|
});
|
|
495
602
|
}
|
|
496
603
|
clearAll() {
|
|
@@ -515,50 +622,47 @@ class IterablePatchStore {
|
|
|
515
622
|
const patch = this.#drafts().get(id);
|
|
516
623
|
return patch != null && !isEmptyObject(patch);
|
|
517
624
|
}
|
|
518
|
-
patch(id, patch, options) {
|
|
625
|
+
patch(id, patch, options = this.#options.mergeOptions) {
|
|
519
626
|
this.#drafts.update(drafts => {
|
|
520
|
-
let newDrafts = ImmutableDictionary.create(drafts);
|
|
521
627
|
const prev = drafts.get(id) ?? {};
|
|
522
|
-
let next = deepMerge(prev, patch, options);
|
|
523
|
-
// Fix: use #baseMap for O(1) lookup instead of a linear scan.
|
|
524
628
|
const originalItem = this.#baseMap().get(id);
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
newDrafts = newDrafts.remove(id);
|
|
530
|
-
}
|
|
531
|
-
else {
|
|
532
|
-
newDrafts = newDrafts.put(id, next);
|
|
533
|
-
}
|
|
534
|
-
return newDrafts;
|
|
629
|
+
const next = originalItem
|
|
630
|
+
? applyPatch(originalItem, prev, patch, options)
|
|
631
|
+
: deepMerge(prev, patch, { ...STORE_MERGE_OPTIONS, ...options });
|
|
632
|
+
return isEmptyObject(next) ? drafts.remove(id) : drafts.put(id, next);
|
|
535
633
|
});
|
|
536
634
|
}
|
|
537
|
-
patchMany(patchTuple, options) {
|
|
538
|
-
// Fix: prune each resulting draft against the base item for consistency
|
|
539
|
-
// with patch(), and use #baseMap for O(1) lookups.
|
|
635
|
+
patchMany(patchTuple, options = this.#options.mergeOptions) {
|
|
540
636
|
const baseMap = this.#baseMap();
|
|
541
637
|
this.#drafts.update(drafts => {
|
|
542
|
-
|
|
638
|
+
// Each put on an ImmutableDictionary copies it, so collect the drafts in a Map and build one dictionary.
|
|
639
|
+
const nextDrafts = new Map(drafts.entries());
|
|
543
640
|
for (const [id, value] of patchTuple) {
|
|
544
|
-
const prev =
|
|
545
|
-
let next = deepMerge(prev, value, options);
|
|
641
|
+
const prev = nextDrafts.get(id) ?? {};
|
|
546
642
|
const originalItem = baseMap.get(id);
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
643
|
+
const next = originalItem
|
|
644
|
+
? applyPatch(originalItem, prev, value, options)
|
|
645
|
+
: deepMerge(prev, value, { ...STORE_MERGE_OPTIONS, ...options });
|
|
550
646
|
if (isEmptyObject(next)) {
|
|
551
|
-
|
|
647
|
+
nextDrafts.delete(id);
|
|
552
648
|
}
|
|
553
649
|
else {
|
|
554
|
-
|
|
650
|
+
nextDrafts.set(id, next);
|
|
555
651
|
}
|
|
556
652
|
}
|
|
557
|
-
return
|
|
653
|
+
return ImmutableDictionary.create(nextDrafts);
|
|
558
654
|
});
|
|
559
655
|
}
|
|
560
656
|
patchPath(id, path, value, options) {
|
|
561
|
-
|
|
657
|
+
// Arrays along the path are copied from the item's current view (see buildNested).
|
|
658
|
+
const current = untracked(() => {
|
|
659
|
+
const baseItem = this.#baseMap().get(id);
|
|
660
|
+
const draft = this.#drafts().get(id);
|
|
661
|
+
return baseItem !== undefined && draft
|
|
662
|
+
? deepMerge(baseItem, draft, STORE_MERGE_OPTIONS)
|
|
663
|
+
: (baseItem ?? draft);
|
|
664
|
+
});
|
|
665
|
+
const patch = buildNested(path, value, current);
|
|
562
666
|
this.patch(id, patch, options);
|
|
563
667
|
}
|
|
564
668
|
reset() {
|
|
@@ -566,21 +670,14 @@ class IterablePatchStore {
|
|
|
566
670
|
this.#drafts.set(empty);
|
|
567
671
|
this.#committed.set(empty);
|
|
568
672
|
}
|
|
673
|
+
select(projector, equal = deepEquals) {
|
|
674
|
+
return computed(() => projector(this.view()), { equal });
|
|
675
|
+
}
|
|
569
676
|
setDraft(id, patch) {
|
|
570
|
-
this.#drafts.update(drafts =>
|
|
571
|
-
let newDrafts = ImmutableDictionary.create(drafts);
|
|
572
|
-
newDrafts = newDrafts.put(id, patch);
|
|
573
|
-
return newDrafts;
|
|
574
|
-
});
|
|
677
|
+
this.#drafts.update(drafts => isEmptyObject(patch) ? drafts.remove(id) : drafts.put(id, patch));
|
|
575
678
|
}
|
|
576
679
|
setPath(id, path, value, options) {
|
|
577
|
-
|
|
578
|
-
// preserving any unrelated in-progress changes.
|
|
579
|
-
const patch = buildNested(path, value);
|
|
580
|
-
this.patch(id, patch, options);
|
|
581
|
-
}
|
|
582
|
-
select(projector, equal = deepEquals) {
|
|
583
|
-
return computed(() => projector(this.view()), { equal });
|
|
680
|
+
this.patchPath(id, path, value, options);
|
|
584
681
|
}
|
|
585
682
|
}
|
|
586
683
|
const createIterablePatchStore = (options) => new IterablePatchStore(options);
|
|
@@ -594,11 +691,12 @@ const classInputToClass = (input) => {
|
|
|
594
691
|
return input;
|
|
595
692
|
}
|
|
596
693
|
if (Array.isArray(input)) {
|
|
597
|
-
return input.join(" ");
|
|
694
|
+
return input.filter(Boolean).join(" ");
|
|
598
695
|
}
|
|
599
696
|
if (typeof input === "object") {
|
|
600
697
|
return Object.entries(input)
|
|
601
|
-
.
|
|
698
|
+
.filter(([, value]) => value)
|
|
699
|
+
.map(([key]) => key)
|
|
602
700
|
.join(" ");
|
|
603
701
|
}
|
|
604
702
|
return "";
|
|
@@ -609,24 +707,32 @@ const classInputToClass = (input) => {
|
|
|
609
707
|
* @param value The current value.
|
|
610
708
|
* @param min The minimum value.
|
|
611
709
|
* @param max The maximum value.
|
|
612
|
-
* @param precision The number of decimal places to round the percentage to.
|
|
710
|
+
* @param precision The number of decimal places to round the percentage to, clamped to 0–100.
|
|
613
711
|
* @default 2
|
|
614
|
-
* @returns The percentage of the value within the range, rounded to the specified precision
|
|
712
|
+
* @returns The percentage of the value within the range, rounded to the specified precision, or 0 when the range
|
|
713
|
+
* or the value is not finite.
|
|
615
714
|
*/
|
|
616
715
|
const getPercentage = (value, min, max, precision = 2) => {
|
|
617
|
-
if (
|
|
716
|
+
if (!isFinite(value) || !isFinite(min) || !isFinite(max) || max <= min) {
|
|
618
717
|
return 0;
|
|
619
718
|
}
|
|
620
719
|
const raw = ((value - min) / (max - min)) * 100;
|
|
621
|
-
|
|
720
|
+
const digits = Math.min(100, Math.max(0, Math.trunc(precision) || 0));
|
|
721
|
+
return Number(Math.min(100, Math.max(0, raw)).toFixed(digits));
|
|
622
722
|
};
|
|
623
723
|
|
|
724
|
+
/**
|
|
725
|
+
* Moves the items at `indices` by `offset` positions, keeping their relative order. Returns a copy of the array
|
|
726
|
+
* unchanged when the offset is 0 or not an integer, or when any index is not an integer, is out of bounds, or would
|
|
727
|
+
* move out of bounds. Duplicate indices count once.
|
|
728
|
+
*/
|
|
624
729
|
function moveIndices(array, indices, offset) {
|
|
625
|
-
if (offset === 0 || indices.length === 0) {
|
|
730
|
+
if (offset === 0 || indices.length === 0 || !Number.isInteger(offset)) {
|
|
626
731
|
return [...array];
|
|
627
732
|
}
|
|
628
|
-
|
|
629
|
-
|
|
733
|
+
indices = [...new Set(indices)];
|
|
734
|
+
// Validate indices are integers within bounds
|
|
735
|
+
if (indices.some(i => !Number.isInteger(i) || i < 0 || i >= array.length)) {
|
|
630
736
|
return [...array];
|
|
631
737
|
}
|
|
632
738
|
// Check if any index would go out of bounds after the move
|
package/package.json
CHANGED
|
@@ -60,7 +60,12 @@ declare class ContainsPipe implements PipeTransform {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
declare class SlicePipe implements PipeTransform {
|
|
63
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Returns the elements from `start` up to, but not including, `end`, with the semantics of `Array.prototype.slice`:
|
|
65
|
+
* negative indices count from the end, and an omitted `end` slices to the end. Returns an empty array for
|
|
66
|
+
* `null` or `undefined`, such as the first value of the `async` pipe.
|
|
67
|
+
*/
|
|
68
|
+
transform<T>(value: Iterable<T> | null | undefined, start: number, end?: number): T[];
|
|
64
69
|
static ɵfac: i0.ɵɵFactoryDeclaration<SlicePipe, never>;
|
|
65
70
|
static ɵpipe: i0.ɵɵPipeDeclaration<SlicePipe, "monaSlice", true>;
|
|
66
71
|
}
|
|
@@ -76,10 +81,12 @@ type BuiltIn = Primitive | Date | RegExp | Function | Map<unknown, unknown> | Se
|
|
|
76
81
|
type DeepPartial<T> = T extends BuiltIn ? T : T extends Array<infer U> ? Array<DeepPartial<U>> : {
|
|
77
82
|
[P in keyof T]?: DeepPartial<T[P]>;
|
|
78
83
|
};
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Every key path into `T`. Paths stop at built-in values (dates, maps, sets, functions), and index arrays by
|
|
86
|
+
* number, so `['tags', 0]` is a path and `['tags', 'map']` is not.
|
|
87
|
+
*/
|
|
88
|
+
type Path<T> = T extends BuiltIn ? never : T extends ReadonlyArray<infer U> ? [number] | (Path<U> extends infer P ? (P extends ReadonlyArray<PropertyKey> ? [number, ...P] : never) : never) : T extends object ? {
|
|
89
|
+
[K in keyof T]-?: [K] | (Path<T[K]> extends infer P ? (P extends ReadonlyArray<PropertyKey> ? [K, ...P] : never) : never);
|
|
83
90
|
}[keyof T] : never;
|
|
84
91
|
type PathValue<T, P> = P extends readonly [infer K, ...infer R] ? K extends keyof T ? R["length"] extends 0 ? T[K] : PathValue<T[K], Extract<R, ReadonlyArray<PropertyKey>>> : never : never;
|
|
85
92
|
type ArrayStrategy = {
|
|
@@ -92,18 +99,40 @@ type ArrayStrategy = {
|
|
|
92
99
|
};
|
|
93
100
|
interface DeepMergeOptions {
|
|
94
101
|
arrayStrategy?: ArrayStrategy | ((key: PropertyKey, path: ReadonlyArray<PropertyKey>) => ArrayStrategy);
|
|
102
|
+
/**
|
|
103
|
+
* What an own `undefined` value in the patch does. `"skip"` (the default) keeps the base value;
|
|
104
|
+
* `"assign"` sets the field to `undefined`.
|
|
105
|
+
*/
|
|
106
|
+
undefinedValues?: "assign" | "skip";
|
|
95
107
|
}
|
|
96
108
|
declare const isEmptyObject: (obj: Record<PropertyKey, unknown>) => boolean;
|
|
97
109
|
declare const isPlainObject: (v: unknown) => v is Record<PropertyKey, unknown>;
|
|
110
|
+
/**
|
|
111
|
+
* Merges `patch` into a copy of `base`. Only plain objects (object literals, `JSON.parse` output) are merged
|
|
112
|
+
* key by key. Any other value in the patch, including a class instance, a `Date`, a `Map` or a `Set`, replaces
|
|
113
|
+
* the base value as a whole, so a partial patch for a class-instance field must carry the whole value.
|
|
114
|
+
* `__proto__` keys in the patch are ignored.
|
|
115
|
+
*/
|
|
98
116
|
declare function deepMerge<T>(base: T, patch: DeepPartial<T>, options?: DeepMergeOptions, path?: ReadonlyArray<PropertyKey>): T;
|
|
99
117
|
declare function deepMerge<T>(base: DeepPartial<T>, patch: DeepPartial<T>, options?: DeepMergeOptions, path?: ReadonlyArray<PropertyKey>): DeepPartial<T>;
|
|
100
118
|
declare function mergeInto<T>(base: T, patch: DeepPartial<T>, options?: DeepMergeOptions): T;
|
|
101
119
|
declare function mergePatchInto<T>(base: DeepPartial<T>, patch: DeepPartial<T>, options?: DeepMergeOptions): DeepPartial<T>;
|
|
102
|
-
|
|
103
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Builds a patch that sets `leaf` at `keys`. When `current` (the value the patch will be merged into) holds an
|
|
122
|
+
* array along the path, the patch carries a copy of that array with the one element replaced, since arrays are
|
|
123
|
+
* replaced rather than merged. Without `current`, every level is an object.
|
|
124
|
+
*/
|
|
125
|
+
declare function buildNested(keys: ReadonlyArray<PropertyKey>, leaf: unknown, current?: unknown): Record<PropertyKey, unknown>;
|
|
126
|
+
declare function patchAt<T, P extends Path<T>>(path: P, value: PathValue<T, P>, current?: T): DeepPartial<T>;
|
|
104
127
|
declare function setAt<T, P extends Path<T>>(obj: T, path: P, value: PathValue<T, P>, options?: DeepMergeOptions): T;
|
|
105
128
|
declare function prunePatch<T>(base: T, patch: DeepPartial<T>): DeepPartial<T> | undefined;
|
|
106
129
|
declare function prunePatch<T>(base: DeepPartial<T>, patch: DeepPartial<T>): DeepPartial<T> | undefined;
|
|
130
|
+
/**
|
|
131
|
+
* Structural equality for plain objects, arrays, `Date`, `RegExp`, `Map` and `Set`. Primitives are equal when
|
|
132
|
+
* `===` says so, and `NaN` equals `NaN`. Dates are equal when they hold the same time (two invalid dates are
|
|
133
|
+
* equal). Map keys and set members are matched by `SameValueZero`, like `Map.prototype.has`. Cyclic values are
|
|
134
|
+
* supported.
|
|
135
|
+
*/
|
|
107
136
|
declare function deepEquals<T>(o1: T, o2: T): boolean;
|
|
108
137
|
|
|
109
138
|
/**
|
|
@@ -142,18 +171,6 @@ interface IObjectPatchStore<T> {
|
|
|
142
171
|
* pending changes (i.e. the draft is an empty object).
|
|
143
172
|
*/
|
|
144
173
|
readonly changes: Signal<DeepPartial<T> | undefined>;
|
|
145
|
-
/**
|
|
146
|
-
* `true` when the current draft is structurally equal to the last
|
|
147
|
-
* committed snapshot, i.e. no unsaved changes exist relative to the
|
|
148
|
-
* last commit point.
|
|
149
|
-
*/
|
|
150
|
-
readonly pristine: Signal<boolean>;
|
|
151
|
-
/**
|
|
152
|
-
* The live merged view of base + draft. Prefer `select()` over reading
|
|
153
|
-
* this directly in computed signals when you only care about a specific
|
|
154
|
-
* slice of the object, to avoid unnecessary downstream invalidations.
|
|
155
|
-
*/
|
|
156
|
-
readonly view: Signal<T>;
|
|
157
174
|
/**
|
|
158
175
|
* Discard the current draft without affecting the committed snapshot.
|
|
159
176
|
* The view reverts to the base value.
|
|
@@ -174,7 +191,8 @@ interface IObjectPatchStore<T> {
|
|
|
174
191
|
* Deep-merge `patch` into the current draft. Only the fields present in
|
|
175
192
|
* `patch` are affected; all other fields retain their current draft or
|
|
176
193
|
* base values. Automatically prunes fields that match the base value so
|
|
177
|
-
* the draft stays minimal.
|
|
194
|
+
* the draft stays minimal. A field set to `undefined` is cleared, unless
|
|
195
|
+
* `options.undefinedValues` is `"skip"`.
|
|
178
196
|
*/
|
|
179
197
|
patch(patch: DeepPartial<T>, options?: DeepMergeOptions): void;
|
|
180
198
|
/**
|
|
@@ -185,26 +203,17 @@ interface IObjectPatchStore<T> {
|
|
|
185
203
|
* store.patchPath(['address', 'city'], 'London');
|
|
186
204
|
*/
|
|
187
205
|
patchPath<P extends Path<T>>(path: P, value: PathValue<T, P>, options?: DeepMergeOptions): void;
|
|
206
|
+
/**
|
|
207
|
+
* `true` when the current draft is structurally equal to the last
|
|
208
|
+
* committed snapshot, i.e. no unsaved changes exist relative to the
|
|
209
|
+
* last commit point.
|
|
210
|
+
*/
|
|
211
|
+
readonly pristine: Signal<boolean>;
|
|
188
212
|
/**
|
|
189
213
|
* Discard both the draft and the committed snapshot, fully resetting
|
|
190
214
|
* the store to a pristine, empty state.
|
|
191
215
|
*/
|
|
192
216
|
reset(): void;
|
|
193
|
-
/**
|
|
194
|
-
* Directly replace the draft with the given patch object, bypassing the
|
|
195
|
-
* merge logic. Use with care — this does not prune against the base.
|
|
196
|
-
* Prefer `patch()` for most use cases.
|
|
197
|
-
*/
|
|
198
|
-
setDraft(patch: DeepPartial<T>): void;
|
|
199
|
-
/**
|
|
200
|
-
* Convenience wrapper over `patch` that sets a single path to a value.
|
|
201
|
-
* Unlike the old implementation, this merges rather than replaces the
|
|
202
|
-
* draft, so unrelated draft fields are preserved.
|
|
203
|
-
*
|
|
204
|
-
* @example
|
|
205
|
-
* store.setPath(['address', 'city'], 'London');
|
|
206
|
-
*/
|
|
207
|
-
setPath<P extends Path<T>>(path: P, value: PathValue<T, P>, options?: DeepMergeOptions): void;
|
|
208
217
|
/**
|
|
209
218
|
* Returns a `Signal` that emits only the slice of `view` selected by
|
|
210
219
|
* `projector`. Uses `deepEquals` as the equality function, so downstream
|
|
@@ -222,6 +231,28 @@ interface IObjectPatchStore<T> {
|
|
|
222
231
|
* readonly city = store.select(v => v.address.city);
|
|
223
232
|
*/
|
|
224
233
|
select<R>(projector: (view: T) => R, equal?: (a: R, b: R) => boolean): Signal<R>;
|
|
234
|
+
/**
|
|
235
|
+
* Directly replace the draft with the given patch object, bypassing the
|
|
236
|
+
* merge logic. Use with care — this does not prune against the base, so
|
|
237
|
+
* a field equal to its base value still counts in `changes` and
|
|
238
|
+
* `hasDraft()`, while `pristine` ignores it. Prefer `patch()` for most use
|
|
239
|
+
* cases.
|
|
240
|
+
*/
|
|
241
|
+
setDraft(patch: DeepPartial<T>): void;
|
|
242
|
+
/**
|
|
243
|
+
* Same as `patchPath`: sets a single path to a value, merging into the
|
|
244
|
+
* draft, so unrelated draft fields are preserved.
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* store.setPath(['address', 'city'], 'London');
|
|
248
|
+
*/
|
|
249
|
+
setPath<P extends Path<T>>(path: P, value: PathValue<T, P>, options?: DeepMergeOptions): void;
|
|
250
|
+
/**
|
|
251
|
+
* The live merged view of base + draft. Prefer `select()` over reading
|
|
252
|
+
* this directly in computed signals when you only care about a specific
|
|
253
|
+
* slice of the object, to avoid unnecessary downstream invalidations.
|
|
254
|
+
*/
|
|
255
|
+
readonly view: Signal<T>;
|
|
225
256
|
}
|
|
226
257
|
declare class ObjectPatchStore<T> implements IObjectPatchStore<T> {
|
|
227
258
|
#private;
|
|
@@ -236,11 +267,16 @@ declare class ObjectPatchStore<T> implements IObjectPatchStore<T> {
|
|
|
236
267
|
patch(patch: DeepPartial<T>, options?: DeepMergeOptions): void;
|
|
237
268
|
patchPath<P extends Path<T>>(path: P, value: PathValue<T, P>, options?: DeepMergeOptions): void;
|
|
238
269
|
reset(): void;
|
|
270
|
+
select<R>(projector: (view: T) => R, equal?: (a: R, b: R) => boolean): Signal<R>;
|
|
239
271
|
setDraft(patch: DeepPartial<T>): void;
|
|
240
272
|
setPath<P extends Path<T>>(path: P, value: PathValue<T, P>, options?: DeepMergeOptions): void;
|
|
241
|
-
select<R>(projector: (view: T) => R, equal?: (a: R, b: R) => boolean): Signal<R>;
|
|
242
273
|
}
|
|
243
274
|
interface IterablePatchStoreOptions<TItem, TId> {
|
|
275
|
+
/**
|
|
276
|
+
* Controls what happens to uncommitted drafts when the base signal emits
|
|
277
|
+
* a new value. Defaults to `'wipe'`.
|
|
278
|
+
*/
|
|
279
|
+
baseChangeStrategy?: BaseChangeStrategy;
|
|
244
280
|
/** Extracts the stable identity key from an item. */
|
|
245
281
|
idOf: (item: TItem) => TId;
|
|
246
282
|
/**
|
|
@@ -248,13 +284,10 @@ interface IterablePatchStoreOptions<TItem, TId> {
|
|
|
248
284
|
* per-row edits). Accepts either a `Map` or an `ImmutableDictionary`.
|
|
249
285
|
*/
|
|
250
286
|
initial?: () => Map<TId, DeepPartial<TItem>> | ImmutableDictionary<TId, DeepPartial<TItem>>;
|
|
287
|
+
/** Default merge options applied to all patch operations unless overridden per-call. */
|
|
288
|
+
mergeOptions?: DeepMergeOptions;
|
|
251
289
|
/** Factory that returns the authoritative base signal for this store. */
|
|
252
290
|
source: () => Signal<Iterable<TItem>>;
|
|
253
|
-
/**
|
|
254
|
-
* Controls what happens to uncommitted drafts when the base signal emits
|
|
255
|
-
* a new value. Defaults to `'wipe'`.
|
|
256
|
-
*/
|
|
257
|
-
baseChangeStrategy?: BaseChangeStrategy;
|
|
258
291
|
}
|
|
259
292
|
interface IIterablePatchStore<TItem, TId> {
|
|
260
293
|
/**
|
|
@@ -262,18 +295,6 @@ interface IIterablePatchStore<TItem, TId> {
|
|
|
262
295
|
* with its current draft. Items without a draft are excluded.
|
|
263
296
|
*/
|
|
264
297
|
readonly changes: Signal<TItem[]>;
|
|
265
|
-
/**
|
|
266
|
-
* `true` when every item's current draft is structurally equal to its
|
|
267
|
-
* last committed snapshot.
|
|
268
|
-
*/
|
|
269
|
-
readonly pristine: Signal<boolean>;
|
|
270
|
-
/**
|
|
271
|
-
* The live merged view of all base items with their drafts applied.
|
|
272
|
-
* Items without a draft are returned as-is (same reference).
|
|
273
|
-
* Prefer `select()` over reading this directly in computed signals when
|
|
274
|
-
* you only care about a specific slice.
|
|
275
|
-
*/
|
|
276
|
-
readonly view: Signal<TItem[]>;
|
|
277
298
|
/**
|
|
278
299
|
* Discard the draft for the item identified by `id`.
|
|
279
300
|
* Has no effect if the item has no draft.
|
|
@@ -298,6 +319,8 @@ interface IIterablePatchStore<TItem, TId> {
|
|
|
298
319
|
* Deep-merge `patch` into the draft for the item identified by `id`.
|
|
299
320
|
* Automatically prunes fields that now match the base item so the draft
|
|
300
321
|
* stays minimal. Removes the draft entry entirely if it becomes empty.
|
|
322
|
+
* A field set to `undefined` is cleared, unless `options.undefinedValues`
|
|
323
|
+
* is `"skip"`.
|
|
301
324
|
*/
|
|
302
325
|
patch(id: TId, patch: DeepPartial<TItem>, options?: DeepMergeOptions): void;
|
|
303
326
|
/**
|
|
@@ -314,26 +337,16 @@ interface IIterablePatchStore<TItem, TId> {
|
|
|
314
337
|
* store.patchPath(itemId, ['address', 'city'], 'London');
|
|
315
338
|
*/
|
|
316
339
|
patchPath<P extends Path<TItem>>(id: TId, path: P, value: PathValue<TItem, P>, options?: DeepMergeOptions): void;
|
|
340
|
+
/**
|
|
341
|
+
* `true` when every item's current draft is structurally equal to its
|
|
342
|
+
* last committed snapshot.
|
|
343
|
+
*/
|
|
344
|
+
readonly pristine: Signal<boolean>;
|
|
317
345
|
/**
|
|
318
346
|
* Discard all drafts and committed snapshots, fully resetting the store
|
|
319
347
|
* to a pristine, empty state.
|
|
320
348
|
*/
|
|
321
349
|
reset(): void;
|
|
322
|
-
/**
|
|
323
|
-
* Directly replace the draft for `id` with the given patch object,
|
|
324
|
-
* bypassing the merge logic. Use with care — this does not prune against
|
|
325
|
-
* the base item. Prefer `patch()` for most use cases.
|
|
326
|
-
*/
|
|
327
|
-
setDraft(id: TId, patch: DeepPartial<TItem>): void;
|
|
328
|
-
/**
|
|
329
|
-
* Convenience wrapper over `patch` that sets a single path to a value
|
|
330
|
-
* for a specific item. Merges into the existing draft rather than
|
|
331
|
-
* replacing it, so unrelated draft fields are preserved.
|
|
332
|
-
*
|
|
333
|
-
* @example
|
|
334
|
-
* store.setPath(itemId, ['address', 'city'], 'London');
|
|
335
|
-
*/
|
|
336
|
-
setPath<P extends Path<TItem>>(id: TId, path: P, value: PathValue<TItem, P>, options?: DeepMergeOptions): void;
|
|
337
350
|
/**
|
|
338
351
|
* Returns a `Signal` that emits only the slice of `view` selected by
|
|
339
352
|
* `projector`. Uses `deepEquals` as the equality function by default,
|
|
@@ -347,6 +360,30 @@ interface IIterablePatchStore<TItem, TId> {
|
|
|
347
360
|
* readonly cities = store.select(items => items.map(i => i.address.city));
|
|
348
361
|
*/
|
|
349
362
|
select<R>(projector: (view: TItem[]) => R, equal?: (a: R, b: R) => boolean): Signal<R>;
|
|
363
|
+
/**
|
|
364
|
+
* Directly replace the draft for `id` with the given patch object,
|
|
365
|
+
* bypassing the merge logic. An empty patch removes the draft. Use with
|
|
366
|
+
* care — this does not prune against the base item, so a field equal to
|
|
367
|
+
* its base value still counts in `changes`, `hasDraft()` and
|
|
368
|
+
* `dirtyCount()`, while `pristine` ignores it. Prefer `patch()` for most
|
|
369
|
+
* use cases.
|
|
370
|
+
*/
|
|
371
|
+
setDraft(id: TId, patch: DeepPartial<TItem>): void;
|
|
372
|
+
/**
|
|
373
|
+
* Same as `patchPath`: sets a single path to a value for a specific item,
|
|
374
|
+
* merging into the existing draft, so unrelated draft fields are preserved.
|
|
375
|
+
*
|
|
376
|
+
* @example
|
|
377
|
+
* store.setPath(itemId, ['address', 'city'], 'London');
|
|
378
|
+
*/
|
|
379
|
+
setPath<P extends Path<TItem>>(id: TId, path: P, value: PathValue<TItem, P>, options?: DeepMergeOptions): void;
|
|
380
|
+
/**
|
|
381
|
+
* The live merged view of all base items with their drafts applied.
|
|
382
|
+
* Items without a draft are returned as-is (same reference).
|
|
383
|
+
* Prefer `select()` over reading this directly in computed signals when
|
|
384
|
+
* you only care about a specific slice.
|
|
385
|
+
*/
|
|
386
|
+
readonly view: Signal<TItem[]>;
|
|
350
387
|
}
|
|
351
388
|
declare class IterablePatchStore<TItem, TId> implements IIterablePatchStore<TItem, TId> {
|
|
352
389
|
#private;
|
|
@@ -360,13 +397,13 @@ declare class IterablePatchStore<TItem, TId> implements IIterablePatchStore<TIte
|
|
|
360
397
|
dirtyCount(): number;
|
|
361
398
|
getCommitted(): ImmutableDictionary<TId, DeepPartial<TItem>>;
|
|
362
399
|
hasDraft(id: TId): boolean;
|
|
363
|
-
patch(id: TId, patch: DeepPartial<TItem>, options?: DeepMergeOptions): void;
|
|
364
|
-
patchMany(patchTuple: Iterable<[TId, DeepPartial<TItem>]>, options?: DeepMergeOptions): void;
|
|
400
|
+
patch(id: TId, patch: DeepPartial<TItem>, options?: DeepMergeOptions | undefined): void;
|
|
401
|
+
patchMany(patchTuple: Iterable<[TId, DeepPartial<TItem>]>, options?: DeepMergeOptions | undefined): void;
|
|
365
402
|
patchPath<P extends Path<TItem>>(id: TId, path: P, value: PathValue<TItem, P>, options?: DeepMergeOptions): void;
|
|
366
403
|
reset(): void;
|
|
404
|
+
select<R>(projector: (view: TItem[]) => R, equal?: (a: R, b: R) => boolean): Signal<R>;
|
|
367
405
|
setDraft(id: TId, patch: DeepPartial<TItem>): void;
|
|
368
406
|
setPath<P extends Path<TItem>>(id: TId, path: P, value: PathValue<TItem, P>, options?: DeepMergeOptions): void;
|
|
369
|
-
select<R>(projector: (view: TItem[]) => R, equal?: (a: R, b: R) => boolean): Signal<R>;
|
|
370
407
|
}
|
|
371
408
|
declare const createIterablePatchStore: <TItem, TId>(options: IterablePatchStoreOptions<TItem, TId>) => IIterablePatchStore<TItem, TId>;
|
|
372
409
|
declare const createObjectPatchStore: <T>(options: ObjectPatchStoreOptions<T>) => IObjectPatchStore<T>;
|
|
@@ -378,12 +415,18 @@ declare const classInputToClass: (input: ClassInputType) => string;
|
|
|
378
415
|
* @param value The current value.
|
|
379
416
|
* @param min The minimum value.
|
|
380
417
|
* @param max The maximum value.
|
|
381
|
-
* @param precision The number of decimal places to round the percentage to.
|
|
418
|
+
* @param precision The number of decimal places to round the percentage to, clamped to 0–100.
|
|
382
419
|
* @default 2
|
|
383
|
-
* @returns The percentage of the value within the range, rounded to the specified precision
|
|
420
|
+
* @returns The percentage of the value within the range, rounded to the specified precision, or 0 when the range
|
|
421
|
+
* or the value is not finite.
|
|
384
422
|
*/
|
|
385
423
|
declare const getPercentage: (value: number, min: number, max: number, precision?: number) => number;
|
|
386
424
|
|
|
425
|
+
/**
|
|
426
|
+
* Moves the items at `indices` by `offset` positions, keeping their relative order. Returns a copy of the array
|
|
427
|
+
* unchanged when the offset is 0 or not an integer, or when any index is not an integer, is out of bounds, or would
|
|
428
|
+
* move out of bounds. Duplicate indices count once.
|
|
429
|
+
*/
|
|
387
430
|
declare function moveIndices<T>(array: T[], indices: number[], offset: number): T[];
|
|
388
431
|
|
|
389
432
|
export { AnyPipe, ContainsPipe, FilterChangeEvent, IterablePatchStore, ObjectPatchStore, PreventableEvent, SlicePipe, TakeFirstPipe, buildNested, classInputToClass, createIterablePatchStore, createObjectPatchStore, deepEquals, deepMerge, getPercentage, isEmptyObject, isPlainObject, mergeInto, mergePatchInto, moveIndices, patchAt, prunePatch, setAt };
|