UNPKG

@taiga-ui/kit

Version:

Taiga UI Angular main components kit

266 lines (259 loc) 19.6 kB
import * as i0 from '@angular/core'; import { inject, computed, effect, signal, Directive, Input, DestroyRef, NgZone, Component, ViewEncapsulation, ChangeDetectionStrategy } from '@angular/core'; import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop'; import * as i2 from '@maskito/angular'; import { MaskitoDirective } from '@maskito/angular'; import { maskitoInitialCalibrationPlugin } from '@maskito/core'; import { maskitoParseNumber, maskitoNumberOptionsGenerator, maskitoCaretGuard } from '@maskito/kit'; import { TuiControl, tuiAsControl, tuiValueTransformerFrom } from '@taiga-ui/cdk/classes'; import { TUI_ALLOW_SIGNAL_WRITES, CHAR_MINUS, CHAR_HYPHEN } from '@taiga-ui/cdk/constants'; import { TUI_IS_IOS } from '@taiga-ui/cdk/tokens'; import { tuiInjectElement } from '@taiga-ui/cdk/utils/dom'; import { tuiIsSafeToRound, tuiClamp } from '@taiga-ui/cdk/utils/math'; import * as i1 from '@taiga-ui/core/components/textfield'; import { TuiTextfieldDirective, TuiWithTextfield, TUI_TEXTFIELD_OPTIONS, TuiTextfieldContent } from '@taiga-ui/core/components/textfield'; import { TUI_NUMBER_FORMAT, TUI_DEFAULT_NUMBER_FORMAT } from '@taiga-ui/core/tokens'; import { tuiFormatNumber } from '@taiga-ui/core/utils/format'; import { tuiMaskito } from '@taiga-ui/kit/utils'; import { tuiCreateOptions } from '@taiga-ui/cdk/utils/di'; import { NgIf } from '@angular/common'; import { tuiZonefree } from '@taiga-ui/cdk/observables'; import { TuiButton } from '@taiga-ui/core/components/button'; import { timer } from 'rxjs'; const TUI_INPUT_NUMBER_DEFAULT_OPTIONS = { min: Number.MIN_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER, prefix: '', postfix: '', step: 0, icons: { increase: '@tui.plus', decrease: '@tui.minus', }, valueTransformer: null, }; const [TUI_INPUT_NUMBER_OPTIONS, tuiInputNumberOptionsProvider] = tuiCreateOptions(TUI_INPUT_NUMBER_DEFAULT_OPTIONS); const DEFAULT_MAX_LENGTH = 18; class TuiInputNumberDirective extends TuiControl { constructor() { super(...arguments); this.textfield = inject(TuiTextfieldDirective); this.isIOS = inject(TUI_IS_IOS); this.numberFormat = toSignal(inject(TUI_NUMBER_FORMAT), { initialValue: TUI_DEFAULT_NUMBER_FORMAT, }); this.formatted = computed(() => maskitoParseNumber(this.textfield.value(), this.numberFormat())); this.precision = computed(() => Number.isNaN(this.numberFormat().precision) ? 2 : this.numberFormat().precision); this.unfinished = computed((value = this.formatted()) => value < 0 ? value > this.max() : value < this.min()); this.onChangeEffect = effect(() => { const value = this.formatted(); if (Number.isNaN(value)) { this.onChange(null); return; } if (this.unfinished() || value < this.min() || value > this.max() || this.value() === value) { return; } this.onChange(value); }, TUI_ALLOW_SIGNAL_WRITES); this.options = inject(TUI_INPUT_NUMBER_OPTIONS); this.element = tuiInjectElement(); this.inputMode = computed(() => { if (this.isIOS) { return this.min() < 0 ? 'text' // iPhone does not have minus sign if inputMode equals to 'numeric' / 'decimal' : 'decimal'; } /** * Samsung Keyboard does not minus sign for `inputmode=decimal` * @see https://github.com/taiga-family/taiga-ui/issues/11061#issuecomment-2939103792 */ return 'numeric'; }); this.defaultMaxLength = computed(() => { const { decimalSeparator, thousandSeparator } = this.numberFormat(); const decimalPart = !!this.precision() && this.textfield.value().includes(decimalSeparator); const precision = decimalPart ? Math.min(this.precision() + 1, 20) : 0; const takeThousand = thousandSeparator.repeat(5).length; return DEFAULT_MAX_LENGTH + precision + takeThousand; }); this.mask = tuiMaskito(computed(({ decimalMode, ...numberFormat } = this.numberFormat(), maximumFractionDigits = this.precision()) => this.computeMask({ ...numberFormat, maximumFractionDigits, min: this.min(), max: this.max(), prefix: this.prefix(), postfix: this.postfix(), minimumFractionDigits: decimalMode === 'always' ? maximumFractionDigits : 0, }))); this.min = signal(this.options.min); this.max = signal(this.options.max); this.prefix = signal(this.options.prefix); this.postfix = signal(this.options.postfix); } set minSetter(x) { this.updateMinMaxLimits(x, this.max()); } set maxSetter(x) { this.updateMinMaxLimits(this.min(), x); } // TODO(v5): replace with signal input set prefixSetter(x) { this.prefix.set(x); } // TODO(v5): replace with signal input set postfixSetter(x) { this.postfix.set(x); } writeValue(value) { super.writeValue(value); this.setValue(this.value()); } setValue(value) { this.textfield.value.set(this.formatNumber(value)); } onBlur() { this.onTouched(); if (!this.unfinished()) { this.setValue(this.value()); } } onFocus() { if (Number.isNaN(this.formatted()) && !this.readOnly()) { this.textfield.value.set(this.prefix() + this.postfix()); } } formatNumber(value) { if (value === null || Number.isNaN(value)) { return ''; } return ((this.prefix() !== CHAR_MINUS ? this.prefix() : '') + tuiFormatNumber(value, { ...this.numberFormat(), /** * Number can satisfy interval [Number.MIN_SAFE_INTEGER; Number.MAX_SAFE_INTEGER] * but its rounding can violate it. * Before BigInt support there is no perfect solution – only trade off. * No rounding is better than lose precision and incorrect mutation of already valid value. */ precision: tuiIsSafeToRound(value, this.precision()) ? this.precision() : Infinity, }).replace(CHAR_HYPHEN, CHAR_MINUS) + this.postfix()); } updateMinMaxLimits(nullableMin, nullableMax) { const min = this.transformer.fromControlValue(nullableMin) ?? this.options.min; const max = this.transformer.fromControlValue(nullableMax) ?? this.options.max; this.min.set(Math.min(min, max)); this.max.set(Math.max(min, max)); } computeMask(params) { const { prefix = '', postfix = '' } = params; const { plugins, ...options } = maskitoNumberOptionsGenerator(params); const initialCalibrationPlugin = maskitoInitialCalibrationPlugin(maskitoNumberOptionsGenerator({ ...params, min: Number.MIN_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER, })); return { ...options, plugins: [ ...plugins, initialCalibrationPlugin, maskitoCaretGuard((value) => [ prefix.length, value.length - postfix.length, ]), ], }; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TuiInputNumberDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: TuiInputNumberDirective, isStandalone: true, selector: "input[tuiInputNumber]", inputs: { minSetter: ["min", "minSetter"], maxSetter: ["max", "maxSetter"], prefixSetter: ["prefix", "prefixSetter"], postfixSetter: ["postfix", "postfixSetter"] }, host: { listeners: { "blur": "onBlur()", "focus": "onFocus()" }, properties: { "disabled": "disabled()", "attr.inputMode": "inputMode()", "attr.maxLength": "element.maxLength > 0 ? element.maxLength : defaultMaxLength()" } }, providers: [ tuiAsControl(TuiInputNumberDirective), tuiValueTransformerFrom(TUI_INPUT_NUMBER_OPTIONS), ], usesInheritance: true, hostDirectives: [{ directive: i1.TuiWithTextfield }, { directive: i2.MaskitoDirective }], ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TuiInputNumberDirective, decorators: [{ type: Directive, args: [{ standalone: true, selector: 'input[tuiInputNumber]', providers: [ tuiAsControl(TuiInputNumberDirective), tuiValueTransformerFrom(TUI_INPUT_NUMBER_OPTIONS), ], hostDirectives: [TuiWithTextfield, MaskitoDirective], host: { '[disabled]': 'disabled()', '[attr.inputMode]': 'inputMode()', '[attr.maxLength]': 'element.maxLength > 0 ? element.maxLength : defaultMaxLength()', '(blur)': 'onBlur()', '(focus)': 'onFocus()', }, }] }], propDecorators: { minSetter: [{ type: Input, args: ['min'] }], maxSetter: [{ type: Input, args: ['max'] }], prefixSetter: [{ type: Input, args: ['prefix'] }], postfixSetter: [{ type: Input, args: ['postfix'] }] } }); class TuiInputNumberStep { constructor() { this.destroyRef = inject(DestroyRef); this.zone = inject(NgZone); this.el = tuiInjectElement(); this.appearance = inject(TUI_TEXTFIELD_OPTIONS).appearance; this.options = inject(TUI_INPUT_NUMBER_OPTIONS); this.input = inject(TuiInputNumberDirective, { self: true }); this.step = signal(this.options.step); this.value = computed(() => this.input.value() ?? NaN); } // TODO(v5): replace with signal input set stepSetter(x) { this.step.set(x); } onStep(step) { const current = Number.isNaN(this.value()) ? 0 : this.value(); const value = tuiClamp(current + step, this.input.min(), this.input.max()); if (Number.isNaN(this.value())) { timer(0) .pipe(tuiZonefree(this.zone), takeUntilDestroyed(this.destroyRef)) .subscribe(() => { const caretIndex = this.el.value.length - this.input.postfix().length; this.el.setSelectionRange(caretIndex, caretIndex); }); } this.input.setValue(value); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TuiInputNumberStep, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: TuiInputNumberStep, isStandalone: true, selector: "input[tuiInputNumber][step]", inputs: { stepSetter: ["step", "stepSetter"] }, host: { attributes: { "ngSkipHydration": "true" }, listeners: { "keydown.arrowDown.prevent": "onStep(-step())", "keydown.arrowUp.prevent": "onStep(step())" }, properties: { "class._with-buttons": "step()" } }, ngImport: i0, template: "<ng-container *tuiTextfieldContent>\n <section\n *ngIf=\"step()\"\n class=\"t-input-number-buttons\"\n >\n <button\n size=\"s\"\n tabindex=\"-1\"\n tuiIconButton\n type=\"button\"\n class=\"t-button\"\n [appearance]=\"appearance()\"\n [disabled]=\"!input.interactive() || value() >= input.max()\"\n [iconStart]=\"options.icons.increase\"\n (click.prevent)=\"onStep(step())\"\n (mousedown.prevent)=\"el.focus()\"\n >\n +\n </button>\n\n <button\n size=\"s\"\n tabindex=\"-1\"\n tuiIconButton\n type=\"button\"\n class=\"t-button\"\n [appearance]=\"appearance()\"\n [disabled]=\"!input.interactive() || value() <= input.min()\"\n [iconStart]=\"options.icons.decrease\"\n (click.prevent)=\"onStep(-step())\"\n (mousedown.prevent)=\"el.focus()\"\n >\n -\n </button>\n </section>\n</ng-container>\n", styles: [".t-input-number-buttons.t-input-number-buttons{position:absolute;right:0;display:flex;block-size:var(--t-height);flex-direction:column;gap:.125rem;border-radius:inherit}@supports (inset-inline-end: 0){.t-input-number-buttons.t-input-number-buttons{right:unset;inset-inline-end:0}}tui-textfield[data-size=s] .t-input-number-buttons.t-input-number-buttons{flex-direction:row-reverse}.t-input-number-buttons.t-input-number-buttons>*{flex:1 1 0;border-radius:0}.t-input-number-buttons.t-input-number-buttons>*:first-child{border-top-right-radius:inherit}.t-input-number-buttons.t-input-number-buttons>*:last-child{border-bottom-right-radius:inherit}[dir=rtl] .t-input-number-buttons.t-input-number-buttons>*:first-child{border-radius:0;border-top-left-radius:inherit}[dir=rtl] .t-input-number-buttons.t-input-number-buttons>*:last-child{border-radius:0;border-bottom-left-radius:inherit}tui-textfield[data-size=l] .t-input-number-buttons.t-input-number-buttons>*{inline-size:var(--tui-height-m)}tui-textfield[data-size=s] .t-input-number-buttons.t-input-number-buttons>*:first-child{border-top-right-radius:inherit;border-bottom-right-radius:inherit}tui-textfield[data-size=s] .t-input-number-buttons.t-input-number-buttons>*:last-child{border-radius:0}[tuiInputNumber]._with-buttons{border-top-right-radius:0;border-bottom-right-radius:0}[dir=rtl] [tuiInputNumber]._with-buttons{border-radius:inherit;border-top-left-radius:0;border-bottom-left-radius:0}tui-textfield[data-size=l]{--t-input-number-offset-end: calc(var(--tui-height-m) + .125rem)}tui-textfield[data-size=m]{--t-input-number-offset-end: calc(var(--tui-height-s) + .125rem)}tui-textfield[data-size=s]{--t-input-number-offset-end: calc(2 * var(--tui-height-s) + .25rem)}[tuiInputNumber]._with-buttons,[tuiInputNumber]._with-buttons~.t-template{inline-size:calc(100% - var(--t-input-number-offset-end));margin-inline-end:var(--t-input-number-offset-end)}[tuiInputNumber]._with-buttons~.t-content{margin-inline-end:var(--t-input-number-offset-end)}\n"], dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: TuiButton, selector: "a[tuiButton],button[tuiButton],a[tuiIconButton],button[tuiIconButton]", inputs: ["size"] }, { kind: "directive", type: TuiTextfieldContent, selector: "ng-template[tuiTextfieldContent]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TuiInputNumberStep, decorators: [{ type: Component, args: [{ standalone: true, selector: 'input[tuiInputNumber][step]', imports: [NgIf, TuiButton, TuiTextfieldContent], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, host: { ngSkipHydration: 'true', '(keydown.arrowDown.prevent)': 'onStep(-step())', '(keydown.arrowUp.prevent)': 'onStep(step())', '[class._with-buttons]': 'step()', }, template: "<ng-container *tuiTextfieldContent>\n <section\n *ngIf=\"step()\"\n class=\"t-input-number-buttons\"\n >\n <button\n size=\"s\"\n tabindex=\"-1\"\n tuiIconButton\n type=\"button\"\n class=\"t-button\"\n [appearance]=\"appearance()\"\n [disabled]=\"!input.interactive() || value() >= input.max()\"\n [iconStart]=\"options.icons.increase\"\n (click.prevent)=\"onStep(step())\"\n (mousedown.prevent)=\"el.focus()\"\n >\n +\n </button>\n\n <button\n size=\"s\"\n tabindex=\"-1\"\n tuiIconButton\n type=\"button\"\n class=\"t-button\"\n [appearance]=\"appearance()\"\n [disabled]=\"!input.interactive() || value() <= input.min()\"\n [iconStart]=\"options.icons.decrease\"\n (click.prevent)=\"onStep(-step())\"\n (mousedown.prevent)=\"el.focus()\"\n >\n -\n </button>\n </section>\n</ng-container>\n", styles: [".t-input-number-buttons.t-input-number-buttons{position:absolute;right:0;display:flex;block-size:var(--t-height);flex-direction:column;gap:.125rem;border-radius:inherit}@supports (inset-inline-end: 0){.t-input-number-buttons.t-input-number-buttons{right:unset;inset-inline-end:0}}tui-textfield[data-size=s] .t-input-number-buttons.t-input-number-buttons{flex-direction:row-reverse}.t-input-number-buttons.t-input-number-buttons>*{flex:1 1 0;border-radius:0}.t-input-number-buttons.t-input-number-buttons>*:first-child{border-top-right-radius:inherit}.t-input-number-buttons.t-input-number-buttons>*:last-child{border-bottom-right-radius:inherit}[dir=rtl] .t-input-number-buttons.t-input-number-buttons>*:first-child{border-radius:0;border-top-left-radius:inherit}[dir=rtl] .t-input-number-buttons.t-input-number-buttons>*:last-child{border-radius:0;border-bottom-left-radius:inherit}tui-textfield[data-size=l] .t-input-number-buttons.t-input-number-buttons>*{inline-size:var(--tui-height-m)}tui-textfield[data-size=s] .t-input-number-buttons.t-input-number-buttons>*:first-child{border-top-right-radius:inherit;border-bottom-right-radius:inherit}tui-textfield[data-size=s] .t-input-number-buttons.t-input-number-buttons>*:last-child{border-radius:0}[tuiInputNumber]._with-buttons{border-top-right-radius:0;border-bottom-right-radius:0}[dir=rtl] [tuiInputNumber]._with-buttons{border-radius:inherit;border-top-left-radius:0;border-bottom-left-radius:0}tui-textfield[data-size=l]{--t-input-number-offset-end: calc(var(--tui-height-m) + .125rem)}tui-textfield[data-size=m]{--t-input-number-offset-end: calc(var(--tui-height-s) + .125rem)}tui-textfield[data-size=s]{--t-input-number-offset-end: calc(2 * var(--tui-height-s) + .25rem)}[tuiInputNumber]._with-buttons,[tuiInputNumber]._with-buttons~.t-template{inline-size:calc(100% - var(--t-input-number-offset-end));margin-inline-end:var(--t-input-number-offset-end)}[tuiInputNumber]._with-buttons~.t-content{margin-inline-end:var(--t-input-number-offset-end)}\n"] }] }], propDecorators: { stepSetter: [{ type: Input, args: ['step'] }] } }); const TuiInputNumber = [TuiInputNumberDirective, TuiInputNumberStep]; /** * Generated bundle index. Do not edit. */ export { TUI_INPUT_NUMBER_DEFAULT_OPTIONS, TUI_INPUT_NUMBER_OPTIONS, TuiInputNumber, TuiInputNumberDirective, TuiInputNumberStep, tuiInputNumberOptionsProvider }; //# sourceMappingURL=taiga-ui-kit-components-input-number.mjs.map