tmw-numeric 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,319 @@
1
+ import * as i0 from '@angular/core';
2
+ import { forwardRef, Directive, Input, HostListener } from '@angular/core';
3
+ import { NG_VALUE_ACCESSOR } from '@angular/forms';
4
+
5
+ /**
6
+ * Modalità di digitazione — nomi/valori/comportamento invariati rispetto a
7
+ * `@eqproject/eqp-numeric` (`EqpNumericInputMode`), per parità di comportamento.
8
+ */
9
+ var TmwNumericInputMode;
10
+ (function (TmwNumericInputMode) {
11
+ /**
12
+ * I numeri partono dalla precisione più alta (i decimali): digitare uno shifta tutto a
13
+ * sinistra, il carattere decimale digitato viene ignorato. Es. `'12'` → `'0.12'`,
14
+ * `'1234'` → `'12.34'`.
15
+ */
16
+ TmwNumericInputMode["FINANCIAL"] = "FINANCIAL";
17
+ /**
18
+ * I numeri partono a sinistra del separatore decimale, come un campo di testo qualunque.
19
+ * Es. `'1234'` → `'1234'`, `'12.34'` → `'12.34'`.
20
+ */
21
+ TmwNumericInputMode["NATURAL"] = "NATURAL";
22
+ })(TmwNumericInputMode || (TmwNumericInputMode = {}));
23
+ /**
24
+ * Default identici a `@eqproject/eqp-numeric` — nessun consumer TMED chiamava
25
+ * `EqpNumericModule.forRoot(...)` né passava `[options]`, quindi questi default SONO il
26
+ * comportamento reale in produzione (v. AI_BUILD_PLAN.md per i 5 punti d'uso reali).
27
+ */
28
+ const TMW_NUMERIC_DEFAULT_OPTIONS = {
29
+ align: 'right',
30
+ allowNegative: true,
31
+ decimal: '.',
32
+ precision: 2,
33
+ prefix: '$ ',
34
+ suffix: '',
35
+ thousands: ',',
36
+ nullable: false,
37
+ min: undefined,
38
+ max: undefined,
39
+ inputMode: TmwNumericInputMode.NATURAL,
40
+ };
41
+
42
+ /**
43
+ * Sostituto di `[eqpNumericMask]` (`@eqproject/eqp-numeric`) — stesso `[options]`, stessi nomi
44
+ * di opzione, stessi default (v. `TMW_NUMERIC_DEFAULT_OPTIONS`). Nessun `forRoot()`: nessun
45
+ * consumer reale in TMED lo usava, i default del pacchetto originale erano già il comportamento
46
+ * effettivo in produzione.
47
+ *
48
+ * Formatta un `<input>` come numero mascherato (valuta/decimale) mantenendo il model
49
+ * (`ngModel`/`formControlName`) come `number | null` — l'utente vede `'$ 1,234.56'`, il form
50
+ * riceve `1234.56`.
51
+ *
52
+ * In modalità NATURAL la formattazione "live" (durante la digitazione) NON forza il padding a
53
+ * piena precisione: farlo ad ogni tasto arrotonderebbe via le cifre appena digitate (es. digitare
54
+ * "1" su un campo vuoto renderizzerebbe subito "0.00", perdendo la cifra) — il padding/
55
+ * arrotondamento pieno avviene solo al `blur`/`writeValue`. In modalità FINANCIAL questo problema
56
+ * non esiste (il valore si ricalcola sempre da zero dalle sole cifre digitate, insensibile alla
57
+ * formattazione precedente), quindi lì si usa direttamente il formato pieno ad ogni tasto.
58
+ */
59
+ class TmwNumericMaskDirective {
60
+ set options(value) {
61
+ this._options = { ...TMW_NUMERIC_DEFAULT_OPTIONS, ...(value ?? {}) };
62
+ this._applyAlignment();
63
+ this._renderFinal(this._value);
64
+ }
65
+ constructor(_elementRef, _renderer) {
66
+ this._elementRef = _elementRef;
67
+ this._renderer = _renderer;
68
+ this._options = { ...TMW_NUMERIC_DEFAULT_OPTIONS };
69
+ this._value = null;
70
+ this._onChange = () => { };
71
+ this._onTouched = () => { };
72
+ this._disabled = false;
73
+ /**
74
+ * Stato PERSISTENTE (non ri-derivato ogni volta dalla stringa già formattata): true dal
75
+ * momento in cui l'utente ha digitato/incollato un carattere decimale, false quando il campo
76
+ * è vuoto o quel carattere viene cancellato. Necessario perché in formato euro `thousands` e
77
+ * l'alternativa mobile-friendly per `decimal` possono coincidere entrambi su `'.'` — v.
78
+ * `_tokenize` per come risolve l'ambiguità usando questo stato invece di ri-cercare "il primo
79
+ * punto" nella stringa (che potrebbe essere un separatore delle migliaia già inserito da noi).
80
+ */
81
+ this._hasDecimalState = false;
82
+ this._applyAlignment();
83
+ // Senza un hint esplicito, mobile/tablet mostrano la tastiera alfanumerica completa per un
84
+ // <input> senza type/inputmode — sbagliato per un campo numerico. Impostato solo se il
85
+ // consumer non ha già messo il proprio `type`/`inputmode` nel template (es. `type="tel"`,
86
+ // suggerito anche dal README dell'originale `eqp-numeric` per Ionic).
87
+ const el = this._elementRef.nativeElement;
88
+ if (!el.hasAttribute('inputmode') && !el.hasAttribute('type')) {
89
+ this._renderer.setAttribute(el, 'inputmode', 'decimal');
90
+ }
91
+ }
92
+ //#region ControlValueAccessor
93
+ writeValue(value) {
94
+ this._value = this._roundToPrecision(this._clampRange(value == null ? null : Number(value)));
95
+ this._hasDecimalState = false;
96
+ this._renderFinal(this._value);
97
+ }
98
+ registerOnChange(fn) {
99
+ this._onChange = fn;
100
+ }
101
+ registerOnTouched(fn) {
102
+ this._onTouched = fn;
103
+ }
104
+ setDisabledState(isDisabled) {
105
+ this._disabled = isDisabled;
106
+ this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
107
+ }
108
+ //#endregion
109
+ onInput(event) {
110
+ if (this._disabled) {
111
+ return;
112
+ }
113
+ const rawValue = event.target.value;
114
+ const caretFromEnd = this._captureCaretFromEnd();
115
+ if (this._options.inputMode === TmwNumericInputMode.FINANCIAL) {
116
+ this._value = this._clampRange(this._parseFinancial(rawValue));
117
+ this._onChange(this._value);
118
+ this._renderFinal(this._value);
119
+ }
120
+ else {
121
+ const { decimal } = this._options;
122
+ const insertedText = event.data;
123
+ // Il carattere/testo appena digitato o incollato (`event.data`) dice INEQUIVOCABILMENTE se
124
+ // l'utente sta esprimendo l'intento "questo è il separatore decimale" — a differenza di
125
+ // ri-cercare "il primo punto" nell'intera stringa già formattata, che in formato euro
126
+ // (`thousands: '.'`) confonderebbe un punto delle migliaia già inserito da noi con il
127
+ // punto decimale appena digitato (v. commento su `_hasDecimalState`).
128
+ if (insertedText && (insertedText.includes(decimal) || insertedText.includes('.'))) {
129
+ this._hasDecimalState = true;
130
+ }
131
+ if (rawValue.length === 0) {
132
+ this._hasDecimalState = false;
133
+ }
134
+ const { integerDigits, decimalDigits, hasDecimalSeparator, isNegative } = this._tokenize(rawValue, this._hasDecimalState);
135
+ this._hasDecimalState = hasDecimalSeparator;
136
+ this._value = this._clampRange(this._toNumber(integerDigits, decimalDigits, isNegative));
137
+ this._onChange(this._value);
138
+ this._renderLive(integerDigits, decimalDigits, hasDecimalSeparator, isNegative);
139
+ }
140
+ this._restoreCaretFromEnd(caretFromEnd);
141
+ }
142
+ onBlur() {
143
+ this._onTouched();
144
+ this._value = this._roundToPrecision(this._value);
145
+ this._onChange(this._value);
146
+ this._renderFinal(this._value);
147
+ }
148
+ _applyAlignment() {
149
+ this._renderer.setStyle(this._elementRef.nativeElement, 'text-align', this._options.align);
150
+ }
151
+ /**
152
+ * Modalità FINANCIAL: TUTTE le cifre della stringa (incluse quelle già formattate dal render
153
+ * precedente — prefisso/migliaia/decimali) vengono lette come un unico intero in centesimi
154
+ * (o nella più piccola unità di `precision`) e ridivise per `10^precision` — è quello che dà
155
+ * l'effetto "shift da destra" tipico delle casse, insensibile a qualunque formattazione
156
+ * precedente (a differenza di NATURAL non serve un formato "live" separato).
157
+ */
158
+ _parseFinancial(rawValue) {
159
+ const { precision, allowNegative, nullable } = this._options;
160
+ const isNegative = allowNegative && this._hasTypedMinus(rawValue);
161
+ const digitsOnly = rawValue.replace(/[^0-9]/g, '');
162
+ if (digitsOnly.length === 0) {
163
+ return nullable ? null : 0;
164
+ }
165
+ const units = parseInt(digitsOnly, 10);
166
+ const value = units / Math.pow(10, precision);
167
+ return isNegative ? -value : value;
168
+ }
169
+ /**
170
+ * `rawValue.startsWith('-')` non basta: il segno digitato dall'utente finisce SEMPRE dopo un
171
+ * eventuale prefisso (es. `"$ -5"`), mai a inizio stringa assoluto — si cerca quindi il
172
+ * carattere ovunque nella stringa.
173
+ */
174
+ _hasTypedMinus(rawValue) {
175
+ return rawValue.includes('-');
176
+ }
177
+ /**
178
+ * Estrae dalla stringa grezza le sole cifre intere/decimali digitate. Usato SOLO in modalità
179
+ * NATURAL. `hasDecimalHint` (stato persistente, v. `_hasDecimalState`) dice se un separatore
180
+ * decimale è già stato digitato in un keystroke precedente:
181
+ *
182
+ * - `false`: nessun separatore ancora — si scartano TUTTI i caratteri non-cifra (incluso un
183
+ * eventuale punto delle migliaia già presente dal render precedente), tutto ciò che resta è
184
+ * parte intera.
185
+ * - `true`: si cerca l'ULTIMA occorrenza di `decimal` o `'.'` nella stringa — mai la prima. Le
186
+ * migliaia (anche quando coincidono con `'.'`, formato euro) possono comparire SOLO PRIMA del
187
+ * separatore decimale reale (la formattazione non raggruppa mai le cifre decimali), quindi
188
+ * l'ultima occorrenza è sempre quella vera, senza ambiguità. Se non se ne trova più nessuna
189
+ * (l'utente ha cancellato il separatore), si ritorna a "nessun decimale".
190
+ */
191
+ _tokenize(rawValue, hasDecimalHint) {
192
+ const { decimal, allowNegative, precision } = this._options;
193
+ const isNegative = allowNegative && this._hasTypedMinus(rawValue);
194
+ if (!hasDecimalHint) {
195
+ return { integerDigits: rawValue.replace(/[^0-9]/g, ''), decimalDigits: '', hasDecimalSeparator: false, isNegative };
196
+ }
197
+ let splitIndex = -1;
198
+ for (let i = rawValue.length - 1; i >= 0; i--) {
199
+ if (rawValue[i] === decimal || rawValue[i] === '.') {
200
+ splitIndex = i;
201
+ break;
202
+ }
203
+ }
204
+ if (splitIndex === -1) {
205
+ return { integerDigits: rawValue.replace(/[^0-9]/g, ''), decimalDigits: '', hasDecimalSeparator: false, isNegative };
206
+ }
207
+ const integerDigits = rawValue.slice(0, splitIndex).replace(/[^0-9]/g, '');
208
+ const decimalDigits = rawValue.slice(splitIndex + 1).replace(/[^0-9]/g, '').slice(0, precision);
209
+ return { integerDigits, decimalDigits, hasDecimalSeparator: true, isNegative };
210
+ }
211
+ _toNumber(integerDigits, decimalDigits, isNegative) {
212
+ if (integerDigits.length === 0 && decimalDigits.length === 0) {
213
+ return this._options.nullable ? null : 0;
214
+ }
215
+ const value = parseFloat(`${integerDigits || '0'}.${decimalDigits || '0'}`);
216
+ return isNegative ? -value : value;
217
+ }
218
+ /** Applica solo `allowNegative`/`min`/`max` — NON arrotonda a `precision` (v. commento di classe). */
219
+ _clampRange(value) {
220
+ if (value == null) {
221
+ return value;
222
+ }
223
+ const { min, max, allowNegative } = this._options;
224
+ let result = value;
225
+ if (!allowNegative && result < 0) {
226
+ result = -result;
227
+ }
228
+ if (min != null && result < min) {
229
+ result = min;
230
+ }
231
+ if (max != null && result > max) {
232
+ result = max;
233
+ }
234
+ return result;
235
+ }
236
+ _roundToPrecision(value) {
237
+ if (value == null) {
238
+ return value;
239
+ }
240
+ const factor = Math.pow(10, this._options.precision);
241
+ return Math.round(value * factor) / factor;
242
+ }
243
+ /** Formato "live", usato in NATURAL durante la digitazione: nessun padding/arrotondamento. */
244
+ _renderLive(integerDigits, decimalDigits, hasDecimalSeparator, isNegative) {
245
+ const { prefix, suffix, thousands, decimal, precision } = this._options;
246
+ const groupedInteger = integerDigits.replace(/\B(?=(\d{3})+(?!\d))/g, thousands);
247
+ const decimalSegment = precision > 0 && hasDecimalSeparator ? decimal + decimalDigits : '';
248
+ const formatted = `${isNegative ? '-' : ''}${prefix}${groupedInteger}${decimalSegment}${suffix}`;
249
+ this._renderer.setProperty(this._elementRef.nativeElement, 'value', formatted);
250
+ }
251
+ /** Formato pieno (padding a `precision`, arrotondato) — usato a `blur`/`writeValue`/opzioni cambiate. */
252
+ _renderFinal(value) {
253
+ const formatted = value == null ? '' : this._formatFull(value);
254
+ this._renderer.setProperty(this._elementRef.nativeElement, 'value', formatted);
255
+ }
256
+ _formatFull(value) {
257
+ const { prefix, suffix, thousands, decimal, precision } = this._options;
258
+ const isNegative = value < 0;
259
+ const fixed = Math.abs(value).toFixed(precision);
260
+ const [integerPart, decimalPart] = fixed.split('.');
261
+ const groupedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, thousands);
262
+ const decimalSegment = precision > 0 ? decimal + decimalPart : '';
263
+ return `${isNegative ? '-' : ''}${prefix}${groupedInteger}${decimalSegment}${suffix}`;
264
+ }
265
+ _captureCaretFromEnd() {
266
+ const el = this._elementRef.nativeElement;
267
+ const caret = el.selectionStart ?? el.value.length;
268
+ return el.value.length - caret;
269
+ }
270
+ _restoreCaretFromEnd(distanceFromEnd) {
271
+ const el = this._elementRef.nativeElement;
272
+ const position = Math.max(0, el.value.length - distanceFromEnd);
273
+ // Il reformat avviene sincronamente nello stesso ciclo dell'evento `input`: il browser
274
+ // applica la posizione del cursore SUBITO dopo, senza bisogno di un setTimeout/microtask.
275
+ el.setSelectionRange(position, position);
276
+ }
277
+ }
278
+ TmwNumericMaskDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.9", ngImport: i0, type: TmwNumericMaskDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive });
279
+ TmwNumericMaskDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "15.2.9", type: TmwNumericMaskDirective, isStandalone: true, selector: "[tmwNumericMask]", inputs: { options: "options" }, host: { listeners: { "input": "onInput($event)", "blur": "onBlur()" } }, providers: [
280
+ {
281
+ provide: NG_VALUE_ACCESSOR,
282
+ useExisting: forwardRef(() => TmwNumericMaskDirective),
283
+ multi: true,
284
+ },
285
+ ], ngImport: i0 });
286
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.9", ngImport: i0, type: TmwNumericMaskDirective, decorators: [{
287
+ type: Directive,
288
+ args: [{
289
+ selector: '[tmwNumericMask]',
290
+ standalone: true,
291
+ providers: [
292
+ {
293
+ provide: NG_VALUE_ACCESSOR,
294
+ useExisting: forwardRef(() => TmwNumericMaskDirective),
295
+ multi: true,
296
+ },
297
+ ],
298
+ }]
299
+ }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.Renderer2 }]; }, propDecorators: { options: [{
300
+ type: Input,
301
+ args: ['options']
302
+ }], onInput: [{
303
+ type: HostListener,
304
+ args: ['input', ['$event']]
305
+ }], onBlur: [{
306
+ type: HostListener,
307
+ args: ['blur']
308
+ }] } });
309
+
310
+ /*
311
+ * Public API Surface of tmw-numeric
312
+ */
313
+
314
+ /**
315
+ * Generated bundle index. Do not edit.
316
+ */
317
+
318
+ export { TMW_NUMERIC_DEFAULT_OPTIONS, TmwNumericInputMode, TmwNumericMaskDirective };
319
+ //# sourceMappingURL=tmw-numeric.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tmw-numeric.mjs","sources":["../../../projects/tmw-numeric/src/lib/models/tmw-numeric-options.model.ts","../../../projects/tmw-numeric/src/lib/tmw-numeric-mask.directive.ts","../../../projects/tmw-numeric/src/public-api.ts","../../../projects/tmw-numeric/src/tmw-numeric.ts"],"sourcesContent":["/**\n * Modalità di digitazione — nomi/valori/comportamento invariati rispetto a\n * `@eqproject/eqp-numeric` (`EqpNumericInputMode`), per parità di comportamento.\n */\nexport enum TmwNumericInputMode {\n /**\n * I numeri partono dalla precisione più alta (i decimali): digitare uno shifta tutto a\n * sinistra, il carattere decimale digitato viene ignorato. Es. `'12'` → `'0.12'`,\n * `'1234'` → `'12.34'`.\n */\n FINANCIAL = 'FINANCIAL',\n /**\n * I numeri partono a sinistra del separatore decimale, come un campo di testo qualunque.\n * Es. `'1234'` → `'1234'`, `'12.34'` → `'12.34'`.\n */\n NATURAL = 'NATURAL',\n}\n\nexport interface TmwNumericOptions {\n /** Allineamento testo nell'input. Default `'right'`. */\n align?: 'left' | 'right';\n /** Se `true` consente valori negativi. Default `true`. */\n allowNegative?: boolean;\n /** Separatore dei decimali. Default `'.'`. */\n decimal?: string;\n /** Numero di cifre decimali. Default `2`. */\n precision?: number;\n /** Prefisso (es. simbolo valuta). Default `'$ '`. */\n prefix?: string;\n /** Suffisso. Default `''`. */\n suffix?: string;\n /** Separatore delle migliaia. Default `','`. */\n thousands?: string;\n /** Se `true`, un campo vuoto produce `null` invece di `0`. Default `false`. */\n nullable?: boolean;\n /** Valore minimo consentito. Default `undefined` (nessun limite). */\n min?: number;\n /** Valore massimo consentito. Default `undefined` (nessun limite). */\n max?: number;\n /** Modalità di digitazione. Default `NATURAL`. */\n inputMode?: TmwNumericInputMode;\n}\n\n/**\n * Default identici a `@eqproject/eqp-numeric` — nessun consumer TMED chiamava\n * `EqpNumericModule.forRoot(...)` né passava `[options]`, quindi questi default SONO il\n * comportamento reale in produzione (v. AI_BUILD_PLAN.md per i 5 punti d'uso reali).\n */\nexport const TMW_NUMERIC_DEFAULT_OPTIONS: Required<TmwNumericOptions> = {\n align: 'right',\n allowNegative: true,\n decimal: '.',\n precision: 2,\n prefix: '$ ',\n suffix: '',\n thousands: ',',\n nullable: false,\n min: undefined as unknown as number,\n max: undefined as unknown as number,\n inputMode: TmwNumericInputMode.NATURAL,\n};\n","import { Directive, ElementRef, HostListener, Input, Renderer2, forwardRef } from '@angular/core';\nimport { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';\nimport { TMW_NUMERIC_DEFAULT_OPTIONS, TmwNumericInputMode, TmwNumericOptions } from './models/tmw-numeric-options.model';\n\n/**\n * Sostituto di `[eqpNumericMask]` (`@eqproject/eqp-numeric`) — stesso `[options]`, stessi nomi\n * di opzione, stessi default (v. `TMW_NUMERIC_DEFAULT_OPTIONS`). Nessun `forRoot()`: nessun\n * consumer reale in TMED lo usava, i default del pacchetto originale erano già il comportamento\n * effettivo in produzione.\n *\n * Formatta un `<input>` come numero mascherato (valuta/decimale) mantenendo il model\n * (`ngModel`/`formControlName`) come `number | null` — l'utente vede `'$ 1,234.56'`, il form\n * riceve `1234.56`.\n *\n * In modalità NATURAL la formattazione \"live\" (durante la digitazione) NON forza il padding a\n * piena precisione: farlo ad ogni tasto arrotonderebbe via le cifre appena digitate (es. digitare\n * \"1\" su un campo vuoto renderizzerebbe subito \"0.00\", perdendo la cifra) — il padding/\n * arrotondamento pieno avviene solo al `blur`/`writeValue`. In modalità FINANCIAL questo problema\n * non esiste (il valore si ricalcola sempre da zero dalle sole cifre digitate, insensibile alla\n * formattazione precedente), quindi lì si usa direttamente il formato pieno ad ogni tasto.\n */\n@Directive({\n selector: '[tmwNumericMask]',\n standalone: true,\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => TmwNumericMaskDirective),\n multi: true,\n },\n ],\n})\nexport class TmwNumericMaskDirective implements ControlValueAccessor {\n private _options: Required<TmwNumericOptions> = { ...TMW_NUMERIC_DEFAULT_OPTIONS };\n\n @Input('options')\n set options(value: TmwNumericOptions | undefined) {\n this._options = { ...TMW_NUMERIC_DEFAULT_OPTIONS, ...(value ?? {}) };\n this._applyAlignment();\n this._renderFinal(this._value);\n }\n\n private _value: number | null = null;\n private _onChange: (value: number | null) => void = () => {};\n private _onTouched: () => void = () => {};\n private _disabled = false;\n /**\n * Stato PERSISTENTE (non ri-derivato ogni volta dalla stringa già formattata): true dal\n * momento in cui l'utente ha digitato/incollato un carattere decimale, false quando il campo\n * è vuoto o quel carattere viene cancellato. Necessario perché in formato euro `thousands` e\n * l'alternativa mobile-friendly per `decimal` possono coincidere entrambi su `'.'` — v.\n * `_tokenize` per come risolve l'ambiguità usando questo stato invece di ri-cercare \"il primo\n * punto\" nella stringa (che potrebbe essere un separatore delle migliaia già inserito da noi).\n */\n private _hasDecimalState = false;\n\n constructor(private _elementRef: ElementRef<HTMLInputElement>, private _renderer: Renderer2) {\n this._applyAlignment();\n // Senza un hint esplicito, mobile/tablet mostrano la tastiera alfanumerica completa per un\n // <input> senza type/inputmode — sbagliato per un campo numerico. Impostato solo se il\n // consumer non ha già messo il proprio `type`/`inputmode` nel template (es. `type=\"tel\"`,\n // suggerito anche dal README dell'originale `eqp-numeric` per Ionic).\n const el = this._elementRef.nativeElement;\n if (!el.hasAttribute('inputmode') && !el.hasAttribute('type')) {\n this._renderer.setAttribute(el, 'inputmode', 'decimal');\n }\n }\n\n //#region ControlValueAccessor\n\n writeValue(value: number | null): void {\n this._value = this._roundToPrecision(this._clampRange(value == null ? null : Number(value)));\n this._hasDecimalState = false;\n this._renderFinal(this._value);\n }\n\n registerOnChange(fn: (value: number | null) => void): void {\n this._onChange = fn;\n }\n\n registerOnTouched(fn: () => void): void {\n this._onTouched = fn;\n }\n\n setDisabledState(isDisabled: boolean): void {\n this._disabled = isDisabled;\n this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled);\n }\n\n //#endregion\n\n @HostListener('input', ['$event'])\n onInput(event: InputEvent): void {\n if (this._disabled) {\n return;\n }\n const rawValue = (event.target as HTMLInputElement).value;\n const caretFromEnd = this._captureCaretFromEnd();\n\n if (this._options.inputMode === TmwNumericInputMode.FINANCIAL) {\n this._value = this._clampRange(this._parseFinancial(rawValue));\n this._onChange(this._value);\n this._renderFinal(this._value);\n } else {\n const { decimal } = this._options;\n const insertedText = event.data;\n // Il carattere/testo appena digitato o incollato (`event.data`) dice INEQUIVOCABILMENTE se\n // l'utente sta esprimendo l'intento \"questo è il separatore decimale\" — a differenza di\n // ri-cercare \"il primo punto\" nell'intera stringa già formattata, che in formato euro\n // (`thousands: '.'`) confonderebbe un punto delle migliaia già inserito da noi con il\n // punto decimale appena digitato (v. commento su `_hasDecimalState`).\n if (insertedText && (insertedText.includes(decimal) || insertedText.includes('.'))) {\n this._hasDecimalState = true;\n }\n if (rawValue.length === 0) {\n this._hasDecimalState = false;\n }\n const { integerDigits, decimalDigits, hasDecimalSeparator, isNegative } = this._tokenize(rawValue, this._hasDecimalState);\n this._hasDecimalState = hasDecimalSeparator;\n this._value = this._clampRange(this._toNumber(integerDigits, decimalDigits, isNegative));\n this._onChange(this._value);\n this._renderLive(integerDigits, decimalDigits, hasDecimalSeparator, isNegative);\n }\n this._restoreCaretFromEnd(caretFromEnd);\n }\n\n @HostListener('blur')\n onBlur(): void {\n this._onTouched();\n this._value = this._roundToPrecision(this._value);\n this._onChange(this._value);\n this._renderFinal(this._value);\n }\n\n private _applyAlignment(): void {\n this._renderer.setStyle(this._elementRef.nativeElement, 'text-align', this._options.align);\n }\n\n /**\n * Modalità FINANCIAL: TUTTE le cifre della stringa (incluse quelle già formattate dal render\n * precedente — prefisso/migliaia/decimali) vengono lette come un unico intero in centesimi\n * (o nella più piccola unità di `precision`) e ridivise per `10^precision` — è quello che dà\n * l'effetto \"shift da destra\" tipico delle casse, insensibile a qualunque formattazione\n * precedente (a differenza di NATURAL non serve un formato \"live\" separato).\n */\n private _parseFinancial(rawValue: string): number | null {\n const { precision, allowNegative, nullable } = this._options;\n const isNegative = allowNegative && this._hasTypedMinus(rawValue);\n const digitsOnly = rawValue.replace(/[^0-9]/g, '');\n if (digitsOnly.length === 0) {\n return nullable ? null : 0;\n }\n const units = parseInt(digitsOnly, 10);\n const value = units / Math.pow(10, precision);\n return isNegative ? -value : value;\n }\n\n /**\n * `rawValue.startsWith('-')` non basta: il segno digitato dall'utente finisce SEMPRE dopo un\n * eventuale prefisso (es. `\"$ -5\"`), mai a inizio stringa assoluto — si cerca quindi il\n * carattere ovunque nella stringa.\n */\n private _hasTypedMinus(rawValue: string): boolean {\n return rawValue.includes('-');\n }\n\n /**\n * Estrae dalla stringa grezza le sole cifre intere/decimali digitate. Usato SOLO in modalità\n * NATURAL. `hasDecimalHint` (stato persistente, v. `_hasDecimalState`) dice se un separatore\n * decimale è già stato digitato in un keystroke precedente:\n *\n * - `false`: nessun separatore ancora — si scartano TUTTI i caratteri non-cifra (incluso un\n * eventuale punto delle migliaia già presente dal render precedente), tutto ciò che resta è\n * parte intera.\n * - `true`: si cerca l'ULTIMA occorrenza di `decimal` o `'.'` nella stringa — mai la prima. Le\n * migliaia (anche quando coincidono con `'.'`, formato euro) possono comparire SOLO PRIMA del\n * separatore decimale reale (la formattazione non raggruppa mai le cifre decimali), quindi\n * l'ultima occorrenza è sempre quella vera, senza ambiguità. Se non se ne trova più nessuna\n * (l'utente ha cancellato il separatore), si ritorna a \"nessun decimale\".\n */\n private _tokenize(rawValue: string, hasDecimalHint: boolean): {\n integerDigits: string;\n decimalDigits: string;\n hasDecimalSeparator: boolean;\n isNegative: boolean;\n } {\n const { decimal, allowNegative, precision } = this._options;\n const isNegative = allowNegative && this._hasTypedMinus(rawValue);\n\n if (!hasDecimalHint) {\n return { integerDigits: rawValue.replace(/[^0-9]/g, ''), decimalDigits: '', hasDecimalSeparator: false, isNegative };\n }\n\n let splitIndex = -1;\n for (let i = rawValue.length - 1; i >= 0; i--) {\n if (rawValue[i] === decimal || rawValue[i] === '.') {\n splitIndex = i;\n break;\n }\n }\n if (splitIndex === -1) {\n return { integerDigits: rawValue.replace(/[^0-9]/g, ''), decimalDigits: '', hasDecimalSeparator: false, isNegative };\n }\n const integerDigits = rawValue.slice(0, splitIndex).replace(/[^0-9]/g, '');\n const decimalDigits = rawValue.slice(splitIndex + 1).replace(/[^0-9]/g, '').slice(0, precision);\n return { integerDigits, decimalDigits, hasDecimalSeparator: true, isNegative };\n }\n\n private _toNumber(integerDigits: string, decimalDigits: string, isNegative: boolean): number | null {\n if (integerDigits.length === 0 && decimalDigits.length === 0) {\n return this._options.nullable ? null : 0;\n }\n const value = parseFloat(`${integerDigits || '0'}.${decimalDigits || '0'}`);\n return isNegative ? -value : value;\n }\n\n /** Applica solo `allowNegative`/`min`/`max` — NON arrotonda a `precision` (v. commento di classe). */\n private _clampRange(value: number | null): number | null {\n if (value == null) {\n return value;\n }\n const { min, max, allowNegative } = this._options;\n let result = value;\n if (!allowNegative && result < 0) {\n result = -result;\n }\n if (min != null && result < min) {\n result = min;\n }\n if (max != null && result > max) {\n result = max;\n }\n return result;\n }\n\n private _roundToPrecision(value: number | null): number | null {\n if (value == null) {\n return value;\n }\n const factor = Math.pow(10, this._options.precision);\n return Math.round(value * factor) / factor;\n }\n\n /** Formato \"live\", usato in NATURAL durante la digitazione: nessun padding/arrotondamento. */\n private _renderLive(integerDigits: string, decimalDigits: string, hasDecimalSeparator: boolean, isNegative: boolean): void {\n const { prefix, suffix, thousands, decimal, precision } = this._options;\n const groupedInteger = integerDigits.replace(/\\B(?=(\\d{3})+(?!\\d))/g, thousands);\n const decimalSegment = precision > 0 && hasDecimalSeparator ? decimal + decimalDigits : '';\n const formatted = `${isNegative ? '-' : ''}${prefix}${groupedInteger}${decimalSegment}${suffix}`;\n this._renderer.setProperty(this._elementRef.nativeElement, 'value', formatted);\n }\n\n /** Formato pieno (padding a `precision`, arrotondato) — usato a `blur`/`writeValue`/opzioni cambiate. */\n private _renderFinal(value: number | null): void {\n const formatted = value == null ? '' : this._formatFull(value);\n this._renderer.setProperty(this._elementRef.nativeElement, 'value', formatted);\n }\n\n private _formatFull(value: number): string {\n const { prefix, suffix, thousands, decimal, precision } = this._options;\n const isNegative = value < 0;\n const fixed = Math.abs(value).toFixed(precision);\n const [integerPart, decimalPart] = fixed.split('.');\n const groupedInteger = integerPart.replace(/\\B(?=(\\d{3})+(?!\\d))/g, thousands);\n const decimalSegment = precision > 0 ? decimal + decimalPart : '';\n return `${isNegative ? '-' : ''}${prefix}${groupedInteger}${decimalSegment}${suffix}`;\n }\n\n private _captureCaretFromEnd(): number {\n const el = this._elementRef.nativeElement;\n const caret = el.selectionStart ?? el.value.length;\n return el.value.length - caret;\n }\n\n private _restoreCaretFromEnd(distanceFromEnd: number): void {\n const el = this._elementRef.nativeElement;\n const position = Math.max(0, el.value.length - distanceFromEnd);\n // Il reformat avviene sincronamente nello stesso ciclo dell'evento `input`: il browser\n // applica la posizione del cursore SUBITO dopo, senza bisogno di un setTimeout/microtask.\n el.setSelectionRange(position, position);\n }\n}\n","/*\n * Public API Surface of tmw-numeric\n */\n\nexport * from './lib/tmw-numeric-mask.directive';\nexport * from './lib/models/tmw-numeric-options.model';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
package/index.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Generated bundle index. Do not edit.
3
+ */
4
+ /// <amd-module name="tmw-numeric" />
5
+ export * from './public-api';
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Modalità di digitazione — nomi/valori/comportamento invariati rispetto a
3
+ * `@eqproject/eqp-numeric` (`EqpNumericInputMode`), per parità di comportamento.
4
+ */
5
+ export declare enum TmwNumericInputMode {
6
+ /**
7
+ * I numeri partono dalla precisione più alta (i decimali): digitare uno shifta tutto a
8
+ * sinistra, il carattere decimale digitato viene ignorato. Es. `'12'` → `'0.12'`,
9
+ * `'1234'` → `'12.34'`.
10
+ */
11
+ FINANCIAL = "FINANCIAL",
12
+ /**
13
+ * I numeri partono a sinistra del separatore decimale, come un campo di testo qualunque.
14
+ * Es. `'1234'` → `'1234'`, `'12.34'` → `'12.34'`.
15
+ */
16
+ NATURAL = "NATURAL"
17
+ }
18
+ export interface TmwNumericOptions {
19
+ /** Allineamento testo nell'input. Default `'right'`. */
20
+ align?: 'left' | 'right';
21
+ /** Se `true` consente valori negativi. Default `true`. */
22
+ allowNegative?: boolean;
23
+ /** Separatore dei decimali. Default `'.'`. */
24
+ decimal?: string;
25
+ /** Numero di cifre decimali. Default `2`. */
26
+ precision?: number;
27
+ /** Prefisso (es. simbolo valuta). Default `'$ '`. */
28
+ prefix?: string;
29
+ /** Suffisso. Default `''`. */
30
+ suffix?: string;
31
+ /** Separatore delle migliaia. Default `','`. */
32
+ thousands?: string;
33
+ /** Se `true`, un campo vuoto produce `null` invece di `0`. Default `false`. */
34
+ nullable?: boolean;
35
+ /** Valore minimo consentito. Default `undefined` (nessun limite). */
36
+ min?: number;
37
+ /** Valore massimo consentito. Default `undefined` (nessun limite). */
38
+ max?: number;
39
+ /** Modalità di digitazione. Default `NATURAL`. */
40
+ inputMode?: TmwNumericInputMode;
41
+ }
42
+ /**
43
+ * Default identici a `@eqproject/eqp-numeric` — nessun consumer TMED chiamava
44
+ * `EqpNumericModule.forRoot(...)` né passava `[options]`, quindi questi default SONO il
45
+ * comportamento reale in produzione (v. AI_BUILD_PLAN.md per i 5 punti d'uso reali).
46
+ */
47
+ export declare const TMW_NUMERIC_DEFAULT_OPTIONS: Required<TmwNumericOptions>;
@@ -0,0 +1,90 @@
1
+ import { ElementRef, Renderer2 } from '@angular/core';
2
+ import { ControlValueAccessor } from '@angular/forms';
3
+ import { TmwNumericOptions } from './models/tmw-numeric-options.model';
4
+ import * as i0 from "@angular/core";
5
+ /**
6
+ * Sostituto di `[eqpNumericMask]` (`@eqproject/eqp-numeric`) — stesso `[options]`, stessi nomi
7
+ * di opzione, stessi default (v. `TMW_NUMERIC_DEFAULT_OPTIONS`). Nessun `forRoot()`: nessun
8
+ * consumer reale in TMED lo usava, i default del pacchetto originale erano già il comportamento
9
+ * effettivo in produzione.
10
+ *
11
+ * Formatta un `<input>` come numero mascherato (valuta/decimale) mantenendo il model
12
+ * (`ngModel`/`formControlName`) come `number | null` — l'utente vede `'$ 1,234.56'`, il form
13
+ * riceve `1234.56`.
14
+ *
15
+ * In modalità NATURAL la formattazione "live" (durante la digitazione) NON forza il padding a
16
+ * piena precisione: farlo ad ogni tasto arrotonderebbe via le cifre appena digitate (es. digitare
17
+ * "1" su un campo vuoto renderizzerebbe subito "0.00", perdendo la cifra) — il padding/
18
+ * arrotondamento pieno avviene solo al `blur`/`writeValue`. In modalità FINANCIAL questo problema
19
+ * non esiste (il valore si ricalcola sempre da zero dalle sole cifre digitate, insensibile alla
20
+ * formattazione precedente), quindi lì si usa direttamente il formato pieno ad ogni tasto.
21
+ */
22
+ export declare class TmwNumericMaskDirective implements ControlValueAccessor {
23
+ private _elementRef;
24
+ private _renderer;
25
+ private _options;
26
+ set options(value: TmwNumericOptions | undefined);
27
+ private _value;
28
+ private _onChange;
29
+ private _onTouched;
30
+ private _disabled;
31
+ /**
32
+ * Stato PERSISTENTE (non ri-derivato ogni volta dalla stringa già formattata): true dal
33
+ * momento in cui l'utente ha digitato/incollato un carattere decimale, false quando il campo
34
+ * è vuoto o quel carattere viene cancellato. Necessario perché in formato euro `thousands` e
35
+ * l'alternativa mobile-friendly per `decimal` possono coincidere entrambi su `'.'` — v.
36
+ * `_tokenize` per come risolve l'ambiguità usando questo stato invece di ri-cercare "il primo
37
+ * punto" nella stringa (che potrebbe essere un separatore delle migliaia già inserito da noi).
38
+ */
39
+ private _hasDecimalState;
40
+ constructor(_elementRef: ElementRef<HTMLInputElement>, _renderer: Renderer2);
41
+ writeValue(value: number | null): void;
42
+ registerOnChange(fn: (value: number | null) => void): void;
43
+ registerOnTouched(fn: () => void): void;
44
+ setDisabledState(isDisabled: boolean): void;
45
+ onInput(event: InputEvent): void;
46
+ onBlur(): void;
47
+ private _applyAlignment;
48
+ /**
49
+ * Modalità FINANCIAL: TUTTE le cifre della stringa (incluse quelle già formattate dal render
50
+ * precedente — prefisso/migliaia/decimali) vengono lette come un unico intero in centesimi
51
+ * (o nella più piccola unità di `precision`) e ridivise per `10^precision` — è quello che dà
52
+ * l'effetto "shift da destra" tipico delle casse, insensibile a qualunque formattazione
53
+ * precedente (a differenza di NATURAL non serve un formato "live" separato).
54
+ */
55
+ private _parseFinancial;
56
+ /**
57
+ * `rawValue.startsWith('-')` non basta: il segno digitato dall'utente finisce SEMPRE dopo un
58
+ * eventuale prefisso (es. `"$ -5"`), mai a inizio stringa assoluto — si cerca quindi il
59
+ * carattere ovunque nella stringa.
60
+ */
61
+ private _hasTypedMinus;
62
+ /**
63
+ * Estrae dalla stringa grezza le sole cifre intere/decimali digitate. Usato SOLO in modalità
64
+ * NATURAL. `hasDecimalHint` (stato persistente, v. `_hasDecimalState`) dice se un separatore
65
+ * decimale è già stato digitato in un keystroke precedente:
66
+ *
67
+ * - `false`: nessun separatore ancora — si scartano TUTTI i caratteri non-cifra (incluso un
68
+ * eventuale punto delle migliaia già presente dal render precedente), tutto ciò che resta è
69
+ * parte intera.
70
+ * - `true`: si cerca l'ULTIMA occorrenza di `decimal` o `'.'` nella stringa — mai la prima. Le
71
+ * migliaia (anche quando coincidono con `'.'`, formato euro) possono comparire SOLO PRIMA del
72
+ * separatore decimale reale (la formattazione non raggruppa mai le cifre decimali), quindi
73
+ * l'ultima occorrenza è sempre quella vera, senza ambiguità. Se non se ne trova più nessuna
74
+ * (l'utente ha cancellato il separatore), si ritorna a "nessun decimale".
75
+ */
76
+ private _tokenize;
77
+ private _toNumber;
78
+ /** Applica solo `allowNegative`/`min`/`max` — NON arrotonda a `precision` (v. commento di classe). */
79
+ private _clampRange;
80
+ private _roundToPrecision;
81
+ /** Formato "live", usato in NATURAL durante la digitazione: nessun padding/arrotondamento. */
82
+ private _renderLive;
83
+ /** Formato pieno (padding a `precision`, arrotondato) — usato a `blur`/`writeValue`/opzioni cambiate. */
84
+ private _renderFinal;
85
+ private _formatFull;
86
+ private _captureCaretFromEnd;
87
+ private _restoreCaretFromEnd;
88
+ static ɵfac: i0.ɵɵFactoryDeclaration<TmwNumericMaskDirective, never>;
89
+ static ɵdir: i0.ɵɵDirectiveDeclaration<TmwNumericMaskDirective, "[tmwNumericMask]", never, { "options": "options"; }, {}, never, never, true, never>;
90
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "tmw-numeric",
3
+ "version": "0.1.1",
4
+ "description": "Masked numeric/currency input directive for Angular — no Material, no CDK, standalone, form-ready",
5
+ "author": {
6
+ "name": "EqProject"
7
+ },
8
+ "peerDependencies": {
9
+ "@angular/common": "^15.2.0",
10
+ "@angular/core": "^15.2.0",
11
+ "@angular/forms": "^15.2.0"
12
+ },
13
+ "dependencies": {
14
+ "tslib": "^2.3.0"
15
+ },
16
+ "sideEffects": false,
17
+ "module": "fesm2015/tmw-numeric.mjs",
18
+ "es2020": "fesm2020/tmw-numeric.mjs",
19
+ "esm2020": "esm2020/tmw-numeric.mjs",
20
+ "fesm2020": "fesm2020/tmw-numeric.mjs",
21
+ "fesm2015": "fesm2015/tmw-numeric.mjs",
22
+ "typings": "index.d.ts",
23
+ "exports": {
24
+ "./package.json": {
25
+ "default": "./package.json"
26
+ },
27
+ ".": {
28
+ "types": "./index.d.ts",
29
+ "esm2020": "./esm2020/tmw-numeric.mjs",
30
+ "es2020": "./fesm2020/tmw-numeric.mjs",
31
+ "es2015": "./fesm2015/tmw-numeric.mjs",
32
+ "node": "./fesm2015/tmw-numeric.mjs",
33
+ "default": "./fesm2020/tmw-numeric.mjs"
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,2 @@
1
+ export * from './lib/tmw-numeric-mask.directive';
2
+ export * from './lib/models/tmw-numeric-options.model';