@nanahoshi/mona-ui 0.23.3 → 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-auto-complete.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-breadcrumb.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-button-group.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-calendar.mjs +13 -11
- package/fesm2022/nanahoshi-mona-ui-card.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-chart.mjs +4 -2
- package/fesm2022/nanahoshi-mona-ui-chip.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-color-gradient.mjs +5 -3
- package/fesm2022/nanahoshi-mona-ui-color-palette.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-color-picker.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-combo-box.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-common.mjs +265 -159
- package/fesm2022/nanahoshi-mona-ui-date-picker.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-datetime-picker.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-dialog.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-dropdown-list.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-dropdowns.mjs +5 -3
- package/fesm2022/nanahoshi-mona-ui-editor.mjs +5 -3
- package/fesm2022/nanahoshi-mona-ui-filter.mjs +3 -1
- package/fesm2022/{nanahoshi-mona-ui-grid-grid-footer-row.component-BSVlcB9_.mjs → nanahoshi-mona-ui-grid-grid-footer-row.component-CnPhVIr4.mjs} +2 -2
- package/fesm2022/{nanahoshi-mona-ui-grid-grid-list.component-B2pBOuLa.mjs → nanahoshi-mona-ui-grid-grid-list.component-DxY6bfTX.mjs} +4 -4
- package/fesm2022/{nanahoshi-mona-ui-grid-grid-row.component-Buf4evf5.mjs → nanahoshi-mona-ui-grid-grid-row.component-CaO5Lae6.mjs} +3 -3
- package/fesm2022/{nanahoshi-mona-ui-grid-grid-virtual-list.component-1RdqLEa9.mjs → nanahoshi-mona-ui-grid-grid-virtual-list.component-jNYtqg5B.mjs} +4 -4
- package/fesm2022/{nanahoshi-mona-ui-grid-nanahoshi-mona-ui-grid-DeRc_QcP.mjs → nanahoshi-mona-ui-grid-nanahoshi-mona-ui-grid-9FtfCutf.mjs} +12 -10
- package/fesm2022/nanahoshi-mona-ui-grid.mjs +1 -1
- package/fesm2022/nanahoshi-mona-ui-i18n-icu.mjs +888 -0
- package/fesm2022/nanahoshi-mona-ui-i18n.mjs +337 -104
- package/fesm2022/nanahoshi-mona-ui-internal-list.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-internal-tree.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-list-box.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-locales-ar-sa.mjs +401 -0
- package/fesm2022/nanahoshi-mona-ui-locales-de-de.mjs +399 -0
- package/fesm2022/nanahoshi-mona-ui-locales-es-es.mjs +399 -0
- package/fesm2022/nanahoshi-mona-ui-locales-fr-fr.mjs +399 -0
- package/fesm2022/nanahoshi-mona-ui-locales-id-id.mjs +397 -0
- package/fesm2022/nanahoshi-mona-ui-locales-it-it.mjs +399 -0
- package/fesm2022/nanahoshi-mona-ui-locales-ja-jp.mjs +397 -0
- package/fesm2022/nanahoshi-mona-ui-locales-ko-kr.mjs +397 -0
- package/fesm2022/nanahoshi-mona-ui-locales-pt-br.mjs +399 -0
- package/fesm2022/nanahoshi-mona-ui-locales-ru-ru.mjs +401 -0
- package/fesm2022/nanahoshi-mona-ui-locales-tr-tr.mjs +397 -0
- package/fesm2022/nanahoshi-mona-ui-locales-zh-cn.mjs +397 -0
- package/fesm2022/nanahoshi-mona-ui-locales-zh-tw.mjs +397 -0
- package/fesm2022/nanahoshi-mona-ui-locales.mjs +76 -5060
- package/fesm2022/nanahoshi-mona-ui-multi-select.mjs +5 -3
- package/fesm2022/nanahoshi-mona-ui-notification.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-numeric-text-box.mjs +299 -167
- package/fesm2022/nanahoshi-mona-ui-otp-input.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-pager.mjs +5 -3
- package/fesm2022/nanahoshi-mona-ui-rating.mjs +4 -2
- package/fesm2022/nanahoshi-mona-ui-scroll-view.mjs +5 -3
- package/fesm2022/nanahoshi-mona-ui-sheet.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-slider.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-spinner.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-split-button.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-splitter.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-stepper.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-tabs.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-text-box.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-time-picker.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-time-selector.mjs +2 -0
- package/fesm2022/nanahoshi-mona-ui-window.mjs +2 -0
- package/i18n/catalogs/en-US.messages.json +373 -0
- package/i18n/icu/package.json +4 -0
- package/locales/ar-sa/ar-sa.messages.json +373 -0
- package/locales/ar-sa/package.json +4 -0
- package/locales/de-de/de-de.messages.json +373 -0
- package/locales/de-de/package.json +4 -0
- package/locales/es-es/es-es.messages.json +373 -0
- package/locales/es-es/package.json +4 -0
- package/locales/fr-fr/fr-fr.messages.json +373 -0
- package/locales/fr-fr/package.json +4 -0
- package/locales/id-id/id-id.messages.json +373 -0
- package/locales/id-id/package.json +4 -0
- package/locales/it-it/it-it.messages.json +373 -0
- package/locales/it-it/package.json +4 -0
- package/locales/ja-jp/ja-jp.messages.json +373 -0
- package/locales/ja-jp/package.json +4 -0
- package/locales/ko-kr/ko-kr.messages.json +373 -0
- package/locales/ko-kr/package.json +4 -0
- package/locales/pt-br/package.json +4 -0
- package/locales/pt-br/pt-br.messages.json +373 -0
- package/locales/ru-ru/package.json +4 -0
- package/locales/ru-ru/ru-ru.messages.json +373 -0
- package/locales/tr-tr/package.json +4 -0
- package/locales/tr-tr/tr-tr.messages.json +373 -0
- package/locales/zh-cn/package.json +4 -0
- package/locales/zh-cn/zh-cn.messages.json +373 -0
- package/locales/zh-tw/package.json +4 -0
- package/locales/zh-tw/zh-tw.messages.json +373 -0
- package/package.json +111 -53
- package/types/nanahoshi-mona-ui-common.d.ts +116 -73
- package/types/nanahoshi-mona-ui-i18n-icu.d.ts +74 -0
- package/types/nanahoshi-mona-ui-i18n.d.ts +108 -6
- package/types/nanahoshi-mona-ui-locales-ar-sa.d.ts +379 -0
- package/types/nanahoshi-mona-ui-locales-de-de.d.ts +379 -0
- package/types/nanahoshi-mona-ui-locales-es-es.d.ts +379 -0
- package/types/nanahoshi-mona-ui-locales-fr-fr.d.ts +379 -0
- package/types/nanahoshi-mona-ui-locales-id-id.d.ts +379 -0
- package/types/nanahoshi-mona-ui-locales-it-it.d.ts +379 -0
- package/types/nanahoshi-mona-ui-locales-ja-jp.d.ts +379 -0
- package/types/nanahoshi-mona-ui-locales-ko-kr.d.ts +379 -0
- package/types/nanahoshi-mona-ui-locales-pt-br.d.ts +379 -0
- package/types/nanahoshi-mona-ui-locales-ru-ru.d.ts +379 -0
- package/types/nanahoshi-mona-ui-locales-tr-tr.d.ts +379 -0
- package/types/nanahoshi-mona-ui-locales-zh-cn.d.ts +379 -0
- package/types/nanahoshi-mona-ui-locales-zh-tw.d.ts +379 -0
- package/types/nanahoshi-mona-ui-locales.d.ts +30 -4915
- package/types/nanahoshi-mona-ui-numeric-text-box.d.ts +113 -37
|
@@ -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
|