@taiga-ui/kit
Version:
Taiga UI Angular main components kit
266 lines (259 loc) • 19.6 kB
JavaScript
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