@nanahoshi/mona-ui 0.23.0 → 0.23.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/nanahoshi-mona-ui-dropdown-list.mjs +1 -1
- package/fesm2022/{nanahoshi-mona-ui-grid-grid-footer-row.component--nF_Q7Bd.mjs → nanahoshi-mona-ui-grid-grid-footer-row.component-CIMXz60d.mjs} +2 -2
- package/fesm2022/{nanahoshi-mona-ui-grid-grid-list.component-S2qxwP2M.mjs → nanahoshi-mona-ui-grid-grid-list.component-CpeheGXU.mjs} +4 -4
- package/fesm2022/{nanahoshi-mona-ui-grid-grid-row.component-Dwy8hCm7.mjs → nanahoshi-mona-ui-grid-grid-row.component-B_5ay_Nz.mjs} +2 -2
- package/fesm2022/{nanahoshi-mona-ui-grid-grid-virtual-list.component-DArxdAah.mjs → nanahoshi-mona-ui-grid-grid-virtual-list.component-BzKywU5b.mjs} +4 -4
- package/fesm2022/{nanahoshi-mona-ui-grid-nanahoshi-mona-ui-grid-bduDEm-x.mjs → nanahoshi-mona-ui-grid-nanahoshi-mona-ui-grid-IewqeT4m.mjs} +8 -8
- package/fesm2022/nanahoshi-mona-ui-grid.mjs +1 -1
- package/fesm2022/nanahoshi-mona-ui-i18n.mjs +150 -90
- package/fesm2022/nanahoshi-mona-ui-internal-filter-input.mjs +47 -7
- package/fesm2022/nanahoshi-mona-ui-internal-list.mjs +386 -180
- package/fesm2022/nanahoshi-mona-ui-internal-tree.mjs +266 -99
- package/fesm2022/nanahoshi-mona-ui-internal.mjs +35 -32
- package/fesm2022/nanahoshi-mona-ui-locales.mjs +13 -0
- package/fesm2022/nanahoshi-mona-ui-numeric-text-box.mjs +27 -1
- package/fesm2022/nanahoshi-mona-ui-popup-menu.mjs +1 -1
- package/fesm2022/nanahoshi-mona-ui-tree-view.mjs +1 -1
- package/package.json +1 -1
- package/types/nanahoshi-mona-ui-i18n.d.ts +21 -4
- package/types/nanahoshi-mona-ui-internal-filter-input.d.ts +35 -1
- package/types/nanahoshi-mona-ui-internal-list.d.ts +82 -16
- package/types/nanahoshi-mona-ui-internal-tree.d.ts +50 -3
- package/types/nanahoshi-mona-ui-internal.d.ts +18 -4
- package/types/nanahoshi-mona-ui-locales.d.ts +13 -0
- package/types/nanahoshi-mona-ui-numeric-text-box.d.ts +5 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, inject, signal, computed, Injectable, ElementRef, DestroyRef, makeEnvironmentProviders } from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, signal, computed, Injectable, isDevMode, ElementRef, DestroyRef, makeEnvironmentProviders } from '@angular/core';
|
|
3
3
|
import { Directionality } from '@angular/cdk/bidi';
|
|
4
4
|
import { DateTime } from 'luxon';
|
|
5
5
|
|
|
@@ -13,6 +13,45 @@ const MONA_I18N_CONFIG = new InjectionToken("MONA_I18N_CONFIG", {
|
|
|
13
13
|
factory: () => ({})
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Locale id used whenever a supplied id cannot be turned into a valid BCP 47 tag.
|
|
18
|
+
* Matches the id of `MONA_DEFAULT_LOCALE`.
|
|
19
|
+
*/
|
|
20
|
+
const MONA_FALLBACK_LOCALE_ID = "en-US";
|
|
21
|
+
const canonicalLocaleCache = new Map();
|
|
22
|
+
/**
|
|
23
|
+
* Converts a locale id to a canonical BCP 47 tag, or returns `null` when it is not a valid tag.
|
|
24
|
+
*
|
|
25
|
+
* POSIX/Java style ids are accepted: `en_US` becomes `en-US`, and an encoding or modifier suffix
|
|
26
|
+
* (`de_DE.UTF-8`, `de_DE@euro`) is dropped.
|
|
27
|
+
*/
|
|
28
|
+
function tryCanonicalizeLocaleId(localeId) {
|
|
29
|
+
const raw = (localeId ?? "").trim();
|
|
30
|
+
if (!raw) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (canonicalLocaleCache.has(raw)) {
|
|
34
|
+
return canonicalLocaleCache.get(raw) ?? null;
|
|
35
|
+
}
|
|
36
|
+
let canonical = null;
|
|
37
|
+
try {
|
|
38
|
+
const tag = raw.replace(/[.@].*$/, "").replace(/_/g, "-");
|
|
39
|
+
canonical = Intl.getCanonicalLocales(tag)[0] ?? null;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
canonical = null;
|
|
43
|
+
}
|
|
44
|
+
canonicalLocaleCache.set(raw, canonical);
|
|
45
|
+
return canonical;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Converts a locale id to a canonical BCP 47 tag. Ids that are empty or not valid tags resolve to
|
|
49
|
+
* {@link MONA_FALLBACK_LOCALE_ID}, so every formatter receives the same, valid locale.
|
|
50
|
+
*/
|
|
51
|
+
function canonicalizeLocaleId(localeId) {
|
|
52
|
+
return tryCanonicalizeLocaleId(localeId) ?? MONA_FALLBACK_LOCALE_ID;
|
|
53
|
+
}
|
|
54
|
+
|
|
16
55
|
function isPlainObject(value) {
|
|
17
56
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
18
57
|
}
|
|
@@ -43,6 +82,9 @@ function mergeMessages(fallback, localeMessages, overrideMessages) {
|
|
|
43
82
|
}
|
|
44
83
|
|
|
45
84
|
class MonaI18nService {
|
|
85
|
+
#config;
|
|
86
|
+
#locale;
|
|
87
|
+
#overrides;
|
|
46
88
|
constructor() {
|
|
47
89
|
this.#config = inject(MONA_I18N_CONFIG);
|
|
48
90
|
this.#locale = signal(this.#config.locale ?? MONA_DEFAULT_LOCALE, /* @ts-ignore */
|
|
@@ -52,12 +94,14 @@ class MonaI18nService {
|
|
|
52
94
|
this.direction = computed(() => this.#locale().direction, /* @ts-ignore */
|
|
53
95
|
...(ngDevMode ? [{ debugName: "direction" }] : /* istanbul ignore next */ []));
|
|
54
96
|
this.locale = this.#locale.asReadonly();
|
|
55
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Canonical BCP 47 id of the active locale. POSIX ids such as `en_US` are converted to `en-US`,
|
|
99
|
+
* and invalid ids resolve to `MONA_FALLBACK_LOCALE_ID` so formatters always receive a valid tag.
|
|
100
|
+
*/
|
|
101
|
+
this.localeId = computed(() => canonicalizeLocaleId(this.#locale().id), /* @ts-ignore */
|
|
56
102
|
...(ngDevMode ? [{ debugName: "localeId" }] : /* istanbul ignore next */ []));
|
|
103
|
+
warnOnInvalidLocaleId(this.#locale().id);
|
|
57
104
|
}
|
|
58
|
-
#config;
|
|
59
|
-
#locale;
|
|
60
|
-
#overrides;
|
|
61
105
|
clearMessages() {
|
|
62
106
|
this.#overrides.set({});
|
|
63
107
|
}
|
|
@@ -75,6 +119,7 @@ class MonaI18nService {
|
|
|
75
119
|
this.#overrides.set(messages);
|
|
76
120
|
}
|
|
77
121
|
use(locale) {
|
|
122
|
+
warnOnInvalidLocaleId(locale.id);
|
|
78
123
|
this.#locale.set(locale);
|
|
79
124
|
}
|
|
80
125
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: MonaI18nService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
@@ -85,7 +130,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImpor
|
|
|
85
130
|
args: [{
|
|
86
131
|
providedIn: "root"
|
|
87
132
|
}]
|
|
88
|
-
}] });
|
|
133
|
+
}], ctorParameters: () => [] });
|
|
134
|
+
function warnOnInvalidLocaleId(localeId) {
|
|
135
|
+
if (isDevMode() && tryCanonicalizeLocaleId(localeId) == null) {
|
|
136
|
+
console.warn(`[mona-ui] "${localeId}" is not a valid BCP 47 locale id. Falling back to "${MONA_FALLBACK_LOCALE_ID}".`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
89
139
|
|
|
90
140
|
/**
|
|
91
141
|
* Reads semantic document direction for a connected browser element using `:dir(rtl)` and `:dir(ltr)`.
|
|
@@ -366,6 +416,12 @@ function provideMonaI18n(config = {}) {
|
|
|
366
416
|
]);
|
|
367
417
|
}
|
|
368
418
|
|
|
419
|
+
/** Full-width forms of printable ASCII ("!" to "~"), offset from ASCII by U+FEE0. */
|
|
420
|
+
const FULLWIDTH_ASCII_START = 0xff01;
|
|
421
|
+
const FULLWIDTH_ASCII_END = 0xff5e;
|
|
422
|
+
const FULLWIDTH_ASCII_OFFSET = 0xfee0;
|
|
423
|
+
const DECIMAL_DIGIT_REGEX = /^\p{Nd}$/u;
|
|
424
|
+
const asciiDigitCache = new Map();
|
|
369
425
|
const numberSymbolsCache = new Map();
|
|
370
426
|
const numberFormatterCache = new Map();
|
|
371
427
|
const numberGroupingCache = new Map();
|
|
@@ -376,7 +432,8 @@ function serializeOptions(options) {
|
|
|
376
432
|
const keys = Object.keys(options).sort();
|
|
377
433
|
return keys.map(k => `${k}:${String(options[k])}`).join(";");
|
|
378
434
|
}
|
|
379
|
-
function getNumberGroupingPattern(
|
|
435
|
+
function getNumberGroupingPattern(locale) {
|
|
436
|
+
const localeId = canonicalizeLocaleId(locale);
|
|
380
437
|
let pattern = numberGroupingCache.get(localeId);
|
|
381
438
|
if (!pattern) {
|
|
382
439
|
try {
|
|
@@ -439,7 +496,8 @@ function getNumberGroupingPattern(localeId) {
|
|
|
439
496
|
}
|
|
440
497
|
return pattern;
|
|
441
498
|
}
|
|
442
|
-
function getNumberSymbols(
|
|
499
|
+
function getNumberSymbols(locale) {
|
|
500
|
+
const localeId = canonicalizeLocaleId(locale);
|
|
443
501
|
let symbols = numberSymbolsCache.get(localeId);
|
|
444
502
|
if (!symbols) {
|
|
445
503
|
const digitMap = new Map();
|
|
@@ -482,7 +540,7 @@ function getNumberSymbols(localeId) {
|
|
|
482
540
|
return symbols;
|
|
483
541
|
}
|
|
484
542
|
function getNumberFormatter(locale, options) {
|
|
485
|
-
const cleanLocale = (locale
|
|
543
|
+
const cleanLocale = canonicalizeLocaleId(locale);
|
|
486
544
|
const key = `${cleanLocale}:${serializeOptions(options)}`;
|
|
487
545
|
let formatter = numberFormatterCache.get(key);
|
|
488
546
|
if (!formatter) {
|
|
@@ -499,6 +557,30 @@ function getNumberFormatter(locale, options) {
|
|
|
499
557
|
function formatNumber(value, locale, options) {
|
|
500
558
|
return getNumberFormatter(locale, options).format(value);
|
|
501
559
|
}
|
|
560
|
+
/**
|
|
561
|
+
* ASCII value of a Unicode decimal digit (\p{Nd}), or null for any other character. Unicode encodes every
|
|
562
|
+
* decimal digit set as a contiguous 0-9 run, so the value is the distance from the start of the run.
|
|
563
|
+
*/
|
|
564
|
+
function toAsciiDigit(char) {
|
|
565
|
+
let digit = asciiDigitCache.get(char);
|
|
566
|
+
if (digit === undefined) {
|
|
567
|
+
digit = null;
|
|
568
|
+
if (DECIMAL_DIGIT_REGEX.test(char)) {
|
|
569
|
+
const code = char.codePointAt(0);
|
|
570
|
+
let runStart = code;
|
|
571
|
+
while (DECIMAL_DIGIT_REGEX.test(String.fromCodePoint(runStart - 1))) {
|
|
572
|
+
runStart--;
|
|
573
|
+
}
|
|
574
|
+
digit = String((code - runStart) % 10);
|
|
575
|
+
}
|
|
576
|
+
asciiDigitCache.set(char, digit);
|
|
577
|
+
}
|
|
578
|
+
return digit;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Converts digits of any script to ASCII digits. Full-width forms typed by CJK input methods
|
|
582
|
+
* ("12.5", "-3", "1,000") are folded to ASCII first.
|
|
583
|
+
*/
|
|
502
584
|
function normalizeLocalizedDigits(text, localeId) {
|
|
503
585
|
if (!text) {
|
|
504
586
|
return text;
|
|
@@ -506,27 +588,11 @@ function normalizeLocalizedDigits(text, localeId) {
|
|
|
506
588
|
const symbols = localeId ? getNumberSymbols(localeId) : null;
|
|
507
589
|
let result = "";
|
|
508
590
|
for (const char of text) {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
if (code >= 0x0660 && code <= 0x0669) {
|
|
515
|
-
result += String(code - 0x0660);
|
|
516
|
-
}
|
|
517
|
-
else if (code >= 0x06f0 && code <= 0x06f9) {
|
|
518
|
-
result += String(code - 0x06f0);
|
|
519
|
-
}
|
|
520
|
-
else if (code >= 0x09e6 && code <= 0x09ef) {
|
|
521
|
-
result += String(code - 0x09e6);
|
|
522
|
-
}
|
|
523
|
-
else if (code >= 0x0966 && code <= 0x096f) {
|
|
524
|
-
result += String(code - 0x0966);
|
|
525
|
-
}
|
|
526
|
-
else {
|
|
527
|
-
result += char;
|
|
528
|
-
}
|
|
529
|
-
}
|
|
591
|
+
const code = char.codePointAt(0);
|
|
592
|
+
const folded = code >= FULLWIDTH_ASCII_START && code <= FULLWIDTH_ASCII_END
|
|
593
|
+
? String.fromCharCode(code - FULLWIDTH_ASCII_OFFSET)
|
|
594
|
+
: char;
|
|
595
|
+
result += symbols?.digits.get(folded) ?? toAsciiDigit(folded) ?? folded;
|
|
530
596
|
}
|
|
531
597
|
return result;
|
|
532
598
|
}
|
|
@@ -548,6 +614,21 @@ function normalizeLocalizedInput(text, localeId) {
|
|
|
548
614
|
cleaned = normalizeLocalizedDigits(cleaned, localeId);
|
|
549
615
|
return cleaned;
|
|
550
616
|
}
|
|
617
|
+
/**
|
|
618
|
+
* Decimal separators accepted while editing: the locale's own, plus "." and "," so a separator typed on a
|
|
619
|
+
* keyboard layout that does not match the locale still works. An alternate is dropped when it is the locale's
|
|
620
|
+
* group separator; otherwise "1,234" typed in en-US (or "1.234" in de-DE) would be read as 1.234.
|
|
621
|
+
*/
|
|
622
|
+
function getEditDecimalSeparators(symbols) {
|
|
623
|
+
const primary = symbols.decimal || ".";
|
|
624
|
+
const separators = new Set([primary]);
|
|
625
|
+
for (const alternate of [".", ","]) {
|
|
626
|
+
if (alternate !== symbols.group) {
|
|
627
|
+
separators.add(alternate);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return separators;
|
|
631
|
+
}
|
|
551
632
|
function parseLocalizedNumber(text, localeId, options) {
|
|
552
633
|
if (text == null) {
|
|
553
634
|
return null;
|
|
@@ -699,27 +780,7 @@ function parseLocalizedNumber(text, localeId, options) {
|
|
|
699
780
|
if (/[\s\u00A0\u202F]/.test(cleaned)) {
|
|
700
781
|
return null;
|
|
701
782
|
}
|
|
702
|
-
const
|
|
703
|
-
const candidateSeparators = new Set();
|
|
704
|
-
candidateSeparators.add(primaryDec);
|
|
705
|
-
if (primaryDec === ".") {
|
|
706
|
-
candidateSeparators.add(",");
|
|
707
|
-
}
|
|
708
|
-
else if (primaryDec === ",") {
|
|
709
|
-
candidateSeparators.add(".");
|
|
710
|
-
}
|
|
711
|
-
else {
|
|
712
|
-
candidateSeparators.add(".");
|
|
713
|
-
candidateSeparators.add(",");
|
|
714
|
-
}
|
|
715
|
-
if (primaryDec === "\u066B") {
|
|
716
|
-
candidateSeparators.add(".");
|
|
717
|
-
candidateSeparators.add(",");
|
|
718
|
-
}
|
|
719
|
-
if (primaryDec === "/") {
|
|
720
|
-
candidateSeparators.add(".");
|
|
721
|
-
candidateSeparators.add(",");
|
|
722
|
-
}
|
|
783
|
+
const candidateSeparators = getEditDecimalSeparators(symbols);
|
|
723
784
|
let foundSep = null;
|
|
724
785
|
let sepIndex = -1;
|
|
725
786
|
for (let i = 0; i < cleaned.length; i++) {
|
|
@@ -878,25 +939,7 @@ function validateLocalizedNumberEdit(text, localeId, options) {
|
|
|
878
939
|
if (normalized === "") {
|
|
879
940
|
return { valid: true, complete: false, value: null };
|
|
880
941
|
}
|
|
881
|
-
|
|
882
|
-
const primaryDec = symbols.decimal || ".";
|
|
883
|
-
// Candidate separators: primary decimal, plus alternate '.' or ','
|
|
884
|
-
const candidateSeparators = new Set();
|
|
885
|
-
candidateSeparators.add(primaryDec);
|
|
886
|
-
if (primaryDec === ".") {
|
|
887
|
-
candidateSeparators.add(",");
|
|
888
|
-
}
|
|
889
|
-
else if (primaryDec === ",") {
|
|
890
|
-
candidateSeparators.add(".");
|
|
891
|
-
}
|
|
892
|
-
else {
|
|
893
|
-
candidateSeparators.add(".");
|
|
894
|
-
candidateSeparators.add(",");
|
|
895
|
-
}
|
|
896
|
-
if (primaryDec === "\u066B") {
|
|
897
|
-
candidateSeparators.add(".");
|
|
898
|
-
candidateSeparators.add(",");
|
|
899
|
-
}
|
|
942
|
+
const candidateSeparators = getEditDecimalSeparators(symbols);
|
|
900
943
|
// Count how many separators from the candidate set appear
|
|
901
944
|
let foundSep = null;
|
|
902
945
|
let sepIndex = -1;
|
|
@@ -957,22 +1000,20 @@ function validateLocalizedNumberEdit(text, localeId, options) {
|
|
|
957
1000
|
/**
|
|
958
1001
|
* Normalizes a locale string to ensure it uses the Gregorian calendar system
|
|
959
1002
|
* and doesn't contain a conflicting -u-ca-* extension.
|
|
1003
|
+
* Empty or malformed ids resolve to `MONA_FALLBACK_LOCALE_ID` (en-US).
|
|
960
1004
|
*/
|
|
961
1005
|
function normalizeGregorianLocale(localeId) {
|
|
962
|
-
const
|
|
963
|
-
if (!trimmed) {
|
|
964
|
-
return trimmed;
|
|
965
|
-
}
|
|
1006
|
+
const canonical = canonicalizeLocaleId(localeId);
|
|
966
1007
|
try {
|
|
967
1008
|
if (typeof Intl !== "undefined" && typeof Intl.Locale === "function") {
|
|
968
|
-
const loc = new Intl.Locale(
|
|
1009
|
+
const loc = new Intl.Locale(canonical, { calendar: "gregory" });
|
|
969
1010
|
return loc.toString();
|
|
970
1011
|
}
|
|
971
1012
|
}
|
|
972
1013
|
catch {
|
|
973
|
-
//
|
|
1014
|
+
// Canonical tags are always accepted; keep the canonical id if Intl.Locale still refuses it
|
|
974
1015
|
}
|
|
975
|
-
return
|
|
1016
|
+
return canonical;
|
|
976
1017
|
}
|
|
977
1018
|
/**
|
|
978
1019
|
* Creates a Luxon DateTime from a Date instance, reconfigured to the specified locale
|
|
@@ -1023,17 +1064,29 @@ function stripBidiControls(value) {
|
|
|
1023
1064
|
function sanitizeDateText(value) {
|
|
1024
1065
|
return stripBidiControls(value).replace(/[\u00A0\u2009\u202F]/g, " ");
|
|
1025
1066
|
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Luxon throws for formats that mix incompatible fields (e.g. ordinal and month/day tokens). Callers treat
|
|
1069
|
+
* such input as unparsable, so the error becomes an invalid DateTime.
|
|
1070
|
+
*/
|
|
1071
|
+
function fromFormatSafe(text, format, options) {
|
|
1072
|
+
try {
|
|
1073
|
+
return DateTime.fromFormat(text, format, options);
|
|
1074
|
+
}
|
|
1075
|
+
catch (error) {
|
|
1076
|
+
return DateTime.invalid("unparsable", error instanceof Error ? error.message : String(error));
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1026
1079
|
/**
|
|
1027
1080
|
* Parses a date string using the specified format and locale, forcing Gregorian calendar.
|
|
1028
1081
|
* Localized digits are normalized first to support locales that use native numbering systems.
|
|
1029
1082
|
*/
|
|
1030
1083
|
function parseGregorianDate(text, format, localeId) {
|
|
1031
1084
|
const locale = normalizeGregorianLocale(localeId);
|
|
1032
|
-
let dt =
|
|
1085
|
+
let dt = fromFormatSafe(text, format, { locale, outputCalendar: "gregory" });
|
|
1033
1086
|
if (!dt.isValid) {
|
|
1034
1087
|
const sanitizedText = sanitizeDateText(text);
|
|
1035
1088
|
const sanitizedFormat = sanitizeDateText(format);
|
|
1036
|
-
dt =
|
|
1089
|
+
dt = fromFormatSafe(sanitizedText, sanitizedFormat, { locale, outputCalendar: "gregory" });
|
|
1037
1090
|
if (!dt.isValid) {
|
|
1038
1091
|
let normalizedText = normalizeLocalizedDigits(sanitizedText, locale);
|
|
1039
1092
|
let latnLocale = locale;
|
|
@@ -1045,20 +1098,20 @@ function parseGregorianDate(text, format, localeId) {
|
|
|
1045
1098
|
catch {
|
|
1046
1099
|
// fallback
|
|
1047
1100
|
}
|
|
1048
|
-
dt =
|
|
1101
|
+
dt = fromFormatSafe(normalizedText, sanitizedFormat, { locale: latnLocale, outputCalendar: "gregory" });
|
|
1049
1102
|
if (!dt.isValid) {
|
|
1050
|
-
dt =
|
|
1103
|
+
dt = fromFormatSafe(normalizedText, sanitizedFormat, { locale, outputCalendar: "gregory" });
|
|
1051
1104
|
}
|
|
1052
1105
|
if (!dt.isValid) {
|
|
1053
1106
|
const effectiveLocale = latnLocale !== locale ? latnLocale : locale;
|
|
1054
1107
|
if (sanitizedFormat.includes(". ") && !normalizedText.includes(". ")) {
|
|
1055
1108
|
const textWithDotSpaces = normalizedText.replace(/\.(?!\s|$)/g, ". ");
|
|
1056
|
-
dt =
|
|
1109
|
+
dt = fromFormatSafe(textWithDotSpaces, sanitizedFormat, {
|
|
1057
1110
|
locale: effectiveLocale,
|
|
1058
1111
|
outputCalendar: "gregory"
|
|
1059
1112
|
});
|
|
1060
1113
|
if (!dt.isValid && effectiveLocale !== locale) {
|
|
1061
|
-
dt =
|
|
1114
|
+
dt = fromFormatSafe(textWithDotSpaces, sanitizedFormat, {
|
|
1062
1115
|
locale,
|
|
1063
1116
|
outputCalendar: "gregory"
|
|
1064
1117
|
});
|
|
@@ -1071,12 +1124,12 @@ function parseGregorianDate(text, format, localeId) {
|
|
|
1071
1124
|
if (sanitizedFormat.includes("dd. ") &&
|
|
1072
1125
|
/(\b\d{4}\.\s*\d{1,2}\.\s*\d{1,2})\.?\s+/.test(normalizedText)) {
|
|
1073
1126
|
const withDotBeforeTime = normalizedText.replace(/(\b\d{4}\.\s*\d{1,2}\.\s*\d{1,2})\.?\s+/, (_match, datePart) => `${datePart}. `);
|
|
1074
|
-
dt =
|
|
1127
|
+
dt = fromFormatSafe(withDotBeforeTime, sanitizedFormat, {
|
|
1075
1128
|
locale: effectiveLocale,
|
|
1076
1129
|
outputCalendar: "gregory"
|
|
1077
1130
|
});
|
|
1078
1131
|
if (!dt.isValid && effectiveLocale !== locale) {
|
|
1079
|
-
dt =
|
|
1132
|
+
dt = fromFormatSafe(withDotBeforeTime, sanitizedFormat, {
|
|
1080
1133
|
locale,
|
|
1081
1134
|
outputCalendar: "gregory"
|
|
1082
1135
|
});
|
|
@@ -1089,12 +1142,12 @@ function parseGregorianDate(text, format, localeId) {
|
|
|
1089
1142
|
const trimmedText = normalizedText.trim();
|
|
1090
1143
|
if (trimmedFormat.endsWith(".")) {
|
|
1091
1144
|
const textWithDot = trimmedText.endsWith(".") ? trimmedText : trimmedText + ".";
|
|
1092
|
-
dt =
|
|
1145
|
+
dt = fromFormatSafe(textWithDot, trimmedFormat, {
|
|
1093
1146
|
locale: effectiveLocale,
|
|
1094
1147
|
outputCalendar: "gregory"
|
|
1095
1148
|
});
|
|
1096
1149
|
if (!dt.isValid && effectiveLocale !== locale) {
|
|
1097
|
-
dt =
|
|
1150
|
+
dt = fromFormatSafe(textWithDot, trimmedFormat, {
|
|
1098
1151
|
locale,
|
|
1099
1152
|
outputCalendar: "gregory"
|
|
1100
1153
|
});
|
|
@@ -4560,8 +4613,15 @@ const dateFormatCache = new Map();
|
|
|
4560
4613
|
const timeFormatCache = new Map();
|
|
4561
4614
|
const dateTimeFormatCache = new Map();
|
|
4562
4615
|
const firstDayCache = new Map();
|
|
4616
|
+
/**
|
|
4617
|
+
* Turns an Intl literal part into Luxon format text. ASCII letters would otherwise be read as format tokens
|
|
4618
|
+
* (the "h" in fr-CA "21 h 30", the "klo" in fi-FI "15.09.2026 klo 21.30"), so letter runs are quoted and
|
|
4619
|
+
* apostrophes are doubled.
|
|
4620
|
+
*/
|
|
4563
4621
|
function normalizeEditableLiteral(value) {
|
|
4564
|
-
return sanitizeDateText(value)
|
|
4622
|
+
return sanitizeDateText(value)
|
|
4623
|
+
.replace(/'/g, "''")
|
|
4624
|
+
.replace(/[A-Za-z]+/g, run => `'${run}'`);
|
|
4565
4625
|
}
|
|
4566
4626
|
/**
|
|
4567
4627
|
* Derives a localized numeric date format (e.g. "yyyy/MM/dd" or "dd/MM/yyyy") for the given locale.
|
|
@@ -4721,7 +4781,7 @@ function getLocaleDateTimeInputFormat(localeId, options) {
|
|
|
4721
4781
|
* Intl.Locale.getWeekInfo(), or CLDR fallbacks.
|
|
4722
4782
|
*/
|
|
4723
4783
|
function getLocaleFirstDayOfWeek(localeId) {
|
|
4724
|
-
const normalizedLocaleId = (localeId
|
|
4784
|
+
const normalizedLocaleId = canonicalizeLocaleId(localeId);
|
|
4725
4785
|
let firstDay = firstDayCache.get(normalizedLocaleId);
|
|
4726
4786
|
if (!firstDay) {
|
|
4727
4787
|
firstDay = resolveLocaleFirstDayOfWeek(normalizedLocaleId);
|
|
@@ -4870,5 +4930,5 @@ function generatePseudoLocale(catalogs, options) {
|
|
|
4870
4930
|
* Generated bundle index. Do not edit.
|
|
4871
4931
|
*/
|
|
4872
4932
|
|
|
4873
|
-
export { BIDI_CONTROL_REGEX, MONA_DEFAULT_LOCALE, MONA_I18N_CONFIG, MonaDirectionService, MonaI18nService, formatGregorianDateToLocaleString, formatNumber, generatePseudoLocale, getLocaleDateInputFormat, getLocaleDateTimeInputFormat, getLocaleFirstDayOfWeek, getLocaleTimeInputFormat, getNumberFormatter, getNumberGroupingPattern, getNumberSymbols, gregorianDateTime, gregorianDateTimeFromObject, injectComponentDirection, mergeMessages, mergeTwo, normalizeGregorianLocale, normalizeLocalizedDigits, normalizeLocalizedInput, normalizeLocalizedMinus, observeComponentDirection, parseGregorianDate, parseLocalizedNumber, provideMonaI18n, pseudoLocalize, pseudoLocalizeCatalog, resolveComponentDirection, sanitizeDateText, stripBidiControls, validateLocalizedNumber, validateLocalizedNumberEdit };
|
|
4933
|
+
export { BIDI_CONTROL_REGEX, MONA_DEFAULT_LOCALE, MONA_I18N_CONFIG, MonaDirectionService, MonaI18nService, formatGregorianDateToLocaleString, formatNumber, generatePseudoLocale, getEditDecimalSeparators, getLocaleDateInputFormat, getLocaleDateTimeInputFormat, getLocaleFirstDayOfWeek, getLocaleTimeInputFormat, getNumberFormatter, getNumberGroupingPattern, getNumberSymbols, gregorianDateTime, gregorianDateTimeFromObject, injectComponentDirection, mergeMessages, mergeTwo, normalizeGregorianLocale, normalizeLocalizedDigits, normalizeLocalizedInput, normalizeLocalizedMinus, observeComponentDirection, parseGregorianDate, parseLocalizedNumber, provideMonaI18n, pseudoLocalize, pseudoLocalizeCatalog, resolveComponentDirection, sanitizeDateText, stripBidiControls, validateLocalizedNumber, validateLocalizedNumberEdit };
|
|
4874
4934
|
//# sourceMappingURL=nanahoshi-mona-ui-i18n.mjs.map
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, DestroyRef, ElementRef, signal, input, output, effect, untracked, afterNextRender, Component } from '@angular/core';
|
|
3
|
-
import { LucideSearch } from '@lucide/angular';
|
|
2
|
+
import { inject, DestroyRef, ElementRef, signal, computed, input, output, effect, untracked, afterNextRender, Component } from '@angular/core';
|
|
4
3
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
5
4
|
import * as i1 from '@angular/forms';
|
|
6
5
|
import { FormsModule } from '@angular/forms';
|
|
6
|
+
import { LucideSearch } from '@lucide/angular';
|
|
7
7
|
import { FilterChangeEvent } from '@nanahoshi/mona-ui/common';
|
|
8
8
|
import { TextBoxComponent, TextBoxPrefixTemplateDirective } from '@nanahoshi/mona-ui/text-box';
|
|
9
|
-
import { Subject, fromEvent,
|
|
9
|
+
import { Subject, fromEvent, debounce, timer } from 'rxjs';
|
|
10
10
|
|
|
11
11
|
class FilterInputComponent {
|
|
12
12
|
#destroyRef;
|
|
@@ -17,6 +17,44 @@ class FilterInputComponent {
|
|
|
17
17
|
this.filter$ = new Subject();
|
|
18
18
|
this.filterText = signal("", /* @ts-ignore */
|
|
19
19
|
...(ngDevMode ? [{ debugName: "filterText" }] : /* istanbul ignore next */ []));
|
|
20
|
+
/**
|
|
21
|
+
* When the input controls a list, it acts as a combobox (WAI-ARIA APG): focus stays in the input
|
|
22
|
+
* and the highlighted option is exposed through `aria-activedescendant`.
|
|
23
|
+
*/
|
|
24
|
+
this.inputAttributes = computed(() => {
|
|
25
|
+
const controls = this.controls();
|
|
26
|
+
const ariaLabel = this.ariaLabel() || null;
|
|
27
|
+
if (!controls) {
|
|
28
|
+
return { "aria-label": ariaLabel };
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
role: "combobox",
|
|
32
|
+
"aria-label": ariaLabel,
|
|
33
|
+
"aria-activedescendant": this.activeDescendant() || null,
|
|
34
|
+
"aria-autocomplete": "list",
|
|
35
|
+
"aria-controls": controls,
|
|
36
|
+
"aria-expanded": true
|
|
37
|
+
};
|
|
38
|
+
}, /* @ts-ignore */
|
|
39
|
+
...(ngDevMode ? [{ debugName: "inputAttributes" }] : /* istanbul ignore next */ []));
|
|
40
|
+
/**
|
|
41
|
+
* @description Id of the highlighted option in the controlled list, exposed as `aria-activedescendant`.
|
|
42
|
+
* @default ""
|
|
43
|
+
*/
|
|
44
|
+
this.activeDescendant = input("", /* @ts-ignore */
|
|
45
|
+
...(ngDevMode ? [{ debugName: "activeDescendant" }] : /* istanbul ignore next */ []));
|
|
46
|
+
/**
|
|
47
|
+
* @description Accessible name of the input. The placeholder is not a reliable label, so set this whenever possible.
|
|
48
|
+
* @default ""
|
|
49
|
+
*/
|
|
50
|
+
this.ariaLabel = input("", /* @ts-ignore */
|
|
51
|
+
...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
52
|
+
/**
|
|
53
|
+
* @description Id of the list this input filters. When set, the input is exposed as a combobox controlling it.
|
|
54
|
+
* @default ""
|
|
55
|
+
*/
|
|
56
|
+
this.controls = input("", /* @ts-ignore */
|
|
57
|
+
...(ngDevMode ? [{ debugName: "controls" }] : /* istanbul ignore next */ []));
|
|
20
58
|
this.debounce = input(0, /* @ts-ignore */
|
|
21
59
|
...(ngDevMode ? [{ debugName: "debounce" }] : /* istanbul ignore next */ []));
|
|
22
60
|
this.filter = input("", /* @ts-ignore */
|
|
@@ -59,7 +97,9 @@ class FilterInputComponent {
|
|
|
59
97
|
this.setSubscriptions();
|
|
60
98
|
}
|
|
61
99
|
setSubscriptions() {
|
|
62
|
-
this.filter
|
|
100
|
+
this.filter$
|
|
101
|
+
.pipe(debounce(() => timer(this.debounce())), takeUntilDestroyed(this.#destroyRef))
|
|
102
|
+
.subscribe(value => {
|
|
63
103
|
const event = new FilterChangeEvent(value);
|
|
64
104
|
this.filterChange.emit(event);
|
|
65
105
|
if (!event.isDefaultPrevented()) {
|
|
@@ -69,12 +109,12 @@ class FilterInputComponent {
|
|
|
69
109
|
});
|
|
70
110
|
}
|
|
71
111
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: FilterInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
72
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.6", type: FilterInputComponent, isStandalone: true, selector: "mona-filter-input", inputs: { debounce: { classPropertyName: "debounce", publicName: "debounce", isSignal: true, isRequired: false, transformFunction: null }, filter: { classPropertyName: "filter", publicName: "filter", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { filterChange: "filterChange", inputBlur: "inputBlur", inputFocus: "inputFocus" }, ngImport: i0, template: "<mona-text-box\n [ngModel]=\"filterText()\"\n [clearButton]=\"true\"\n [placeholder]=\"placeholder()\"\n (inputFocus)=\"inputFocus.emit($event)\"\n (inputBlur)=\"inputBlur.emit($event)\"\n [tabindex]=\"0\"\n [size]=\"size()\"\n class=\"outline-none shadow-none focus-within:ring-1 focus-within:ring-primary/40\"\n (ngModelChange)=\"filter$.next($event)\">\n <ng-template monaTextBoxPrefixTemplate>\n <div class=\"h-full flex items-center justify-center ps-2\">\n <svg lucideSearch [size]=\"16\" aria-hidden=\"true\"></svg>\n </div>\n </ng-template>\n</mona-text-box>\n", dependencies: [{ kind: "component", type: LucideSearch, selector: "svg[lucideSearch]" }, { kind: "component", type: TextBoxComponent, selector: "mona-text-box", inputs: ["clearButton", "disabled", "inputAttributes", "inputClass", "inputStyle", "invalid", "placeholder", "readonly", "required", "rounded", "size", "touched", "type", "class", "value"], outputs: ["inputBlur", "inputFocus", "touch", "valueChange"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: TextBoxPrefixTemplateDirective, selector: "ng-template[monaTextBoxPrefixTemplate]" }] }); }
|
|
112
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.6", type: FilterInputComponent, isStandalone: true, selector: "mona-filter-input", inputs: { activeDescendant: { classPropertyName: "activeDescendant", publicName: "activeDescendant", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, controls: { classPropertyName: "controls", publicName: "controls", isSignal: true, isRequired: false, transformFunction: null }, debounce: { classPropertyName: "debounce", publicName: "debounce", isSignal: true, isRequired: false, transformFunction: null }, filter: { classPropertyName: "filter", publicName: "filter", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { filterChange: "filterChange", inputBlur: "inputBlur", inputFocus: "inputFocus" }, ngImport: i0, template: "<mona-text-box\n [ngModel]=\"filterText()\"\n [clearButton]=\"true\"\n [placeholder]=\"placeholder()\"\n (inputFocus)=\"inputFocus.emit($event)\"\n (inputBlur)=\"inputBlur.emit($event)\"\n [tabindex]=\"0\"\n [inputAttributes]=\"inputAttributes()\"\n [size]=\"size()\"\n class=\"outline-none shadow-none focus-within:ring-1 focus-within:ring-primary/40\"\n (ngModelChange)=\"filter$.next($event)\">\n <ng-template monaTextBoxPrefixTemplate>\n <div class=\"h-full flex items-center justify-center ps-2\">\n <svg lucideSearch [size]=\"16\" aria-hidden=\"true\"></svg>\n </div>\n </ng-template>\n</mona-text-box>\n", dependencies: [{ kind: "component", type: LucideSearch, selector: "svg[lucideSearch]" }, { kind: "component", type: TextBoxComponent, selector: "mona-text-box", inputs: ["clearButton", "disabled", "inputAttributes", "inputClass", "inputStyle", "invalid", "placeholder", "readonly", "required", "rounded", "size", "touched", "type", "class", "value"], outputs: ["inputBlur", "inputFocus", "touch", "valueChange"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: TextBoxPrefixTemplateDirective, selector: "ng-template[monaTextBoxPrefixTemplate]" }] }); }
|
|
73
113
|
}
|
|
74
114
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: FilterInputComponent, decorators: [{
|
|
75
115
|
type: Component,
|
|
76
|
-
args: [{ selector: "mona-filter-input", imports: [LucideSearch, TextBoxComponent, FormsModule, TextBoxPrefixTemplateDirective], template: "<mona-text-box\n [ngModel]=\"filterText()\"\n [clearButton]=\"true\"\n [placeholder]=\"placeholder()\"\n (inputFocus)=\"inputFocus.emit($event)\"\n (inputBlur)=\"inputBlur.emit($event)\"\n [tabindex]=\"0\"\n [size]=\"size()\"\n class=\"outline-none shadow-none focus-within:ring-1 focus-within:ring-primary/40\"\n (ngModelChange)=\"filter$.next($event)\">\n <ng-template monaTextBoxPrefixTemplate>\n <div class=\"h-full flex items-center justify-center ps-2\">\n <svg lucideSearch [size]=\"16\" aria-hidden=\"true\"></svg>\n </div>\n </ng-template>\n</mona-text-box>\n" }]
|
|
77
|
-
}], ctorParameters: () => [], propDecorators: { debounce: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounce", required: false }] }], filter: [{ type: i0.Input, args: [{ isSignal: true, alias: "filter", required: false }] }], filterChange: [{ type: i0.Output, args: ["filterChange"] }], inputBlur: [{ type: i0.Output, args: ["inputBlur"] }], inputFocus: [{ type: i0.Output, args: ["inputFocus"] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
|
|
116
|
+
args: [{ selector: "mona-filter-input", imports: [LucideSearch, TextBoxComponent, FormsModule, TextBoxPrefixTemplateDirective], template: "<mona-text-box\n [ngModel]=\"filterText()\"\n [clearButton]=\"true\"\n [placeholder]=\"placeholder()\"\n (inputFocus)=\"inputFocus.emit($event)\"\n (inputBlur)=\"inputBlur.emit($event)\"\n [tabindex]=\"0\"\n [inputAttributes]=\"inputAttributes()\"\n [size]=\"size()\"\n class=\"outline-none shadow-none focus-within:ring-1 focus-within:ring-primary/40\"\n (ngModelChange)=\"filter$.next($event)\">\n <ng-template monaTextBoxPrefixTemplate>\n <div class=\"h-full flex items-center justify-center ps-2\">\n <svg lucideSearch [size]=\"16\" aria-hidden=\"true\"></svg>\n </div>\n </ng-template>\n</mona-text-box>\n" }]
|
|
117
|
+
}], ctorParameters: () => [], propDecorators: { activeDescendant: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeDescendant", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], controls: [{ type: i0.Input, args: [{ isSignal: true, alias: "controls", required: false }] }], debounce: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounce", required: false }] }], filter: [{ type: i0.Input, args: [{ isSignal: true, alias: "filter", required: false }] }], filterChange: [{ type: i0.Output, args: ["filterChange"] }], inputBlur: [{ type: i0.Output, args: ["inputBlur"] }], inputFocus: [{ type: i0.Output, args: ["inputFocus"] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
|
|
78
118
|
|
|
79
119
|
/*
|
|
80
120
|
* Public API Surface of @nanahoshi/mona-ui/filter-input
|