ngwr 13.0.0 → 14.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -6
- package/color-picker/styles/_index.scss +43 -5
- package/fesm2022/ngwr-alert.mjs +5 -5
- package/fesm2022/ngwr-alert.mjs.map +1 -1
- package/fesm2022/ngwr-back-top.mjs +11 -1
- package/fesm2022/ngwr-back-top.mjs.map +1 -1
- package/fesm2022/ngwr-blur-text.mjs +13 -2
- package/fesm2022/ngwr-blur-text.mjs.map +1 -1
- package/fesm2022/ngwr-border-glow.mjs +12 -3
- package/fesm2022/ngwr-border-glow.mjs.map +1 -1
- package/fesm2022/ngwr-breadcrumbs.mjs +14 -3
- package/fesm2022/ngwr-breadcrumbs.mjs.map +1 -1
- package/fesm2022/ngwr-button.mjs +4 -4
- package/fesm2022/ngwr-button.mjs.map +1 -1
- package/fesm2022/ngwr-calendar-testing.mjs +15 -0
- package/fesm2022/ngwr-calendar-testing.mjs.map +1 -1
- package/fesm2022/ngwr-calendar.mjs +51 -5
- package/fesm2022/ngwr-calendar.mjs.map +1 -1
- package/fesm2022/ngwr-circular-text.mjs +20 -1
- package/fesm2022/ngwr-circular-text.mjs.map +1 -1
- package/fesm2022/ngwr-collapse.mjs.map +1 -1
- package/fesm2022/ngwr-color-picker-testing.mjs +23 -7
- package/fesm2022/ngwr-color-picker-testing.mjs.map +1 -1
- package/fesm2022/ngwr-color-picker.mjs +89 -10
- package/fesm2022/ngwr-color-picker.mjs.map +1 -1
- package/fesm2022/ngwr-command-palette.mjs +27 -3
- package/fesm2022/ngwr-command-palette.mjs.map +1 -1
- package/fesm2022/ngwr-context-menu.mjs +13 -3
- package/fesm2022/ngwr-context-menu.mjs.map +1 -1
- package/fesm2022/ngwr-date-adapters-fns.mjs +101 -33
- package/fesm2022/ngwr-date-adapters-fns.mjs.map +1 -1
- package/fesm2022/ngwr-date-picker-testing.mjs +10 -2
- package/fesm2022/ngwr-date-picker-testing.mjs.map +1 -1
- package/fesm2022/ngwr-date-picker.mjs +103 -13
- package/fesm2022/ngwr-date-picker.mjs.map +1 -1
- package/fesm2022/ngwr-date.mjs +480 -79
- package/fesm2022/ngwr-date.mjs.map +1 -1
- package/fesm2022/ngwr-dialog.mjs +22 -16
- package/fesm2022/ngwr-dialog.mjs.map +1 -1
- package/fesm2022/ngwr-directives.mjs +5 -2
- package/fesm2022/ngwr-directives.mjs.map +1 -1
- package/fesm2022/ngwr-drag-drop.mjs +30 -2
- package/fesm2022/ngwr-drag-drop.mjs.map +1 -1
- package/fesm2022/ngwr-drawer.mjs +24 -18
- package/fesm2022/ngwr-drawer.mjs.map +1 -1
- package/fesm2022/ngwr-dropdown.mjs +8 -3
- package/fesm2022/ngwr-dropdown.mjs.map +1 -1
- package/fesm2022/ngwr-event-calendar-testing.mjs.map +1 -1
- package/fesm2022/ngwr-event-calendar.mjs +1 -1
- package/fesm2022/ngwr-event-calendar.mjs.map +1 -1
- package/fesm2022/ngwr-file-upload.mjs +52 -12
- package/fesm2022/ngwr-file-upload.mjs.map +1 -1
- package/fesm2022/ngwr-fuzzy-text.mjs +21 -4
- package/fesm2022/ngwr-fuzzy-text.mjs.map +1 -1
- package/fesm2022/ngwr-gradient-text.mjs +9 -2
- package/fesm2022/ngwr-gradient-text.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-en.mjs +63 -1
- package/fesm2022/ngwr-i18n-en.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-ru.mjs +28 -1
- package/fesm2022/ngwr-i18n-ru.mjs.map +1 -1
- package/fesm2022/ngwr-i18n.mjs +150 -22
- package/fesm2022/ngwr-i18n.mjs.map +1 -1
- package/fesm2022/ngwr-icon-adapters-feather.mjs +11 -0
- package/fesm2022/ngwr-icon-adapters-feather.mjs.map +1 -1
- package/fesm2022/ngwr-icon-adapters-lucide.mjs +22 -2
- package/fesm2022/ngwr-icon-adapters-lucide.mjs.map +1 -1
- package/fesm2022/ngwr-icon.mjs +474 -37
- package/fesm2022/ngwr-icon.mjs.map +1 -1
- package/fesm2022/ngwr-input-number.mjs +3 -3
- package/fesm2022/ngwr-input-number.mjs.map +1 -1
- package/fesm2022/ngwr-input-otp.mjs +21 -3
- package/fesm2022/ngwr-input-otp.mjs.map +1 -1
- package/fesm2022/ngwr-input.mjs +14 -7
- package/fesm2022/ngwr-input.mjs.map +1 -1
- package/fesm2022/ngwr-knob-testing.mjs.map +1 -1
- package/fesm2022/ngwr-knob.mjs +20 -2
- package/fesm2022/ngwr-knob.mjs.map +1 -1
- package/fesm2022/ngwr-loading-bar-router.mjs +73 -0
- package/fesm2022/ngwr-loading-bar-router.mjs.map +1 -0
- package/fesm2022/ngwr-loading-bar.mjs +21 -32
- package/fesm2022/ngwr-loading-bar.mjs.map +1 -1
- package/fesm2022/ngwr-markdown.mjs +63 -11
- package/fesm2022/ngwr-markdown.mjs.map +1 -1
- package/fesm2022/ngwr-mention.mjs +104 -14
- package/fesm2022/ngwr-mention.mjs.map +1 -1
- package/fesm2022/ngwr-overlay.mjs +36 -7
- package/fesm2022/ngwr-overlay.mjs.map +1 -1
- package/fesm2022/ngwr-pagination-testing.mjs +11 -10
- package/fesm2022/ngwr-pagination-testing.mjs.map +1 -1
- package/fesm2022/ngwr-pagination.mjs +65 -30
- package/fesm2022/ngwr-pagination.mjs.map +1 -1
- package/fesm2022/ngwr-popconfirm.mjs +3 -2
- package/fesm2022/ngwr-popconfirm.mjs.map +1 -1
- package/fesm2022/ngwr-popover.mjs +2 -1
- package/fesm2022/ngwr-popover.mjs.map +1 -1
- package/fesm2022/ngwr-qr.mjs +38 -1
- package/fesm2022/ngwr-qr.mjs.map +1 -1
- package/fesm2022/ngwr-select.mjs +93 -34
- package/fesm2022/ngwr-select.mjs.map +1 -1
- package/fesm2022/ngwr-shiny-text.mjs +10 -3
- package/fesm2022/ngwr-shiny-text.mjs.map +1 -1
- package/fesm2022/ngwr-splitter-testing.mjs.map +1 -1
- package/fesm2022/ngwr-squircle.mjs +2 -1
- package/fesm2022/ngwr-squircle.mjs.map +1 -1
- package/fesm2022/ngwr-statistic.mjs +35 -4
- package/fesm2022/ngwr-statistic.mjs.map +1 -1
- package/fesm2022/ngwr-table.mjs +35 -12
- package/fesm2022/ngwr-table.mjs.map +1 -1
- package/fesm2022/ngwr-tabs-router.mjs +158 -0
- package/fesm2022/ngwr-tabs-router.mjs.map +1 -0
- package/fesm2022/ngwr-tabs-testing.mjs +13 -11
- package/fesm2022/ngwr-tabs-testing.mjs.map +1 -1
- package/fesm2022/ngwr-tabs.mjs +104 -12
- package/fesm2022/ngwr-tabs.mjs.map +1 -1
- package/fesm2022/ngwr-textarea.mjs +20 -1
- package/fesm2022/ngwr-textarea.mjs.map +1 -1
- package/fesm2022/ngwr-tour.mjs +1 -1
- package/fesm2022/ngwr-tour.mjs.map +1 -1
- package/fesm2022/ngwr-transfer.mjs +2 -2
- package/fesm2022/ngwr-transfer.mjs.map +1 -1
- package/fesm2022/ngwr-tree.mjs +18 -2
- package/fesm2022/ngwr-tree.mjs.map +1 -1
- package/fesm2022/ngwr-utils.mjs +143 -1
- package/fesm2022/ngwr-utils.mjs.map +1 -1
- package/fesm2022/ngwr-version.mjs +1 -1
- package/fesm2022/ngwr-version.mjs.map +1 -1
- package/fesm2022/ngwr-window-testing.mjs +10 -2
- package/fesm2022/ngwr-window-testing.mjs.map +1 -1
- package/fesm2022/ngwr-window.mjs +4 -4
- package/fesm2022/ngwr-window.mjs.map +1 -1
- package/i18n/en.json +33 -6
- package/i18n/ru.json +33 -6
- package/llms-full.txt +20 -11
- package/llms.txt +47 -16
- package/mcp/catalog.js +3 -3
- package/mcp/providers.js +11 -0
- package/package.json +11 -3
- package/schematics/migrations/v13/index.js +101 -0
- package/schematics/migrations/v14/index.js +422 -0
- package/schematics/migrations.json +10 -0
- package/schematics/page/templates.js +1 -1
- package/schematics/use/symbol-map.json +1 -0
- package/skills/ngwr/SKILL.md +4 -1
- package/skills/ngwr/references/catalog.md +2 -0
- package/skills/ngwr/references/setup.md +4 -0
- package/stepper/styles/_index.scss +19 -1
- package/textarea/styles/_index.scss +14 -6
- package/toast/styles/_index.scss +12 -4
- package/types/ngwr-alert.d.ts +3 -3
- package/types/ngwr-blur-text.d.ts +8 -0
- package/types/ngwr-breadcrumbs.d.ts +1 -0
- package/types/ngwr-button.d.ts +2 -2
- package/types/ngwr-calendar-testing.d.ts +21 -4
- package/types/ngwr-calendar.d.ts +37 -0
- package/types/ngwr-circular-text.d.ts +9 -0
- package/types/ngwr-collapse.d.ts +19 -15
- package/types/ngwr-color-picker-testing.d.ts +10 -4
- package/types/ngwr-color-picker.d.ts +70 -2
- package/types/ngwr-command-palette.d.ts +14 -0
- package/types/ngwr-context-menu.d.ts +1 -1
- package/types/ngwr-date-adapters-fns.d.ts +71 -29
- package/types/ngwr-date-picker-testing.d.ts +206 -185
- package/types/ngwr-date-picker.d.ts +7 -0
- package/types/ngwr-date.d.ts +143 -13
- package/types/ngwr-drag-drop.d.ts +19 -0
- package/types/ngwr-dropdown.d.ts +1 -1
- package/types/ngwr-event-calendar-testing.d.ts +12 -9
- package/types/ngwr-file-upload.d.ts +31 -3
- package/types/ngwr-fuzzy-text.d.ts +5 -0
- package/types/ngwr-i18n.d.ts +89 -7
- package/types/ngwr-icon-adapters-feather.d.ts +11 -0
- package/types/ngwr-icon.d.ts +42 -1
- package/types/ngwr-input-number.d.ts +1 -1
- package/types/ngwr-input-otp.d.ts +3 -0
- package/types/ngwr-input.d.ts +11 -4
- package/types/ngwr-knob-testing.d.ts +8 -4
- package/types/ngwr-knob.d.ts +18 -0
- package/types/ngwr-loading-bar-router.d.ts +35 -0
- package/types/ngwr-loading-bar.d.ts +19 -9
- package/types/ngwr-mention.d.ts +13 -0
- package/types/ngwr-pagination-testing.d.ts +11 -10
- package/types/ngwr-pagination.d.ts +39 -10
- package/types/ngwr-popconfirm.d.ts +1 -1
- package/types/ngwr-popover.d.ts +1 -1
- package/types/ngwr-select.d.ts +65 -10
- package/types/ngwr-shiny-text.d.ts +1 -0
- package/types/ngwr-splitter-testing.d.ts +9 -4
- package/types/ngwr-squircle.d.ts +1 -1
- package/types/ngwr-statistic.d.ts +22 -2
- package/types/ngwr-table.d.ts +24 -5
- package/types/ngwr-tabs-router.d.ts +67 -0
- package/types/ngwr-tabs-testing.d.ts +12 -10
- package/types/ngwr-tabs.d.ts +103 -6
- package/types/ngwr-textarea.d.ts +9 -0
- package/types/ngwr-tree.d.ts +7 -0
- package/types/ngwr-utils.d.ts +79 -1
- package/types/ngwr-version.d.ts +1 -1
- package/types/ngwr-window-testing.d.ts +11 -3
- package/types/ngwr-window.d.ts +11 -4
- package/window/styles/_index.scss +1 -1
package/fesm2022/ngwr-date.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, inject,
|
|
3
|
-
import { isPlatformBrowser } from '@angular/common';
|
|
2
|
+
import { InjectionToken, inject, LOCALE_ID, Service, makeEnvironmentProviders } from '@angular/core';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* @license
|
|
@@ -42,31 +41,38 @@ class WrDateAdapter {
|
|
|
42
41
|
*/
|
|
43
42
|
/**
|
|
44
43
|
* BCP 47 locale tag the {@link WrDateAdapter} implementations read. Defaults to
|
|
45
|
-
* `
|
|
46
|
-
*
|
|
44
|
+
* Angular's `LOCALE_ID`; override via {@link provideWrDateAdapter}'s `locale`
|
|
45
|
+
* option.
|
|
46
|
+
*
|
|
47
|
+
* **It used to default to `navigator.language`, and that was wrong in the
|
|
48
|
+
* documented case.** The provider's own first example is the no-argument call,
|
|
49
|
+
* so the path a reader copies was the broken one: an app that set
|
|
50
|
+
* `LOCALE_ID: 'ru-RU'` and translated every string still rendered `March 2026`,
|
|
51
|
+
* `SUN MON TUE …` and a week starting on Sunday, because the calendar was
|
|
52
|
+
* asking the browser rather than the application. `navigator.language` is the
|
|
53
|
+
* user's browser preference; `LOCALE_ID` is what the app says it is, and only
|
|
54
|
+
* the app can be right about that. Reading the browser also made a prerendered
|
|
55
|
+
* page and its hydrated self disagree — the server has no `navigator.language`
|
|
56
|
+
* to match. An app that genuinely wants the browser's tag can still say so:
|
|
57
|
+
* `provideWrDateAdapter({ locale: navigator.language })`.
|
|
58
|
+
*
|
|
59
|
+
* The token is a plain string, resolved once. It is therefore the *pinned* date
|
|
60
|
+
* locale, not a live one — `WrI18n.use()` moves catalog text and cannot move
|
|
61
|
+
* this. `WrI18n`'s class docs carry the full precedence and `use()` warns about
|
|
62
|
+
* the gap in dev mode.
|
|
47
63
|
*
|
|
48
64
|
* **How far it reaches depends on the adapter, and the difference matters.**
|
|
49
|
-
* The luxon
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* lever (`setDefaultOptions`).
|
|
65
|
+
* The native and luxon adapters thread it through everything. The date-fns
|
|
66
|
+
* adapter cannot: date-fns resolves its patterns against a `Locale` OBJECT, not
|
|
67
|
+
* a tag, so this token drives only the parts that go through `Intl` (the month
|
|
68
|
+
* and weekday names) and the OBJECT drives `format` / `parse`. Pass it as
|
|
69
|
+
* `provideWrDateFnsAdapter({ dateFnsLocale: ru })` — with only a tag the calendar
|
|
70
|
+
* headings localize and the field does not, which the adapter warns about once
|
|
71
|
+
* in dev mode.
|
|
57
72
|
*/
|
|
58
73
|
const WR_DATE_LOCALE = new InjectionToken('WR_DATE_LOCALE', {
|
|
59
74
|
providedIn: 'root',
|
|
60
|
-
factory: () =>
|
|
61
|
-
// Node 21+ also defines `navigator`, and reports the *build machine's*
|
|
62
|
-
// locale — so a `typeof` probe silently bakes that locale into
|
|
63
|
-
// prerendered output (wrong month names, wrong first day of week).
|
|
64
|
-
// Ask the platform instead.
|
|
65
|
-
if (isPlatformBrowser(inject(PLATFORM_ID)) && navigator.language) {
|
|
66
|
-
return navigator.language;
|
|
67
|
-
}
|
|
68
|
-
return 'en-US';
|
|
69
|
-
},
|
|
75
|
+
factory: () => inject(LOCALE_ID),
|
|
70
76
|
});
|
|
71
77
|
|
|
72
78
|
/**
|
|
@@ -83,6 +89,90 @@ const NAMED_KEYS = new Set([
|
|
|
83
89
|
'shortDateTime',
|
|
84
90
|
'mediumDateTime',
|
|
85
91
|
]);
|
|
92
|
+
/**
|
|
93
|
+
* Bidi control marks ICU embeds in a right-to-left date pattern — ar-SA writes its
|
|
94
|
+
* separator as U+200F followed by a slash. No keyboard produces one, so both the
|
|
95
|
+
* pattern and the typed string drop them before they are compared.
|
|
96
|
+
*/
|
|
97
|
+
const BIDI_RE = /[\u200e\u200f\u061c\u2066-\u2069]/g;
|
|
98
|
+
const REGEX_META_RE = /[.*+?^${}()|[\]\\]/g;
|
|
99
|
+
function stripBidi(value) {
|
|
100
|
+
return value.replace(BIDI_RE, '');
|
|
101
|
+
}
|
|
102
|
+
function escapeRegex(value) {
|
|
103
|
+
return value.replace(REGEX_META_RE, '\\$&');
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* One literal run of an `Intl` pattern, as a regex fragment.
|
|
107
|
+
*
|
|
108
|
+
* Whitespace inside a literal is advisory, not exact: ICU separates `02:30` from `PM`
|
|
109
|
+
* with a NARROW NO-BREAK SPACE (U+202F) and no keyboard emits one, so every whitespace
|
|
110
|
+
* run matches `\s*`. A literal that is nothing BUT whitespace keeps `\s+` — relaxing it
|
|
111
|
+
* there would let `15 2026` also read as `152026`, which is how a lenient parser invents
|
|
112
|
+
* a date nobody typed.
|
|
113
|
+
*/
|
|
114
|
+
function literalToRegex(literal) {
|
|
115
|
+
const text = stripBidi(literal);
|
|
116
|
+
if (!text)
|
|
117
|
+
return '';
|
|
118
|
+
if (!text.trim())
|
|
119
|
+
return '\\s+';
|
|
120
|
+
return text.split(/\s+/).map(escapeRegex).join('\\s*');
|
|
121
|
+
}
|
|
122
|
+
/** Longest first, so `March` is never consumed as `Mar` with `ch` left over. */
|
|
123
|
+
function alternation(names) {
|
|
124
|
+
return [...new Set(names)]
|
|
125
|
+
.sort((a, b) => b.length - a.length)
|
|
126
|
+
.map(name => escapeRegex(stripBidi(name)))
|
|
127
|
+
.join('|');
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The ten digits a locale's numbering system writes, index 0 to 9 — `null` when the
|
|
131
|
+
* system is algorithmic (roman, hebrew) and there is no digit-by-digit reading to do.
|
|
132
|
+
*/
|
|
133
|
+
function numeralsFor(locale, numberingSystem) {
|
|
134
|
+
let formatter;
|
|
135
|
+
try {
|
|
136
|
+
formatter = new Intl.NumberFormat(locale, { numberingSystem, useGrouping: false });
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const digits = [];
|
|
142
|
+
for (let i = 0; i < 10; i++) {
|
|
143
|
+
const digit = formatter.format(i);
|
|
144
|
+
if ([...digit].length !== 1 || digits.includes(digit))
|
|
145
|
+
return null;
|
|
146
|
+
digits.push(digit);
|
|
147
|
+
}
|
|
148
|
+
return digits;
|
|
149
|
+
}
|
|
150
|
+
/** A character class accepting the locale's own digits AND ASCII ones. */
|
|
151
|
+
function digitClass(numerals) {
|
|
152
|
+
const extra = numerals
|
|
153
|
+
.filter(digit => digit < '0' || digit > '9')
|
|
154
|
+
.map(digit => `\\u${digit.codePointAt(0).toString(16).padStart(4, '0')}`)
|
|
155
|
+
.join('');
|
|
156
|
+
return `[0-9${extra}]`;
|
|
157
|
+
}
|
|
158
|
+
/** `١٥` to `15`, and ASCII through unchanged. `NaN` for anything else. */
|
|
159
|
+
function digitsToNumber(text, numerals) {
|
|
160
|
+
let out = '';
|
|
161
|
+
for (const ch of text) {
|
|
162
|
+
if (ch >= '0' && ch <= '9') {
|
|
163
|
+
out += ch;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const index = numerals.indexOf(ch);
|
|
167
|
+
if (index < 0)
|
|
168
|
+
return Number.NaN;
|
|
169
|
+
out += String(index);
|
|
170
|
+
}
|
|
171
|
+
return out ? Number(out) : Number.NaN;
|
|
172
|
+
}
|
|
173
|
+
function isAllDigits(text, numerals) {
|
|
174
|
+
return text.length > 0 && !Number.isNaN(digitsToNumber(text, numerals));
|
|
175
|
+
}
|
|
86
176
|
const INTL_OPTIONS = {
|
|
87
177
|
shortDate: { year: 'numeric', month: 'numeric', day: 'numeric' },
|
|
88
178
|
mediumDate: { year: 'numeric', month: 'short', day: 'numeric' },
|
|
@@ -107,24 +197,30 @@ function pad(n, width = 2) {
|
|
|
107
197
|
function isNamedFormat(value) {
|
|
108
198
|
return NAMED_KEYS.has(value);
|
|
109
199
|
}
|
|
110
|
-
|
|
200
|
+
/**
|
|
201
|
+
* `digits` is a character class rather than `\d`, and it always accepts ASCII as well as
|
|
202
|
+
* the locale's own numerals. The token formatter WRITES ASCII, so the round trip never
|
|
203
|
+
* needed more — but a reader on an Arabic keyboard types `١٥` into the same field, and
|
|
204
|
+
* `\d` made that silently unparseable with no feedback anywhere.
|
|
205
|
+
*/
|
|
206
|
+
function tokenToRegex(token, digits) {
|
|
111
207
|
switch (token) {
|
|
112
208
|
case 'yyyy':
|
|
113
|
-
return
|
|
209
|
+
return `(${digits}{4})`;
|
|
114
210
|
case 'yy':
|
|
115
|
-
return
|
|
211
|
+
return `(${digits}{2})`;
|
|
116
212
|
case 'MM':
|
|
117
213
|
case 'dd':
|
|
118
214
|
case 'HH':
|
|
119
215
|
case 'hh':
|
|
120
216
|
case 'mm':
|
|
121
217
|
case 'ss':
|
|
122
|
-
return
|
|
218
|
+
return `(${digits}{2})`;
|
|
123
219
|
case 'M':
|
|
124
220
|
case 'd':
|
|
125
221
|
case 'H':
|
|
126
222
|
case 'h':
|
|
127
|
-
return
|
|
223
|
+
return `(${digits}{1,2})`;
|
|
128
224
|
case 'a':
|
|
129
225
|
return '(am|pm|AM|PM)';
|
|
130
226
|
default:
|
|
@@ -238,14 +334,28 @@ class WrNativeDateAdapter extends WrDateAdapter {
|
|
|
238
334
|
}
|
|
239
335
|
return this.formatWithTokens(date, formatKeyOrString);
|
|
240
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* Read back what {@link format} wrote.
|
|
339
|
+
*
|
|
340
|
+
* A named format used to delegate to `new Date(raw)`, which understands only
|
|
341
|
+
* Anglo-American forms — so the field could not read its own output anywhere else.
|
|
342
|
+
* `new Date('15.3.2026')` and `new Date('14:30')` are both `Invalid Date`, while
|
|
343
|
+
* `new Date('1')` is 1 January 2001: retyping a de-DE date committed the year 2001 on
|
|
344
|
+
* the first keystroke and then refused every one after it, and `mode="time"` collapsed
|
|
345
|
+
* to midnight in EVERY locale, en-US included. The named branch now builds its parser
|
|
346
|
+
* from the SAME `Intl.DateTimeFormat` that printed the string, so the round trip holds
|
|
347
|
+
* by construction rather than by luck.
|
|
348
|
+
*
|
|
349
|
+
* Returns `null` rather than a guess whenever the string does not match — the caller's
|
|
350
|
+
* committed value is then left exactly as it was, which is the contract
|
|
351
|
+
* `wr-input-number` follows for unparseable text.
|
|
352
|
+
*/
|
|
241
353
|
parse(value, formatKeyOrString) {
|
|
242
354
|
const raw = value?.trim();
|
|
243
355
|
if (!raw)
|
|
244
356
|
return null;
|
|
245
|
-
// Named formats fall back to `new Date()` parsing — best-effort, locale-fragile.
|
|
246
357
|
if (isNamedFormat(formatKeyOrString)) {
|
|
247
|
-
|
|
248
|
-
return this.isValid(d) ? d : null;
|
|
358
|
+
return this.parseNamed(raw, formatKeyOrString);
|
|
249
359
|
}
|
|
250
360
|
return this.parseWithTokens(raw, formatKeyOrString);
|
|
251
361
|
}
|
|
@@ -277,16 +387,32 @@ class WrNativeDateAdapter extends WrDateAdapter {
|
|
|
277
387
|
* can be this simple.
|
|
278
388
|
*/
|
|
279
389
|
formatters = new Map();
|
|
390
|
+
/**
|
|
391
|
+
* Every `Intl` call this adapter makes pins `calendar: 'gregory'`, and that is a
|
|
392
|
+
* correctness fix rather than a preference.
|
|
393
|
+
*
|
|
394
|
+
* The value type is a JS `Date`, every accessor on this class (`getYear`,
|
|
395
|
+
* `getMonth`, `getDate`) is Gregorian, `createDate` builds a Gregorian date and the
|
|
396
|
+
* calendar grid draws Gregorian months. A locale whose DEFAULT calendar is something
|
|
397
|
+
* else — `fa-IR` resolves to `persian`, `th-TH` to `buddhist`, `ar-SA` to
|
|
398
|
+
* `islamic-umalqura` in several engines — printed a field the grid beside it could
|
|
399
|
+
* never agree with (`۱۴۰۴/۱۲/۲۴` over a grid headed 2026), and no parser can turn
|
|
400
|
+
* that back into the `Date` it came from. Pinning the calendar makes format and parse
|
|
401
|
+
* describe the same one.
|
|
402
|
+
*/
|
|
403
|
+
intlOptions(options) {
|
|
404
|
+
return { ...options, calendar: 'gregory' };
|
|
405
|
+
}
|
|
280
406
|
intl(key) {
|
|
281
407
|
let found = this.formatters.get(key);
|
|
282
408
|
if (!found) {
|
|
283
|
-
found = new Intl.DateTimeFormat(this.locale, INTL_OPTIONS[key]);
|
|
409
|
+
found = new Intl.DateTimeFormat(this.locale, this.intlOptions(INTL_OPTIONS[key]));
|
|
284
410
|
this.formatters.set(key, found);
|
|
285
411
|
}
|
|
286
412
|
return found;
|
|
287
413
|
}
|
|
288
414
|
getDayOfWeekNames(style) {
|
|
289
|
-
const formatter = new Intl.DateTimeFormat(this.locale, { weekday: style });
|
|
415
|
+
const formatter = new Intl.DateTimeFormat(this.locale, this.intlOptions({ weekday: style }));
|
|
290
416
|
const first = this.getFirstDayOfWeek();
|
|
291
417
|
// Pick a reference Sunday and walk seven days.
|
|
292
418
|
const base = new Date(2024, 0, 7); // Sunday 7 Jan 2024
|
|
@@ -298,15 +424,96 @@ class WrNativeDateAdapter extends WrDateAdapter {
|
|
|
298
424
|
}
|
|
299
425
|
return out;
|
|
300
426
|
}
|
|
427
|
+
/**
|
|
428
|
+
* Month names in the STANDALONE (nominative) form — what a calendar heading wants.
|
|
429
|
+
* Inside a date the same name often declines; {@link monthNames} serves that form to
|
|
430
|
+
* the token formatter, and this method deliberately keeps the heading one.
|
|
431
|
+
*/
|
|
301
432
|
getMonthNames(style) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
433
|
+
return this.monthNames(style, 'standalone');
|
|
434
|
+
}
|
|
435
|
+
numeralsCache = null;
|
|
436
|
+
/** The locale's own ten digits, cached — empty when its system is algorithmic. */
|
|
437
|
+
localeNumerals() {
|
|
438
|
+
this.numeralsCache ??=
|
|
439
|
+
numeralsFor(this.locale, new Intl.NumberFormat(this.locale).resolvedOptions().numberingSystem) ?? [];
|
|
440
|
+
return this.numeralsCache;
|
|
441
|
+
}
|
|
442
|
+
dayPeriodCache = null;
|
|
443
|
+
/**
|
|
444
|
+
* `[am, pm]` as the LOCALE writes them — `PM`, `م`, `午後`.
|
|
445
|
+
*
|
|
446
|
+
* The `a` token used to return a hard-coded lowercase `pm`, byte for byte identical in
|
|
447
|
+
* every locale, which made it the one token in the pattern language that could not be
|
|
448
|
+
* localised at all. `Intl` is the source rather than the i18n catalog because
|
|
449
|
+
* `ngwr/date` deliberately depends on nothing: the panel next to it reads
|
|
450
|
+
* `datePicker.am` / `datePicker.pm`, so an app that wants its own wording overrides it
|
|
451
|
+
* there and passes a 24-hour pattern here.
|
|
452
|
+
*/
|
|
453
|
+
dayPeriods() {
|
|
454
|
+
if (this.dayPeriodCache)
|
|
455
|
+
return this.dayPeriodCache;
|
|
456
|
+
let periods = ['AM', 'PM'];
|
|
457
|
+
try {
|
|
458
|
+
const formatter = new Intl.DateTimeFormat(this.locale, this.intlOptions({ hour: 'numeric', hour12: true }));
|
|
459
|
+
const read = (hour) => formatter.formatToParts(new Date(2024, 0, 15, hour)).find(p => p.type === 'dayPeriod')?.value ?? '';
|
|
460
|
+
const [am, pm] = [read(9), read(21)];
|
|
461
|
+
if (am && pm && am !== pm)
|
|
462
|
+
periods = [am, pm];
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
// No `Intl` day period for this locale — the English pair stays.
|
|
466
|
+
}
|
|
467
|
+
this.dayPeriodCache = periods;
|
|
468
|
+
return periods;
|
|
469
|
+
}
|
|
470
|
+
monthNameCache = new Map();
|
|
471
|
+
/**
|
|
472
|
+
* `context: 'format'` asks `Intl` for the month name as it appears BESIDE A DAY, which
|
|
473
|
+
* is where a case language stops using the nominative: ru writes `15 марта`, not
|
|
474
|
+
* `15 март`, and fi writes `maaliskuuta`, not `maaliskuu`. The name comes out of a
|
|
475
|
+
* `{ day, month }` formatter's parts, because a `{ month }` formatter alone can only
|
|
476
|
+
* ever answer with the standalone form.
|
|
477
|
+
*
|
|
478
|
+
* Some locales write the month NUMERICALLY in that position — fi abbreviates `15.3.`,
|
|
479
|
+
* ja writes `3` with `月` as a separate literal — and there the standalone name is the
|
|
480
|
+
* only name there is, so a purely numeric answer falls back to it. Without that guard
|
|
481
|
+
* `dd MMMM yyyy` would render `15 3 2026` in Japanese.
|
|
482
|
+
*/
|
|
483
|
+
monthNames(style, context) {
|
|
484
|
+
const cacheKey = `${style}:${context}`;
|
|
485
|
+
const cached = this.monthNameCache.get(cacheKey);
|
|
486
|
+
if (cached)
|
|
487
|
+
return cached;
|
|
488
|
+
const standaloneFormatter = new Intl.DateTimeFormat(this.locale, this.intlOptions({ month: style }));
|
|
489
|
+
const standalone = [];
|
|
490
|
+
for (let i = 0; i < 12; i++)
|
|
491
|
+
standalone.push(standaloneFormatter.format(new Date(2024, i, 15)));
|
|
492
|
+
let out = standalone;
|
|
493
|
+
if (context === 'format') {
|
|
494
|
+
const numerals = numeralsFor(this.locale, standaloneFormatter.resolvedOptions().numberingSystem) ?? [];
|
|
495
|
+
const inDate = new Intl.DateTimeFormat(this.locale, this.intlOptions({ day: 'numeric', month: style }));
|
|
496
|
+
out = standalone.map((name, i) => {
|
|
497
|
+
const part = inDate.formatToParts(new Date(2024, i, 15)).find(p => p.type === 'month')?.value;
|
|
498
|
+
if (!part || isAllDigits(part, numerals))
|
|
499
|
+
return name;
|
|
500
|
+
return part;
|
|
501
|
+
});
|
|
306
502
|
}
|
|
503
|
+
this.monthNameCache.set(cacheKey, out);
|
|
307
504
|
return out;
|
|
308
505
|
}
|
|
309
506
|
// Internals: token-based format / parse
|
|
507
|
+
/** Whether a token pattern also writes the day, which decides the month's form. */
|
|
508
|
+
hasDayToken(pattern) {
|
|
509
|
+
let found = false;
|
|
510
|
+
pattern.replace(PART_RE, (part, quoted) => {
|
|
511
|
+
if (quoted === undefined && (part === 'd' || part === 'dd'))
|
|
512
|
+
found = true;
|
|
513
|
+
return part;
|
|
514
|
+
});
|
|
515
|
+
return found;
|
|
516
|
+
}
|
|
310
517
|
formatWithTokens(date, pattern) {
|
|
311
518
|
const year = date.getFullYear();
|
|
312
519
|
const month = date.getMonth();
|
|
@@ -315,8 +522,11 @@ class WrNativeDateAdapter extends WrDateAdapter {
|
|
|
315
522
|
const hours12 = hours24 % 12 === 0 ? 12 : hours24 % 12;
|
|
316
523
|
const minutes = date.getMinutes();
|
|
317
524
|
const seconds = date.getSeconds();
|
|
318
|
-
|
|
319
|
-
|
|
525
|
+
// A pattern that writes the day wants the month declined for that position — `MMMM`
|
|
526
|
+
// used to hand `15 март 2026` to a Russian field, which reads as machine output.
|
|
527
|
+
const context = this.hasDayToken(pattern) ? 'format' : 'standalone';
|
|
528
|
+
const monthNames = this.monthNames('long', context);
|
|
529
|
+
const monthShort = this.monthNames('short', context);
|
|
320
530
|
return pattern.replace(PART_RE, (part, quoted) => {
|
|
321
531
|
// A quoted run is emitted as-is; `''` is how a single quote is written.
|
|
322
532
|
if (quoted !== undefined)
|
|
@@ -352,15 +562,19 @@ class WrNativeDateAdapter extends WrDateAdapter {
|
|
|
352
562
|
case 'ss':
|
|
353
563
|
return pad(seconds);
|
|
354
564
|
case 'a':
|
|
355
|
-
return hours24 < 12 ?
|
|
565
|
+
return this.dayPeriods()[hours24 < 12 ? 0 : 1];
|
|
356
566
|
}
|
|
357
567
|
return token;
|
|
358
568
|
});
|
|
359
569
|
}
|
|
360
570
|
parseWithTokens(value, pattern) {
|
|
361
571
|
const tokens = [];
|
|
362
|
-
|
|
363
|
-
|
|
572
|
+
// Both forms are accepted on the way IN: the formatter writes the declined one beside
|
|
573
|
+
// a day (`15 марта`), and a heading or an older stored string carries the nominative.
|
|
574
|
+
const monthLong = [this.monthNames('long', 'format'), this.monthNames('long', 'standalone')];
|
|
575
|
+
const monthShort = [this.monthNames('short', 'format'), this.monthNames('short', 'standalone')];
|
|
576
|
+
const numerals = this.localeNumerals();
|
|
577
|
+
const digits = digitClass(numerals);
|
|
364
578
|
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
365
579
|
const regex = escaped.replace(PART_RE, (part, quoted) => {
|
|
366
580
|
if (quoted !== undefined) {
|
|
@@ -369,83 +583,266 @@ class WrNativeDateAdapter extends WrDateAdapter {
|
|
|
369
583
|
return quoted === '' ? "'" : quoted;
|
|
370
584
|
}
|
|
371
585
|
tokens.push(part);
|
|
372
|
-
|
|
586
|
+
// `a` is the one token whose text is locale-dependent, so its alternation is built
|
|
587
|
+
// here rather than in the pure `tokenToRegex`: the marker this adapter now PRINTS
|
|
588
|
+
// has to be one the same adapter can read.
|
|
589
|
+
if (part === 'a')
|
|
590
|
+
return `(${alternation([...this.dayPeriods(), 'am', 'pm'])})`;
|
|
591
|
+
return tokenToRegex(part, digits);
|
|
373
592
|
});
|
|
374
|
-
const match = new RegExp(`^${regex}
|
|
593
|
+
const match = new RegExp(`^${regex}$`, 'i').exec(value);
|
|
375
594
|
if (!match)
|
|
376
595
|
return null;
|
|
377
|
-
|
|
378
|
-
let month = 0;
|
|
379
|
-
let day = 1;
|
|
380
|
-
let hours = 0;
|
|
381
|
-
let minutes = 0;
|
|
382
|
-
let seconds = 0;
|
|
383
|
-
let isPm = false;
|
|
384
|
-
let hadHour12 = false;
|
|
385
|
-
let hour12 = 0;
|
|
596
|
+
const parts = {};
|
|
386
597
|
for (let i = 0; i < tokens.length; i++) {
|
|
387
598
|
const token = tokens[i];
|
|
388
599
|
const raw = match[i + 1];
|
|
389
|
-
const n =
|
|
600
|
+
const n = digitsToNumber(raw, numerals);
|
|
390
601
|
switch (token) {
|
|
391
602
|
case 'yyyy':
|
|
392
|
-
year = n;
|
|
603
|
+
parts.year = n;
|
|
393
604
|
break;
|
|
394
605
|
case 'yy':
|
|
395
|
-
year = 2000 + n;
|
|
606
|
+
parts.year = 2000 + n;
|
|
396
607
|
break;
|
|
397
608
|
case 'MM':
|
|
398
609
|
case 'M':
|
|
399
|
-
month = n - 1;
|
|
610
|
+
parts.month = n - 1;
|
|
400
611
|
break;
|
|
401
612
|
case 'MMM':
|
|
402
613
|
case 'MMMM': {
|
|
403
|
-
const
|
|
404
|
-
const
|
|
405
|
-
|
|
614
|
+
const sets = token === 'MMM' ? monthShort : monthLong;
|
|
615
|
+
const wanted = raw.toLowerCase();
|
|
616
|
+
const found = sets
|
|
617
|
+
.map(names => names.findIndex(name => name.toLowerCase() === wanted))
|
|
618
|
+
.find(index => index >= 0);
|
|
619
|
+
if (found === undefined)
|
|
406
620
|
return null;
|
|
407
|
-
month = found;
|
|
621
|
+
parts.month = found;
|
|
408
622
|
break;
|
|
409
623
|
}
|
|
410
624
|
case 'dd':
|
|
411
625
|
case 'd':
|
|
412
|
-
day = n;
|
|
626
|
+
parts.day = n;
|
|
413
627
|
break;
|
|
414
628
|
case 'HH':
|
|
415
629
|
case 'H':
|
|
416
|
-
hours = n;
|
|
630
|
+
parts.hours = n;
|
|
417
631
|
break;
|
|
418
632
|
case 'hh':
|
|
419
633
|
case 'h':
|
|
420
|
-
|
|
421
|
-
hour12 = n;
|
|
634
|
+
parts.hour12 = n;
|
|
422
635
|
break;
|
|
423
636
|
case 'mm':
|
|
424
|
-
minutes = n;
|
|
637
|
+
parts.minutes = n;
|
|
425
638
|
break;
|
|
426
639
|
case 'ss':
|
|
427
|
-
seconds = n;
|
|
640
|
+
parts.seconds = n;
|
|
428
641
|
break;
|
|
429
|
-
case 'a':
|
|
430
|
-
|
|
642
|
+
case 'a': {
|
|
643
|
+
const marker = raw.toLowerCase();
|
|
644
|
+
parts.isPm = marker === 'pm' || marker === this.dayPeriods()[1].toLowerCase();
|
|
431
645
|
break;
|
|
646
|
+
}
|
|
432
647
|
}
|
|
433
648
|
}
|
|
434
|
-
|
|
435
|
-
|
|
649
|
+
return this.compose(parts);
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Build the parser for one named format out of the very formatter that prints it.
|
|
653
|
+
*
|
|
654
|
+
* `formatToParts` is the only honest source for a locale's field ORDER and its
|
|
655
|
+
* literals — ru writes `dd.MM.y` with a two-digit month while its own
|
|
656
|
+
* `{ month: 'numeric' }` request says one digit, ja puts the year first, and ar-SA
|
|
657
|
+
* separates fields with U+200F. Names (a month, AM/PM) become an alternation of the
|
|
658
|
+
* real strings; numbers accept the locale's digits AND ASCII, because a physical
|
|
659
|
+
* keyboard produces the latter whatever the display shows.
|
|
660
|
+
*
|
|
661
|
+
* Returns `null` — meaning "this format cannot be read back" — rather than guessing,
|
|
662
|
+
* for a field type nothing here asks for (an era, a weekday) or a numbering system
|
|
663
|
+
* with no digit-by-digit reading (roman numerals). `parse` then refuses, which leaves
|
|
664
|
+
* the caller's committed value untouched.
|
|
665
|
+
*/
|
|
666
|
+
buildNamedParser(key) {
|
|
667
|
+
const formatter = this.intl(key);
|
|
668
|
+
const resolved = formatter.resolvedOptions();
|
|
669
|
+
const numerals = numeralsFor(this.locale, resolved.numberingSystem);
|
|
670
|
+
if (!numerals)
|
|
671
|
+
return null;
|
|
672
|
+
const digits = digitClass(numerals);
|
|
673
|
+
// Distinct values in every field, so nothing below can read one field's reference
|
|
674
|
+
// value out of another's position.
|
|
675
|
+
const reference = new Date(2024, 0, 15, 13, 45, 56);
|
|
676
|
+
const fields = [];
|
|
677
|
+
const fragments = [];
|
|
678
|
+
for (const part of formatter.formatToParts(reference)) {
|
|
679
|
+
switch (part.type) {
|
|
680
|
+
case 'literal':
|
|
681
|
+
fragments.push({ source: literalToRegex(part.value), literal: true });
|
|
682
|
+
break;
|
|
683
|
+
case 'year':
|
|
684
|
+
fields.push({ kind: 'year' });
|
|
685
|
+
// Exactly two digits or exactly four — the shapes a year is actually written
|
|
686
|
+
// in. `{1,4}` would let the third keystroke of `15.3.2026` read as the year 2
|
|
687
|
+
// and commit it, which is a smaller version of the defect this replaces.
|
|
688
|
+
fragments.push({ source: `(${digits}{2}|${digits}{4})`, literal: false });
|
|
689
|
+
break;
|
|
690
|
+
case 'month': {
|
|
691
|
+
const names = this.namedMonthNames(formatter);
|
|
692
|
+
if (names.every(name => isAllDigits(name, numerals))) {
|
|
693
|
+
fields.push({ kind: 'month' });
|
|
694
|
+
fragments.push({ source: `(${digits}{1,2})`, literal: false });
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
fields.push({ kind: 'monthName', names });
|
|
698
|
+
fragments.push({ source: `(${alternation(names)})`, literal: false });
|
|
699
|
+
}
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
case 'day':
|
|
703
|
+
fields.push({ kind: 'day' });
|
|
704
|
+
fragments.push({ source: `(${digits}{1,2})`, literal: false });
|
|
705
|
+
break;
|
|
706
|
+
case 'hour':
|
|
707
|
+
fields.push({ kind: 'hour' });
|
|
708
|
+
fragments.push({ source: `(${digits}{1,2})`, literal: false });
|
|
709
|
+
break;
|
|
710
|
+
case 'minute':
|
|
711
|
+
fields.push({ kind: 'minute' });
|
|
712
|
+
fragments.push({ source: `(${digits}{1,2})`, literal: false });
|
|
713
|
+
break;
|
|
714
|
+
case 'second':
|
|
715
|
+
fields.push({ kind: 'second' });
|
|
716
|
+
fragments.push({ source: `(${digits}{1,2})`, literal: false });
|
|
717
|
+
break;
|
|
718
|
+
case 'dayPeriod': {
|
|
719
|
+
const names = this.dayPeriodNames(formatter);
|
|
720
|
+
fields.push({ kind: 'dayPeriod', names });
|
|
721
|
+
fragments.push({ source: `(${alternation(names)})`, literal: false });
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
default:
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (!fields.length)
|
|
729
|
+
return null;
|
|
730
|
+
// A trailing literal is optional. ru's medium date ends ` г.` and fi's short date
|
|
731
|
+
// ends with a bare `.`; someone retyping the field types the fields and stops, and
|
|
732
|
+
// refusing that would reintroduce the very "cannot read its own output" defect for
|
|
733
|
+
// the one part of the string that carries no information.
|
|
734
|
+
const source = fragments
|
|
735
|
+
.map((fragment, i) => i === fragments.length - 1 && fragment.literal && fragment.source ? `(?:${fragment.source})?` : fragment.source)
|
|
736
|
+
.join('');
|
|
737
|
+
return { regex: new RegExp(`^${source}$`, 'iu'), fields, numerals, hourCycle: resolved.hourCycle };
|
|
738
|
+
}
|
|
739
|
+
/** The twelve month strings THIS formatter writes, in its own style and context. */
|
|
740
|
+
namedMonthNames(formatter) {
|
|
741
|
+
const out = [];
|
|
742
|
+
for (let i = 0; i < 12; i++) {
|
|
743
|
+
out.push(formatter.formatToParts(new Date(2024, i, 15)).find(p => p.type === 'month')?.value ?? '');
|
|
744
|
+
}
|
|
745
|
+
return out;
|
|
746
|
+
}
|
|
747
|
+
/** `[am, pm]` as THIS formatter writes them — `ص` / `م` in Arabic, not AM / PM. */
|
|
748
|
+
dayPeriodNames(formatter) {
|
|
749
|
+
const read = (hour) => formatter.formatToParts(new Date(2024, 0, 15, hour, 30)).find(p => p.type === 'dayPeriod')?.value ?? '';
|
|
750
|
+
return [read(9), read(21)];
|
|
751
|
+
}
|
|
752
|
+
namedParsers = new Map();
|
|
753
|
+
parseNamed(raw, key) {
|
|
754
|
+
let parser = this.namedParsers.get(key);
|
|
755
|
+
if (parser === undefined) {
|
|
756
|
+
parser = this.buildNamedParser(key);
|
|
757
|
+
this.namedParsers.set(key, parser);
|
|
436
758
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
759
|
+
if (!parser)
|
|
760
|
+
return null;
|
|
761
|
+
const match = parser.regex.exec(stripBidi(raw));
|
|
762
|
+
if (!match)
|
|
763
|
+
return null;
|
|
764
|
+
const parts = {};
|
|
765
|
+
const twelveHour = parser.fields.some(field => field.kind === 'dayPeriod');
|
|
766
|
+
for (let i = 0; i < parser.fields.length; i++) {
|
|
767
|
+
const field = parser.fields[i];
|
|
768
|
+
const captured = match[i + 1] ?? '';
|
|
769
|
+
if (field.kind === 'monthName' || field.kind === 'dayPeriod') {
|
|
770
|
+
const wanted = stripBidi(captured).toLowerCase();
|
|
771
|
+
const index = (field.names ?? []).findIndex(name => stripBidi(name).toLowerCase() === wanted);
|
|
772
|
+
if (index < 0)
|
|
773
|
+
return null;
|
|
774
|
+
if (field.kind === 'monthName')
|
|
775
|
+
parts.month = index;
|
|
776
|
+
else
|
|
777
|
+
parts.isPm = index === 1;
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
const n = digitsToNumber(captured, parser.numerals);
|
|
781
|
+
if (Number.isNaN(n))
|
|
782
|
+
return null;
|
|
783
|
+
switch (field.kind) {
|
|
784
|
+
case 'year':
|
|
785
|
+
// Two digits mean this century, the same reading the `yy` token gives — the
|
|
786
|
+
// picker's own doc promises `1/5/25` cleans up to 2025.
|
|
787
|
+
parts.year = captured.length <= 2 ? 2000 + n : n;
|
|
788
|
+
break;
|
|
789
|
+
case 'month':
|
|
790
|
+
parts.month = n - 1;
|
|
791
|
+
break;
|
|
792
|
+
case 'day':
|
|
793
|
+
parts.day = n;
|
|
794
|
+
break;
|
|
795
|
+
case 'hour':
|
|
796
|
+
if (twelveHour)
|
|
797
|
+
parts.hour12 = n;
|
|
798
|
+
// `hourCycle: 'h24'` numbers midnight 24 rather than 0.
|
|
799
|
+
else
|
|
800
|
+
parts.hours = n === 24 && parser.hourCycle === 'h24' ? 0 : n;
|
|
801
|
+
break;
|
|
802
|
+
case 'minute':
|
|
803
|
+
parts.minutes = n;
|
|
804
|
+
break;
|
|
805
|
+
case 'second':
|
|
806
|
+
parts.seconds = n;
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return this.compose(parts);
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Assemble whatever was read into a `Date`, or refuse.
|
|
814
|
+
*
|
|
815
|
+
* `new Date(2025, 12, 45)` does not fail — it rolls forward into the next year — and
|
|
816
|
+
* `isValid` is happy with the result, so a text field used to turn nonsense into a
|
|
817
|
+
* confident wrong answer. The clock parts are range-checked directly; the month and the
|
|
818
|
+
* day are checked by comparing what came back, which catches every rollover including a
|
|
819
|
+
* month of 12 or -1.
|
|
820
|
+
*/
|
|
821
|
+
compose(parts) {
|
|
822
|
+
const now = new Date();
|
|
823
|
+
const year = parts.year ?? now.getFullYear();
|
|
824
|
+
const month = parts.month ?? now.getMonth();
|
|
825
|
+
let hours = parts.hours ?? 0;
|
|
826
|
+
if (parts.hour12 !== undefined)
|
|
827
|
+
hours = (parts.hour12 % 12) + (parts.isPm ? 12 : 0);
|
|
828
|
+
const minutes = parts.minutes ?? 0;
|
|
829
|
+
const seconds = parts.seconds ?? 0;
|
|
442
830
|
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59 || seconds < 0 || seconds > 59)
|
|
443
831
|
return null;
|
|
832
|
+
// A time-only format carries no date, and the date it should imply is TODAY — the
|
|
833
|
+
// previous default was 1 January, so a picker in `mode="time"` moved the model back
|
|
834
|
+
// to New Year's Day as a side effect of reading a clock. The default is clamped to
|
|
835
|
+
// the month it lands in, because an unclamped 31st would roll a February string
|
|
836
|
+
// forward and the rollover check below would then refuse a well-formed string.
|
|
837
|
+
const monthLength = new Date(year, month + 1, 0).getDate();
|
|
838
|
+
const day = parts.day ?? Math.min(now.getDate(), monthLength);
|
|
444
839
|
const out = new Date(year, month, day, hours, minutes, seconds);
|
|
840
|
+
// Same trap `createDate` documents: a year under 100 maps onto 1900 + year unless it
|
|
841
|
+
// is written back explicitly, so `0025-08-11` used to parse as 1925.
|
|
842
|
+
if (year >= 0 && year < 100)
|
|
843
|
+
out.setFullYear(year);
|
|
445
844
|
if (!this.isValid(out))
|
|
446
845
|
return null;
|
|
447
|
-
// The day is range-checked by construction: anything the calendar rolled over comes
|
|
448
|
-
// back as a different month or a different day.
|
|
449
846
|
if (out.getMonth() !== month || out.getDate() !== day)
|
|
450
847
|
return null;
|
|
451
848
|
return out;
|
|
@@ -467,12 +864,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
|
|
|
467
864
|
* Register the date adapter and locale used by the calendar / date picker /
|
|
468
865
|
* time picker. Call once at bootstrap.
|
|
469
866
|
*
|
|
867
|
+
* Omit `locale` and dates follow Angular's `LOCALE_ID`, which is what the app
|
|
868
|
+
* already says it is. Pass one only to pin dates to something else — see
|
|
869
|
+
* {@link WR_DATE_LOCALE}, and `WrI18n` for how the three locale inputs rank.
|
|
870
|
+
*
|
|
470
871
|
* @example
|
|
471
872
|
* ```ts
|
|
472
873
|
* bootstrapApplication(AppComponent, {
|
|
473
874
|
* providers: [
|
|
474
|
-
* provideWrDateAdapter(), // native,
|
|
475
|
-
* // provideWrDateAdapter({ locale: 'ru-RU' }),
|
|
875
|
+
* provideWrDateAdapter(), // native adapter, LOCALE_ID
|
|
876
|
+
* // provideWrDateAdapter({ locale: 'ru-RU' }), // pin the dates
|
|
476
877
|
* // provideWrDateAdapter({ adapter: MyDateFnsAdapter, locale: 'fr' }),
|
|
477
878
|
* ],
|
|
478
879
|
* });
|