@ngneers/controls 0.0.1-next.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.
Files changed (186) hide show
  1. package/fesm2022/ngneers-controls-accordion.mjs +234 -0
  2. package/fesm2022/ngneers-controls-accordion.mjs.map +1 -0
  3. package/fesm2022/ngneers-controls-api-ng.mjs +959 -0
  4. package/fesm2022/ngneers-controls-api-ng.mjs.map +1 -0
  5. package/fesm2022/ngneers-controls-api-resize.mjs +712 -0
  6. package/fesm2022/ngneers-controls-api-resize.mjs.map +1 -0
  7. package/fesm2022/ngneers-controls-api.mjs +347 -0
  8. package/fesm2022/ngneers-controls-api.mjs.map +1 -0
  9. package/fesm2022/ngneers-controls-avatar.mjs +77 -0
  10. package/fesm2022/ngneers-controls-avatar.mjs.map +1 -0
  11. package/fesm2022/ngneers-controls-base.mjs +620 -0
  12. package/fesm2022/ngneers-controls-base.mjs.map +1 -0
  13. package/fesm2022/ngneers-controls-breadcrumb.mjs +100 -0
  14. package/fesm2022/ngneers-controls-breadcrumb.mjs.map +1 -0
  15. package/fesm2022/ngneers-controls-button-group.mjs +110 -0
  16. package/fesm2022/ngneers-controls-button-group.mjs.map +1 -0
  17. package/fesm2022/ngneers-controls-button.mjs +83 -0
  18. package/fesm2022/ngneers-controls-button.mjs.map +1 -0
  19. package/fesm2022/ngneers-controls-calendar-time-CJT5AFlA.mjs +74 -0
  20. package/fesm2022/ngneers-controls-calendar-time-CJT5AFlA.mjs.map +1 -0
  21. package/fesm2022/ngneers-controls-calendar.mjs +801 -0
  22. package/fesm2022/ngneers-controls-calendar.mjs.map +1 -0
  23. package/fesm2022/ngneers-controls-checkbox.mjs +63 -0
  24. package/fesm2022/ngneers-controls-checkbox.mjs.map +1 -0
  25. package/fesm2022/ngneers-controls-chip.mjs +55 -0
  26. package/fesm2022/ngneers-controls-chip.mjs.map +1 -0
  27. package/fesm2022/ngneers-controls-default-icons.mjs +82 -0
  28. package/fesm2022/ngneers-controls-default-icons.mjs.map +1 -0
  29. package/fesm2022/ngneers-controls-defer.mjs +72 -0
  30. package/fesm2022/ngneers-controls-defer.mjs.map +1 -0
  31. package/fesm2022/ngneers-controls-dialog.mjs +407 -0
  32. package/fesm2022/ngneers-controls-dialog.mjs.map +1 -0
  33. package/fesm2022/ngneers-controls-directives.mjs +498 -0
  34. package/fesm2022/ngneers-controls-directives.mjs.map +1 -0
  35. package/fesm2022/ngneers-controls-drawer.mjs +271 -0
  36. package/fesm2022/ngneers-controls-drawer.mjs.map +1 -0
  37. package/fesm2022/ngneers-controls-edit-inplace.mjs +127 -0
  38. package/fesm2022/ngneers-controls-edit-inplace.mjs.map +1 -0
  39. package/fesm2022/ngneers-controls-errors.mjs +329 -0
  40. package/fesm2022/ngneers-controls-errors.mjs.map +1 -0
  41. package/fesm2022/ngneers-controls-filter.mjs +708 -0
  42. package/fesm2022/ngneers-controls-filter.mjs.map +1 -0
  43. package/fesm2022/ngneers-controls-hint.mjs +153 -0
  44. package/fesm2022/ngneers-controls-hint.mjs.map +1 -0
  45. package/fesm2022/ngneers-controls-i18n-translations-de.mjs +170 -0
  46. package/fesm2022/ngneers-controls-i18n-translations-de.mjs.map +1 -0
  47. package/fesm2022/ngneers-controls-i18n-translations-en.mjs +170 -0
  48. package/fesm2022/ngneers-controls-i18n-translations-en.mjs.map +1 -0
  49. package/fesm2022/ngneers-controls-i18n.mjs +57 -0
  50. package/fesm2022/ngneers-controls-i18n.mjs.map +1 -0
  51. package/fesm2022/ngneers-controls-icon.mjs +195 -0
  52. package/fesm2022/ngneers-controls-icon.mjs.map +1 -0
  53. package/fesm2022/ngneers-controls-inplace.mjs +150 -0
  54. package/fesm2022/ngneers-controls-inplace.mjs.map +1 -0
  55. package/fesm2022/ngneers-controls-input-field.mjs +165 -0
  56. package/fesm2022/ngneers-controls-input-field.mjs.map +1 -0
  57. package/fesm2022/ngneers-controls-input.mjs +72 -0
  58. package/fesm2022/ngneers-controls-input.mjs.map +1 -0
  59. package/fesm2022/ngneers-controls-item-view.mjs +473 -0
  60. package/fesm2022/ngneers-controls-item-view.mjs.map +1 -0
  61. package/fesm2022/ngneers-controls-list-box.mjs +281 -0
  62. package/fesm2022/ngneers-controls-list-box.mjs.map +1 -0
  63. package/fesm2022/ngneers-controls-mask-input.mjs +888 -0
  64. package/fesm2022/ngneers-controls-mask-input.mjs.map +1 -0
  65. package/fesm2022/ngneers-controls-menu.mjs +371 -0
  66. package/fesm2022/ngneers-controls-menu.mjs.map +1 -0
  67. package/fesm2022/ngneers-controls-message.mjs +30 -0
  68. package/fesm2022/ngneers-controls-message.mjs.map +1 -0
  69. package/fesm2022/ngneers-controls-number-input.mjs +366 -0
  70. package/fesm2022/ngneers-controls-number-input.mjs.map +1 -0
  71. package/fesm2022/ngneers-controls-paginator.mjs +129 -0
  72. package/fesm2022/ngneers-controls-paginator.mjs.map +1 -0
  73. package/fesm2022/ngneers-controls-popover.mjs +215 -0
  74. package/fesm2022/ngneers-controls-popover.mjs.map +1 -0
  75. package/fesm2022/ngneers-controls-progress.mjs +98 -0
  76. package/fesm2022/ngneers-controls-progress.mjs.map +1 -0
  77. package/fesm2022/ngneers-controls-radio.mjs +152 -0
  78. package/fesm2022/ngneers-controls-radio.mjs.map +1 -0
  79. package/fesm2022/ngneers-controls-roving-focus.mjs +323 -0
  80. package/fesm2022/ngneers-controls-roving-focus.mjs.map +1 -0
  81. package/fesm2022/ngneers-controls-scroll-shadow.mjs +65 -0
  82. package/fesm2022/ngneers-controls-scroll-shadow.mjs.map +1 -0
  83. package/fesm2022/ngneers-controls-scroller.mjs +308 -0
  84. package/fesm2022/ngneers-controls-scroller.mjs.map +1 -0
  85. package/fesm2022/ngneers-controls-select-button.mjs +58 -0
  86. package/fesm2022/ngneers-controls-select-button.mjs.map +1 -0
  87. package/fesm2022/ngneers-controls-select.mjs +417 -0
  88. package/fesm2022/ngneers-controls-select.mjs.map +1 -0
  89. package/fesm2022/ngneers-controls-slider.mjs +167 -0
  90. package/fesm2022/ngneers-controls-slider.mjs.map +1 -0
  91. package/fesm2022/ngneers-controls-snackbar.mjs +414 -0
  92. package/fesm2022/ngneers-controls-snackbar.mjs.map +1 -0
  93. package/fesm2022/ngneers-controls-spin-buttons.mjs +126 -0
  94. package/fesm2022/ngneers-controls-spin-buttons.mjs.map +1 -0
  95. package/fesm2022/ngneers-controls-spinner.mjs +184 -0
  96. package/fesm2022/ngneers-controls-spinner.mjs.map +1 -0
  97. package/fesm2022/ngneers-controls-splitter.mjs +604 -0
  98. package/fesm2022/ngneers-controls-splitter.mjs.map +1 -0
  99. package/fesm2022/ngneers-controls-state.mjs +185 -0
  100. package/fesm2022/ngneers-controls-state.mjs.map +1 -0
  101. package/fesm2022/ngneers-controls-switch.mjs +39 -0
  102. package/fesm2022/ngneers-controls-switch.mjs.map +1 -0
  103. package/fesm2022/ngneers-controls-table.mjs +2634 -0
  104. package/fesm2022/ngneers-controls-table.mjs.map +1 -0
  105. package/fesm2022/ngneers-controls-tabs.mjs +336 -0
  106. package/fesm2022/ngneers-controls-tabs.mjs.map +1 -0
  107. package/fesm2022/ngneers-controls-tag.mjs +30 -0
  108. package/fesm2022/ngneers-controls-tag.mjs.map +1 -0
  109. package/fesm2022/ngneers-controls-toast.mjs +350 -0
  110. package/fesm2022/ngneers-controls-toast.mjs.map +1 -0
  111. package/fesm2022/ngneers-controls-toggle-button.mjs +70 -0
  112. package/fesm2022/ngneers-controls-toggle-button.mjs.map +1 -0
  113. package/fesm2022/ngneers-controls-tooltip.mjs +506 -0
  114. package/fesm2022/ngneers-controls-tooltip.mjs.map +1 -0
  115. package/fesm2022/ngneers-controls-tree.mjs +599 -0
  116. package/fesm2022/ngneers-controls-tree.mjs.map +1 -0
  117. package/fesm2022/ngneers-controls-upload.mjs +377 -0
  118. package/fesm2022/ngneers-controls-upload.mjs.map +1 -0
  119. package/fesm2022/ngneers-controls-utils-ng.mjs +411 -0
  120. package/fesm2022/ngneers-controls-utils-ng.mjs.map +1 -0
  121. package/fesm2022/ngneers-controls-utils.mjs +261 -0
  122. package/fesm2022/ngneers-controls-utils.mjs.map +1 -0
  123. package/fesm2022/ngneers-controls.mjs +6 -0
  124. package/fesm2022/ngneers-controls.mjs.map +1 -0
  125. package/package.json +302 -0
  126. package/types/ngneers-controls-accordion.d.ts +139 -0
  127. package/types/ngneers-controls-api-ng.d.ts +364 -0
  128. package/types/ngneers-controls-api-resize.d.ts +318 -0
  129. package/types/ngneers-controls-api.d.ts +145 -0
  130. package/types/ngneers-controls-avatar.d.ts +54 -0
  131. package/types/ngneers-controls-base.d.ts +277 -0
  132. package/types/ngneers-controls-breadcrumb.d.ts +272 -0
  133. package/types/ngneers-controls-button-group.d.ts +46 -0
  134. package/types/ngneers-controls-button.d.ts +48 -0
  135. package/types/ngneers-controls-calendar.d.ts +466 -0
  136. package/types/ngneers-controls-checkbox.d.ts +40 -0
  137. package/types/ngneers-controls-chip.d.ts +202 -0
  138. package/types/ngneers-controls-default-icons.d.ts +5 -0
  139. package/types/ngneers-controls-defer.d.ts +48 -0
  140. package/types/ngneers-controls-dialog.d.ts +399 -0
  141. package/types/ngneers-controls-directives.d.ts +171 -0
  142. package/types/ngneers-controls-drawer.d.ts +308 -0
  143. package/types/ngneers-controls-edit-inplace.d.ts +263 -0
  144. package/types/ngneers-controls-errors.d.ts +155 -0
  145. package/types/ngneers-controls-filter.d.ts +609 -0
  146. package/types/ngneers-controls-hint.d.ts +88 -0
  147. package/types/ngneers-controls-i18n-translations-de.d.ts +164 -0
  148. package/types/ngneers-controls-i18n-translations-en.d.ts +164 -0
  149. package/types/ngneers-controls-i18n.d.ts +186 -0
  150. package/types/ngneers-controls-icon.d.ts +68 -0
  151. package/types/ngneers-controls-inplace.d.ts +98 -0
  152. package/types/ngneers-controls-input-field.d.ts +248 -0
  153. package/types/ngneers-controls-input.d.ts +30 -0
  154. package/types/ngneers-controls-item-view.d.ts +215 -0
  155. package/types/ngneers-controls-list-box.d.ts +334 -0
  156. package/types/ngneers-controls-mask-input.d.ts +434 -0
  157. package/types/ngneers-controls-menu.d.ts +192 -0
  158. package/types/ngneers-controls-message.d.ts +21 -0
  159. package/types/ngneers-controls-number-input.d.ts +146 -0
  160. package/types/ngneers-controls-paginator.d.ts +255 -0
  161. package/types/ngneers-controls-popover.d.ts +116 -0
  162. package/types/ngneers-controls-progress.d.ts +49 -0
  163. package/types/ngneers-controls-radio.d.ts +91 -0
  164. package/types/ngneers-controls-roving-focus.d.ts +107 -0
  165. package/types/ngneers-controls-scroll-shadow.d.ts +25 -0
  166. package/types/ngneers-controls-scroller.d.ts +151 -0
  167. package/types/ngneers-controls-select-button.d.ts +51 -0
  168. package/types/ngneers-controls-select.d.ts +409 -0
  169. package/types/ngneers-controls-slider.d.ts +57 -0
  170. package/types/ngneers-controls-snackbar.d.ts +330 -0
  171. package/types/ngneers-controls-spin-buttons.d.ts +69 -0
  172. package/types/ngneers-controls-spinner.d.ts +117 -0
  173. package/types/ngneers-controls-splitter.d.ts +465 -0
  174. package/types/ngneers-controls-state.d.ts +102 -0
  175. package/types/ngneers-controls-switch.d.ts +18 -0
  176. package/types/ngneers-controls-table.d.ts +3127 -0
  177. package/types/ngneers-controls-tabs.d.ts +118 -0
  178. package/types/ngneers-controls-tag.d.ts +21 -0
  179. package/types/ngneers-controls-toast.d.ts +298 -0
  180. package/types/ngneers-controls-toggle-button.d.ts +48 -0
  181. package/types/ngneers-controls-tooltip.d.ts +189 -0
  182. package/types/ngneers-controls-tree.d.ts +409 -0
  183. package/types/ngneers-controls-upload.d.ts +347 -0
  184. package/types/ngneers-controls-utils-ng.d.ts +129 -0
  185. package/types/ngneers-controls-utils.d.ts +130 -0
  186. package/types/ngneers-controls.d.ts +2 -0
