@progress/kendo-angular-inputs
Version:
Kendo UI for Angular Inputs Package - Everything you need to build professional form functionality (Checkbox, ColorGradient, ColorPalette, ColorPicker, FlatColorPicker, FormField, MaskedTextBox, NumericTextBox, RadioButton, RangeSlider, Slider, Switch, Te
1,301 lines • 50.2 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* Copyright © 2025 Progress Software Corporation. All rights reserved.
* Licensed under commercial license. See LICENSE.md in the project root for more information
*-------------------------------------------------------------------------------------------*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Component, ElementRef, EventEmitter, HostBinding, Input, Output, Renderer2, ViewChild, forwardRef, isDevMode, NgZone, ChangeDetectorRef, Injector, ContentChild } from '@angular/core';
import { anyChanged, hasObservers, Keys, guid, KendoInput, SuffixTemplateDirective, PrefixTemplateDirective, setHTMLAttributes, isControlRequired, isObjectPresent, removeHTMLAttributes, parseAttributes, EventsOutsideAngularDirective } from '@progress/kendo-angular-common';
import { areSame, getStylingClasses, requiresZoneOnBlur } from '../common/utils';
import { invokeElementMethod } from '../common/dom-utils';
import { add, toFixedPrecision, limitPrecision } from '../common/math';
import { createMaxValidator } from '../validators/max.validator';
import { createMinValidator } from '../validators/min.validator';
import { NG_VALUE_ACCESSOR, NG_VALIDATORS, NgControl } from '@angular/forms';
import { IntlService } from '@progress/kendo-angular-intl';
import { L10N_PREFIX, LocalizationService } from '@progress/kendo-angular-l10n';
import { validatePackage } from '@progress/kendo-licensing';
import { packageMetadata } from '../package-metadata';
import { MIN_DOC_LINK, MAX_DOC_LINK, POINT, INITIAL_SPIN_DELAY, SPIN_DELAY, EXPONENT_REGEX } from './constants';
import { defined, noop, numericRegex, isNumber, pad, decimalPart, getDeltaFromMouseWheel, getCaretPosition, extractSignificantNumericChars, isRightClick } from './utils';
import { ArrowDirection } from './arrow-direction';
import { mobileOS } from '@progress/kendo-common';
import { caretAltUpIcon, caretAltDownIcon } from '@progress/kendo-svg-icons';
import { InputSeparatorComponent } from '../shared/input-separator.component';
import { NgIf, NgTemplateOutlet } from '@angular/common';
import { SharedInputEventsDirective } from '../shared/shared-events.directive';
import { LocalizedNumericTextBoxMessagesDirective } from './localization/localized-numerictextbox-messages.directive';
import { IconWrapperComponent } from '@progress/kendo-angular-icons';
import * as i0 from "@angular/core";
import * as i1 from "@progress/kendo-angular-intl";
import * as i2 from "@progress/kendo-angular-l10n";
const PARSABLE_OPTIONS = ['min', 'max', 'step', 'decimals'];
const PARSABLE_DEFAULTS = {
decimals: null,
max: null,
min: null,
step: 1
};
const FOCUSED = 'k-focus';
const DEFAULT_SIZE = 'medium';
const DEFAULT_ROUNDED = 'medium';
const DEFAULT_FILL_MODE = 'solid';
/**
* Represents the [Kendo UI NumericTextBox component for Angular]({% slug overview_numerictextbox %}).
*/
export class NumericTextBoxComponent {
intl;
renderer;
localizationService;
injector;
ngZone;
changeDetector;
hostElement;
/**
* @hidden
*/
focusableId = `k-${guid()}`;
/**
* Determines whether the NumericTextBox is disabled ([see example]({% slug disabled_numerictextbox %})). To learn how to disable the component in reactive forms, refer to the article on [Forms Support](slug:formssupport_numerictextbox#toc-managing-the-numerictextbox-disabled-state-in-reactive-forms).
*/
disabled = false;
/**
* Determines whether the NumericTextBox is in its read-only state ([see example]({% slug readonly_numerictextbox %})).
*
* @default false
*/
readonly = false;
/**
* Sets the title of the `input` element of the NumericTextBox.
*/
title = '';
/**
* Specifies whether the value will be auto-corrected based on the minimum and maximum values
* ([see example]({% slug precision_numerictextbox %})).
*/
autoCorrect = false;
/**
* Specifies the number format which is used when the NumericTextBox is not focused
* ([see example]({% slug formats_numerictextbox %})).
* If `format` is set to `null` or `undefined`, the default format will be used.
*/
get format() {
const format = this._format;
return format !== null && format !== undefined ? format : 'n2';
}
set format(value) {
this._format = value;
}
/**
* Specifies the greatest value that is valid
* ([see example]({% slug precision_numerictextbox %}#toc-value-ranges)).
*/
max;
/**
* Specifies the smallest value that is valid
* ([see example]({% slug precision_numerictextbox %}#toc-value-ranges)).
*/
min;
/**
* Specifies the number of decimals that the user can enter when the input is focused
* ([see example]({% slug precision_numerictextbox %})).
*/
decimals = null;
/**
* Specifies the input placeholder.
*/
placeholder;
/**
* Specifies the value that is used to increment or decrement the component value
* ([see example]({% slug predefinedsteps_numerictextbox %})).
*/
step = 1;
/**
* Specifies whether the **Up** and **Down** spin buttons will be rendered
* ([see example]({% slug spinbuttons_numerictextbox %})).
*/
spinners = true;
/**
* Determines whether the built-in minimum or maximum validators are enforced when a form is validated.
*
* > The 4.2.0 Angular version introduces the `min` and `max` validation directives. As a result, even if you set `rangeValidation`
* to `false`, the built-in Angular validators will be executed.
*/
rangeValidation = true;
/**
* Specifies the [tabindex](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) of the component.
*/
tabindex = 0;
/**
* @hidden
*/
set tabIndex(tabIndex) {
this.tabindex = tabIndex;
}
get tabIndex() {
return this.tabindex;
}
/**
* Determines whether the value of the NumericTextBox will be changed via scrolling. Defaults to `true`.
*
* @default true
*/
changeValueOnScroll = true;
/**
* Determines whether the whole value will be selected when the NumericTextBox is clicked. Defaults to `true`.
*/
selectOnFocus = true;
/**
* Specifies the value of the NumericTextBox
* ([see example]({% slug formats_numerictextbox %})).
*/
value = null;
/**
* Specifies the maximum number of characters the end user can type or paste in the input.
* The locale-specific decimal separator and negative sign (`-`) are included in the length of the value when present.
* The `maxlength` restriction is not applied to the length of the formatted value when the component is not focused.
*/
maxlength;
/**
* The size property specifies padding of the NumericTextBox internal input element
* ([see example]({% slug appearance_numerictextbox %}#toc-size)).
* The possible values are:
* * `small`
* * `medium` (default)
* * `large`
* * `none`
*/
set size(size) {
const newSize = size ? size : DEFAULT_SIZE;
this.handleClasses(newSize, 'size');
this._size = newSize;
}
get size() {
return this._size;
}
/**
* The `rounded` property specifies the border radius of the NumericTextBox
* ([see example](slug:appearance_numerictextbox#toc-roundness)).
* The possible values are:
* * `small`
* * `medium` (default)
* * `large`
* * `none`
*/
set rounded(rounded) {
const newRounded = rounded ? rounded : DEFAULT_ROUNDED;
this.handleClasses(newRounded, 'rounded');
this._rounded = newRounded;
}
get rounded() {
return this._rounded;
}
/**
* The `fillMode` property specifies the background and border styles of the NumericTextBox
* ([see example](slug:appearance_numerictextbox#toc-fill-mode)).
* The possible values are:
* * `flat`
* * `solid` (default)
* * `outline`
* * `none`
*/
set fillMode(fillMode) {
const newFillMode = fillMode ? fillMode : DEFAULT_FILL_MODE;
this.handleClasses(newFillMode, 'fillMode');
this._fillMode = newFillMode;
}
get fillMode() {
return this._fillMode;
}
/**
* Sets the HTML attributes of the inner focusable input element. Attributes which are essential for certain component functionalities cannot be changed.
*/
set inputAttributes(attributes) {
if (isObjectPresent(this.parsedAttributes)) {
removeHTMLAttributes(this.parsedAttributes, this.renderer, this.numericInput.nativeElement);
}
this._inputAttributes = attributes;
this.parsedAttributes = this.inputAttributes ?
parseAttributes(this.inputAttributes, this.defaultAttributes) :
this.inputAttributes;
this.setInputAttributes();
}
get inputAttributes() {
return this._inputAttributes;
}
/**
* Fires each time the user selects a new value ([see example](slug:events_numerictextbox)).
*/
valueChange = new EventEmitter();
/**
* Fires each time the user focuses the NumericTextBox element ([see example](slug:events_numerictextbox)).
*/
onFocus = new EventEmitter();
/**
* Fires each time the NumericTextBox component gets blurred ([see example](slug:events_numerictextbox)).
*/
onBlur = new EventEmitter();
/**
* Fires each time the user focuses the `input` element.
*/
inputFocus = new EventEmitter();
/**
* Fires each time the `input` element gets blurred.
*/
inputBlur = new EventEmitter();
/**
* @hidden
*/
numericInput;
/**
* @hidden
*/
suffixTemplate;
/**
* @hidden
*/
prefixTemplate;
direction;
/**
* @hidden
*/
ArrowDirection = ArrowDirection;
/**
* @hidden
*/
arrowDirection = ArrowDirection.None;
get disableClass() {
return this.disabled;
}
hostClasses = true;
/**
* @hidden
*/
arrowUpIcon = caretAltUpIcon;
/**
* @hidden
*/
arrowDownIcon = caretAltDownIcon;
subscriptions;
inputValue = '';
spinTimeout;
isFocused;
minValidateFn = noop;
maxValidateFn = noop;
numericRegex;
_format = "n2";
previousSelection;
pressedKey;
control;
isPasted = false;
mouseDown = false;
_size = 'medium';
_rounded = 'medium';
_fillMode = 'solid';
ngChange = noop;
ngTouched = noop;
ngValidatorChange = noop;
domEvents = [];
_inputAttributes;
parsedAttributes = {};
get defaultAttributes() {
return {
id: this.focusableId,
disabled: this.disabled ? '' : null,
readonly: this.readonly ? '' : null,
tabindex: this.tabIndex,
placeholder: this.placeholder,
title: this.title,
maxlength: this.maxlength,
'aria-valuemin': this.min,
'aria-valuemax': this.max,
'aria-valuenow': this.value,
required: this.isControlRequired ? '' : null,
'aria-invalid': this.isControlInvalid
};
}
get mutableAttributes() {
return {
autocomplete: 'off',
autocorrect: 'off',
role: 'spinbutton'
};
}
constructor(intl, renderer, localizationService, injector, ngZone, changeDetector, hostElement) {
this.intl = intl;
this.renderer = renderer;
this.localizationService = localizationService;
this.injector = injector;
this.ngZone = ngZone;
this.changeDetector = changeDetector;
this.hostElement = hostElement;
validatePackage(packageMetadata);
this.direction = localizationService.rtl ? 'rtl' : 'ltr';
}
ngOnInit() {
this.subscriptions = this.localizationService
.changes
.subscribe(({ rtl }) => {
this.direction = rtl ? 'rtl' : 'ltr';
});
this.subscriptions.add(this.intl.changes.subscribe(this.intlChange.bind(this)));
if (this.hostElement) {
this.renderer.removeAttribute(this.hostElement.nativeElement, "tabindex");
}
this.control = this.injector.get(NgControl, null);
this.ngZone.runOutsideAngular(() => {
this.domEvents.push(this.renderer.listen(this.hostElement.nativeElement, 'mousewheel', this.handleWheel.bind(this)));
this.domEvents.push(this.renderer.listen(this.hostElement.nativeElement, 'DOMMouseScroll', this.handleWheel.bind(this)));
});
}
ngAfterViewInit() {
const stylingInputs = ['size', 'rounded', 'fillMode'];
stylingInputs.forEach(input => {
this.handleClasses(this[input], input);
});
}
/**
* @hidden
*/
increasePress = (e) => {
this.arrowPress(ArrowDirection.Up, e);
};
/**
* @hidden
*/
decreasePress = (e) => {
this.arrowPress(ArrowDirection.Down, e);
};
/**
* @hidden
*/
releaseArrow = () => {
clearTimeout(this.spinTimeout);
if (this.arrowDirection !== ArrowDirection.None) {
this.arrowDirection = ArrowDirection.None;
this.changeDetector.detectChanges();
}
};
/**
* @hidden
*/
ngOnChanges(changes) {
if (anyChanged(PARSABLE_OPTIONS, changes, false)) {
this.parseOptions(PARSABLE_OPTIONS.filter(option => changes[option]));
}
this.verifySettings();
if (anyChanged(['min', 'max', 'rangeValidation'], changes, false)) {
this.minValidateFn = this.rangeValidation ? createMinValidator(this.min) : noop;
this.maxValidateFn = this.rangeValidation ? createMaxValidator(this.max) : noop;
this.ngValidatorChange();
}
if (anyChanged(['autoCorrect', 'decimals', 'min'], changes)) {
delete this.numericRegex;
}
if (anyChanged(['value', 'format'], changes, false)) {
this.verifyValue(this.value);
this.value = this.restrictModelValue(this.value);
if (!this.focused || (this.intl.parseNumber(this.elementValue) !== this.value)) {
this.setInputValue();
}
}
}
/**
* @hidden
*/
ngOnDestroy() {
if (this.subscriptions) {
this.subscriptions.unsubscribe();
}
clearTimeout(this.spinTimeout);
this.domEvents.forEach(unbindHandler => unbindHandler());
}
/**
* @hidden
*/
validate(control) {
return this.minValidateFn(control) || this.maxValidateFn(control);
}
/**
* @hidden
*/
registerOnValidatorChange(fn) {
this.ngValidatorChange = fn;
}
/**
* @hidden
*/
writeValue(value) {
this.verifyValue(value);
const restrictedValue = this.restrictModelValue(value);
this.value = restrictedValue;
this.setInputValue();
}
/**
* @hidden
*/
registerOnChange(fn) {
this.ngChange = fn;
}
/**
* @hidden
*/
registerOnTouched(fn) {
this.ngTouched = fn;
}
/**
* @hidden
* Called when the status of the component changes to or from `disabled`.
* Depending on the value, it enables or disables the appropriate DOM element.
*
* @param isDisabled
*/
setDisabledState(isDisabled) {
this.changeDetector.markForCheck();
this.disabled = isDisabled;
}
/**
* Focuses the NumericTextBox.
*
* @example
* ```ts-no-run
* _@Component({
* selector: 'my-app',
* template: `
* <button (click)="numerictextbox.focus()">Focus NumericTextBox</button>
* <kendo-numerictextbox #numerictextbox></kendo-numerictextbox>
* `
* })
* class AppComponent { }
* ```
*/
focus() {
invokeElementMethod(this.numericInput, 'focus');
}
/**
* Blurs the NumericTextBox.
*/
blur() {
invokeElementMethod(this.numericInput, 'blur');
}
/**
* Notifies the `NumericTextBoxComponent` that the input value should be changed.
* Can be used to update the input after setting the component properties directly.
*/
notifyValueChange() {
this.setInputValue();
}
/**
* @hidden
*/
handlePaste = () => {
this.isPasted = true;
};
/**
* @hidden
*/
handleInput = () => {
const input = this.numericInput.nativeElement;
let { selectionStart, selectionEnd, value: inputValue } = input;
if (this.pressedKey === Keys.NumpadDecimal) {
inputValue = this.replaceNumpadDotValue();
}
if (this.isPasted) {
inputValue = this.formatInputValue(this.intl.parseNumber(inputValue));
}
if (!this.isValid(inputValue)) {
input.value = this.inputValue;
this.setSelection(selectionStart - 1, selectionEnd - 1);
return;
}
const parsedValue = this.intl.parseNumber(inputValue);
let value = this.restrictDecimals(parsedValue);
if (this.autoCorrect) {
const limited = this.limitInputValue(value);
value = limited.value;
selectionStart = limited.selectionStart;
selectionEnd = limited.selectionEnd;
}
if (parsedValue !== value || this.hasTrailingZeros(inputValue) || !this.focused) {
this.setInputValue(value);
this.setSelection(selectionStart, selectionEnd);
}
else {
this.inputValue = inputValue;
}
if (this.isPasted) {
input.value = this.inputValue;
}
this.updateValue(value);
this.previousSelection = null;
this.isPasted = false;
};
/**
* @hidden
*/
handleDragEnter = () => {
if (!this.focused && !this.isDisabled) {
this.setInputValue(this.value, true);
}
};
/**
* @hidden
*/
handleMouseDown = () => {
this.mouseDown = true;
};
/**
* @hidden
*/
handleInputFocus = () => {
if (!this.focused) {
this.focused = true;
if (!this.isDisabled) {
const shouldSelectAll = this.selectOnFocus || !this.mouseDown;
this.ngZone.runOutsideAngular(() => {
setTimeout(() => {
if (shouldSelectAll) {
this.selectAll();
}
else {
this.selectCaret();
}
}, 0);
});
}
if (hasObservers(this.onFocus)) {
this.ngZone.run(() => {
this.onFocus.emit();
});
}
}
this.mouseDown = false;
if (hasObservers(this.inputFocus)) {
this.ngZone.run(() => {
this.inputFocus.emit();
});
}
};
/**
* @hidden
*/
handleFocus() {
this.ngZone.run(() => {
if (!this.focused && hasObservers(this.onFocus)) {
this.onFocus.emit();
}
this.focused = true;
});
}
/**
* @hidden
*/
handleBlur = () => {
this.changeDetector.markForCheck();
this.focused = false;
//blur is thrown before input when dragging the input text in IE
if (this.inputValue !== this.elementValue) {
this.handleInput();
}
this.setInputValue();
if (hasObservers(this.onBlur)) {
this.ngZone.run(() => {
this.ngTouched();
this.onBlur.emit();
});
}
};
/**
* @hidden
*/
handleInputBlur = () => {
this.changeDetector.markForCheck();
//blur is thrown before input when dragging the input text in IE
if (this.inputValue !== this.elementValue) {
this.handleInput();
}
this.setInputValue();
if (hasObservers(this.inputBlur) || requiresZoneOnBlur(this.control)) {
this.ngZone.run(() => {
this.ngTouched();
this.inputBlur.emit();
});
}
};
/**
* @hidden
*/
handleKeyDown = (e) => {
if (this.isDisabled) {
return;
}
let step;
if (e.keyCode === Keys.ArrowDown) {
step = -1;
}
else if (e.keyCode === Keys.ArrowUp) {
step = 1;
}
if (step && this.step) {
e.preventDefault();
this.addStep(step);
}
const input = this.numericInput.nativeElement;
this.previousSelection = {
end: input.selectionEnd,
start: input.selectionStart
};
this.pressedKey = e.keyCode;
};
/**
* @hidden
*/
handleWheel = (e) => {
if (this.focused && !this.isDisabled && this.changeValueOnScroll) {
e.preventDefault();
const delta = getDeltaFromMouseWheel(e);
this.addStep(delta);
}
};
/**
* @hidden
*/
get incrementTitle() {
return this.localizationService.get('increment');
}
/**
* @hidden
*/
get decrementTitle() {
return this.localizationService.get('decrement');
}
/**
* @hidden
*/
get isControlInvalid() {
return this.control && this.control.touched && !this.control.valid;
}
/**
* @hidden
*/
get isControlRequired() {
return isControlRequired(this.control?.control);
}
/**
* @hidden
*/
get focused() {
return this.isFocused;
}
/**
* @hidden
*/
set focused(value) {
if (this.isFocused !== value && this.hostElement) {
const wrap = this.hostElement.nativeElement;
if (value) {
this.renderer.addClass(wrap, FOCUSED);
}
else {
this.renderer.removeClass(wrap, FOCUSED);
}
this.isFocused = value;
}
}
get decimalSeparator() {
const numberSymbols = this.intl.numberSymbols();
return numberSymbols.decimal;
}
get elementValue() {
return this.numericInput.nativeElement.value;
}
set elementValue(value) {
this.renderer.setProperty(this.numericInput.nativeElement, 'value', value);
}
get hasDecimals() {
return this.decimals !== null && this.decimals >= 0;
}
get isDisabled() {
return this.disabled || this.readonly;
}
arrowPress(direction, e) {
e.preventDefault();
if (this.isDisabled || isRightClick(e)) {
return;
}
if (!mobileOS) {
this.focus();
this.focused = true;
}
if (this.arrowDirection !== direction) {
this.arrowDirection = direction;
this.changeDetector.detectChanges();
}
if (this.step) {
this.spin(direction, INITIAL_SPIN_DELAY);
}
else {
this.setInputValue();
}
}
updateValue(value) {
if (!areSame(this.value, value)) {
this.ngZone.run(() => {
this.value = value;
this.ngChange(value);
this.valueChange.emit(value);
this.changeDetector.markForCheck();
});
}
}
replaceNumpadDotValue() {
let value = this.inputValue || "";
if (this.previousSelection) {
const input = this.numericInput.nativeElement;
const { selectionStart, selectionEnd } = input;
const { start, end } = this.previousSelection;
input.value = value = value.substring(0, start) + this.decimalSeparator + value.substring(end);
this.setSelection(selectionStart, selectionEnd);
}
return value;
}
isValid(value) {
if (!this.numericRegex) {
this.numericRegex = numericRegex({
autoCorrect: this.autoCorrect,
decimals: this.decimals,
min: this.min,
separator: this.decimalSeparator
});
}
return this.numericRegex.test(value);
}
spin(step, timeout) {
clearTimeout(this.spinTimeout);
this.spinTimeout = window.setTimeout(() => {
this.spin(step, SPIN_DELAY);
}, timeout);
this.addStep(step);
}
addStep(step) {
let value = add(this.value || 0, this.step * step);
value = this.limitValue(value);
value = this.restrictDecimals(value);
this.setInputValue(value);
this.updateValue(value);
}
setSelection(start, end) {
if (this.focused) {
invokeElementMethod(this.numericInput, 'setSelectionRange', start, end);
}
}
limitValue(value) {
let result = value;
if (!this.isInRange(value)) {
if (isNumber(this.max) && value > this.max) {
result = this.max;
}
if (isNumber(this.min) && value < this.min) {
result = this.min;
}
}
return result;
}
limitInputValue(value) {
const { selectionStart, selectionEnd, value: enteredValue } = this.numericInput.nativeElement;
let limitedValue = value;
let selectToEnd = false;
if (!this.isInRange(value)) {
const lengthChange = enteredValue.length - String(this.inputValue).length;
const { min, max } = this;
const hasMax = isNumber(max);
const hasMin = isNumber(min);
let padLimit, replaceNext;
let correctedValue = value;
if (selectionStart === 0 && this.inputValue.substr(1) === enteredValue) {
return {
selectionEnd: selectionEnd,
selectionStart: selectionStart,
value: null
};
}
if (hasMax && value > max) {
if (value > 0) {
replaceNext = true;
}
else {
padLimit = max;
}
}
else if (hasMin && value < min) {
if (value > 0) {
padLimit = min;
}
else {
replaceNext = true;
}
}
if (padLimit) {
const paddedValue = this.tryPadValue(value, padLimit);
if (paddedValue && decimalPart(value) !== decimalPart(padLimit)) {
correctedValue = paddedValue;
selectToEnd = true;
}
}
else if (replaceNext) {
if (this.inputValue && selectionStart !== enteredValue.length) {
correctedValue = parseFloat(enteredValue.substr(0, selectionStart) +
enteredValue.substr(selectionStart + lengthChange));
}
}
limitedValue = this.limitValue(correctedValue);
selectToEnd = (selectToEnd || limitedValue !== correctedValue) && this.previousSelection &&
(this.previousSelection.end - this.previousSelection.start + lengthChange) > 0;
}
return {
selectionEnd: selectToEnd ? String(limitedValue).length : selectionEnd,
selectionStart: selectionStart,
value: limitedValue
};
}
tryPadValue(value, limit) {
const limitLength = String(Math.floor(limit)).length;
const zeroPadded = pad(value, limitLength);
const zeroPaddedNext = pad(value, limitLength + 1);
let result;
if (this.isInRange(zeroPadded)) {
result = zeroPadded;
}
else if (this.isInRange(zeroPaddedNext)) {
result = zeroPaddedNext;
}
return result;
}
isInRange(value) {
return !isNumber(value) || ((!isNumber(this.min) || this.min <= value) && (!isNumber(this.max) || value <= this.max));
}
restrictModelValue(value) {
let result = this.restrictDecimals(value, true);
if (this.autoCorrect && this.limitValue(result) !== result) {
result = null;
}
return result;
}
restrictDecimals(value, round) {
let result = value;
if (value && this.hasDecimals) {
const decimals = this.decimals;
const stringValue = String(value);
if (round || EXPONENT_REGEX.test(stringValue)) {
result = toFixedPrecision(value, decimals);
}
else {
const parts = stringValue.split(POINT);
let fraction = parts[1];
if (fraction && fraction.length > decimals) {
fraction = fraction.substr(0, decimals);
result = parseFloat(`${parts[0]}${POINT}${fraction}`);
}
}
}
return result;
}
formatInputValue(value) {
let stringValue = Object.is(value, -0) ? '-0' : String(value);
const exponentMatch = EXPONENT_REGEX.exec(stringValue);
if (exponentMatch) {
stringValue = value.toFixed(limitPrecision(parseInt(exponentMatch[1], 10)));
}
return stringValue.replace(POINT, this.decimalSeparator);
}
formatValue(value, focused) {
let formattedValue;
if (value === null || !defined(value) || value === '') {
formattedValue = '';
}
else if (focused && !this.readonly) {
formattedValue = this.formatInputValue(value);
}
else {
formattedValue = this.intl.formatNumber(value, this.format);
}
return formattedValue;
}
setInputValue(value = this.value, focused = this.focused) {
const formattedValue = this.formatValue(value, focused);
this.elementValue = formattedValue;
this.inputValue = formattedValue;
}
verifySettings() {
if (!isDevMode()) {
return;
}
if (this.min !== null && this.max !== null && this.min > this.max) {
throw new Error(`The max value should be bigger than the min. See ${MIN_DOC_LINK} and ${MAX_DOC_LINK}.`);
}
}
verifyValue(value) {
if (isDevMode() && value && typeof value !== 'number') {
throw new Error(`The NumericTextBox component requires value of type Number and ${JSON.stringify(value)} was set.`);
}
}
parseOptions(options) {
for (let idx = 0; idx < options.length; idx++) {
const name = options[idx];
const value = this[name];
if (typeof value === 'string') {
const parsed = parseFloat(value);
const valid = !isNaN(parsed);
if (isDevMode() && !valid && value !== '') {
throw new Error('The NumericTextBox component requires value of type Number or a String representing ' +
`a number for the ${name} property and ${JSON.stringify(value)} was set.`);
}
this[name] = valid ? parsed : PARSABLE_DEFAULTS[name];
}
}
}
intlChange() {
delete this.numericRegex;
if (this.numericInput && (!this.focused || !this.isValid(this.elementValue))) {
this.setInputValue();
}
}
hasTrailingZeros(inputValue) {
if (this.hasDecimals && this.focused) {
const fraction = inputValue.split(this.decimalSeparator)[1];
return fraction && fraction.length > this.decimals && fraction.lastIndexOf('0') === fraction.length - 1;
}
}
selectAll() {
this.setInputValue();
this.setSelection(0, this.inputValue.length);
}
selectCaret() {
const caretPosition = getCaretPosition(this.numericInput.nativeElement);
const formattedValue = this.elementValue;
const partialValue = formattedValue.substring(0, caretPosition);
this.setInputValue();
if (partialValue.length) {
const significantCharsInFormattedValue = extractSignificantNumericChars(partialValue, this.decimalSeparator);
const adjustedSignificantChars = this.adjustSignificantChars(formattedValue, significantCharsInFormattedValue);
this.setSelection(adjustedSignificantChars, adjustedSignificantChars);
}
else {
this.setSelection(0, 0);
}
}
numberOfLeadingZeroes(formattedValue) {
const separatorIndex = formattedValue.indexOf(this.decimalSeparator);
const matchedLeadingZeroes = formattedValue.match(/^[^1-9]*?(0+)/);
if (matchedLeadingZeroes) {
const lengthOfMatch = matchedLeadingZeroes[0].length;
const lengthOfLeadingZeroesMatch = matchedLeadingZeroes[1].length;
return lengthOfMatch === separatorIndex ? lengthOfLeadingZeroesMatch - 1 : lengthOfLeadingZeroesMatch;
}
return 0;
}
adjustSignificantChars(formattedValue, significantChars) {
const leadingZeroes = this.numberOfLeadingZeroes(formattedValue);
if (leadingZeroes > 0) {
return Math.max(0, significantChars - leadingZeroes);
}
return significantChars;
}
handleClasses(value, input) {
const elem = this.hostElement.nativeElement;
const classes = getStylingClasses('input', input, this[input], value);
if (classes.toRemove) {
this.renderer.removeClass(elem, classes.toRemove);
}
if (classes.toAdd) {
this.renderer.addClass(elem, classes.toAdd);
}
}
setInputAttributes() {
const attributesToRender = Object.assign({}, this.mutableAttributes, this.parsedAttributes);
setHTMLAttributes(attributesToRender, this.renderer, this.numericInput.nativeElement, this.ngZone);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NumericTextBoxComponent, deps: [{ token: i1.IntlService }, { token: i0.Renderer2 }, { token: i2.LocalizationService }, { token: i0.Injector }, { token: i0.NgZone }, { token: i0.ChangeDetectorRef }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: NumericTextBoxComponent, isStandalone: true, selector: "kendo-numerictextbox", inputs: { focusableId: "focusableId", disabled: "disabled", readonly: "readonly", title: "title", autoCorrect: "autoCorrect", format: "format", max: "max", min: "min", decimals: "decimals", placeholder: "placeholder", step: "step", spinners: "spinners", rangeValidation: "rangeValidation", tabindex: "tabindex", tabIndex: "tabIndex", changeValueOnScroll: "changeValueOnScroll", selectOnFocus: "selectOnFocus", value: "value", maxlength: "maxlength", size: "size", rounded: "rounded", fillMode: "fillMode", inputAttributes: "inputAttributes" }, outputs: { valueChange: "valueChange", onFocus: "focus", onBlur: "blur", inputFocus: "inputFocus", inputBlur: "inputBlur" }, host: { properties: { "class.k-readonly": "this.readonly", "attr.dir": "this.direction", "class.k-disabled": "this.disableClass", "class.k-input": "this.hostClasses", "class.k-numerictextbox": "this.hostClasses" } }, providers: [
LocalizationService,
{ provide: L10N_PREFIX, useValue: 'kendo.numerictextbox' },
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NumericTextBoxComponent), multi: true },
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => NumericTextBoxComponent), multi: true },
{ provide: KendoInput, useExisting: forwardRef(() => NumericTextBoxComponent) }
], queries: [{ propertyName: "suffixTemplate", first: true, predicate: SuffixTemplateDirective, descendants: true }, { propertyName: "prefixTemplate", first: true, predicate: PrefixTemplateDirective, descendants: true }], viewQueries: [{ propertyName: "numericInput", first: true, predicate: ["numericInput"], descendants: true, static: true }], exportAs: ["kendoNumericTextBox"], usesOnChanges: true, ngImport: i0, template: `
<ng-container kendoNumericTextBoxLocalizedMessages
i18n-increment="kendo.numerictextbox.increment|The title for the **Increment** button in the NumericTextBox"
increment="Increase value"
i18n-decrement="kendo.numerictextbox.decrement|The title for the **Decrement** button in the NumericTextBox"
decrement="Decrease value"
>
</ng-container>
<ng-container
kendoInputSharedEvents
[hostElement]="hostElement"
[(isFocused)]="focused"
(handleBlur)="handleBlur()"
(onFocus)="handleFocus()"
>
<span *ngIf="prefixTemplate" class="k-input-prefix k-input-prefix-horizontal">
<ng-template [ngTemplateOutlet]="prefixTemplate?.templateRef">
</ng-template>
</span>
<kendo-input-separator *ngIf="prefixTemplate && prefixTemplate.showSeparator"></kendo-input-separator>
<input #numericInput
class="k-input-inner"
role="spinbutton"
autocomplete="off"
autocorrect="off"
[id]="focusableId"
[attr.aria-valuemin]="min"
[attr.aria-valuemax]="max"
[attr.aria-valuenow]="value"
[attr.title]="title"
[attr.placeholder]="placeholder"
[attr.maxLength]="maxlength"
[tabindex]="tabIndex"
[disabled]="disabled"
[readonly]="readonly"
[attr.aria-invalid]="isControlInvalid"
[attr.required]="isControlRequired ? '' : null"
[kendoEventsOutsideAngular]="{
mousedown: handleMouseDown,
dragenter: handleDragEnter,
keydown: handleKeyDown,
input: handleInput,
focus: handleInputFocus,
blur: handleInputBlur,
paste: handlePaste
}"/>
<kendo-input-separator *ngIf="suffixTemplate && suffixTemplate?.showSeparator"></kendo-input-separator>
<span *ngIf="suffixTemplate" class="k-input-suffix k-input-suffix-horizontal">
<ng-template [ngTemplateOutlet]="suffixTemplate?.templateRef">
</ng-template>
</span>
<span
class="k-input-spinner k-spin-button" *ngIf="spinners"
[kendoEventsOutsideAngular]="{ mouseup: releaseArrow, mouseleave: releaseArrow }"
>
<button
type="button"
[kendoEventsOutsideAngular]="{ mousedown: increasePress }"
[attr.aria-hidden]="true"
[attr.aria-label]="incrementTitle"
[title]="incrementTitle"
class="k-spinner-increase k-button k-button-md k-icon-button k-button-solid k-button-solid-base"
[class.k-active]="arrowDirection === ArrowDirection.Up"
tabindex="-1"
>
<kendo-icon-wrapper
name="caret-alt-up"
innerCssClass="k-button-icon"
[svgIcon]="arrowUpIcon"
>
</kendo-icon-wrapper>
</button>
<button
type="button"
[kendoEventsOutsideAngular]="{ mousedown: decreasePress }"
[attr.aria-hidden]="true"
[attr.aria-label]="decrementTitle"
[title]="decrementTitle"
[class.k-active]="arrowDirection === ArrowDirection.Down"
class="k-spinner-decrease k-button k-button-md k-icon-button k-button-solid k-button-solid-base"
tabindex="-1"
>
<kendo-icon-wrapper
name="caret-alt-down"
innerCssClass="k-button-icon"
[svgIcon]="arrowDownIcon"
>
</kendo-icon-wrapper>
</button>
</span>
</ng-container>
`, isInline: true, dependencies: [{ kind: "directive", type: LocalizedNumericTextBoxMessagesDirective, selector: "[kendoNumericTextBoxLocalizedMessages]" }, { kind: "directive", type: SharedInputEventsDirective, selector: "[kendoInputSharedEvents]", inputs: ["hostElement", "clearButtonClicked", "isFocused"], outputs: ["isFocusedChange", "onFocus", "handleBlur"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: InputSeparatorComponent, selector: "kendo-input-separator, kendo-textbox-separator", inputs: ["orientation"] }, { kind: "directive", type: EventsOutsideAngularDirective, selector: "[kendoEventsOutsideAngular]", inputs: ["kendoEventsOutsideAngular", "scope"] }, { kind: "component", type: IconWrapperComponent, selector: "kendo-icon-wrapper", inputs: ["name", "svgIcon", "innerCssClass", "customFontClass", "size"], exportAs: ["kendoIconWrapper"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NumericTextBoxComponent, decorators: [{
type: Component,
args: [{
exportAs: 'kendoNumericTextBox',
providers: [
LocalizationService,
{ provide: L10N_PREFIX, useValue: 'kendo.numerictextbox' },
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NumericTextBoxComponent), multi: true },
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => NumericTextBoxComponent), multi: true },
{ provide: KendoInput, useExisting: forwardRef(() => NumericTextBoxComponent) }
],
selector: 'kendo-numerictextbox',
template: `
<ng-container kendoNumericTextBoxLocalizedMessages
i18n-increment="kendo.numerictextbox.increment|The title for the **Increment** button in the NumericTextBox"
increment="Increase value"
i18n-decrement="kendo.numerictextbox.decrement|The title for the **Decrement** button in the NumericTextBox"
decrement="Decrease value"
>
</ng-container>
<ng-container
kendoInputSharedEvents
[hostElement]="hostElement"
[(isFocused)]="focused"
(handleBlur)="handleBlur()"
(onFocus)="handleFocus()"
>
<span *ngIf="prefixTemplate" class="k-input-prefix k-input-prefix-horizontal">
<ng-template [ngTemplateOutlet]="prefixTemplate?.templateRef">
</ng-template>
</span>
<kendo-input-separator *ngIf="prefixTemplate && prefixTemplate.showSeparator"></kendo-input-separator>
<input #numericInput
class="k-input-inner"
role="spinbutton"
autocomplete="off"
autocorrect="off"
[id]="focusableId"
[attr.aria-valuemin]="min"
[attr.aria-valuemax]="max"
[attr.aria-valuenow]="value"
[attr.title]="title"
[attr.placeholder]="placeholder"
[attr.maxLength]="maxlength"
[tabindex]="tabIndex"
[disabled]="disabled"
[readonly]="readonly"
[attr.aria-invalid]="isControlInvalid"
[attr.required]="isControlRequired ? '' : null"
[kendoEventsOutsideAngular]="{
mousedown: handleMouseDown,
dragenter: handleDragEnter,
keydown: handleKeyDown,
input: handleInput,
focus: handleInputFocus,
blur: handleInputBlur,
paste: handlePaste
}"/>
<kendo-input-separator *ngIf="suffixTemplate && suffixTemplate?.showSeparator"></kendo-input-separator>
<span *ngIf="suffixTemplate" class="k-input-suffix k-input-suffix-horizontal">
<ng-template [ngTemplateOutlet]="suffixTemplate?.templateRef">
</ng-template>
</span>
<span
class="k-input-spinner k-spin-button" *ngIf="spinners"
[kendoEventsOutsideAngular]="{ mouseup: releaseArrow, mouseleave: releaseArrow }"
>
<button
type="button"
[kendoEventsOutsideAngular]="{ mousedown: increasePress }"
[attr.aria-hidden]="true"
[attr.aria-label]="incrementTitle"
[title]="incrementTitle"
class="k-spinner-increase k-button k-button-md k-icon-button k-button-solid k-button-solid-base"
[class.k-active]="arrowDirection === ArrowDirection.Up"
tabindex="-1"
>
<kendo-icon-wrapper
name="caret-alt-up"
innerCssClass="k-button-icon"
[svgIcon]="arrowUpIcon"
>
</kendo-icon-wrapper>
</button>
<button
type="button"
[kendoEventsOutsideAngular]="{ mousedown: decreasePress }"
[attr.aria-hidden]="true"
[attr.aria-label]="decrementTitle"
[title]="decrementTitle"
[class.k-active]="arrowDirection === ArrowDirection.Down"
class="k-spinner-decrease k-button k-button-md k-icon-button k-button-solid k-button-solid-base"
tabindex="-1"
>
<kendo-icon-wrapper
name="caret-alt-down"
innerCssClass="k-button-icon"
[svgIcon]="arrowDownIcon"
>
</kendo-icon-wrapper>
</button>
</span>
</ng-container>
`,
standalone: true,
imports: [LocalizedNumericTextBoxMessagesDirective, SharedInputEventsDirective, NgIf, NgTemplateOutlet, InputSeparatorComponent, EventsOutsideAngularDirective, IconWrapperComponent]
}]
}], ctorParameters: function () { return [{ type: i1.IntlService }, { type: i0.Renderer2 }, { type: i2.LocalizationService }, { type: i0.Injector }, { type: i0.NgZone }, { type: i0.ChangeDetectorRef }, { type: i0.ElementRef }]; }, propDecorators: { focusableId: [{
type: Input
}], disabled: [{
type: Input
}], readonly: [{
type: Input
}, {
type: HostBinding,
args: ['class.k-readonly']
}], title: [{
type: Input
}], autoCorrect: [{
type: Input
}], format: [{
type: Input
}], max: [{
type: Input
}], min: [{
type: Input
}], decimals: [{
type: Input
}], placeholder: [{
type: Input
}], step: [{
type: Input
}], spinners: [{
type: Input
}], rangeValidation: [{
type: Input
}], tabindex: [{
type: Input
}], tabIndex: [{
type: Input
}], changeValueOnScroll: [{
type: Input
}], selectOnFocus: [{
type: Input
}], value: [{
type: Input
}], maxlength: [{
type: Input
}], size: [{
type: Input
}], rounded: [{
type: Input
}], fillMode: [{
type: Input
}], inputAttributes: [{
type: Input
}], valueChange: [{
type: Output
}], onFocus: [{
type: Output,
args: ['focus']
}], onBlur: [{
type: Output,
args: ['blur']
}], inputFocus: [{
type: Output
}], inputBlur: [{
type: Output
}], numericInput: [{
type: ViewChild,
args: ['numericInput', { static: true }]
}], suffixTemplate: [{
type: ContentChild,
args: [SuffixTemplateDirective]
}], prefixTemplate: [{
type: ContentChild,
args: [PrefixTemplateDirective]
}], direction: [{
type: HostBinding,
args: ['attr.dir']
}], disableClass: [{
type: HostBinding,
args: ['class.k-disabled']
}], hostClasses: [{
type: