ng-primitives 0.111.0 → 0.112.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/fesm2022/ng-primitives-number-field.mjs +660 -0
- package/fesm2022/ng-primitives-number-field.mjs.map +1 -0
- package/fesm2022/ng-primitives-portal.mjs +44 -2
- package/fesm2022/ng-primitives-portal.mjs.map +1 -1
- package/number-field/README.md +3 -0
- package/number-field/index.d.ts +329 -0
- package/package.json +5 -1
- package/portal/index.d.ts +17 -0
- package/schematics/ng-generate/templates/number-field/number-field.__fileSuffix@dasherize__.ts.template +109 -0
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { signal, computed, input, numberAttribute, output, booleanAttribute, Directive, inject, Injector } from '@angular/core';
|
|
3
|
+
import { uniqueId, injectDisposables } from 'ng-primitives/utils';
|
|
4
|
+
import { injectElementRef, explicitEffect } from 'ng-primitives/internal';
|
|
5
|
+
import { createPrimitive, controlled, emitter, attrBinding, dataBinding, deprecatedSetter, listener } from 'ng-primitives/state';
|
|
6
|
+
import { DOCUMENT } from '@angular/common';
|
|
7
|
+
import { ngpFormControl } from 'ng-primitives/form-field';
|
|
8
|
+
import { ngpInteractions } from 'ng-primitives/interactions';
|
|
9
|
+
|
|
10
|
+
const [NgpNumberFieldStateToken, ngpNumberField, injectNumberFieldState, provideNumberFieldState,] = createPrimitive('NgpNumberField', ({ id = signal(uniqueId('ngp-number-field')), value: _value = signal(null), min = signal(-Infinity), max = signal(Infinity), step = signal(1), largeStep: _largeStep = signal(10), disabled: _disabled = signal(false), readonly: _readonly = signal(false), onValueChange, }) => {
|
|
11
|
+
const element = injectElementRef();
|
|
12
|
+
const value = controlled(_value);
|
|
13
|
+
const disabled = controlled(_disabled);
|
|
14
|
+
const readonly = controlled(_readonly);
|
|
15
|
+
const valueChange = emitter();
|
|
16
|
+
const canIncrement = computed(() => {
|
|
17
|
+
if (disabled() || readonly())
|
|
18
|
+
return false;
|
|
19
|
+
if (value() === null)
|
|
20
|
+
return true;
|
|
21
|
+
return value() < max();
|
|
22
|
+
}, ...(ngDevMode ? [{ debugName: "canIncrement" }] : []));
|
|
23
|
+
const canDecrement = computed(() => {
|
|
24
|
+
if (disabled() || readonly())
|
|
25
|
+
return false;
|
|
26
|
+
if (value() === null)
|
|
27
|
+
return true;
|
|
28
|
+
return value() > min();
|
|
29
|
+
}, ...(ngDevMode ? [{ debugName: "canDecrement" }] : []));
|
|
30
|
+
// Host bindings
|
|
31
|
+
attrBinding(element, 'role', () => 'group');
|
|
32
|
+
dataBinding(element, 'data-disabled', disabled);
|
|
33
|
+
dataBinding(element, 'data-readonly', readonly);
|
|
34
|
+
/**
|
|
35
|
+
* Count the number of decimal places in a number.
|
|
36
|
+
*/
|
|
37
|
+
function getDecimalPlaces(n) {
|
|
38
|
+
const str = String(n);
|
|
39
|
+
const dotIndex = str.indexOf('.');
|
|
40
|
+
return dotIndex === -1 ? 0 : str.length - dotIndex - 1;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Round a number to a specific number of decimal places to avoid
|
|
44
|
+
* floating point precision issues (e.g. 0.1 + 0.2 = 0.30000000000000004).
|
|
45
|
+
*/
|
|
46
|
+
function roundToPrecision(val, precision) {
|
|
47
|
+
if (precision === 0)
|
|
48
|
+
return Math.round(val);
|
|
49
|
+
return parseFloat(val.toFixed(precision));
|
|
50
|
+
}
|
|
51
|
+
function clampAndStep(val) {
|
|
52
|
+
const clamped = Math.min(max(), Math.max(min(), val));
|
|
53
|
+
// Round to nearest step
|
|
54
|
+
if (isFinite(step()) && step() > 0) {
|
|
55
|
+
const base = isFinite(min()) ? min() : 0;
|
|
56
|
+
const precision = Math.max(getDecimalPlaces(step()), getDecimalPlaces(base));
|
|
57
|
+
const stepped = roundToPrecision(Math.round((clamped - base) / step()) * step() + base, precision);
|
|
58
|
+
return Math.min(max(), Math.max(min(), stepped));
|
|
59
|
+
}
|
|
60
|
+
return clamped;
|
|
61
|
+
}
|
|
62
|
+
let suppressEmit = false;
|
|
63
|
+
function setValue(newValue) {
|
|
64
|
+
if (disabled() || readonly())
|
|
65
|
+
return;
|
|
66
|
+
if (newValue !== null && isNaN(newValue))
|
|
67
|
+
return;
|
|
68
|
+
const finalValue = newValue !== null ? clampAndStep(newValue) : null;
|
|
69
|
+
// Skip emit when value is unchanged
|
|
70
|
+
if (finalValue === value())
|
|
71
|
+
return;
|
|
72
|
+
value.set(finalValue);
|
|
73
|
+
if (!suppressEmit) {
|
|
74
|
+
onValueChange?.(finalValue);
|
|
75
|
+
valueChange.emit(finalValue);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
let inputCommitFn = null;
|
|
79
|
+
function registerInputCommit(commitFn) {
|
|
80
|
+
inputCommitFn = commitFn;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Commit any pending input value without emitting change events.
|
|
84
|
+
* This ensures increment/decrement operates on the displayed value
|
|
85
|
+
* while only emitting the final stepped result.
|
|
86
|
+
*/
|
|
87
|
+
function commitPendingInputSilently() {
|
|
88
|
+
if (!inputCommitFn)
|
|
89
|
+
return;
|
|
90
|
+
suppressEmit = true;
|
|
91
|
+
try {
|
|
92
|
+
inputCommitFn();
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
suppressEmit = false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function getStepPrecision() {
|
|
99
|
+
const base = isFinite(min()) ? min() : 0;
|
|
100
|
+
return Math.max(getDecimalPlaces(step()), getDecimalPlaces(base));
|
|
101
|
+
}
|
|
102
|
+
function increment(multiplier = 1) {
|
|
103
|
+
if (!canIncrement())
|
|
104
|
+
return;
|
|
105
|
+
const valueBefore = value();
|
|
106
|
+
commitPendingInputSilently();
|
|
107
|
+
const valueAfterCommit = value();
|
|
108
|
+
const current = valueAfterCommit ?? (isFinite(min()) ? min() : 0);
|
|
109
|
+
const precision = getStepPrecision();
|
|
110
|
+
setValue(roundToPrecision(current + step() * multiplier, precision));
|
|
111
|
+
// If the silent commit changed the value but setValue was a no-op
|
|
112
|
+
// (stepped result clamped back to the committed value), emit the change
|
|
113
|
+
// so the parent learns about the new value.
|
|
114
|
+
if (valueBefore !== value() && valueAfterCommit === value()) {
|
|
115
|
+
onValueChange?.(value());
|
|
116
|
+
valueChange.emit(value());
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function decrement(multiplier = 1) {
|
|
120
|
+
if (!canDecrement())
|
|
121
|
+
return;
|
|
122
|
+
const valueBefore = value();
|
|
123
|
+
commitPendingInputSilently();
|
|
124
|
+
const valueAfterCommit = value();
|
|
125
|
+
const current = valueAfterCommit ?? (isFinite(max()) ? max() : 0);
|
|
126
|
+
const precision = getStepPrecision();
|
|
127
|
+
setValue(roundToPrecision(current - step() * multiplier, precision));
|
|
128
|
+
// If the silent commit changed the value but setValue was a no-op
|
|
129
|
+
// (stepped result clamped back to the committed value), emit the change
|
|
130
|
+
// so the parent learns about the new value.
|
|
131
|
+
if (valueBefore !== value() && valueAfterCommit === value()) {
|
|
132
|
+
onValueChange?.(value());
|
|
133
|
+
valueChange.emit(value());
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function setDisabled(isDisabled) {
|
|
137
|
+
disabled.set(isDisabled);
|
|
138
|
+
}
|
|
139
|
+
function setReadonly(isReadonly) {
|
|
140
|
+
readonly.set(isReadonly);
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
id,
|
|
144
|
+
value,
|
|
145
|
+
min,
|
|
146
|
+
max,
|
|
147
|
+
step,
|
|
148
|
+
largeStep: _largeStep,
|
|
149
|
+
disabled: deprecatedSetter(disabled, 'setDisabled'),
|
|
150
|
+
readonly: deprecatedSetter(readonly, 'setReadonly'),
|
|
151
|
+
canIncrement,
|
|
152
|
+
canDecrement,
|
|
153
|
+
valueChange: valueChange.asObservable(),
|
|
154
|
+
setValue,
|
|
155
|
+
increment,
|
|
156
|
+
decrement,
|
|
157
|
+
setDisabled,
|
|
158
|
+
setReadonly,
|
|
159
|
+
registerInputCommit,
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Apply the `ngpNumberField` directive to an element that represents the number field
|
|
165
|
+
* and contains the input, increment, and decrement buttons.
|
|
166
|
+
*/
|
|
167
|
+
class NgpNumberField {
|
|
168
|
+
constructor() {
|
|
169
|
+
/**
|
|
170
|
+
* The id of the number field. If not provided, a unique id will be generated.
|
|
171
|
+
*/
|
|
172
|
+
this.id = input(uniqueId('ngp-number-field'), ...(ngDevMode ? [{ debugName: "id" }] : []));
|
|
173
|
+
/**
|
|
174
|
+
* The value of the number field.
|
|
175
|
+
*/
|
|
176
|
+
this.value = input(null, ...(ngDevMode ? [{ debugName: "value", alias: 'ngpNumberFieldValue',
|
|
177
|
+
transform: (v) => v === null || v === undefined || v === '' ? null : numberAttribute(v) }] : [{
|
|
178
|
+
alias: 'ngpNumberFieldValue',
|
|
179
|
+
transform: (v) => v === null || v === undefined || v === '' ? null : numberAttribute(v),
|
|
180
|
+
}]));
|
|
181
|
+
/**
|
|
182
|
+
* Emits when the value changes.
|
|
183
|
+
*/
|
|
184
|
+
this.valueChange = output({
|
|
185
|
+
alias: 'ngpNumberFieldValueChange',
|
|
186
|
+
});
|
|
187
|
+
/**
|
|
188
|
+
* The minimum value.
|
|
189
|
+
*/
|
|
190
|
+
this.min = input(-Infinity, ...(ngDevMode ? [{ debugName: "min", alias: 'ngpNumberFieldMin',
|
|
191
|
+
transform: numberAttribute }] : [{
|
|
192
|
+
alias: 'ngpNumberFieldMin',
|
|
193
|
+
transform: numberAttribute,
|
|
194
|
+
}]));
|
|
195
|
+
/**
|
|
196
|
+
* The maximum value.
|
|
197
|
+
*/
|
|
198
|
+
this.max = input(Infinity, ...(ngDevMode ? [{ debugName: "max", alias: 'ngpNumberFieldMax',
|
|
199
|
+
transform: numberAttribute }] : [{
|
|
200
|
+
alias: 'ngpNumberFieldMax',
|
|
201
|
+
transform: numberAttribute,
|
|
202
|
+
}]));
|
|
203
|
+
/**
|
|
204
|
+
* The step value.
|
|
205
|
+
*/
|
|
206
|
+
this.step = input(1, ...(ngDevMode ? [{ debugName: "step", alias: 'ngpNumberFieldStep',
|
|
207
|
+
transform: numberAttribute }] : [{
|
|
208
|
+
alias: 'ngpNumberFieldStep',
|
|
209
|
+
transform: numberAttribute,
|
|
210
|
+
}]));
|
|
211
|
+
/**
|
|
212
|
+
* The large step value (used with Shift key).
|
|
213
|
+
*/
|
|
214
|
+
this.largeStep = input(10, ...(ngDevMode ? [{ debugName: "largeStep", alias: 'ngpNumberFieldLargeStep',
|
|
215
|
+
transform: numberAttribute }] : [{
|
|
216
|
+
alias: 'ngpNumberFieldLargeStep',
|
|
217
|
+
transform: numberAttribute,
|
|
218
|
+
}]));
|
|
219
|
+
/**
|
|
220
|
+
* The disabled state of the number field.
|
|
221
|
+
*/
|
|
222
|
+
this.disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", alias: 'ngpNumberFieldDisabled',
|
|
223
|
+
transform: booleanAttribute }] : [{
|
|
224
|
+
alias: 'ngpNumberFieldDisabled',
|
|
225
|
+
transform: booleanAttribute,
|
|
226
|
+
}]));
|
|
227
|
+
/**
|
|
228
|
+
* The readonly state of the number field.
|
|
229
|
+
*/
|
|
230
|
+
this.readonly = input(false, ...(ngDevMode ? [{ debugName: "readonly", alias: 'ngpNumberFieldReadonly',
|
|
231
|
+
transform: booleanAttribute }] : [{
|
|
232
|
+
alias: 'ngpNumberFieldReadonly',
|
|
233
|
+
transform: booleanAttribute,
|
|
234
|
+
}]));
|
|
235
|
+
/**
|
|
236
|
+
* @internal
|
|
237
|
+
*/
|
|
238
|
+
this.state = ngpNumberField({
|
|
239
|
+
id: this.id,
|
|
240
|
+
value: this.value,
|
|
241
|
+
min: this.min,
|
|
242
|
+
max: this.max,
|
|
243
|
+
step: this.step,
|
|
244
|
+
largeStep: this.largeStep,
|
|
245
|
+
disabled: this.disabled,
|
|
246
|
+
readonly: this.readonly,
|
|
247
|
+
onValueChange: value => this.valueChange.emit(value),
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Set the value of the number field.
|
|
252
|
+
*/
|
|
253
|
+
setValue(value) {
|
|
254
|
+
this.state.setValue(value);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Increment the value.
|
|
258
|
+
*/
|
|
259
|
+
increment(multiplier) {
|
|
260
|
+
this.state.increment(multiplier);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Decrement the value.
|
|
264
|
+
*/
|
|
265
|
+
decrement(multiplier) {
|
|
266
|
+
this.state.decrement(multiplier);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Set the disabled state.
|
|
270
|
+
*/
|
|
271
|
+
setDisabled(disabled) {
|
|
272
|
+
this.state.setDisabled(disabled);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Set the readonly state.
|
|
276
|
+
*/
|
|
277
|
+
setReadonly(readonly) {
|
|
278
|
+
this.state.setReadonly(readonly);
|
|
279
|
+
}
|
|
280
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NgpNumberField, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
281
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.9", type: NgpNumberField, isStandalone: true, selector: "[ngpNumberField]", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "ngpNumberFieldValue", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "ngpNumberFieldMin", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "ngpNumberFieldMax", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "ngpNumberFieldStep", isSignal: true, isRequired: false, transformFunction: null }, largeStep: { classPropertyName: "largeStep", publicName: "ngpNumberFieldLargeStep", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "ngpNumberFieldDisabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "ngpNumberFieldReadonly", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "ngpNumberFieldValueChange" }, providers: [provideNumberFieldState()], exportAs: ["ngpNumberField"], ngImport: i0 }); }
|
|
282
|
+
}
|
|
283
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NgpNumberField, decorators: [{
|
|
284
|
+
type: Directive,
|
|
285
|
+
args: [{
|
|
286
|
+
selector: '[ngpNumberField]',
|
|
287
|
+
exportAs: 'ngpNumberField',
|
|
288
|
+
providers: [provideNumberFieldState()],
|
|
289
|
+
}]
|
|
290
|
+
}], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngpNumberFieldValue", required: false }] }], valueChange: [{ type: i0.Output, args: ["ngpNumberFieldValueChange"] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngpNumberFieldMin", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngpNumberFieldMax", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngpNumberFieldStep", required: false }] }], largeStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngpNumberFieldLargeStep", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngpNumberFieldDisabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngpNumberFieldReadonly", required: false }] }] } });
|
|
291
|
+
|
|
292
|
+
const [NgpNumberFieldInputStateToken, ngpNumberFieldInput, injectNumberFieldInputState, provideNumberFieldInputState,] = createPrimitive('NgpNumberFieldInput', ({ allowWheelScrub = signal(false) }) => {
|
|
293
|
+
const elementRef = injectElementRef();
|
|
294
|
+
const numberField = injectNumberFieldState();
|
|
295
|
+
const document = inject(DOCUMENT);
|
|
296
|
+
// Form control integration — sets id, aria-labelledby, aria-describedby on the input
|
|
297
|
+
ngpFormControl({ id: numberField().id, disabled: numberField().disabled });
|
|
298
|
+
const tabindex = computed(() => (numberField().disabled() ? -1 : 0), ...(ngDevMode ? [{ debugName: "tabindex" }] : []));
|
|
299
|
+
// Host bindings
|
|
300
|
+
const inputMode = computed(() => {
|
|
301
|
+
const minVal = numberField().min();
|
|
302
|
+
const stepVal = numberField().step();
|
|
303
|
+
const allowsNegative = !isFinite(minVal) || minVal < 0;
|
|
304
|
+
const hasDecimals = stepVal % 1 !== 0;
|
|
305
|
+
// Some mobile keyboards can't show both minus sign and decimal point
|
|
306
|
+
if (allowsNegative && hasDecimals)
|
|
307
|
+
return 'text';
|
|
308
|
+
if (hasDecimals)
|
|
309
|
+
return 'decimal';
|
|
310
|
+
if (!allowsNegative)
|
|
311
|
+
return 'numeric';
|
|
312
|
+
return 'text';
|
|
313
|
+
}, ...(ngDevMode ? [{ debugName: "inputMode" }] : []));
|
|
314
|
+
attrBinding(elementRef, 'role', 'spinbutton');
|
|
315
|
+
attrBinding(elementRef, 'type', 'text');
|
|
316
|
+
attrBinding(elementRef, 'inputmode', inputMode);
|
|
317
|
+
attrBinding(elementRef, 'autocomplete', 'off');
|
|
318
|
+
attrBinding(elementRef, 'autocorrect', 'off');
|
|
319
|
+
attrBinding(elementRef, 'spellcheck', 'false');
|
|
320
|
+
attrBinding(elementRef, 'aria-valuemin', () => {
|
|
321
|
+
const min = numberField().min();
|
|
322
|
+
return isFinite(min) ? min.toString() : null;
|
|
323
|
+
});
|
|
324
|
+
attrBinding(elementRef, 'aria-valuemax', () => {
|
|
325
|
+
const max = numberField().max();
|
|
326
|
+
return isFinite(max) ? max.toString() : null;
|
|
327
|
+
});
|
|
328
|
+
attrBinding(elementRef, 'aria-valuenow', () => numberField().value()?.toString() ?? null);
|
|
329
|
+
attrBinding(elementRef, 'tabindex', () => tabindex().toString());
|
|
330
|
+
attrBinding(elementRef, 'readonly', () => (numberField().readonly() ? '' : null));
|
|
331
|
+
dataBinding(elementRef, 'data-readonly', () => numberField().readonly());
|
|
332
|
+
ngpInteractions({
|
|
333
|
+
hover: true,
|
|
334
|
+
focusVisible: true,
|
|
335
|
+
disabled: numberField().disabled,
|
|
336
|
+
});
|
|
337
|
+
let isFocused = false;
|
|
338
|
+
/**
|
|
339
|
+
* Parse text and set the number field value accordingly.
|
|
340
|
+
*/
|
|
341
|
+
function parseAndSetValue(text) {
|
|
342
|
+
const trimmed = text.trim();
|
|
343
|
+
if (trimmed === '' || trimmed === '-') {
|
|
344
|
+
numberField().setValue(null);
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
const parsed = parseFloat(trimmed);
|
|
348
|
+
if (!isNaN(parsed)) {
|
|
349
|
+
numberField().setValue(parsed);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Commit the current input text to the number field value.
|
|
355
|
+
* Called before increment/decrement so they operate on the displayed value.
|
|
356
|
+
*/
|
|
357
|
+
function commitInputValue() {
|
|
358
|
+
if (!isFocused)
|
|
359
|
+
return;
|
|
360
|
+
parseAndSetValue(elementRef.nativeElement.value);
|
|
361
|
+
}
|
|
362
|
+
// Register the commit function with the number field so buttons can trigger it
|
|
363
|
+
numberField().registerInputCommit(commitInputValue);
|
|
364
|
+
function formatDisplayValue() {
|
|
365
|
+
const val = numberField().value();
|
|
366
|
+
return val !== null ? String(val) : '';
|
|
367
|
+
}
|
|
368
|
+
// Sync input display value when the number field value changes
|
|
369
|
+
// (programmatically, via stepping, or on commit)
|
|
370
|
+
explicitEffect([() => numberField().value()], ([value]) => {
|
|
371
|
+
elementRef.nativeElement.value = value !== null ? String(value) : '';
|
|
372
|
+
});
|
|
373
|
+
listener(elementRef, 'focus', () => {
|
|
374
|
+
isFocused = true;
|
|
375
|
+
});
|
|
376
|
+
listener(elementRef, 'blur', () => {
|
|
377
|
+
isFocused = false;
|
|
378
|
+
parseAndSetValue(elementRef.nativeElement.value);
|
|
379
|
+
// Always sync the display value on blur to show the clamped/stepped value
|
|
380
|
+
elementRef.nativeElement.value = formatDisplayValue();
|
|
381
|
+
});
|
|
382
|
+
// Reject characters that can't form a valid number
|
|
383
|
+
listener(elementRef, 'beforeinput', (event) => {
|
|
384
|
+
if (numberField().disabled() || numberField().readonly())
|
|
385
|
+
return;
|
|
386
|
+
// Only filter insertions (typing, paste, drop)
|
|
387
|
+
const insertTypes = ['insertText', 'insertFromPaste', 'insertFromDrop'];
|
|
388
|
+
if (!insertTypes.includes(event.inputType) || !event.data)
|
|
389
|
+
return;
|
|
390
|
+
const input = elementRef.nativeElement;
|
|
391
|
+
const selStart = input.selectionStart ?? 0;
|
|
392
|
+
const selEnd = input.selectionEnd ?? 0;
|
|
393
|
+
const current = input.value;
|
|
394
|
+
const proposed = current.slice(0, selStart) + event.data + current.slice(selEnd);
|
|
395
|
+
const minVal = numberField().min();
|
|
396
|
+
const allowsNegative = !isFinite(minVal) || minVal < 0;
|
|
397
|
+
// Build a regex for valid partial number input
|
|
398
|
+
const pattern = allowsNegative ? /^-?(\d+\.?\d*|\.\d*)?$/ : /^(\d+\.?\d*|\.\d*)?$/;
|
|
399
|
+
if (!pattern.test(proposed)) {
|
|
400
|
+
event.preventDefault();
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
// Keyboard interactions
|
|
404
|
+
listener(elementRef, 'keydown', (event) => {
|
|
405
|
+
if (numberField().disabled() || numberField().readonly())
|
|
406
|
+
return;
|
|
407
|
+
const useLargeStep = event.shiftKey;
|
|
408
|
+
function getLargeStepMultiplier() {
|
|
409
|
+
const s = numberField().step();
|
|
410
|
+
if (!isFinite(s) || s <= 0)
|
|
411
|
+
return 1;
|
|
412
|
+
return numberField().largeStep() / s;
|
|
413
|
+
}
|
|
414
|
+
switch (event.key) {
|
|
415
|
+
case 'ArrowUp':
|
|
416
|
+
event.preventDefault();
|
|
417
|
+
numberField().increment(useLargeStep ? getLargeStepMultiplier() : 1);
|
|
418
|
+
elementRef.nativeElement.value = formatDisplayValue();
|
|
419
|
+
break;
|
|
420
|
+
case 'ArrowDown':
|
|
421
|
+
event.preventDefault();
|
|
422
|
+
numberField().decrement(useLargeStep ? getLargeStepMultiplier() : 1);
|
|
423
|
+
elementRef.nativeElement.value = formatDisplayValue();
|
|
424
|
+
break;
|
|
425
|
+
case 'Home':
|
|
426
|
+
if (isFinite(numberField().min())) {
|
|
427
|
+
event.preventDefault();
|
|
428
|
+
numberField().setValue(numberField().min());
|
|
429
|
+
elementRef.nativeElement.value = formatDisplayValue();
|
|
430
|
+
}
|
|
431
|
+
break;
|
|
432
|
+
case 'End':
|
|
433
|
+
if (isFinite(numberField().max())) {
|
|
434
|
+
event.preventDefault();
|
|
435
|
+
numberField().setValue(numberField().max());
|
|
436
|
+
elementRef.nativeElement.value = formatDisplayValue();
|
|
437
|
+
}
|
|
438
|
+
break;
|
|
439
|
+
case 'Enter':
|
|
440
|
+
parseAndSetValue(elementRef.nativeElement.value);
|
|
441
|
+
elementRef.nativeElement.value = formatDisplayValue();
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
// Mouse wheel support
|
|
446
|
+
listener(elementRef, 'wheel', (event) => {
|
|
447
|
+
if (!allowWheelScrub() || numberField().disabled() || numberField().readonly())
|
|
448
|
+
return;
|
|
449
|
+
// Don't intercept browser zoom (Ctrl+wheel / Cmd+wheel)
|
|
450
|
+
if (event.ctrlKey || event.metaKey)
|
|
451
|
+
return;
|
|
452
|
+
// Only handle when focused
|
|
453
|
+
if (document.activeElement !== elementRef.nativeElement)
|
|
454
|
+
return;
|
|
455
|
+
event.preventDefault();
|
|
456
|
+
if (event.deltaY < 0) {
|
|
457
|
+
numberField().increment();
|
|
458
|
+
}
|
|
459
|
+
else if (event.deltaY > 0) {
|
|
460
|
+
numberField().decrement();
|
|
461
|
+
}
|
|
462
|
+
elementRef.nativeElement.value = formatDisplayValue();
|
|
463
|
+
}, { config: { passive: false } });
|
|
464
|
+
function focus() {
|
|
465
|
+
elementRef.nativeElement.focus({ preventScroll: true });
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
focus,
|
|
469
|
+
};
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Apply the `ngpNumberFieldInput` directive to an input element within a number field.
|
|
474
|
+
*/
|
|
475
|
+
class NgpNumberFieldInput {
|
|
476
|
+
constructor() {
|
|
477
|
+
/**
|
|
478
|
+
* Whether mouse wheel changes the value when the input is focused.
|
|
479
|
+
*/
|
|
480
|
+
this.allowWheelScrub = input(false, ...(ngDevMode ? [{ debugName: "allowWheelScrub", alias: 'ngpNumberFieldInputAllowWheelScrub',
|
|
481
|
+
transform: booleanAttribute }] : [{
|
|
482
|
+
alias: 'ngpNumberFieldInputAllowWheelScrub',
|
|
483
|
+
transform: booleanAttribute,
|
|
484
|
+
}]));
|
|
485
|
+
ngpNumberFieldInput({
|
|
486
|
+
allowWheelScrub: this.allowWheelScrub,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NgpNumberFieldInput, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
490
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.9", type: NgpNumberFieldInput, isStandalone: true, selector: "[ngpNumberFieldInput]", inputs: { allowWheelScrub: { classPropertyName: "allowWheelScrub", publicName: "ngpNumberFieldInputAllowWheelScrub", isSignal: true, isRequired: false, transformFunction: null } }, providers: [provideNumberFieldInputState()], exportAs: ["ngpNumberFieldInput"], ngImport: i0 }); }
|
|
491
|
+
}
|
|
492
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NgpNumberFieldInput, decorators: [{
|
|
493
|
+
type: Directive,
|
|
494
|
+
args: [{
|
|
495
|
+
selector: '[ngpNumberFieldInput]',
|
|
496
|
+
exportAs: 'ngpNumberFieldInput',
|
|
497
|
+
providers: [provideNumberFieldInputState()],
|
|
498
|
+
}]
|
|
499
|
+
}], ctorParameters: () => [], propDecorators: { allowWheelScrub: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngpNumberFieldInputAllowWheelScrub", required: false }] }] } });
|
|
500
|
+
|
|
501
|
+
const [NgpNumberFieldIncrementStateToken, ngpNumberFieldIncrement, injectNumberFieldIncrementState, provideNumberFieldIncrementState,] = createPrimitive('NgpNumberFieldIncrement', ({}) => {
|
|
502
|
+
const elementRef = injectElementRef();
|
|
503
|
+
const numberField = injectNumberFieldState();
|
|
504
|
+
const injector = inject(Injector);
|
|
505
|
+
const disposables = injectDisposables();
|
|
506
|
+
const isDisabled = computed(() => !numberField().canIncrement(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
|
|
507
|
+
// Host bindings
|
|
508
|
+
attrBinding(elementRef, 'type', 'button');
|
|
509
|
+
attrBinding(elementRef, 'tabindex', '-1');
|
|
510
|
+
attrBinding(elementRef, 'disabled', () => (isDisabled() ? '' : null));
|
|
511
|
+
dataBinding(elementRef, 'data-disabled', isDisabled);
|
|
512
|
+
ngpInteractions({
|
|
513
|
+
hover: true,
|
|
514
|
+
focusVisible: true,
|
|
515
|
+
press: true,
|
|
516
|
+
disabled: isDisabled,
|
|
517
|
+
});
|
|
518
|
+
let cleanupRepeat = null;
|
|
519
|
+
function stopRepeat() {
|
|
520
|
+
cleanupRepeat?.();
|
|
521
|
+
cleanupRepeat = null;
|
|
522
|
+
}
|
|
523
|
+
listener(elementRef, 'pointerdown', (event) => {
|
|
524
|
+
event.preventDefault();
|
|
525
|
+
if (isDisabled())
|
|
526
|
+
return;
|
|
527
|
+
numberField().increment();
|
|
528
|
+
// Start auto-repeat: 400ms initial delay, then 60ms interval
|
|
529
|
+
stopRepeat();
|
|
530
|
+
let intervalCleanup = null;
|
|
531
|
+
const delayCleanup = disposables.setTimeout(() => {
|
|
532
|
+
intervalCleanup = disposables.setInterval(() => {
|
|
533
|
+
if (!numberField().canIncrement()) {
|
|
534
|
+
stopRepeat();
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
numberField().increment();
|
|
538
|
+
}, 60);
|
|
539
|
+
}, 400);
|
|
540
|
+
// Set up document-level listeners to stop on pointer release
|
|
541
|
+
const pointerUpCleanup = listener(document, 'pointerup', stopRepeat, {
|
|
542
|
+
config: false,
|
|
543
|
+
injector,
|
|
544
|
+
});
|
|
545
|
+
const pointerCancelCleanup = listener(document, 'pointercancel', stopRepeat, {
|
|
546
|
+
config: false,
|
|
547
|
+
injector,
|
|
548
|
+
});
|
|
549
|
+
cleanupRepeat = () => {
|
|
550
|
+
delayCleanup();
|
|
551
|
+
intervalCleanup?.();
|
|
552
|
+
pointerUpCleanup();
|
|
553
|
+
pointerCancelCleanup();
|
|
554
|
+
};
|
|
555
|
+
});
|
|
556
|
+
return {};
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Apply the `ngpNumberFieldIncrement` directive to a button element that increments the number field value.
|
|
561
|
+
*/
|
|
562
|
+
class NgpNumberFieldIncrement {
|
|
563
|
+
constructor() {
|
|
564
|
+
ngpNumberFieldIncrement({});
|
|
565
|
+
}
|
|
566
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NgpNumberFieldIncrement, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
567
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.9", type: NgpNumberFieldIncrement, isStandalone: true, selector: "[ngpNumberFieldIncrement]", providers: [provideNumberFieldIncrementState()], exportAs: ["ngpNumberFieldIncrement"], ngImport: i0 }); }
|
|
568
|
+
}
|
|
569
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NgpNumberFieldIncrement, decorators: [{
|
|
570
|
+
type: Directive,
|
|
571
|
+
args: [{
|
|
572
|
+
selector: '[ngpNumberFieldIncrement]',
|
|
573
|
+
exportAs: 'ngpNumberFieldIncrement',
|
|
574
|
+
providers: [provideNumberFieldIncrementState()],
|
|
575
|
+
}]
|
|
576
|
+
}], ctorParameters: () => [] });
|
|
577
|
+
|
|
578
|
+
const [NgpNumberFieldDecrementStateToken, ngpNumberFieldDecrement, injectNumberFieldDecrementState, provideNumberFieldDecrementState,] = createPrimitive('NgpNumberFieldDecrement', ({}) => {
|
|
579
|
+
const elementRef = injectElementRef();
|
|
580
|
+
const numberField = injectNumberFieldState();
|
|
581
|
+
const injector = inject(Injector);
|
|
582
|
+
const disposables = injectDisposables();
|
|
583
|
+
const isDisabled = computed(() => !numberField().canDecrement(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
|
|
584
|
+
// Host bindings
|
|
585
|
+
attrBinding(elementRef, 'type', 'button');
|
|
586
|
+
attrBinding(elementRef, 'tabindex', '-1');
|
|
587
|
+
attrBinding(elementRef, 'disabled', () => (isDisabled() ? '' : null));
|
|
588
|
+
dataBinding(elementRef, 'data-disabled', isDisabled);
|
|
589
|
+
ngpInteractions({
|
|
590
|
+
hover: true,
|
|
591
|
+
focusVisible: true,
|
|
592
|
+
press: true,
|
|
593
|
+
disabled: isDisabled,
|
|
594
|
+
});
|
|
595
|
+
let cleanupRepeat = null;
|
|
596
|
+
function stopRepeat() {
|
|
597
|
+
cleanupRepeat?.();
|
|
598
|
+
cleanupRepeat = null;
|
|
599
|
+
}
|
|
600
|
+
listener(elementRef, 'pointerdown', (event) => {
|
|
601
|
+
event.preventDefault();
|
|
602
|
+
if (isDisabled())
|
|
603
|
+
return;
|
|
604
|
+
numberField().decrement();
|
|
605
|
+
// Start auto-repeat: 400ms initial delay, then 60ms interval
|
|
606
|
+
stopRepeat();
|
|
607
|
+
let intervalCleanup = null;
|
|
608
|
+
const delayCleanup = disposables.setTimeout(() => {
|
|
609
|
+
intervalCleanup = disposables.setInterval(() => {
|
|
610
|
+
if (!numberField().canDecrement()) {
|
|
611
|
+
stopRepeat();
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
numberField().decrement();
|
|
615
|
+
}, 60);
|
|
616
|
+
}, 400);
|
|
617
|
+
// Set up document-level listeners to stop on pointer release
|
|
618
|
+
const pointerUpCleanup = listener(document, 'pointerup', stopRepeat, {
|
|
619
|
+
config: false,
|
|
620
|
+
injector,
|
|
621
|
+
});
|
|
622
|
+
const pointerCancelCleanup = listener(document, 'pointercancel', stopRepeat, {
|
|
623
|
+
config: false,
|
|
624
|
+
injector,
|
|
625
|
+
});
|
|
626
|
+
cleanupRepeat = () => {
|
|
627
|
+
delayCleanup();
|
|
628
|
+
intervalCleanup?.();
|
|
629
|
+
pointerUpCleanup();
|
|
630
|
+
pointerCancelCleanup();
|
|
631
|
+
};
|
|
632
|
+
});
|
|
633
|
+
return {};
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Apply the `ngpNumberFieldDecrement` directive to a button element that decrements the number field value.
|
|
638
|
+
*/
|
|
639
|
+
class NgpNumberFieldDecrement {
|
|
640
|
+
constructor() {
|
|
641
|
+
ngpNumberFieldDecrement({});
|
|
642
|
+
}
|
|
643
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NgpNumberFieldDecrement, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
644
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.9", type: NgpNumberFieldDecrement, isStandalone: true, selector: "[ngpNumberFieldDecrement]", providers: [provideNumberFieldDecrementState()], exportAs: ["ngpNumberFieldDecrement"], ngImport: i0 }); }
|
|
645
|
+
}
|
|
646
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NgpNumberFieldDecrement, decorators: [{
|
|
647
|
+
type: Directive,
|
|
648
|
+
args: [{
|
|
649
|
+
selector: '[ngpNumberFieldDecrement]',
|
|
650
|
+
exportAs: 'ngpNumberFieldDecrement',
|
|
651
|
+
providers: [provideNumberFieldDecrementState()],
|
|
652
|
+
}]
|
|
653
|
+
}], ctorParameters: () => [] });
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Generated bundle index. Do not edit.
|
|
657
|
+
*/
|
|
658
|
+
|
|
659
|
+
export { NgpNumberField, NgpNumberFieldDecrement, NgpNumberFieldIncrement, NgpNumberFieldInput, injectNumberFieldDecrementState, injectNumberFieldIncrementState, injectNumberFieldInputState, injectNumberFieldState, ngpNumberField, ngpNumberFieldDecrement, ngpNumberFieldIncrement, ngpNumberFieldInput, provideNumberFieldDecrementState, provideNumberFieldIncrementState, provideNumberFieldInputState, provideNumberFieldState };
|
|
660
|
+
//# sourceMappingURL=ng-primitives-number-field.mjs.map
|