@@ -0,0 +1,888 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, input, computed, signal, viewChild, effect, untracked, Component } from '@angular/core';
3
+ import { domEventHandler } from '@ngneers/controls/api/ng';
4
+ import { ValueControlBase, NgnPt, provideSelf } from '@ngneers/controls/base';
5
+ import { I18n } from '@ngneers/controls/i18n';
6
+ import { NgnRovingGroup, NgnRovingItem } from '@ngneers/controls/roving-focus';
7
+ import { maskInputControlTemplate } from '@ngneers/controls-themes/templates/mask-input';
8
+
9
+ const phoneNumber = [
10
+ '(',
11
+ { kind: 'number', segment: 'areaCode', min: 0, max: 999, length: 3, placeholder: '000' },
12
+ ')',
13
+ ' ',
14
+ { kind: 'number', segment: 'exchange', min: 0, max: 999, length: 3, placeholder: '000' },
15
+ '-',
16
+ { kind: 'number', segment: 'subscriber', min: 0, max: 9999, length: 4, placeholder: '0000' },
17
+ ];
18
+ const creditCard = [
19
+ { kind: 'number', segment: 'cardPart1', min: 0, max: 9999, length: 4, placeholder: '0000' },
20
+ ' ',
21
+ { kind: 'number', segment: 'cardPart2', min: 0, max: 9999, length: 4, placeholder: '0000' },
22
+ ' ',
23
+ { kind: 'number', segment: 'cardPart3', min: 0, max: 9999, length: 4, placeholder: '0000' },
24
+ ' ',
25
+ { kind: 'number', segment: 'cardPart4', min: 0, max: 9999, length: 4, placeholder: '0000' },
26
+ ];
27
+ const ssn = [
28
+ { kind: 'number', segment: 'areaNumber', min: 0, max: 999, length: 3, placeholder: '000' },
29
+ '-',
30
+ { kind: 'number', segment: 'groupNumber', min: 0, max: 99, length: 2, placeholder: '00' },
31
+ '-',
32
+ { kind: 'number', segment: 'serialNumber', min: 0, max: 9999, length: 4, placeholder: '0000' },
33
+ ];
34
+ const zipCode = [
35
+ { kind: 'number', segment: 'zipBase', min: 0, max: 99999, length: 5, placeholder: '00000' },
36
+ '-',
37
+ { kind: 'number', segment: 'zipPlus4', min: 0, max: 9999, length: 4, placeholder: '0000' },
38
+ ];
39
+ const currency = [
40
+ '$',
41
+ {
42
+ kind: 'number',
43
+ segment: 'dollars',
44
+ min: 0,
45
+ max: 999999999,
46
+ length: 9,
47
+ placeholder: '000000000',
48
+ },
49
+ '.',
50
+ { kind: 'number', segment: 'cents', min: 0, max: 99, length: 2, placeholder: '00' },
51
+ ];
52
+ const percentage = [
53
+ { kind: 'number', segment: 'percent', min: 0, max: 100, length: 3, placeholder: '000' },
54
+ '%',
55
+ ];
56
+ const MASKS = {
57
+ phoneNumber,
58
+ creditCard,
59
+ ssn,
60
+ zipCode,
61
+ currency,
62
+ percentage,
63
+ };
64
+
65
+ const formatSymbols = {
66
+ d: 'day',
67
+ M: 'month',
68
+ y: 'year',
69
+ H: 'hour24',
70
+ h: 'hour12',
71
+ m: 'minute',
72
+ s: 'second',
73
+ a: 'period',
74
+ S: 'fractionalSecond',
75
+ };
76
+ /**
77
+ * Returns the input mask configuration for a given date or time mask.
78
+ * @param mask The mask string representing the date or time format.
79
+ * For example, 'MM/dd/yyyy' for date, 'HH:mm:ss' for time, 'hh:mm:ss a' for 12-hour time with period.
80
+ * @returns The input mask configuration that can be used with the input mask component.
81
+ */
82
+ function getDateOrTimeMask(mask) {
83
+ const cfg = [];
84
+ let i = 0;
85
+ while (i < mask.length) {
86
+ const char = mask[i];
87
+ const symbol = formatSymbols[char];
88
+ if (symbol) {
89
+ // Count how many times this symbol repeats
90
+ let length = 1;
91
+ while (i + length < mask.length && mask[i + length] === char) {
92
+ length++;
93
+ }
94
+ cfg.push({ symbol, length });
95
+ i += length;
96
+ }
97
+ else {
98
+ cfg.push({ literal: char });
99
+ i++;
100
+ }
101
+ }
102
+ const result = cfg.map(segment => {
103
+ if ('literal' in segment) {
104
+ return segment.literal;
105
+ }
106
+ else {
107
+ const { symbol, length } = segment;
108
+ // A single-letter token (length 1) yields a variable-length, non-padded field
109
+ // (`length` omitted); a doubled token (length >= 2) yields a fixed, zero-padded field.
110
+ const len = length >= 2 ? length : undefined;
111
+ switch (symbol) {
112
+ case 'day':
113
+ return {
114
+ segment: 'day',
115
+ kind: 'number',
116
+ min: 1,
117
+ max: 31,
118
+ length: len,
119
+ placeholder: 'DD',
120
+ };
121
+ case 'month':
122
+ return {
123
+ segment: 'month',
124
+ kind: 'number',
125
+ min: 1,
126
+ max: 12,
127
+ length: len,
128
+ placeholder: 'MM',
129
+ };
130
+ case 'year':
131
+ return {
132
+ segment: 'year',
133
+ kind: 'number',
134
+ min: 0,
135
+ max: 9999,
136
+ length: len,
137
+ placeholder: 'YYYY',
138
+ };
139
+ case 'hour24':
140
+ return {
141
+ segment: 'hour24',
142
+ kind: 'number',
143
+ min: 0,
144
+ max: 23,
145
+ length: len,
146
+ placeholder: 'HH',
147
+ };
148
+ case 'hour12':
149
+ return {
150
+ segment: 'hour12',
151
+ kind: 'number',
152
+ min: 1,
153
+ max: 12,
154
+ length: len,
155
+ placeholder: 'HH',
156
+ };
157
+ case 'minute':
158
+ return {
159
+ segment: 'minute',
160
+ kind: 'number',
161
+ min: 0,
162
+ max: 59,
163
+ length: len,
164
+ placeholder: 'MM',
165
+ };
166
+ case 'second':
167
+ return {
168
+ segment: 'second',
169
+ kind: 'number',
170
+ min: 0,
171
+ max: 59,
172
+ length: len,
173
+ placeholder: 'SS',
174
+ };
175
+ case 'period':
176
+ return {
177
+ segment: 'period',
178
+ kind: 'enum',
179
+ values: ['AM', 'PM'],
180
+ };
181
+ case 'fractionalSecond':
182
+ return {
183
+ segment: 'fractionalSecond',
184
+ kind: 'number',
185
+ min: 0,
186
+ max: 999,
187
+ length: len,
188
+ placeholder: 'SSS',
189
+ };
190
+ }
191
+ }
192
+ });
193
+ return result;
194
+ }
195
+ const DATE_TIME_MASKS = {
196
+ date: getDateOrTimeMask('MM/dd/yyyy'),
197
+ time: getDateOrTimeMask('HH:mm:ss'),
198
+ timeShort: getDateOrTimeMask('HH:mm'),
199
+ time12: getDateOrTimeMask('hh:mm:ss a'),
200
+ };
201
+
202
+ function resolveMask(input) {
203
+ if (!input)
204
+ return null;
205
+ const cfg = typeof input === 'string'
206
+ ? (MASKS[input] ??
207
+ DATE_TIME_MASKS[input])
208
+ : input;
209
+ if (!cfg)
210
+ return null; // unknown raw string → unsupported
211
+ return cfg.map(item => {
212
+ if (typeof item === 'string')
213
+ return { kind: 'sep', text: item };
214
+ if (item.kind === 'number') {
215
+ const maxLen = item.length ?? String(item.max).length;
216
+ const field = {
217
+ kind: 'number',
218
+ name: item.segment,
219
+ min: item.min,
220
+ max: item.max,
221
+ maxLen,
222
+ pad: item.length != null,
223
+ placeholder: item.placeholder ?? '-'.repeat(maxLen),
224
+ };
225
+ return field;
226
+ }
227
+ // Guard an empty values array: Math.max(...[]) is -Infinity, which would
228
+ // throw in '-'.repeat(maxLen). An empty enum is degenerate but must not crash.
229
+ const maxLen = item.values.length === 0 ? 0 : Math.max(...item.values.map(v => v.length));
230
+ const field = {
231
+ kind: 'enum',
232
+ name: item.segment,
233
+ values: [...item.values],
234
+ maxLen,
235
+ placeholder: item.placeholder ?? (maxLen > 0 ? '-'.repeat(maxLen) : ''),
236
+ };
237
+ return field;
238
+ });
239
+ }
240
+ function fieldList(parts) {
241
+ return parts.filter((p) => p.kind !== 'sep');
242
+ }
243
+ /**
244
+ * Index of the section whose horizontal box is nearest `clientX`.
245
+ * - `clientX` left of the first section → 0
246
+ * - `clientX` right of the last section → last index
247
+ * - inside a section → that section
248
+ * - in a gap/separator between two sections → the nearer one
249
+ * Returns -1 for an empty list.
250
+ */
251
+ function nearestSectionIndex(rects, clientX) {
252
+ if (rects.length === 0)
253
+ return -1;
254
+ let best = 0;
255
+ let bestDist = Infinity;
256
+ for (let i = 0; i < rects.length; i++) {
257
+ const r = rects[i];
258
+ if (!r)
259
+ continue;
260
+ const dist = clientX < r.left ? r.left - clientX : clientX > r.right ? clientX - r.right : 0;
261
+ if (dist < bestDist) {
262
+ bestDist = dist;
263
+ best = i;
264
+ }
265
+ if (dist === 0)
266
+ break; // inside a section — exact hit
267
+ }
268
+ return best;
269
+ }
270
+ /**
271
+ * Enforce the no-gaps invariant: scanning from index 0, once the first empty
272
+ * field is seen, blank every field after it. A delete that empties an earlier
273
+ * field therefore clears the now-orphaned later fields too.
274
+ */
275
+ function truncateGaps(values) {
276
+ let seenEmpty = false;
277
+ return values.map(v => {
278
+ if (seenEmpty)
279
+ return '';
280
+ if (!v) {
281
+ seenEmpty = true;
282
+ return '';
283
+ }
284
+ return v;
285
+ });
286
+ }
287
+ // ---- New per-section pure functions ----
288
+ /**
289
+ * Type a char into ONE section. Returns the new section value + whether focus
290
+ * should auto-advance to the next section, or null if the char is rejected.
291
+ *
292
+ * For number fields:
293
+ * - Non-digit → null.
294
+ * - If current is already complete (length===maxLen or value*10>max) a digit
295
+ * starts a fresh value (replace).
296
+ * - Otherwise appending is attempted. If the appended candidate exceeds max or
297
+ * maxLen, restart with the bare digit instead.
298
+ * - advance:true when the resulting value is isSectionComplete.
299
+ *
300
+ * For enum fields:
301
+ * - Match by case-insensitive first char; on match returns {value, advance:true}.
302
+ * - No match → null.
303
+ *
304
+ * NOTE: padding is NOT applied to the returned value — that is a display concern.
305
+ */
306
+ function typeIntoSection(field, current, ch) {
307
+ if (field.kind === 'enum') {
308
+ const match = field.values.find(val => val[0]?.toLowerCase() === ch.toLowerCase());
309
+ if (!match)
310
+ return null;
311
+ return { value: match, advance: true };
312
+ }
313
+ // number field — a single digit only (multi-char data is rejected outright).
314
+ if (!/^\d$/.test(ch))
315
+ return null;
316
+ // If current section is already complete, start fresh
317
+ const currentComplete = current !== '' && isSectionComplete(field, current);
318
+ let candidate;
319
+ if (currentComplete) {
320
+ candidate = ch;
321
+ }
322
+ else {
323
+ const appended = current + ch;
324
+ // If appending would exceed max or length, restart with bare digit
325
+ if (appended.length > field.maxLen || Number(appended) > field.max) {
326
+ // A single digit that is itself out of range can never be a valid value
327
+ // for this field (only possible for tiny fields with max < 10) → reject.
328
+ if (Number(ch) > field.max)
329
+ return null;
330
+ candidate = ch;
331
+ }
332
+ else {
333
+ candidate = appended;
334
+ }
335
+ }
336
+ // A full-width candidate below the field minimum (e.g. '00' for a day/month
337
+ // whose min is 1) is invalid — reject rather than letting it "complete".
338
+ if (candidate.length === field.maxLen && Number(candidate) < field.min)
339
+ return null;
340
+ const advance = isSectionComplete(field, candidate);
341
+ return { value: candidate, advance };
342
+ }
343
+ /**
344
+ * Step/cycle a section value up (1) or down (-1). Empty defaults sensibly.
345
+ *
346
+ * For number fields:
347
+ * - empty → field.min (no extra dir step on the empty→min transition).
348
+ * - Non-empty: n = Number(current) + dir, wrap at >max → min, <min → max.
349
+ * - Returns padded (padStart maxLen '0') iff field.pad.
350
+ *
351
+ * For enum fields:
352
+ * - Find current (case-insensitive) in values; step by dir with wraparound.
353
+ * - empty/not-found → first value (dir:1) or last value (dir:-1).
354
+ */
355
+ function stepSection(field, current, dir) {
356
+ if (field.kind === 'enum') {
357
+ const i = field.values.findIndex(v => v.toLowerCase() === current.toLowerCase());
358
+ if (i === -1) {
359
+ return (dir === 1 ? field.values[0] : field.values[field.values.length - 1]) ?? '';
360
+ }
361
+ const next = (i + dir + field.values.length) % field.values.length;
362
+ return field.values[next] ?? field.values[0] ?? '';
363
+ }
364
+ // number field
365
+ if (current === '') {
366
+ const min = field.min;
367
+ return field.pad ? String(min).padStart(field.maxLen, '0') : String(min);
368
+ }
369
+ let n = Number(current) + dir;
370
+ if (n > field.max)
371
+ n = field.min;
372
+ if (n < field.min)
373
+ n = field.max;
374
+ return field.pad ? String(n).padStart(field.maxLen, '0') : String(n);
375
+ }
376
+ /**
377
+ * Whether a single section value is "done" (would auto-advance or is a fully
378
+ * matched enum). Empty → always false.
379
+ *
380
+ * number: value.length===maxLen OR Number(value)*10 > max.
381
+ * enum: value is literally in the values list (case-sensitive).
382
+ */
383
+ function isSectionComplete(field, value) {
384
+ if (value === '')
385
+ return false;
386
+ if (field.kind === 'enum')
387
+ return field.values.includes(value);
388
+ if (!/^\d+$/.test(value))
389
+ return false;
390
+ const n = Number(value);
391
+ // A complete number value must be within range AND either fill the width or be
392
+ // unable to grow further (value*10 would exceed max).
393
+ return n >= field.min && n <= field.max && (value.length === field.maxLen || n * 10 > field.max);
394
+ }
395
+ /**
396
+ * Whether all fields in the mask have a complete value.
397
+ * Empty field list → false.
398
+ */
399
+ function isComplete(parts, values) {
400
+ const fields = fieldList(parts);
401
+ if (fields.length === 0)
402
+ return false;
403
+ return fields.every((field, i) => isSectionComplete(field, values[i] ?? ''));
404
+ }
405
+ /**
406
+ * Render each part as a RenderToken for display. No offsets.
407
+ *
408
+ * Separator → {kind:'sep', text}.
409
+ * Field → {kind:'section', ord, text, placeholder}:
410
+ * - empty value → text=field.placeholder, placeholder=true
411
+ * - filled → text=padded(number&&pad) or raw value, placeholder=false
412
+ *
413
+ * `ord` is the 0-based field ordinal (index among fields, not among all parts).
414
+ *
415
+ * When `activeOrd` is provided, a padded number field that is currently active
416
+ * AND not yet complete renders its raw typed digits instead of zero-padded text.
417
+ * This matches native date-field behaviour: padding is deferred until the section
418
+ * is committed (complete or navigated away from).
419
+ * When `activeOrd` is omitted, all non-empty padded fields are padded (preserving
420
+ * the "fully padded" output for callers that don't track the active section).
421
+ */
422
+ function composeDisplay(parts, values, activeOrd) {
423
+ const tokens = [];
424
+ let fieldOrd = 0;
425
+ for (const part of parts) {
426
+ if (part.kind === 'sep') {
427
+ tokens.push({ kind: 'sep', text: part.text });
428
+ }
429
+ else {
430
+ const o = fieldOrd;
431
+ const value = values[o] ?? '';
432
+ if (value === '') {
433
+ tokens.push({ kind: 'section', ord: o, text: part.placeholder, placeholder: true });
434
+ }
435
+ else {
436
+ const padded = part.kind === 'number' && part.pad && (o !== activeOrd || isSectionComplete(part, value));
437
+ const text = padded ? value.padStart(part.maxLen, '0') : value;
438
+ tokens.push({ kind: 'section', ord: o, text, placeholder: false });
439
+ }
440
+ fieldOrd++;
441
+ }
442
+ }
443
+ return tokens;
444
+ }
445
+ /**
446
+ * Compose the full serialized output string (padded per field definition).
447
+ * Separators are appended verbatim. Number fields are padded iff field.pad.
448
+ * Enum fields are verbatim. Empty values are included as-is (empty string).
449
+ */
450
+ function serialize(parts, values) {
451
+ let result = '';
452
+ let fieldOrd = 0;
453
+ for (const part of parts) {
454
+ if (part.kind === 'sep') {
455
+ result += part.text;
456
+ }
457
+ else {
458
+ const value = values[fieldOrd] ?? '';
459
+ if (part.kind === 'number' && part.pad) {
460
+ result += value.padStart(part.maxLen, '0');
461
+ }
462
+ else {
463
+ result += value;
464
+ }
465
+ fieldOrd++;
466
+ }
467
+ }
468
+ return result;
469
+ }
470
+ /** Consume the maximal in-range digit run for a number field at `cursor`. */
471
+ function takeNumberField(field, str, cursor) {
472
+ let take = '';
473
+ while (take.length < field.maxLen) {
474
+ const c = str[cursor] ?? '';
475
+ if (!/\d/.test(c))
476
+ break;
477
+ take += c;
478
+ cursor++;
479
+ }
480
+ if (take === '')
481
+ return { value: '', cursor, ok: true, stop: true };
482
+ const n = Number(take);
483
+ if (n < field.min || n > field.max)
484
+ return { value: '', cursor, ok: false, stop: false };
485
+ return { value: take, cursor, ok: true, stop: false };
486
+ }
487
+ /** Match an enum value (exact case-insensitive, else first char) at `cursor`. */
488
+ function takeEnumField(field, str, cursor) {
489
+ const rest = str.slice(cursor);
490
+ if (rest === '')
491
+ return { value: '', cursor, ok: true, stop: true };
492
+ const exact = field.values.find(v => v.toUpperCase() === rest.slice(0, v.length).toUpperCase());
493
+ const match = exact ?? field.values.find(v => v[0]?.toUpperCase() === rest[0]?.toUpperCase());
494
+ if (!match)
495
+ return { value: '', cursor, ok: false, stop: false };
496
+ return { value: match, cursor: cursor + match.length, ok: true, stop: false };
497
+ }
498
+ function feedString(parts, str) {
499
+ const fields = fieldList(parts);
500
+ const values = fields.map(() => '');
501
+ let cursor = 0;
502
+ let o = 0; // field ordinal
503
+ let ok = true;
504
+ for (const part of parts) {
505
+ if (part.kind === 'sep') {
506
+ if (str.startsWith(part.text, cursor))
507
+ cursor += part.text.length;
508
+ continue;
509
+ }
510
+ const res = part.kind === 'number'
511
+ ? takeNumberField(part, str, cursor)
512
+ : takeEnumField(part, str, cursor);
513
+ if (!res.ok) {
514
+ ok = false;
515
+ break;
516
+ }
517
+ if (res.stop)
518
+ break; // partial input — leave this and the rest empty
519
+ values[o] = res.value;
520
+ cursor = res.cursor;
521
+ o++;
522
+ }
523
+ // Any unconsumed trailing input means the string didn't fully match the mask.
524
+ // `paste` treats this as invalid (returns null); `deserialize` ignores `ok`
525
+ // and keeps the best-effort prefix.
526
+ if (cursor < str.length)
527
+ ok = false;
528
+ return { values: truncateGaps(values), ok };
529
+ }
530
+ function paste(parts, text) {
531
+ const { values, ok } = feedString(parts, text);
532
+ if (!ok)
533
+ return null; // any invalid field rejects the whole paste
534
+ // offset: count the committed characters in a simple left-to-right walk
535
+ let offset = 0;
536
+ let fieldOrd = 0;
537
+ for (const part of parts) {
538
+ if (part.kind === 'sep') {
539
+ // include separator only if the next field has a value
540
+ const nextValue = values[fieldOrd] ?? '';
541
+ if (nextValue)
542
+ offset += part.text.length;
543
+ }
544
+ else {
545
+ const v = values[fieldOrd] ?? '';
546
+ if (v)
547
+ offset += v.length;
548
+ fieldOrd++;
549
+ }
550
+ }
551
+ return { values, offset };
552
+ }
553
+ function deserialize(parts, str) {
554
+ // Best-effort separator-aware parse. On an invalid/out-of-range field we keep
555
+ // the valid prefix (feedString already stopped there, leaving the rest empty)
556
+ // rather than producing garbage. Never throws.
557
+ return feedString(parts, str).values;
558
+ }
559
+
560
+ /**
561
+ * @category control
562
+ */
563
+ class NgnMaskInput extends ValueControlBase {
564
+ isFieldControl = true;
565
+ theme = this.injectThemeTemplate(maskInputControlTemplate, 'root');
566
+ i18n = inject(I18n).translations;
567
+ /**
568
+ * The mask to apply to the input. Can be a predefined mask key or a custom mask configuration.
569
+ */
570
+ mask = input(null, /* @ts-ignore */
571
+ ...(ngDevMode ? [{ debugName: "mask" }] : /* istanbul ignore next */ []));
572
+ _parts = computed(() => resolveMask(this.mask()), /* @ts-ignore */
573
+ ...(ngDevMode ? [{ debugName: "_parts" }] : /* istanbul ignore next */ []));
574
+ _fields = computed(() => {
575
+ const p = this._parts();
576
+ return p ? fieldList(p) : [];
577
+ }, /* @ts-ignore */
578
+ ...(ngDevMode ? [{ debugName: "_fields" }] : /* istanbul ignore next */ []));
579
+ /**
580
+ * Structural signature of the resolved mask — field kinds/names/widths and
581
+ * separators, but deliberately NOT numeric min/max. The section values are
582
+ * reset only when this changes, so a consumer narrowing a field's range (e.g.
583
+ * a calendar shrinking the day field's max to the current month's length) does
584
+ * not wipe what the user has already typed.
585
+ */
586
+ _structureSig = computed(() => {
587
+ const parts = this._parts();
588
+ if (!parts)
589
+ return '';
590
+ return parts
591
+ .map(p => p.kind === 'sep'
592
+ ? `sep:${p.text}`
593
+ : p.kind === 'number'
594
+ ? `num:${p.name}:${p.maxLen}:${p.pad}`
595
+ : `enum:${p.name}:${p.values.join('|')}`)
596
+ .join(';');
597
+ }, /* @ts-ignore */
598
+ ...(ngDevMode ? [{ debugName: "_structureSig" }] : /* istanbul ignore next */ []));
599
+ /** Source of truth: one raw string per field ordinal. */
600
+ values = signal([], /* @ts-ignore */
601
+ ...(ngDevMode ? [{ debugName: "values" }] : /* istanbul ignore next */ []));
602
+ /** Whether the proxy input currently holds focus. The active-section
603
+ * highlight and "actively edited" treatment only apply while focused. */
604
+ focused = signal(false, /* @ts-ignore */
605
+ ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
606
+ /** RenderTokens for template `@for` loop. While unfocused there is no active
607
+ * section, so padded fields render in their resting (padded) form. */
608
+ tokens = computed(() => {
609
+ const p = this._parts();
610
+ if (!p)
611
+ return [];
612
+ return composeDisplay(p, this.values(), this.focused() ? this.activeOrd() : undefined);
613
+ }, /* @ts-ignore */
614
+ ...(ngDevMode ? [{ debugName: "tokens" }] : /* istanbul ignore next */ []));
615
+ /** The roving group on the proxy input element. */
616
+ rovingGroup = viewChild.required(NgnRovingGroup);
617
+ /** The proxy input element reference. */
618
+ _proxyRef = viewChild.required('proxy');
619
+ /** Active field ordinal (identity: item index === field ordinal). */
620
+ activeOrd = computed(() => this.rovingGroup().activeIndex(), /* @ts-ignore */
621
+ ...(ngDevMode ? [{ debugName: "activeOrd" }] : /* istanbul ignore next */ []));
622
+ proxyInputMode = computed(() => {
623
+ const f = this._fields()[this.activeOrd()];
624
+ return f?.kind === 'enum' ? 'text' : 'numeric';
625
+ }, /* @ts-ignore */
626
+ ...(ngDevMode ? [{ debugName: "proxyInputMode" }] : /* istanbul ignore next */ []));
627
+ liveAnnouncement = signal('', /* @ts-ignore */
628
+ ...(ngDevMode ? [{ debugName: "liveAnnouncement" }] : /* istanbul ignore next */ []));
629
+ // ---- A11y helpers ----
630
+ sectionValueNow(ord) {
631
+ const f = this._fields()[ord];
632
+ const v = this.values()[ord] ?? '';
633
+ if (!f || v === '')
634
+ return null;
635
+ if (f.kind === 'number')
636
+ return Number(v);
637
+ // enum: expose the option index so the spinbutton has a valid numeric value.
638
+ const idx = f.values.indexOf(v);
639
+ return idx >= 0 ? idx : null;
640
+ }
641
+ sectionMin(ord) {
642
+ const f = this._fields()[ord];
643
+ if (!f)
644
+ return null;
645
+ return f.kind === 'number' ? f.min : 0;
646
+ }
647
+ sectionMax(ord) {
648
+ const f = this._fields()[ord];
649
+ if (!f)
650
+ return null;
651
+ return f.kind === 'number' ? f.max : f.values.length - 1;
652
+ }
653
+ /** Accessible name for a section's `role="spinbutton"` — the field's name. */
654
+ sectionName(ord) {
655
+ return this._fields()[ord]?.name ?? '';
656
+ }
657
+ sectionValueText(ord) {
658
+ const f = this._fields()[ord];
659
+ const v = this.values()[ord] ?? '';
660
+ if (!f)
661
+ return '';
662
+ if (v === '')
663
+ return f.placeholder;
664
+ return v;
665
+ }
666
+ /**
667
+ * Whether every section holds a finished value — i.e. the mask is fully
668
+ * filled and a serialized output value has been emitted.
669
+ */
670
+ complete = computed(() => this.value() !== null, /* @ts-ignore */
671
+ ...(ngDevMode ? [{ debugName: "complete" }] : /* istanbul ignore next */ []));
672
+ /** Whether no section holds any value (the whole field is blank). Lets a host
673
+ * (e.g. calendar) distinguish a fully-cleared field from mid-typing, both of
674
+ * which emit a `null` value. */
675
+ empty = computed(() => this.values().every(v => v === ''), /* @ts-ignore */
676
+ ...(ngDevMode ? [{ debugName: "empty" }] : /* istanbul ignore next */ []));
677
+ /**
678
+ * Returns the resolved CSS class string for `section-active` from the theme,
679
+ * used to highlight the currently active section span in the template.
680
+ */
681
+ activeSectionClass = computed(() => this.theme.class('section-active'), /* @ts-ignore */
682
+ ...(ngDevMode ? [{ debugName: "activeSectionClass" }] : /* istanbul ignore next */ []));
683
+ // ---- Segment descriptions for sr ----
684
+ segmentDescriptions = computed(() => {
685
+ const parts = this._parts();
686
+ if (!parts)
687
+ return [];
688
+ return fieldList(parts).map(field => ({
689
+ id: `seg-${field.name}-${this.inputId()}`,
690
+ text: field.kind === 'number'
691
+ ? this.i18n['maskInput_segmentRange']({
692
+ name: field.name,
693
+ min: field.min,
694
+ max: field.max,
695
+ })
696
+ : field.values.join(this.i18n['maskInput_optionSeparator']()),
697
+ }));
698
+ }, /* @ts-ignore */
699
+ ...(ngDevMode ? [{ debugName: "segmentDescriptions" }] : /* istanbul ignore next */ []));
700
+ /** Returns the description span id for a given field ordinal, or null if none. */
701
+ sectionDescId(ord) {
702
+ return this.segmentDescriptions()[ord]?.id ?? null;
703
+ }
704
+ constructor() {
705
+ super();
706
+ // Reset values only when the mask STRUCTURE changes (not a min/max-only
707
+ // tweak), synchronously before the next render, so stale values from a
708
+ // different mask are never composed against the new parts.
709
+ let prevStructureSig = null;
710
+ effect(() => {
711
+ const sig = this._structureSig();
712
+ if (prevStructureSig !== null && prevStructureSig !== sig) {
713
+ this.values.set([]);
714
+ }
715
+ prevStructureSig = sig;
716
+ });
717
+ // External value → internal: guard against write-loop by comparing to serialized form.
718
+ // Use untracked() when reading `values` so that internal typing changes do NOT re-trigger
719
+ // this effect — it must only fire when the external `value` model (from the parent) changes.
720
+ effect(() => {
721
+ const parts = this._parts();
722
+ if (!parts)
723
+ return;
724
+ const ext = this.value() ?? '';
725
+ const current = untracked(() => isComplete(parts, this.values()) ? serialize(parts, this.values()) : '');
726
+ if (ext !== current) {
727
+ this.values.set(deserialize(parts, ext));
728
+ }
729
+ });
730
+ // Internal values → external value model.
731
+ effect(() => {
732
+ const parts = this._parts();
733
+ if (!parts)
734
+ return;
735
+ const vals = this.values();
736
+ if (isComplete(parts, vals)) {
737
+ this.value.set(serialize(parts, vals));
738
+ }
739
+ else {
740
+ this.value.set(null);
741
+ }
742
+ });
743
+ // Wire DOM events on the proxy input.
744
+ domEventHandler(this._proxyRef, 'beforeinput', event => this._onBeforeInput(event));
745
+ domEventHandler(this._proxyRef, 'keydown', event => this._onKeyDown(event));
746
+ // Track focus so the active-section highlight only shows while focused.
747
+ domEventHandler(this._proxyRef, 'focus', () => this.focused.set(true));
748
+ domEventHandler(this._proxyRef, 'blur', () => this.focused.set(false));
749
+ // Pointer down anywhere on the control: focus the proxy and select the
750
+ // section nearest the pointer. Shared with field-delegated clicks via
751
+ // `focusFromPointer`. Works standalone (no surrounding input-field).
752
+ domEventHandler(this.element, 'pointerdown', event => {
753
+ if (this.disabled())
754
+ return;
755
+ event.preventDefault();
756
+ this.focusFromPointer(event);
757
+ });
758
+ }
759
+ /**
760
+ * Places focus on the proxy input and selects the section nearest the
761
+ * pointer's horizontal position. Invoked both by the control's own
762
+ * `pointerdown` and by a surrounding `ngn-input-field` when its padding is
763
+ * clicked (so a click "above" a section selects that section). Returns `true`
764
+ * to signal the field it handled focus.
765
+ */
766
+ focusFromPointer(event) {
767
+ if (this.disabled())
768
+ return false;
769
+ this._proxyRef().nativeElement.focus();
770
+ const ord = this._sectionIndexAt(event.clientX);
771
+ if (ord >= 0)
772
+ this.rovingGroup().setActive(ord);
773
+ return true;
774
+ }
775
+ /** Clears every section (invoked by a surrounding field's clear button). */
776
+ clearValue() {
777
+ if (this.disabled() || this.readonly())
778
+ return false;
779
+ this.values.set([]);
780
+ return true;
781
+ }
782
+ /** Field ordinal of the section nearest `clientX`, or -1 when there are none. */
783
+ _sectionIndexAt(clientX) {
784
+ // Section spans carry role="spinbutton" and appear in DOM order = field
785
+ // ordinal order (matching the activeOrd identity mapping).
786
+ const spans = this.element.nativeElement.querySelectorAll('[role="spinbutton"]');
787
+ const rects = Array.from(spans, el => el.getBoundingClientRect());
788
+ return nearestSectionIndex(rects, clientX);
789
+ }
790
+ _onBeforeInput(event) {
791
+ const parts = this._parts();
792
+ if (!parts)
793
+ return;
794
+ if (this.disabled() || this.readonly())
795
+ return;
796
+ event.preventDefault();
797
+ if (event.inputType === 'insertFromPaste' || event.inputType === 'insertFromDrop') {
798
+ const r = event.data ? paste(parts, event.data) : null;
799
+ if (r) {
800
+ this.values.set(r.values);
801
+ // Advance to the next empty section or stay on last
802
+ const fields = this._fields();
803
+ const nextEmpty = r.values.findIndex(v => v === '');
804
+ const targetOrd = nextEmpty >= 0 ? nextEmpty : fields.length - 1;
805
+ this.rovingGroup().setActive(targetOrd);
806
+ }
807
+ return;
808
+ }
809
+ if (event.data) {
810
+ const ord = this.activeOrd();
811
+ const fields = this._fields();
812
+ const f = fields[ord];
813
+ if (!f)
814
+ return;
815
+ const current = this.values()[ord] ?? '';
816
+ const r = typeIntoSection(f, current, event.data);
817
+ if (r) {
818
+ // Immutable update of the values array
819
+ const newValues = [...this.values()];
820
+ newValues[ord] = r.value;
821
+ this.values.set(newValues);
822
+ if (r.advance) {
823
+ this.rovingGroup().next();
824
+ }
825
+ }
826
+ }
827
+ }
828
+ _onKeyDown(event) {
829
+ const parts = this._parts();
830
+ if (!parts || event.ctrlKey || event.metaKey || event.altKey)
831
+ return;
832
+ if (this.disabled() || this.readonly())
833
+ return;
834
+ const ord = this.activeOrd();
835
+ const fields = this._fields();
836
+ const f = fields[ord];
837
+ switch (event.key) {
838
+ case 'Backspace':
839
+ case 'Delete': {
840
+ event.preventDefault();
841
+ const currentVal = this.values()[ord] ?? '';
842
+ if (currentVal === '') {
843
+ // Already empty — step to previous section
844
+ this.rovingGroup().prev();
845
+ }
846
+ else {
847
+ // Clear the active section; then enforce no-gaps so any filled
848
+ // fields after the newly-emptied one are also cleared.
849
+ const newValues = [...this.values()];
850
+ newValues[ord] = '';
851
+ this.values.set(truncateGaps(newValues));
852
+ }
853
+ break;
854
+ }
855
+ case 'ArrowUp':
856
+ case 'ArrowDown': {
857
+ // The roving group is configured with horizontal orientation, so it
858
+ // deliberately does NOT consume ArrowUp/ArrowDown — the mask owns them
859
+ // for value stepping. This division is intentional, not coincidental:
860
+ // Left/Right move between sections, Up/Down change the active value.
861
+ event.preventDefault();
862
+ if (!f)
863
+ break;
864
+ const dir = event.key === 'ArrowUp' ? 1 : -1;
865
+ const newVal = stepSection(f, this.values()[ord] ?? '', dir);
866
+ const newValues = [...this.values()];
867
+ newValues[ord] = newVal;
868
+ this.values.set(newValues);
869
+ this.liveAnnouncement.set(`${f.name}: ${newVal}`);
870
+ break;
871
+ }
872
+ // ArrowLeft/Right/Home/End are handled by the roving group — do not preventDefault
873
+ }
874
+ }
875
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnMaskInput, deps: [], target: i0.ɵɵFactoryTarget.Component });
876
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: NgnMaskInput, isStandalone: true, selector: "ngn-mask-input", inputs: { mask: { classPropertyName: "mask", publicName: "mask", isSignal: true, isRequired: false, transformFunction: null } }, providers: [provideSelf(NgnMaskInput)], viewQueries: [{ propertyName: "rovingGroup", first: true, predicate: NgnRovingGroup, descendants: true, isSignal: true }, { propertyName: "_proxyRef", first: true, predicate: ["proxy"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div [ptInt]=\"this\" [ptClass]=\"'root'\">\n <input\n #proxy\n #pg=\"ngnRovingGroup\"\n [ptInt]=\"this\"\n [ptClass]=\"'proxy'\"\n ngnRovingGroup\n rovingMode=\"activedescendant\"\n orientation=\"horizontal\"\n [attr.inputmode]=\"proxyInputMode()\"\n [attr.aria-roledescription]=\"i18n['maskInput_roleDescription']()\"\n [attr.aria-label]=\"label()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [disabled]=\"disabled()\"\n [attr.readonly]=\"readonly() || null\"\n tabindex=\"0\"\n />\n @for (tok of tokens(); track $index) {\n @if (tok.kind === 'sep') {\n <span [ptInt]=\"this\" [ptClass]=\"'separator'\" aria-hidden=\"true\">{{ tok.text }}</span>\n } @else {\n <span\n [ngnRovingItem]=\"pg\"\n role=\"spinbutton\"\n [attr.aria-label]=\"sectionName(tok.ord)\"\n [ptInt]=\"this\"\n [ptClass]=\"tok.placeholder ? 'section-placeholder' : 'section'\"\n [class]=\"focused() && activeOrd() === tok.ord ? activeSectionClass() : ''\"\n [attr.aria-valuenow]=\"sectionValueNow(tok.ord)\"\n [attr.aria-valuemin]=\"sectionMin(tok.ord)\"\n [attr.aria-valuemax]=\"sectionMax(tok.ord)\"\n [attr.aria-valuetext]=\"sectionValueText(tok.ord)\"\n [attr.aria-describedby]=\"sectionDescId(tok.ord)\"\n >{{ tok.text }}</span\n >\n }\n }\n @for (seg of segmentDescriptions(); track seg.id) {\n <span [id]=\"seg.id\" [ptInt]=\"this\" [ptClass]=\"'sr-only'\">{{ seg.text }}</span>\n }\n <span aria-live=\"polite\" [ptInt]=\"this\" [ptClass]=\"'sr-only'\">{{ liveAnnouncement() }}</span>\n</div>\n", dependencies: [{ kind: "directive", type: NgnPt, selector: "[ptInt]", inputs: ["ptInt", "ptClass", "ptDep"] }, { kind: "directive", type: NgnRovingGroup, selector: "[ngnRovingGroup]", inputs: ["orientation", "rovingMode", "rovingWrap"], outputs: ["activeItemChange"], exportAs: ["ngnRovingGroup"] }, { kind: "directive", type: NgnRovingItem, selector: "[ngnRovingItem]", inputs: ["ngnRovingItem"], exportAs: ["ngnRovingItem"] }] });
877
+ }
878
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: NgnMaskInput, decorators: [{
879
+ type: Component,
880
+ args: [{ selector: 'ngn-mask-input', imports: [NgnPt, NgnRovingGroup, NgnRovingItem], providers: [provideSelf(NgnMaskInput)], template: "<div [ptInt]=\"this\" [ptClass]=\"'root'\">\n <input\n #proxy\n #pg=\"ngnRovingGroup\"\n [ptInt]=\"this\"\n [ptClass]=\"'proxy'\"\n ngnRovingGroup\n rovingMode=\"activedescendant\"\n orientation=\"horizontal\"\n [attr.inputmode]=\"proxyInputMode()\"\n [attr.aria-roledescription]=\"i18n['maskInput_roleDescription']()\"\n [attr.aria-label]=\"label()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [disabled]=\"disabled()\"\n [attr.readonly]=\"readonly() || null\"\n tabindex=\"0\"\n />\n @for (tok of tokens(); track $index) {\n @if (tok.kind === 'sep') {\n <span [ptInt]=\"this\" [ptClass]=\"'separator'\" aria-hidden=\"true\">{{ tok.text }}</span>\n } @else {\n <span\n [ngnRovingItem]=\"pg\"\n role=\"spinbutton\"\n [attr.aria-label]=\"sectionName(tok.ord)\"\n [ptInt]=\"this\"\n [ptClass]=\"tok.placeholder ? 'section-placeholder' : 'section'\"\n [class]=\"focused() && activeOrd() === tok.ord ? activeSectionClass() : ''\"\n [attr.aria-valuenow]=\"sectionValueNow(tok.ord)\"\n [attr.aria-valuemin]=\"sectionMin(tok.ord)\"\n [attr.aria-valuemax]=\"sectionMax(tok.ord)\"\n [attr.aria-valuetext]=\"sectionValueText(tok.ord)\"\n [attr.aria-describedby]=\"sectionDescId(tok.ord)\"\n >{{ tok.text }}</span\n >\n }\n }\n @for (seg of segmentDescriptions(); track seg.id) {\n <span [id]=\"seg.id\" [ptInt]=\"this\" [ptClass]=\"'sr-only'\">{{ seg.text }}</span>\n }\n <span aria-live=\"polite\" [ptInt]=\"this\" [ptClass]=\"'sr-only'\">{{ liveAnnouncement() }}</span>\n</div>\n" }]
881
+ }], ctorParameters: () => [], propDecorators: { mask: [{ type: i0.Input, args: [{ isSignal: true, alias: "mask", required: false }] }], rovingGroup: [{ type: i0.ViewChild, args: [i0.forwardRef(() => NgnRovingGroup), { isSignal: true }] }], _proxyRef: [{ type: i0.ViewChild, args: ['proxy', { isSignal: true }] }] } });
882
+
883
+ /**
884
+ * Generated bundle index. Do not edit.
885
+ */
886
+
887
+ export { DATE_TIME_MASKS, MASKS, NgnMaskInput, getDateOrTimeMask };
888
+ //# sourceMappingURL=ngneers-controls-mask-input.mjs.map