UNPKG

@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
/**----------------------------------------------------------------------------------------- * 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: